From 6b9421c8352f1d1e529a4dae0bdf5d65257e4d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20P=C3=A9rez=20de=20Castro?= Date: Tue, 22 Sep 2015 01:24:07 +0200 Subject: [PATCH 1/2] IPv4 RFC791 fragmentation --- src/apps/lwaftr/constants.lua | 1 + src/apps/lwaftr/fragmentv4.lua | 333 +++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+) create mode 100644 src/apps/lwaftr/fragmentv4.lua diff --git a/src/apps/lwaftr/constants.lua b/src/apps/lwaftr/constants.lua index 6d6e1c8b97..da3f9f0065 100644 --- a/src/apps/lwaftr/constants.lua +++ b/src/apps/lwaftr/constants.lua @@ -60,6 +60,7 @@ o_ethernet_ethertype = 12 o_ipv4_ver_and_ihl = 0 o_ipv4_dscp_and_ecn = 1 o_ipv4_total_length = 2 +o_ipv4_identification = 4 o_ipv4_flags = 6 o_ipv4_ttl = 8 o_ipv4_proto = 9 diff --git a/src/apps/lwaftr/fragmentv4.lua b/src/apps/lwaftr/fragmentv4.lua new file mode 100644 index 0000000000..170fc6795a --- /dev/null +++ b/src/apps/lwaftr/fragmentv4.lua @@ -0,0 +1,333 @@ +module(..., package.seeall) + +local constants = require("apps.lwaftr.constants") +local lwutil = require("apps.lwaftr.lwutil") +local packet = require("core.packet") +local ipsum = require("lib.checksum").ipsum +local bit = require("bit") +local ffi = require("ffi") + +local rd16, wr16, get_ihl = lwutil.rd16, lwutil.wr16, lwutil.get_ihl +local cast = ffi.cast +local C = ffi.C +local band, bor = bit.band, bit.bor +local ceil = math.ceil + +local ver_and_ihl_offset = constants.ethernet_header_size + constants.o_ipv4_ver_and_ihl +local total_length_offset = constants.ethernet_header_size + constants.o_ipv4_total_length +local frag_id_offset = constants.ethernet_header_size + constants.o_ipv4_identification +local flags_and_frag_offset_offset = constants.ethernet_header_size + constants.o_ipv4_flags +local checksum_offset = constants.ethernet_header_size + constants.o_ipv4_checksum + +-- Constants to manipulate the flags next to the frag-offset field directly +-- as a 16-bit integer, without needing to shift the 3 flag bits. +local flag_dont_fragment_mask = 0x4000 +local flag_more_fragments_mask = 0x2000 +local frag_offset_field_mask = 0x1FFF + + +-- TODO: Consider security/performance tradeoffs of randomization +local fresh_frag_id = (function () + local internal_frag_id = 0x4242 + return function () + internal_frag_id = band(internal_frag_id + 1, 0xFFFF) + return internal_frag_id + end +end)() + + +FRAGMENT_OK = 1 +FRAGMENT_UNNEEDED = 2 +FRAGMENT_FORBIDDEN = 3 + +-- +-- IPv4 fragmentation, as per https://tools.ietf.org/html/rfc791 +-- +-- For an invocation: +-- +-- local statuscode, packets = fragment_ipv4(input_packet, mtu) +-- +-- the possible values for the returned "statuscode" are: +-- +-- * FRAGMENT_OK: the returned "packets" is a list of IPv4 packets, all +-- of them smaller or equal than "mtu" bytes, which contain the payload +-- from the "input_packet" properly fragmented. Note that "input_packet" +-- is modified in-place. +-- +-- * FRAGMENT_UNNEEDED: the returned "packets" is the same object as +-- "input_packet", unmodified. This is the case when the size of packet +-- is smaller or equal than "mtu" bytes. +-- +-- * FRAGMENT_FORBIDDEN: the returned "packets" will be "nil". This is +-- the case when "input_packet" has the "don't fragment" flag set, and +-- its size is bigger than "mtu" bytes. Client code may want to return +-- an ICMP Datagram Too Big (Type 3, Code 4) packet back to the sender. +-- +function fragment_ipv4(ipv4_pkt, mtu) + if ipv4_pkt.length <= mtu then + return FRAGMENT_UNNEEDED, ipv4_pkt + end + + -- Discard packets with the DF (dont't fragment) flag set + do + local flags_and_frag_offset = C.ntohs(rd16(ipv4_pkt.data + flags_and_frag_offset_offset)) + if band(flags_and_frag_offset, flag_dont_fragment_mask) ~= 0 then + return FRAGMENT_FORBIDDEN, nil + end + end + + local ihl = get_ihl(ipv4_pkt) + local header_size = constants.ethernet_header_size + ihl + local payload_size = ipv4_pkt.length - header_size + -- Payload bytes per packet must be a multiple of 8 + local payload_bytes_per_packet = band(mtu - header_size, 0xFFF8) + local total_length_per_packet = payload_bytes_per_packet + ihl + local num_packets = ceil(payload_size / payload_bytes_per_packet) + + local pkts = { ipv4_pkt } + + wr16(ipv4_pkt.data + frag_id_offset, C.htons(fresh_frag_id())) + wr16(ipv4_pkt.data + total_length_offset, C.htons(total_length_per_packet)) + wr16(ipv4_pkt.data + flags_and_frag_offset_offset, C.htons(flag_more_fragments_mask)) + wr16(ipv4_pkt.data + checksum_offset, 0) + + local raw_frag_offset = payload_bytes_per_packet + + for i = 2, num_packets - 1 do + local frag_pkt = packet.allocate() + ffi.copy(frag_pkt.data, ipv4_pkt.data, header_size) + ffi.copy(frag_pkt.data + header_size, + ipv4_pkt.data + header_size + raw_frag_offset, + payload_bytes_per_packet) + wr16(frag_pkt.data + flags_and_frag_offset_offset, + C.htons(bor(flag_more_fragments_mask, + band(frag_offset_field_mask, raw_frag_offset / 8)))) + wr16(frag_pkt.data + checksum_offset, + C.htons(ipsum(frag_pkt.data + ver_and_ihl_offset, ihl, 0))) + frag_pkt.length = header_size + payload_bytes_per_packet + raw_frag_offset = raw_frag_offset + payload_bytes_per_packet + pkts[i] = frag_pkt + end + + -- Last packet + local last_pkt = packet.allocate() + local last_payload_len = payload_size % payload_bytes_per_packet + ffi.copy(last_pkt.data, ipv4_pkt.data, header_size) + ffi.copy(last_pkt.data + header_size, + ipv4_pkt.data + header_size + raw_frag_offset, + last_payload_len) + wr16(last_pkt.data + flags_and_frag_offset_offset, + C.htons(band(frag_offset_field_mask, raw_frag_offset / 8))) + wr16(last_pkt.data + total_length_offset, C.htons(last_payload_len + ihl)) + wr16(last_pkt.data + checksum_offset, + C.htons(ipsum(last_pkt.data + ver_and_ihl_offset, ihl, 0))) + last_pkt.length = header_size + last_payload_len + pkts[num_packets] = last_pkt + + -- Truncate the original packet, and update its checksum + ipv4_pkt.length = header_size + payload_bytes_per_packet + wr16(ipv4_pkt.data + checksum_offset, + C.htons(ipsum(ipv4_pkt.data + ver_and_ihl_offset, ihl, 0))) + + return FRAGMENT_OK, pkts +end + + +function selftest() + print("selftest: lwaftr.fragmentv4.fragment_ipv4") + + local eth_proto = require("lib.protocol.ethernet") + local ip4_proto = require("lib.protocol.ipv4") + + -- Makes an IPv4 packet, with Ethernet framing, with a given payload size + local function make_ipv4_packet(payload_size) + local pkt = packet.allocate() + pkt.length = eth_proto:sizeof() + ip4_proto:sizeof() + payload_size + local eth_header = eth_proto:new_from_mem(pkt.data, pkt.length) + local ip4_header = ip4_proto:new_from_mem(pkt.data + eth_header:sizeof(), + pkt.length - eth_header:sizeof()) + assert(pkt.length == eth_header:sizeof() + ip4_header:sizeof() + payload_size) + + -- Ethernet header + eth_header:src(eth_proto:pton("5c:51:4f:8f:aa:ee")) + eth_header:dst(eth_proto:pton("5c:51:4f:8f:aa:ef")) + eth_header:type(0x0800) -- IPv4 + + -- IPv4 header + ip4_header:version(4) + ip4_header:ihl(ip4_header:sizeof() / 4) + ip4_header:dscp(0) + ip4_header:ecn(0) + ip4_header:total_length(ip4_header:sizeof() + payload_size) + ip4_header:id(0) + ip4_header:flags(0) + ip4_header:frag_off(0) + ip4_header:ttl(15) + ip4_header:protocol(0xFF) + ip4_header:src(ip4_proto:pton("192.168.10.10")) + ip4_header:dst(ip4_proto:pton("192.168.10.20")) + ip4_header:checksum() + + -- We do not fill up the rest of the packet: random contents works fine + -- because we are testing IP fragmentation, so there's no need to care + -- about upper layers. + + return pkt + end + + local function pkt_payload_size(pkt) + assert(pkt.length >= (eth_proto:sizeof() + ip4_proto:sizeof())) + local ip4_header = ip4_proto:new_from_mem(pkt.data + eth_proto:sizeof(), + pkt.length - eth_proto:sizeof()) + local total_length = ip4_header:total_length() + local ihl = ip4_header:ihl() * 4 + assert(ihl == get_ihl(pkt)) + assert(ihl == ip4_header:sizeof()) + assert(total_length - ihl >= 0) + assert(total_length == pkt.length - eth_proto:sizeof()) + return total_length - ihl + end + + local function pkt_frag_offset(pkt) + assert(pkt.length >= (eth_proto:sizeof() + ip4_proto:sizeof())) + local ip4_header = ip4_proto:new_from_mem(pkt.data + eth_proto:sizeof(), + pkt.length - eth_proto:sizeof()) + return ip4_header:frag_off() * 8 + end + + local function pkt_total_length(pkt) + assert(pkt.length >= (eth_proto:sizeof() + ip4_proto:sizeof())) + local ip4_header = ip4_proto:new_from_mem(pkt.data + eth_proto:sizeof(), + pkt.length - eth_proto:sizeof()) + return ip4_header:total_length() + end + + local function check_packet_fragment(orig_pkt, frag_pkt, is_last_fragment) + -- Ethernet fields + local orig_hdr = eth_proto:new_from_mem(orig_pkt.data, orig_pkt.length) + local frag_hdr = eth_proto:new_from_mem(frag_pkt.data, frag_pkt.length) + assert(orig_hdr:src_eq(frag_hdr:src())) + assert(orig_hdr:dst_eq(frag_hdr:dst())) + assert(orig_hdr:type() == frag_hdr:type()) + + -- IPv4 fields + orig_hdr = ip4_proto:new_from_mem(orig_pkt.data + eth_proto:sizeof(), + orig_pkt.length - eth_proto:sizeof()) + frag_hdr = ip4_proto:new_from_mem(frag_pkt.data + eth_proto:sizeof(), + frag_pkt.length - eth_proto:sizeof()) + assert(orig_hdr:ihl() == frag_hdr:ihl()) + assert(orig_hdr:dscp() == frag_hdr:dscp()) + assert(orig_hdr:ecn() == frag_hdr:ecn()) + assert(orig_hdr:ttl() == frag_hdr:ttl()) + assert(orig_hdr:protocol() == frag_hdr:protocol()) + assert(orig_hdr:src_eq(frag_hdr:src())) + assert(orig_hdr:dst_eq(frag_hdr:dst())) + + assert(pkt_payload_size(frag_pkt) == frag_pkt.length - eth_proto:sizeof() - ip4_proto:sizeof()) + + if is_last_fragment then + assert(band(frag_hdr:flags(), 0x1) == 0x0) + else + assert(band(frag_hdr:flags(), 0x1) == 0x1) + end + end + + -- Packet with 1200 bytes of payload + local pkt = assert(make_ipv4_packet(1200)) + + -- MTU bigger than the packet size + local code, result = assert(fragment_ipv4(pkt, 1500)) + assert(code == FRAGMENT_UNNEEDED) + assert(pkt == result) + + -- Keep a copy of the packet, for comparisons + local orig_pkt = packet.allocate() + orig_pkt.length = pkt.length + ffi.copy(orig_pkt.data, pkt.data, pkt.length) + + assert(pkt.length > 1200, "packet short than payload size") + + code, result = assert(fragment_ipv4(pkt, 1000)) + assert(code == FRAGMENT_OK) + assert(#result == 2, "fragmentation returned " .. #result .. " packets (2 expected)") + + for i = 1, #result do + assert(result[i].length <= 1000, "packet " .. i .. " longer than MTU") + local is_last = (i == #result) + check_packet_fragment(orig_pkt, result[i], is_last) + end + + assert(pkt_payload_size(result[1]) + pkt_payload_size(result[2]) == 1200) + assert(pkt_payload_size(result[1]) == pkt_frag_offset(result[2])) + + -- Packet with 1200 bytes of payload, which is fragmented into 4 pieces + pkt = assert(make_ipv4_packet(1200)) + + -- Keep a copy of the packet, for comparisons + orig_pkt = packet.allocate() + orig_pkt.length = pkt.length + ffi.copy(orig_pkt.data, pkt.data, pkt.length) + + code, result = assert(fragment_ipv4(pkt, 400)) + assert(code == FRAGMENT_OK) + assert(#result == 4, + "fragmentation returned " .. #result .. " packets (4 expected)") + for i = 1, #result do + assert(result[i].length <= 1000, "packet " .. i .. " longer than MTU") + local is_last = (i == #result) + check_packet_fragment(orig_pkt, result[i], is_last) + end + + assert(pkt_payload_size(result[1]) + pkt_payload_size(result[2]) + + pkt_payload_size(result[3]) + pkt_payload_size(result[4]) == 1200) + assert(pkt_payload_size(result[1]) == pkt_frag_offset(result[2])) + assert(pkt_payload_size(result[1]) + pkt_payload_size(result[2]) == + pkt_frag_offset(result[3])) + assert(pkt_payload_size(result[1]) + pkt_payload_size(result[2]) + + pkt_payload_size(result[3]) == pkt_frag_offset(result[4])) + + -- Try to fragment a packet with the "don't fragment" flag set + pkt = assert(make_ipv4_packet(1200)) + local ip4_header = ip4_proto:new_from_mem(pkt.data + eth_proto:sizeof(), + pkt.length - eth_proto:sizeof()) + ip4_header:flags(0x2) -- Set "don't fragment" + code, result = fragment_ipv4(pkt, 500) + assert(code == FRAGMENT_FORBIDDEN) + assert(type(result) == "nil") + + -- A 1046 byte packet + local pattern = { 0xCC, 0xAA, 0xFF, 0xEE, 0xBB, 0x11, 0xDD } + local function pattern_fill(array, length) + for i = 0, length-1 do + array[i] = pattern[(i % #pattern) + 1] + end + end + local function pattern_check(array, length) + for i = 0, length-1 do + assert(array[i], pattern[(i % #pattern) + 1], "pos: " .. i) + end + end + + pkt = make_ipv4_packet(1046 - ip4_proto:sizeof() - eth_proto:sizeof()) + pattern_fill(pkt.data + ip4_proto:sizeof() + eth_proto:sizeof(), + pkt.length - ip4_proto:sizeof() - eth_proto:sizeof()) + orig_pkt = packet.allocate() + ffi.copy(orig_pkt.data, pkt.data, pkt.length) + + code, result = fragment_ipv4(pkt, 520) + assert(code == FRAGMENT_OK) + assert(#result == 3) + + assert(pkt_payload_size(result[1]) + pkt_payload_size(result[2]) + + pkt_payload_size(result[3]) == 1046 - ip4_proto:sizeof() - eth_proto:sizeof()) + + local size = pkt_payload_size(result[1]) + pkt_payload_size(result[2]) + pkt_payload_size(result[3]) + local data = ffi.new("uint8_t[?]", size) + + for i = 1, #result do + ffi.copy(data + pkt_frag_offset(result[i]), + result[i].data + eth_proto:sizeof() + get_ihl(result[i]), + pkt_payload_size(result[i])) + end + pattern_check(data, size) +end From 264d747ab17231a59d2d415526fb57b720945c73 Mon Sep 17 00:00:00 2001 From: Katerina Barone-Adesi Date: Mon, 28 Sep 2015 20:17:10 +0100 Subject: [PATCH 2/2] Integrate IPv4 fragmentation into the lwaftr and test suite --- src/apps/lwaftr/default_conf | 3 +++ src/apps/lwaftr/lwaftr.lua | 22 +++++++++++++++--- tests/apps/lwaftr/benchdata/ipv4-1460.pcap | Bin 0 -> 1500 bytes tests/apps/lwaftr/benchdata/ipv4-1500.pcap | Bin 0 -> 1540 bytes tests/apps/lwaftr/benchdata/ipv6-1046.pcap | Bin 1046 -> 1086 bytes tests/apps/lwaftr/benchdata/ipv6-1500.pcap | Bin 0 -> 1540 bytes tests/apps/lwaftr/data/icmp_on_fail.conf | 1 + tests/apps/lwaftr/data/no_hairpin.conf | 1 + tests/apps/lwaftr/data/no_icmp.conf | 1 + .../apps/lwaftr/data/small_ipv4_mtu_icmp.conf | 16 +++++++++++++ .../lwaftr/data/small_ipv6_mtu_no_icmp.conf | 1 + .../data/tcp-ipv4-toinet-2fragments.pcap | Bin 0 -> 1096 bytes .../data/tcp-ipv4-toinet-3fragments.pcap | Bin 0 -> 1600 bytes .../data/tcp-ipv6-fromb4-toinet-1046.pcap | Bin 0 -> 1086 bytes .../data/tcp-ipv6-fromb4-toinet-1500.pcap | Bin 0 -> 1540 bytes tests/apps/lwaftr/data/tunnel_icmp.conf | 1 + tests/apps/lwaftr/end-to-end/end-to-end.sh | 11 ++++++++- 17 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 tests/apps/lwaftr/benchdata/ipv4-1460.pcap create mode 100644 tests/apps/lwaftr/benchdata/ipv4-1500.pcap create mode 100644 tests/apps/lwaftr/benchdata/ipv6-1500.pcap create mode 100644 tests/apps/lwaftr/data/small_ipv4_mtu_icmp.conf create mode 100644 tests/apps/lwaftr/data/tcp-ipv4-toinet-2fragments.pcap create mode 100644 tests/apps/lwaftr/data/tcp-ipv4-toinet-3fragments.pcap create mode 100644 tests/apps/lwaftr/data/tcp-ipv6-fromb4-toinet-1046.pcap create mode 100644 tests/apps/lwaftr/data/tcp-ipv6-fromb4-toinet-1500.pcap diff --git a/src/apps/lwaftr/default_conf b/src/apps/lwaftr/default_conf index aee48801ef..faa232c1bf 100644 --- a/src/apps/lwaftr/default_conf +++ b/src/apps/lwaftr/default_conf @@ -5,7 +5,10 @@ aftr_mac_inet_side = ethernet:pton("12:12:12:12:12:12"), b4_mac = ethernet:pton("44:44:44:44:44:44"), binding_table = bt.get_binding_table(), hairpinning = true, +icmpv6_rate_limiter_n_packets=6e3, +icmpv6_rate_limiter_n_seconds=2, inet_mac = ethernet:pton("68:68:68:68:68:68"), +ipv4_mtu = 1460, ipv6_mtu = 1500, policy_icmpv4_incoming = policies['DROP'], policy_icmpv6_incoming = policies['DROP'], diff --git a/src/apps/lwaftr/lwaftr.lua b/src/apps/lwaftr/lwaftr.lua index 9479b51860..5f6c16d554 100644 --- a/src/apps/lwaftr/lwaftr.lua +++ b/src/apps/lwaftr/lwaftr.lua @@ -1,6 +1,7 @@ module(..., package.seeall) local constants = require("apps.lwaftr.constants") +local fragmentv4 = require("apps.lwaftr.fragmentv4") local fragmentv6 = require("apps.lwaftr.fragmentv6") local icmp = require("apps.lwaftr.icmp") local lwconf = require("apps.lwaftr.conf") @@ -513,7 +514,6 @@ local function clean_fragment_cache(lwstate, frag_id) lwstate.fragment6_cache[frag_id] = nil end --- TODO: rewrite this to use parse local function from_b4(lwstate, pkt) -- TODO: only send ICMP on failure for packets that plausibly would be bound? if fragmentv6.is_ipv6_fragment(pkt) then @@ -579,8 +579,24 @@ local function from_b4(lwstate, pkt) eth_hdr.ether_shost = lwstate.aftr_mac_inet_side eth_hdr.ether_dhost = lwstate.inet_mac eth_hdr.ether_type = C.htons(constants.ethertype_ipv4) - guarded_transmit(pkt, lwstate.o4) - return + + -- Fragment if necessary + if pkt.length > lwstate.ipv4_mtu then + local fragstatus, frags = fragmentv4.fragment_ipv4(pkt, lwstate.ipv4_mtu) + if fragstatus == fragmentv4.FRAGMENT_OK then + for i=1,#frags do + guarded_transmit(frags[i], lwstate.o4) + end + return + else + -- TODO: send ICMPv4 info if allowed by policy + packet.free(pkt) + return + end + else -- No fragmentation needed + guarded_transmit(pkt, lwstate.o4) + return + end end elseif lwstate.policy_icmpv6_outgoing == lwconf.policies['ALLOW'] then icmp_b4_lookup_failed(lwstate, pkt, ipv6_src_ip) diff --git a/tests/apps/lwaftr/benchdata/ipv4-1460.pcap b/tests/apps/lwaftr/benchdata/ipv4-1460.pcap new file mode 100644 index 0000000000000000000000000000000000000000..477fe72ffa53d7e1116cc93c8421ba840e7ac9c4 GIT binary patch literal 1500 zcmca|c+)~A1{MYw_+QV!zzF21%;yXfJI2beg_Qw}Sy`BwfZ~iy%q*-N46Y2U%RtH; z*ffMeLYw@jz2x}A5CAhJfJuQtdM5*ep^>qPshOF%g{8qLb2J=A6TxU^7%d6tRw4ob D)|)TK literal 0 HcmV?d00001 diff --git a/tests/apps/lwaftr/benchdata/ipv4-1500.pcap b/tests/apps/lwaftr/benchdata/ipv4-1500.pcap new file mode 100644 index 0000000000000000000000000000000000000000..de28ecddb0564f7108e1f265f4652eb03e4cfa85 GIT binary patch literal 1540 zcmca|c+)~A1{MYw_+QV!zzF1go6i|$>Bh})hm`@0Sy`BwfZ~iy%q*-N46Y2U=RnFF z*wkNxgf{t4d&%*KApmAd0Fwg4zY+!pLnC7oQ!_Jj3rmAh=4d#KCW6t-Fj^7}x)Kop DjS)28N9jTbU<2Fd72@7i$wK delta 80 zcmdnTF^yw_g3LQ+1_lsz0Rts4XuF%hz`($~W1@|!>JA1*1_paJZN^Rh(_S(%F|#lP XaQp$vfXCM6c~OcFfbSz8Jn1znVDNy8jLbWXgI*q#5G{D0%7!&#CXEVnZenf?Nv4` LC5@()p_gX>C^O!v literal 0 HcmV?d00001 diff --git a/tests/apps/lwaftr/data/tcp-ipv4-toinet-3fragments.pcap b/tests/apps/lwaftr/data/tcp-ipv4-toinet-3fragments.pcap new file mode 100644 index 0000000000000000000000000000000000000000..1e4eadb8c488e4ce843bc5731b3815be49b71b5b GIT binary patch literal 1600 zcmca|c+)~A1{MYw`2U}Qff2?5(pF3moB;+xV8FrP%D|-KXCM6d0s;GB6k#8Jn1znVDNy8jLbWXgI*q1m2Y73`|Mvu#_~KRz~v-wgLl| gl3IWT6bPf2NsMJq&J1n#Y_F+Wa0I8TmMgRZ+ literal 0 HcmV?d00001 diff --git a/tests/apps/lwaftr/data/tcp-ipv6-fromb4-toinet-1046.pcap b/tests/apps/lwaftr/data/tcp-ipv6-fromb4-toinet-1046.pcap new file mode 100644 index 0000000000000000000000000000000000000000..c554b655bfe9476e8aea31eadc79bded90fe3d15 GIT binary patch literal 1086 zcmca|c+)~A1{MYw`2U}Qff2?5(qcd<5Ox6rB`|2an*fwye!%jdQJsN_ff=Zsm4S_c zol%2eKXqcgq Xv5BdfnYo3f!6