diff --git a/src/program/packetblaster/lwaftr/README b/src/program/packetblaster/lwaftr/README index 4c340c98c7..45e5610761 100644 --- a/src/program/packetblaster/lwaftr/README +++ b/src/program/packetblaster/lwaftr/README @@ -14,12 +14,18 @@ Usage: packetblaster lwaftr [OPTIONS] --sock Socket name for virtio interface - --vlan VLANID VLAN tag traffic with VLANID if set + --vlan4 VLANID Encapsulate IPv4 traffic with IEEE 802.1Q with the given VLANID + --vlan6 VLANID Encapsulate IPv6 traffic with IEEE 802.1Q with the given VLANID + --vlan VLANID Same as --vlan4 VLANID --vlan6 VLANID - --src_mac SOURCE Source MAC-Address + --src_mac4 SOURCE Local MAC Address for IPv4 traffic + --src_mac6 SOURCE Local MAC Address for IPv6 traffic + --src_mac SOURCE Same as --src_mac4 SOURCE --src_mac6 SOURCE Default: 00:00:00:00:00:00 - --dst_mac DESTINATION Destination MAC-Address + --dst_mac4 DEST Remote MAC Address for IPv4 traffic + --dst_mac6 DEST Remote MAC Address for IPv6 traffic + --dst_mac DESTINATION Same as --dst_mac4 DEST --dst_mac6 DEST Default: 00:00:00:00:00:00 --size SIZES A comma separated list of numbers. Send packets whose @@ -28,21 +34,22 @@ Usage: packetblaster lwaftr [OPTIONS] headers, and additionally a 4-byte CRC that is written and read by the NIC. - Note that the minimum ethernet frame size is 64 bytes. - While it's technically possible to make smaller frames - and we do allow it, the NIC will pad it up to the + Note that the minimum ethernet frame size is 64 + bytes. While it's technically possible to make + smaller frames, the NIC will pad it up to the minimum before sending, so it's a bit pointless. - Since Snabb does not see the CRC in the packet, that - means that from Snabb's perspective the minimum useful - packet size is 60 bytes. - - The smallest allowed frame size is 46 bytes, - comprising 14 bytes for the ethernet header, 20 for - the IPv4 header, 8 for the UDP header, and 4 - additional bytes for the ethernet checksum. If the - packet has at least 8 bytes of payload, the generated - packets will include a unique identifier in the - payload as well. + We signal an error if the user requests a packet + size that's smaller than 64 bytes, to avoid + misinterpreted benchmarks. + + Packets will consist of 14 bytes for the + ethernet header, 4 additional bytes for the + ethernet checksum, a possible 4 bytes for a VLAN + tag, 20 for the IPv4 header, 8 for the UDP + header, and then a payload. The payload + includes a unique identifier in the first 8 + bytes. IPv6 packets include an additional 40 + bytes for the IPv6 header. Default: 64,64,64,64,64,64,64,594,594,594,1500 diff --git a/src/program/packetblaster/lwaftr/lib.lua b/src/program/packetblaster/lwaftr/lib.lua index 9960fa9783..b672cd6cc2 100644 --- a/src/program/packetblaster/lwaftr/lib.lua +++ b/src/program/packetblaster/lwaftr/lib.lua @@ -10,14 +10,13 @@ local ipv6 = require("lib.protocol.ipv6") local ipsum = require("lib.checksum").ipsum local ffi = require("ffi") -local C = ffi.C local cast = ffi.cast -local copy = ffi.copy +local htons, ntohs = lib.htons, lib.ntohs +local htonl, ntohl = lib.htonl, lib.ntohl local PROTO_IPV4_ENCAPSULATION = 0x4 -local PROTO_VLAN = C.htons(0x8100) -local PROTO_IPV4 = C.htons(0x0800) -local PROTO_IPV6 = C.htons(0x86DD) +local PROTO_IPV4 = htons(0x0800) +local PROTO_IPV6 = htons(0x86DD) local DEFAULT_TTL = 255 local MAGIC = 0xaffeface @@ -30,23 +29,13 @@ struct { } __attribute__((packed)) ]] local ether_header_ptr_type = ffi.typeof("$*", ether_header_t) -local ethernet_header_size = ffi.sizeof(ether_header_t) -local OFFSET_ETHERTYPE = 12 +local ether_header_size = ffi.sizeof(ether_header_t) +local ether_min_frame_size = 64 -- The ethernet CRC field is not included in the packet as seen by -- Snabb, but it is part of the frame and therefore a contributor to the -- frame size. -local ethernet_crc_size = 4 - -local ether_vlan_header_type = ffi.typeof([[ -struct { - uint16_t tag; - uint16_t ether_type; -} -]]) -ether_vlan_header_ptr_type = ffi.typeof("$*", ether_vlan_header_type) -ether_vlan_header_size = ffi.sizeof(ether_vlan_header_type) -local OFFSET_ETHERTYPE_VLAN = OFFSET_ETHERTYPE + ether_vlan_header_size +local ether_crc_size = 4 local ipv4hdr_t = ffi.typeof[[ struct { @@ -97,19 +86,6 @@ struct { local payload_ptr_type = ffi.typeof("$*", payload_t) local payload_size = ffi.sizeof(payload_t) -local uint16_ptr_t = ffi.typeof("uint16_t*") -local uint32_ptr_t = ffi.typeof("uint32_t*") - -local n_cache_src_ipv6 = ipv6:pton("::") - -local function rd32(offset) - return cast(uint32_ptr_t, offset)[0] -end - -local function wr32(offset, val) - cast(uint32_ptr_t, offset)[0] = val -end - local function inc_ipv6(ipv6) for i=15,0,-1 do if ipv6[i] == 255 then @@ -122,202 +98,133 @@ local function inc_ipv6(ipv6) return ipv6 end -Lwaftrgen = { +local function inc_ipv4(ipv4) + ipv4 = cast("uint32_t*", ipv4) + ipv4[0] = htonl(ntohl(ipv4[0]) + 1) +end + +local function printf(fmt, ...) + print(string.format(fmt, ...)) +end + +local receive, transmit = link.receive, link.transmit + +B4Gen = { config = { sizes = {required=true}, - dst_mac = {required=true}, - src_mac = {required=true}, rate = {required=true}, - vlan = {}, - b4_ipv6 = {}, - b4_ipv4 = {}, - public_ipv4 = {}, - aftr_ipv6 = {}, - ipv6_only = {}, - ipv4_only = {}, - b4_port = {}, - protocol = {}, - count = {}, - single_pass = {} + count = {default=1}, + single_pass = {default=false}, + b4_ipv6 = {required=true}, + aftr_ipv6 = {required=true}, + b4_ipv4 = {required=true}, + b4_port = {required=true}, + public_ipv4 = {required=true}, + frame_overhead = {default=0} } } -local receive, transmit = link.receive, link.transmit - -function Lwaftrgen:new(conf) - local dst_mac = ethernet:pton(conf.dst_mac) - local src_mac = ethernet:pton(conf.src_mac) - local vlan = conf.vlan - local b4_ipv6 = conf.b4_ipv6 and ipv6:pton(conf.b4_ipv6) - local b4_ipv4 = conf.b4_ipv4 and ipv4:pton(conf.b4_ipv4) - local public_ipv4 = conf.public_ipv4 and ipv4:pton(conf.public_ipv4) - local aftr_ipv6 = conf.aftr_ipv6 and ipv6:pton(conf.aftr_ipv6) - - local ipv4_pkt = packet.allocate() - ffi.fill(ipv4_pkt.data, packet.max_payload) - local eth_hdr = cast(ether_header_ptr_type, ipv4_pkt.data) - eth_hdr.ether_dhost, eth_hdr.ether_shost = dst_mac, src_mac - - local ipv4_hdr, udp_offset - if vlan then - udp_offset = 38 - eth_hdr.ether_type = PROTO_VLAN - local vlan_hdr = cast(ether_vlan_header_ptr_type, ipv4_pkt.data + ethernet_header_size) - vlan_hdr.ether_type = PROTO_IPV4 - vlan_hdr.tag = C.htons(vlan) - ipv4_hdr = cast(ipv4_header_ptr_type, ipv4_pkt.data + ethernet_header_size + ether_vlan_header_size) - else - udp_offset = 34 - eth_hdr.ether_type = PROTO_IPV4 - ipv4_hdr = cast(ipv4_header_ptr_type, ipv4_pkt.data + ethernet_header_size) +function B4Gen:new(conf) + local b4_ipv6 = ipv6:pton(conf.b4_ipv6) + local b4_ipv4 = ipv4:pton(conf.b4_ipv4) + local public_ipv4 = ipv4:pton(conf.public_ipv4) + local aftr_ipv6 = ipv6:pton(conf.aftr_ipv6) + + -- Template IPv4 in IPv6 packet + local pkt = packet.allocate() + ffi.fill(pkt.data, packet.max_payload) + local function h(ptr_type, offset, size) + return cast(ptr_type, pkt.data + offset), offset + size end + local eth_hdr, ipv6_offset = h(ether_header_ptr_type, 0, ether_header_size) + local ipv6_hdr, ipv4_offset = h(ipv6_header_ptr_type, ipv6_offset, ipv6_header_size) + local ipv4_hdr, udp_offset = h(ipv4_header_ptr_type, ipv4_offset, ipv4_header_size) + local udp_hdr, payload_offset = h(udp_header_ptr_type, udp_offset, udp_header_size) + local payload, min_length = h(payload_ptr_type, payload_offset, payload_size) - ipv4_hdr.src_ip = public_ipv4 - ipv4_hdr.dst_ip = b4_ipv4 - ipv4_hdr.ttl = 15 - ipv4_hdr.ihl_v_tos = C.htons(0x4500) -- v4 - ipv4_hdr.id = 0 - ipv4_hdr.frag_off = 0 + -- The offset in returned packets where we expect to find the payload. + local rx_payload_offset = payload_offset - ipv6_header_size - local ipv4_udp_hdr, ipv4_payload - - ipv4_hdr.protocol = 17 -- UDP(17) - ipv4_udp_hdr = cast(udp_header_ptr_type, ipv4_pkt.data + udp_offset) - ipv4_udp_hdr.src_port = C.htons(12345) - ipv4_udp_hdr.checksum = 0 - ipv4_payload = cast(payload_ptr_type, ipv4_pkt.data + udp_offset + udp_header_size) - ipv4_payload.magic = MAGIC - ipv4_payload.number = 0 - - -- IPv4 in IPv6 packet - copy(n_cache_src_ipv6, b4_ipv6, 16) - local ipv6_pkt = packet.allocate() - ffi.fill(ipv6_pkt.data, packet.max_payload) - local eth_hdr = cast(ether_header_ptr_type, ipv6_pkt.data) - eth_hdr.ether_dhost, eth_hdr.ether_shost = dst_mac, src_mac - - - local ipv6_hdr, ipv6_ipv4_hdr - if vlan then - eth_hdr.ether_type = PROTO_VLAN - local vlan_hdr = cast(ether_vlan_header_ptr_type, ipv6_pkt.data + ethernet_header_size) - vlan_hdr.ether_type = PROTO_IPV6 - vlan_hdr.tag = C.htons(vlan) - ipv6_hdr = cast(ipv6_header_ptr_type, ipv6_pkt.data + ethernet_header_size + ether_vlan_header_size) - ipv6_ipv4_hdr = cast(ipv4_header_ptr_type, ipv6_pkt.data + ethernet_header_size + ether_vlan_header_size + ipv6_header_size) - else - eth_hdr.ether_type = PROTO_IPV6 - ipv6_hdr = cast(ipv6_header_ptr_type, ipv6_pkt.data + ethernet_header_size) - ipv6_ipv4_hdr = cast(ipv4_header_ptr_type, ipv6_pkt.data + ethernet_header_size + ipv6_header_size) - end + eth_hdr.ether_type = PROTO_IPV6 lib.bitfield(32, ipv6_hdr, 'v_tc_fl', 0, 4, 6) -- IPv6 Version lib.bitfield(32, ipv6_hdr, 'v_tc_fl', 4, 8, 1) -- Traffic class ipv6_hdr.next_header = PROTO_IPV4_ENCAPSULATION ipv6_hdr.hop_limit = DEFAULT_TTL + ipv6_hdr.src_ip = b4_ipv6 ipv6_hdr.dst_ip = aftr_ipv6 - ipv6_ipv4_hdr.dst_ip = public_ipv4 - ipv6_ipv4_hdr.ttl = 15 - ipv6_ipv4_hdr.ihl_v_tos = C.htons(0x4500) -- v4 - ipv6_ipv4_hdr.id = 0 - ipv6_ipv4_hdr.frag_off = 0 + ipv4_hdr.src_ip = b4_ipv4 + ipv4_hdr.dst_ip = public_ipv4 + ipv4_hdr.ttl = 15 + ipv4_hdr.ihl_v_tos = htons(0x4500) -- v4 + ipv4_hdr.id = 0 + ipv4_hdr.frag_off = 0 + ipv4_hdr.protocol = 17 -- UDP + + udp_hdr.src_port = htons(conf.b4_port) + udp_hdr.dst_port = htons(12345) + udp_hdr.checksum = 0 - local ipv6_ipv4_udp_hdr, ipv6_payload + payload.magic = MAGIC + payload.number = 0 - local total_packet_count = 0 + -- The sizes are frame sizes, including the 4-byte ethernet CRC + -- that we don't see in Snabb. + local sizes = {} for _,size in ipairs(conf.sizes) do - -- count for IPv4 and IPv6 packets (40 bytes IPv6 encap header) - if conf.ipv4_only or conf.ipv6_only then - total_packet_count = total_packet_count + 1 - else - total_packet_count = total_packet_count + 2 - end + assert(size >= ether_min_frame_size) + table.insert(sizes, size - ether_crc_size - conf.frame_overhead) end - ipv6_ipv4_hdr.protocol = 17 -- UDP(17) - ipv6_ipv4_udp_hdr = cast(udp_header_ptr_type, ipv6_pkt.data + udp_offset + ipv6_header_size) - ipv6_ipv4_udp_hdr.dst_port = C.htons(12345) - ipv6_ipv4_udp_hdr.checksum = 0 - ipv6_payload = cast(payload_ptr_type, ipv6_pkt.data + udp_offset + ipv6_header_size + udp_header_size) - ipv6_payload.magic = MAGIC - ipv6_payload.number = 0 - local o = { b4_ipv6 = b4_ipv6, b4_ipv4 = b4_ipv4, b4_port = conf.b4_port, - current_port = conf.b4_port, - b4_ipv4_offset = 0, - ipv6_address = n_cache_src_ipv6, - count = conf.count, + softwire_idx = 0, + softwire_count = conf.count, single_pass = conf.single_pass, - current_count = 0, - ipv4_pkt = ipv4_pkt, - ipv4_hdr = ipv4_hdr, - ipv4_payload = ipv4_payload, + template_pkt = pkt, ipv6_hdr = ipv6_hdr, - ipv6_pkt = ipv6_pkt, - ipv6_payload = ipv6_payload, - ipv6_ipv4_hdr = ipv6_ipv4_hdr, - ipv4_udp_hdr = ipv4_udp_hdr, - ipv6_ipv4_udp_hdr = ipv6_ipv4_udp_hdr, - ipv4_only = conf.ipv4_only, - ipv6_only = conf.ipv6_only, - vlan = vlan, - udp_offset = udp_offset, - protocol = conf.protocol, + ipv4_hdr = ipv4_hdr, + udp_hdr = udp_hdr, + payload = payload, + rx_payload_offset = rx_payload_offset, rate = conf.rate, - sizes = conf.sizes, - total_packet_count = total_packet_count, + sizes = sizes, bucket_content = conf.rate * 1e6, - ipv4_packets = 0, ipv4_bytes = 0, - ipv6_packets = 0, ipv6_bytes = 0, - ipv4_packet_number = 0, ipv6_packet_number = 0, - last_rx_ipv4_packet_number = 0, last_rx_ipv6_packet_number = 0, + rx_packets = 0, rx_bytes = 0, + tx_packet_number = 0, rx_packet_number = 0, lost_packets = 0 } - return setmetatable(o, {__index=Lwaftrgen}) + return setmetatable(o, {__index=B4Gen}) end -function Lwaftrgen:pull () +function B4Gen:done() return self.stopping end + +function B4Gen:pull () + + if self.stopping then return end local output = self.output.output local input = self.input.input - local ipv6_packets = self.ipv6_packets - local ipv6_bytes = self.ipv6_bytes - local ipv4_packets = self.ipv4_packets - local ipv4_bytes = self.ipv4_bytes + local rx_packets = self.rx_packets + local rx_bytes = self.rx_bytes local lost_packets = self.lost_packets - local udp_offset = self.udp_offset - local o_ethertype = self.vlan and OFFSET_ETHERTYPE_VLAN or OFFSET_ETHERTYPE - - if self.current == 0 then - main.exit(0) - end + local rx_payload_offset = self.rx_payload_offset - -- count and trash incoming packets + -- Count and trash incoming packets. for _=1,link.nreadable(input) do local pkt = receive(input) - if cast(uint16_ptr_t, pkt.data + o_ethertype)[0] == PROTO_IPV6 then - ipv6_bytes = ipv6_bytes + pkt.length - ipv6_packets = ipv6_packets + 1 - local payload = cast(payload_ptr_type, pkt.data + udp_offset + ipv6_header_size + udp_header_size) - if payload.magic == MAGIC then - if self.last_rx_ipv6_packet_number > 0 then - lost_packets = lost_packets + payload.number - self.last_rx_ipv6_packet_number - 1 - end - self.last_rx_ipv6_packet_number = payload.number - end - else - ipv4_bytes = ipv4_bytes + pkt.length - ipv4_packets = ipv4_packets + 1 - local payload = cast(payload_ptr_type, pkt.data + udp_offset + udp_header_size) + if cast(ether_header_ptr_type, pkt.data).ether_type == PROTO_IPV4 then + rx_bytes = rx_bytes + pkt.length + rx_packets = rx_packets + 1 + local payload = cast(payload_ptr_type, pkt.data + rx_payload_offset) if payload.magic == MAGIC then - if self.last_rx_ipv4_packet_number > 0 then - lost_packets = lost_packets + payload.number - self.last_rx_ipv4_packet_number - 1 + if self.last_rx_packet_number and self.last_rx_packet_number > 0 then + lost_packets = lost_packets + payload.number - self.last_rx_packet_number - 1 end - self.last_rx_ipv4_packet_number = payload.number + self.last_rx_packet_number = payload.number end end packet.free(pkt) @@ -327,116 +234,262 @@ function Lwaftrgen:pull () self.period_start = self.period_start or cur_now local elapsed = cur_now - self.period_start if elapsed > 1 then - local ipv6_packet_rate = ipv6_packets / elapsed / 1e6 - local ipv4_packet_rate = ipv4_packets / elapsed / 1e6 - local ipv6_octet_rate = ipv6_bytes * 8 / 1e9 / elapsed - local ipv4_octet_rate = ipv4_bytes * 8 / 1e9 / elapsed - local lost_rate = math.abs(lost_packets / (ipv6_octet_rate + ipv4_octet_rate) / 10000) - print(string.format('v6+v4: %.3f+%.3f = %.6f MPPS, %.3f+%.3f = %.6f Gbps, lost %.3f%%', - ipv6_packet_rate, ipv4_packet_rate, ipv6_packet_rate + ipv4_packet_rate, - ipv6_octet_rate, ipv4_octet_rate, ipv6_octet_rate + ipv4_octet_rate, lost_rate)) + printf('v4 rx: %.6f MPPS, %.6f Gbps, lost %.3f%%', + rx_packets / elapsed / 1e6, + rx_bytes * 8 / 1e9 / elapsed, + lost_packets / (rx_packets + lost_packets) * 100) self.period_start = cur_now - self.ipv6_bytes, self.ipv6_packets = 0, 0 - self.ipv4_bytes, self.ipv4_packets = 0, 0 - self.lost_packets = 0 - else - self.ipv4_bytes, self.ipv4_packets = ipv4_bytes, ipv4_packets - self.ipv6_bytes, self.ipv6_packets = ipv6_bytes, ipv6_packets - self.lost_packets = lost_packets + rx_packets, rx_bytes, lost_packets = 0, 0, 0 end + self.rx_packets = rx_packets + self.rx_bytes = rx_bytes + self.lost_packets = lost_packets - local ipv4_hdr = self.ipv4_hdr local ipv6_hdr = self.ipv6_hdr - local ipv6_ipv4_hdr = self.ipv6_ipv4_hdr - local ipv4_udp_hdr = self.ipv4_udp_hdr - local ipv6_ipv4_udp_hdr = self.ipv6_ipv4_udp_hdr + local ipv4_hdr = self.ipv4_hdr + local udp_hdr = self.udp_hdr + local payload = self.payload local cur_now = tonumber(app.now()) local last_time = self.last_time or cur_now self.bucket_content = self.bucket_content + self.rate * 1e6 * (cur_now - last_time) self.last_time = cur_now - local limit = engine.pull_npackets - while limit > self.total_packet_count and - self.total_packet_count <= self.bucket_content do - limit = limit - 1 - self.bucket_content = self.bucket_content - self.total_packet_count + for _=1, math.min(engine.pull_npackets, self.bucket_content) do + if #self.sizes > self.bucket_content then break end + self.bucket_content = self.bucket_content - #self.sizes - ipv4_hdr.dst_ip = self.b4_ipv4 - ipv6_ipv4_hdr.src_ip = self.b4_ipv4 - ipv6_hdr.src_ip = self.b4_ipv6 - local ipdst = C.ntohl(rd32(ipv4_hdr.dst_ip)) - ipdst = C.htonl(ipdst + self.b4_ipv4_offset) - wr32(ipv4_hdr.dst_ip, ipdst) - wr32(ipv6_ipv4_hdr.src_ip, ipdst) + for _,size in ipairs(self.sizes) do + local ipv4_len = size - ether_header_size + local udp_len = ipv4_len - ipv4_header_size + -- Expectation from callers is to make packets that are SIZE + -- bytes big, *plus* the IPv6 header. + ipv6_hdr.payload_length = htons(ipv4_len) + ipv4_hdr.total_length = htons(ipv4_len) + ipv4_hdr.checksum = 0 + ipv4_hdr.checksum = htons(ipsum(cast("char*", ipv4_hdr), ipv4_header_size, 0)) + udp_hdr.len = htons(udp_len) + self.template_pkt.length = size + ipv6_header_size + payload.number = self.tx_packet_number; + self.tx_packet_number = self.tx_packet_number + 1 + transmit(output, packet.clone(self.template_pkt)) + end - ipv4_udp_hdr.dst_port = C.htons(self.current_port) - ipv6_ipv4_udp_hdr.src_port = C.htons(self.current_port) + -- Next softwire. + inc_ipv6(ipv6_hdr.src_ip) + local next_port = ntohs(udp_hdr.src_port) + self.b4_port + if next_port >= 2^16 then + inc_ipv4(ipv4_hdr.src_ip) + next_port = self.b4_port + end + udp_hdr.src_port = htons(next_port) - -- The sizes are frame sizes, including the 4-byte ethernet CRC - -- that we don't see in Snabb. + self.softwire_idx = self.softwire_idx + 1 + if self.softwire_idx >= self.softwire_count then + if self.single_pass then + printf("generated %d packets for each of %d softwires", + #self.sizes, self.softwire_count) + self.stopping = true + break + end - local vlan_size = self.vlan and ether_vlan_header_size or 0 - local ethernet_total_size = ethernet_header_size + vlan_size - local minimum_size = ethernet_total_size + ipv4_header_size + - udp_header_size + ethernet_crc_size + -- Reset to initial softwire. + self.softwire_idx = 0 + ipv6_hdr.src_ip = self.b4_ipv6 + ipv4_hdr.src_ip = self.b4_ipv4 + udp_hdr.src_port = htons(self.b4_port) + end + end +end - for _,size in ipairs(self.sizes) do - assert(size >= minimum_size) - local packet_len = size - ethernet_crc_size - local ipv4_len = packet_len - ethernet_total_size - local udp_len = ipv4_len - ipv4_header_size - if not self.ipv6_only then - ipv4_hdr.total_length = C.htons(ipv4_len) - ipv4_udp_hdr.len = C.htons(udp_len) - self.ipv4_pkt.length = packet_len - ipv4_hdr.checksum = 0 - ipv4_hdr.checksum = C.htons(ipsum(self.ipv4_pkt.data + ethernet_total_size, 20, 0)) - if size >= minimum_size + payload_size then - self.ipv4_payload.number = self.ipv4_packet_number; - self.ipv4_packet_number = self.ipv4_packet_number + 1 - end - local ipv4_pkt = packet.clone(self.ipv4_pkt) - transmit(output, ipv4_pkt) - end +InetGen = { + config = { + sizes = {required=true}, + rate = {required=true}, + b4_ipv4 = {required=true}, + public_ipv4 = {required=true}, + b4_port = {required=true}, + count = {}, + single_pass = {}, + frame_overhead = {default=0} + } +} + +function InetGen:new(conf) + local b4_ipv4 = ipv4:pton(conf.b4_ipv4) + local public_ipv4 = ipv4:pton(conf.public_ipv4) + + -- Template IPv4 packet + local pkt = packet.allocate() + ffi.fill(pkt.data, packet.max_payload) + local function h(ptr_type, offset, size) + return cast(ptr_type, pkt.data + offset), offset + size + end + local eth_hdr, ipv4_offset = h(ether_header_ptr_type, 0, ether_header_size) + local ipv4_hdr, udp_offset = h(ipv4_header_ptr_type, ipv4_offset, ipv4_header_size) + local udp_hdr, payload_offset = h(udp_header_ptr_type, udp_offset, udp_header_size) + local payload, min_length = h(payload_ptr_type, payload_offset, payload_size) - if not self.ipv4_only then - -- Expectation from callers is to make packets that are SIZE - -- bytes big, *plus* the IPv6 header. - ipv6_hdr.payload_length = C.htons(ipv4_len) - ipv6_ipv4_hdr.total_length = C.htons(ipv4_len) - ipv6_ipv4_udp_hdr.len = C.htons(udp_len) - self.ipv6_pkt.length = packet_len + ipv6_header_size - if size >= minimum_size + payload_size then - self.ipv6_payload.number = self.ipv6_packet_number; - self.ipv6_packet_number = self.ipv6_packet_number + 1 + -- The offset in returned packets where we expect to find the payload. + local rx_payload_offset = payload_offset + ipv6_header_size + + eth_hdr.ether_type = PROTO_IPV4 + + ipv4_hdr.src_ip = public_ipv4 + ipv4_hdr.dst_ip = b4_ipv4 + ipv4_hdr.ttl = 15 + ipv4_hdr.ihl_v_tos = htons(0x4500) -- v4 + ipv4_hdr.id = 0 + ipv4_hdr.frag_off = 0 + ipv4_hdr.protocol = 17 -- UDP + + udp_hdr.src_port = htons(12345) + udp_hdr.dst_port = htons(conf.b4_port) + udp_hdr.checksum = 0 + + payload.magic = MAGIC + payload.number = 0 + + -- The sizes are frame sizes, including the 4-byte ethernet CRC + -- that we don't see in Snabb. + local sizes = {} + for _,size in ipairs(conf.sizes) do + assert(size >= ether_min_frame_size) + table.insert(sizes, size - ether_crc_size - conf.frame_overhead) + end + + local o = { + b4_ipv4 = b4_ipv4, + b4_port = conf.b4_port, + softwire_idx = 0, + softwire_count = conf.count, + single_pass = conf.single_pass, + template_pkt = pkt, + ipv4_hdr = ipv4_hdr, + udp_hdr = udp_hdr, + payload = payload, + rx_payload_offset = rx_payload_offset, + rate = conf.rate, + sizes = sizes, + bucket_content = conf.rate * 1e6, + rx_packets = 0, rx_bytes = 0, + tx_packet_number = 0, rx_packet_number = 0, + lost_packets = 0 + } + return setmetatable(o, {__index=InetGen}) +end + +function InetGen:done() return self.stopping end + +function InetGen:pull () + + if self.stopping then return end + + local output = self.output.output + local input = self.input.input + local rx_packets = self.rx_packets + local rx_bytes = self.rx_bytes + local lost_packets = self.lost_packets + local rx_payload_offset = self.rx_payload_offset + + -- Count and trash incoming packets. + for _=1,link.nreadable(input) do + local pkt = receive(input) + if cast(ether_header_ptr_type, pkt.data).ether_type == PROTO_IPV6 then + rx_bytes = rx_bytes + pkt.length + rx_packets = rx_packets + 1 + local payload = cast(payload_ptr_type, pkt.data + rx_payload_offset) + if payload.magic == MAGIC then + if self.last_rx_packet_number and self.last_rx_packet_number > 0 then + lost_packets = lost_packets + payload.number - self.last_rx_packet_number - 1 end - local ipv6_pkt = packet.clone(self.ipv6_pkt) - transmit(output, ipv6_pkt) + self.last_rx_packet_number = payload.number end + end + packet.free(pkt) + end - end + local cur_now = tonumber(app.now()) + self.period_start = self.period_start or cur_now + local elapsed = cur_now - self.period_start + if elapsed > 1 then + printf('v6 rx: %.6f MPPS, %.6f Gbps, lost %.3f%%', + rx_packets / elapsed / 1e6, + rx_bytes * 8 / 1e9 / elapsed, + lost_packets / (rx_packets + lost_packets) * 100) + self.period_start = cur_now + rx_packets, rx_bytes, lost_packets = 0, 0, 0 + end + self.rx_packets = rx_packets + self.rx_bytes = rx_bytes + self.lost_packets = lost_packets + + local ipv4_hdr = self.ipv4_hdr + local udp_hdr = self.udp_hdr + local payload = self.payload + + local cur_now = tonumber(app.now()) + local last_time = self.last_time or cur_now + self.bucket_content = self.bucket_content + self.rate * 1e6 * (cur_now - last_time) + self.last_time = cur_now - self.b4_ipv6 = inc_ipv6(self.b4_ipv6) - self.current_port = self.current_port + self.b4_port - if self.current_port > 65535 then - self.current_port = self.b4_port - self.b4_ipv4_offset = self.b4_ipv4_offset + 1 - end + for _=1, math.min(engine.pull_npackets, self.bucket_content) do + if #self.sizes > self.bucket_content then break end + self.bucket_content = self.bucket_content - #self.sizes - self.current_count = self.current_count + 1 - if self.current_count >= self.count then + for _,size in ipairs(self.sizes) do + local ipv4_len = size - ether_header_size + local udp_len = ipv4_len - ipv4_header_size + ipv4_hdr.total_length = htons(ipv4_len) + ipv4_hdr.checksum = 0 + ipv4_hdr.checksum = htons(ipsum(cast("char*", ipv4_hdr), ipv4_header_size, 0)) + udp_hdr.len = htons(udp_len) + self.template_pkt.length = size + payload.number = self.tx_packet_number; + self.tx_packet_number = self.tx_packet_number + 1 + transmit(output, packet.clone(self.template_pkt)) + end + + -- Next softwire. + local next_port = ntohs(udp_hdr.dst_port) + self.b4_port + if next_port >= 2^16 then + inc_ipv4(ipv4_hdr.dst_ip) + next_port = self.b4_port + end + udp_hdr.dst_port = htons(next_port) + + self.softwire_idx = self.softwire_idx + 1 + if self.softwire_idx >= self.softwire_count then if self.single_pass then - print(string.format("generated %d packets", self.current_count)) - -- make sure we won't generate more packets in the same breath, then exit - self.current = 0 - self.bucket_content = 0 + printf("generated %d packets for each of %d softwires", + #self.sizes, self.softwire_count) + self.stopping = true + break end - self.current_count = 0 - self.current_port = self.b4_port - self.b4_ipv4_offset = 0 - copy(self.b4_ipv6, self.ipv6_address, 16) - end - end + + -- Reset to initial softwire. + self.softwire_idx = 0 + ipv4_hdr.dst_ip = self.b4_ipv4 + udp_hdr.dst_port = htons(self.b4_port) + end + end +end + +Interleave = {} + +function Interleave:new() + return setmetatable({}, {__index=Interleave}) end +function Interleave:push () + local continue = true + while continue do + continue = false + for _, inport in ipairs(self.input) do + if not link.empty(inport) then + transmit(self.output.output, receive(inport)) + continue = true + end + end + end +end diff --git a/src/program/packetblaster/lwaftr/lwaftr.lua b/src/program/packetblaster/lwaftr/lwaftr.lua index 4fa73b0268..ed4fabc7b4 100644 --- a/src/program/packetblaster/lwaftr/lwaftr.lua +++ b/src/program/packetblaster/lwaftr/lwaftr.lua @@ -6,10 +6,19 @@ local engine = require("core.app") local config = require("core.config") local timer = require("core.timer") local pci = require("lib.hardware.pci") +local ethernet = require("lib.protocol.ethernet") +local ipv4 = require("lib.protocol.ipv4") +local ipv6 = require("lib.protocol.ipv6") local main = require("core.main") local S = require("syscall") -local Lwaftrgen = require("program.packetblaster.lwaftr.lib").Lwaftrgen +local B4Gen = require("program.packetblaster.lwaftr.lib").B4Gen +local InetGen = require("program.packetblaster.lwaftr.lib").InetGen +local Interleave = require("program.packetblaster.lwaftr.lib").Interleave local Tap = require("apps.tap.tap").Tap +local vlan = require("apps.vlan.vlan") +local arp = require("apps.ipv4.arp") +local ndp = require("apps.lwaftr.ndp") +local V4V6 = require("apps.lwaftr.V4V6") local raw = require("apps.socket.raw") local pcap = require("apps.pcap.pcap") local VhostUser = require("apps.vhost.vhost_user").VhostUser @@ -28,7 +37,13 @@ local long_opts = { size = "S", -- frame size list (defaults to IMIX) src_mac = "s", -- source ethernet address dst_mac = "d", -- destination ethernet address + src_mac4 = 1, -- source ethernet address for IPv4 traffic + dst_mac4 = 1, -- destination ethernet address for IPv4 traffic + src_mac6 = 1, -- source ethernet address for IPv6 traffic + dst_mac6 = 1, -- destination ethernet address for IPv6 traffic vlan = "v", -- VLAN id + vlan4 = 1, -- VLAN id for IPv4 traffic + vlan6 = 1, -- VLAN id for IPv6 traffic b4 = "b", -- B4 start IPv6_address,IPv4_address,port aftr = "a", -- fix AFTR public IPv6_address ipv4 = "I", -- fix public IPv4 address @@ -71,11 +86,17 @@ function run (args) end end - local src_mac = "00:00:00:00:00:00" - function opt.s (arg) src_mac = arg end + local v4_src_mac = "00:00:00:00:00:00" + function opt.src_mac4 (arg) v4_src_mac = arg end + local v6_src_mac = "00:00:00:00:00:00" + function opt.src_mac6 (arg) v6_src_mac = arg end + function opt.s (arg) opt.src_mac4(arg); opt.src_mac6(arg) end - local dst_mac = "00:00:00:00:00:00" - function opt.d (arg) dst_mac = arg end + local v4_dst_mac = "00:00:00:00:00:00" + function opt.dst_mac4 (arg) v4_dst_mac = arg end + local v6_dst_mac = "00:00:00:00:00:00" + function opt.dst_mac6 (arg) v6_dst_mac = arg end + function opt.d (arg) opt.dst_mac4(arg); opt.dst_mac6(arg) end local b4_ipv6, b4_ipv4, b4_port = "2001:db8::", "10.0.0.0", 1024 function opt.b (arg) @@ -131,30 +152,36 @@ function run (args) target = sock_interface end - local ipv4_only = false - function opt.v4 () ipv4_only = true end + local v4, v6 = true, true + + function opt.v4 () v6 = false end opt["4"] = opt.v4 - local ipv6_only = false - function opt.v6 () ipv6_only = true end + function opt.v6 () v4 = false end opt["6"] = opt.v6 - local vlan = nil - function opt.v (arg) - vlan = assert(tonumber(arg), "duration is not a number!") + local v4_vlan + function opt.vlan4 (arg) + v4_vlan = assert(tonumber(arg), "vlan is not a number!") + end + local v6_vlan + function opt.vlan6 (arg) + v6_vlan = assert(tonumber(arg), "vlan is not a number!") end + function opt.v (arg) opt.vlan4(arg); opt.vlan6(arg) end - local pcap_file, single_pass + local pcap_file, single_pass = nil, false function opt.o (arg) pcap_file = arg target = pcap_file single_pass = true + rate = 1/0 end args = lib.dogetopt(args, opt, "VD:hS:s:a:d:b:iI:c:r:46p:v:o:t:i:k:", long_opts) for _,s in ipairs(sizes) do - if s < 18 + (vlan and 4 or 0) + 20 + 8 then + if s < 18 + (v4_vlan and v6_vlan and 4 or 0) + 20 + 8 then error("Minimum frame size is 46 bytes (18 ethernet+CRC, 20 IPv4, and 8 UDP)") end end @@ -167,68 +194,184 @@ function run (args) print(string.format("packetblaster lwaftr: Sending %d clients at %.3f MPPS to %s", count, rate, target)) print() - if not ipv4_only then - print(string.format("IPv6: %s > %s: %s:%d > %s:12345", b4_ipv6, aftr_ipv6, b4_ipv4, b4_port, public_ipv4)) - print(" source IPv6 and source IPv4/Port adjusted per client") - local sizes_ipv6 = {} - for i,size in ipairs(sizes) do sizes_ipv6[i] = size + 40 end - print("IPv6 frame sizes: " .. table.concat(sizes_ipv6,",")) + if not (v4 or v6) then + -- Assume that -4 -6 means both instead of neither. + v4, v6 = true, true end - if not ipv6_only then - print() - print(string.format("IPv4: %s:12345 > %s:%d", public_ipv4, b4_ipv4, b4_port)) - print(" destination IPv4 and Port adjusted per client") - print("IPv4 frame sizes: " .. table.concat(sizes,",")) + local v4_input, v4_output, v6_input, v6_output + + local function finish_vlan(input, output, tag) + if not tag then return input, output end + + -- Add and remove the common vlan tag. + config.app(c, "untag", vlan.Untagger, {tag=tag}) + config.app(c, "tag", vlan.Tagger, {tag=tag}) + config.link(c, "tag.output -> " .. input) + config.link(c, input .. " -> untag.input") + return 'tag.input', 'untag.output' end - if ipv4_only and ipv6_only then - print("Remove options v4only and v6only to generate both") - main.exit(1) + local function finish_v4(input, output) + assert(v4) + -- Stamp output with the MAC and make an ARP responder. + local tester_ip = ipv4:pton('1.2.3.4') + local next_ip = nil -- Assume we have a static dst mac. + config.app(c, "arp", arp.ARP, + { self_ip = tester_ip, + self_mac = ethernet:pton(v4_src_mac), + next_mac = ethernet:pton(v4_dst_mac), + next_ip = next_ip }) + config.link(c, output .. ' -> arp.south') + config.link(c, 'arp.south -> ' .. input) + return 'arp.north', 'arp.north' + end + + local function finish_v6(input, output) + assert(v6) + -- Stamp output with the MAC and make an NDP responder. + local tester_ip = ipv6:pton('2001:DB8::1') + local next_ip = nil -- Assume we have a static dst mac. + config.app(c, "ndp", ndp.NDP, + { self_ip = tester_ip, + self_mac = ethernet:pton(v6_src_mac), + next_mac = ethernet:pton(v6_dst_mac), + next_ip = next_ip }) + config.link(c, output .. ' -> ndp.south') + config.link(c, 'ndp.south -> ' .. input) + return 'ndp.north', 'ndp.north' end - config.app(c, "generator", Lwaftrgen, { - sizes = sizes, count = count, aftr_ipv6 = aftr_ipv6, rate = rate, - src_mac = src_mac, dst_mac = dst_mac, vlan = vlan, - b4_ipv6 = b4_ipv6, b4_ipv4 = b4_ipv4, b4_port = b4_port, - public_ipv4 = public_ipv4, single_pass = single_pass, - ipv4_only = ipv4_only, ipv6_only = ipv6_only }) + local function split(input, output) + assert(v4 and v6) + if v4_vlan ~= v6_vlan then + -- Split based on vlan. + config.app(c, "vmux", vlan.VlanMux, {}) + config.link(c, output .. ' -> vmux.trunk') + config.link(c, 'vmux.trunk -> ' .. input) + local v4_link = v4_vlan and 'vmux.vlan'..v4_vlan or 'vmux.native' + v4_input, v4_output = finish_v4(v4_link, v4_link) + local v6_link = v6_vlan and 'vmux.vlan'..v6_vlan or 'vmux.native' + v6_input, v6_output = finish_v6(v6_link, v6_link) + else + input, output = finish_vlan(input, output, v4_vlan) + + -- Split based on ethertype. + config.app(c, "mux", V4V6.V4V6, {}) + config.app(c, "join", Interleave, {}) + v4_input, v4_output = finish_v4('join.v4', 'mux.v4') + v6_input, v6_output = finish_v6('join.v6', 'mux.v6') + config.link(c, output .. " -> mux.input") + config.link(c, "join.output -> " .. input) + end + end - local input, output + local function maybe_split(input, output) + if v4 and v6 then + split(input, output) + elseif v4 then + input, output = finish_vlan(input, output, v4_vlan) + v4_input, v4_output = finish_v4(input, output) + else + input, output = finish_vlan(input, output, v6_vlan) + v6_input, v6_output = finish_v6(input, output) + end + end if tap_interface then if dir_exists(("/sys/devices/virtual/net/%s"):format(tap_interface)) then config.app(c, "tap", Tap, tap_interface) - input, output = "tap.input", "tap.output" else print(string.format("tap interface %s doesn't exist", tap_interface)) main.exit(1) end + maybe_split("tap.input", "tap.output") elseif pciaddr then local device_info = pci.device_info(pciaddr) - if vlan then - print(string.format("vlan set to %d", vlan)) + if v4_vlan then + print(string.format("IPv4 vlan set to %d", v4_vlan)) + end + if v6_vlan then + print(string.format("IPv6 vlan set to %d", v6_vlan)) end - if device_info then + if not device_info then + fatal(("Couldn't find device info for PCI or tap device %s"):format(pciaddr)) + end + if v4 and v6 then + if v4_vlan == v6_vlan and v4_src_mac == v6_src_mac then + config.app(c, "nic", require(device_info.driver).driver, + {pciaddr = pciaddr, vmdq = true, macaddr = v4_src_mac, + mtu = 9500, vlan = v4_vlan}) + maybe_split("nic."..device_info.rx, "nic."..device_info.tx) + else + config.app(c, "v4nic", require(device_info.driver).driver, + {pciaddr = pciaddr, vmdq = true, macaddr = v4_src_mac, + mtu = 9500, vlan = v4_vlan}) + v4_input, v4_output = finish_v4("v4nic."..device_info.rx, + "v4nic."..device_info.tx) + config.app(c, "v6nic", require(device_info.driver).driver, + {pciaddr = pciaddr, vmdq = true, macaddr = v6_src_mac, + mtu = 9500, vlan = v6_vlan}) + v6_input, v6_output = finish_v6("v6nic."..device_info.rx, + "v6nic."..device_info.tx) + end + elseif v4 then config.app(c, "nic", require(device_info.driver).driver, - {pciaddr = pciaddr, vmdq = true, macaddr = src_mac, mtu = 9500}) - input, output = "nic."..device_info.rx, "nic."..device_info.tx + {pciaddr = pciaddr, vmdq = true, macaddr = v4_src_mac, + mtu = 9500, vlan = v4_vlan}) + v4_input, v4_output = finish_v4("nic."..device_info.rx, + "nic."..device_info.tx) else - fatal(("Couldn't find device info for PCI or tap device %s"):format(pciaddr)) + config.app(c, "nic", require(device_info.driver).driver, + {pciaddr = pciaddr, vmdq = true, macaddr = v6_src_mac, + mtu = 9500, vlan = v6_vlan}) + v6_input, v6_output = finish_v6("nic."..device_info.rx, + "nic."..device_info.tx) end elseif int_interface then config.app(c, "int", raw.RawSocket, int_interface) - input, output = "int.rx", "int.tx" + maybe_split("int.rx", "int.tx") elseif sock_interface then config.app(c, "virtio", VhostUser, { socket_path=sock_interface } ) - input, output = "virtio.rx", "virtio.tx" + maybe_split("virtio.rx", "virtio.tx") else config.app(c, "pcap", pcap.PcapWriter, pcap_file) - input, output = "pcap.input", "pcap.output" + maybe_split("pcap.input", "pcap.output") end - config.link(c, output .. " -> generator.input") - config.link(c, "generator.output -> " .. input) + if v4 then + print() + print(string.format("IPv4: %s:12345 > %s:%d", public_ipv4, b4_ipv4, b4_port)) + print(" destination IPv4 and Port adjusted per client") + print("IPv4 frame sizes: " .. table.concat(sizes,",")) + local rate = v6 and rate/2 or rate + config.app(c, "inetgen", InetGen, { + sizes = sizes, rate = rate, count = count, single_pass = single_pass, + b4_ipv4 = b4_ipv4, b4_port = b4_port, public_ipv4 = public_ipv4, + frame_overhead = v4_vlan and 4 or 0}) + if v6_output then + config.link(c, v6_output .. " -> inetgen.input") + end + config.link(c, "inetgen.output -> " .. v4_input) + end + if v6 then + print() + print(string.format("IPv6: %s > %s: %s:%d > %s:12345", b4_ipv6, aftr_ipv6, b4_ipv4, b4_port, public_ipv4)) + print(" source IPv6 and source IPv4/Port adjusted per client") + local sizes_ipv6 = {} + for i,size in ipairs(sizes) do sizes_ipv6[i] = size + 40 end + print("IPv6 frame sizes: " .. table.concat(sizes_ipv6,",")) + local rate = v4 and rate/2 or rate + config.app(c, "b4gen", B4Gen, { + sizes = sizes, rate = rate, count = count, single_pass = single_pass, + b4_ipv6 = b4_ipv6, aftr_ipv6 = aftr_ipv6, + b4_ipv4 = b4_ipv4, b4_port = b4_port, public_ipv4 = public_ipv4, + frame_overhead = v6_vlan and 4 or 0}) + if v4_output then + config.link(c, v4_output .. " -> b4gen.input") + end + config.link(c, "b4gen.output -> " .. v6_input) + end engine.busywait = true engine.configure(c) @@ -243,6 +386,17 @@ function run (args) timer.activate(t) end - if duration then engine.main({duration=duration}) - else engine.main() end + local done + if duration then + done = lib.timeout(duration) + else + local b4gen = engine.app_table.b4gen + local inetgen = engine.app_table.inetgen + print (b4gen, inetgen) + function done() + return ((not b4gen) or b4gen:done()) and ((not inetgen) or inetgen:done()) + end + end + + engine.main({done=done}) end diff --git a/src/program/packetblaster/lwaftr/test_lwaftr_1.pcap b/src/program/packetblaster/lwaftr/test_lwaftr_1.pcap index a3b001e7fb..e90854031d 100644 Binary files a/src/program/packetblaster/lwaftr/test_lwaftr_1.pcap and b/src/program/packetblaster/lwaftr/test_lwaftr_1.pcap differ diff --git a/src/program/packetblaster/lwaftr/test_lwaftr_2.pcap b/src/program/packetblaster/lwaftr/test_lwaftr_2.pcap index 287dd1b8fb..cafd393cbe 100644 Binary files a/src/program/packetblaster/lwaftr/test_lwaftr_2.pcap and b/src/program/packetblaster/lwaftr/test_lwaftr_2.pcap differ diff --git a/src/program/packetblaster/selftest.sh b/src/program/packetblaster/selftest.sh index aa6a1eaf78..ebd7bbba77 100755 --- a/src/program/packetblaster/selftest.sh +++ b/src/program/packetblaster/selftest.sh @@ -16,17 +16,25 @@ function test_lwaftr_pcap { rm $TEMP_PCAP exit 1 fi + if ! which tcpdump; then + echo "Error: no tcpdump to compare packets" + rm $TEMP_PCAP + exit 43 + fi cmp $TEMP_PCAP $PCAP - status=$? + tcpdump -venr $TEMP_PCAP | sort > $TEMP_PCAP.txt rm $TEMP_PCAP - if [ $status != 0 ]; then - echo "Error: lwaftr generated pcap differs from ${PCAP}" + diffies=$(tcpdump -venr $PCAP | sort | diff -u /dev/stdin $TEMP_PCAP.txt) + rm $TEMP_PCAP.txt + if test -n "$diffies"; then + echo "Error: lwaftr generated pcap differs from ${PCAP}:" + echo "$diffies" exit 1 fi } test_lwaftr_pcap program/packetblaster/lwaftr/test_lwaftr_1.pcap --count 1 -test_lwaftr_pcap program/packetblaster/lwaftr/test_lwaftr_2.pcap --count 2 --vlan 100 --size 50 +test_lwaftr_pcap program/packetblaster/lwaftr/test_lwaftr_2.pcap --count 2 --vlan 100 --size 64 # lwaftr tap test sudo ip netns add snabbtest || exit $TEST_SKIPPED