diff --git a/Knot.files b/Knot.files index 757a4387466f91832898cef363fc97bc2d8bf55c..c779ff5c34c14def231686f5e166dee1deea8156 100644 --- a/Knot.files +++ b/Knot.files @@ -415,6 +415,9 @@ src/libknot/xdp/bpf-user.c src/libknot/xdp/bpf-user.h src/libknot/xdp/eth.c src/libknot/xdp/eth.h +src/libknot/xdp/msg.h +src/libknot/xdp/msg_init.h +src/libknot/xdp/protocols.h src/libknot/xdp/xdp.c src/libknot/xdp/xdp.h src/libknot/yparser/yparser.c diff --git a/src/knot/server/udp-handler.c b/src/knot/server/udp-handler.c index b1cb5a871bd076fef88c158387aa6ac56273c3fb..58b6a97a9a1c886947446bd7f3a9ec5777da654c 100644 --- a/src/knot/server/udp-handler.c +++ b/src/knot/server/udp-handler.c @@ -391,7 +391,7 @@ static int xdp_recvmmsg_recv(int fd, void *d, void *xdp_sock) UNUSED(fd); struct xdp_recvmmsg *rq = d; - int ret = knot_xdp_recv(xdp_sock, rq->msgs_rx, XDP_BATCHLEN, &rq->rcvd); + int ret = knot_xdp_recv(xdp_sock, rq->msgs_rx, XDP_BATCHLEN, &rq->rcvd, NULL); return ret == KNOT_EOK ? rq->rcvd : ret; } @@ -407,8 +407,7 @@ static int xdp_recvmmsg_handle(udp_context_t *ctx, void *d, void *xdp_sock) if (rq->msgs_rx[i].payload.iov_len == 0) { continue; // Skip marked (zero length) messages. } - int ret = knot_xdp_send_alloc(xdp_sock, rq->msgs_rx[i].ip_to.sin6_family == AF_INET6, - &rq->msgs_tx[i], &rq->msgs_rx[i]); + int ret = knot_xdp_reply_alloc(xdp_sock, &rq->msgs_rx[i], &rq->msgs_tx[i]); if (ret != KNOT_EOK) { break; // Still free all RX buffers. } diff --git a/src/libknot/Makefile.inc b/src/libknot/Makefile.inc index 6fe35f495f1c773b872b2a8f19d4b1d25b3dfa19..64b54a748b035df7e7b1910f59b638aad8bf2326 100644 --- a/src/libknot/Makefile.inc +++ b/src/libknot/Makefile.inc @@ -83,6 +83,7 @@ libknot_la_LIBADD += $(libbpf_LIBS) nobase_include_libknot_HEADERS += \ libknot/xdp/bpf-consts.h \ libknot/xdp/eth.h \ + libknot/xdp/msg.h \ libknot/xdp/xdp.h libknot_la_SOURCES += \ @@ -91,6 +92,8 @@ libknot_la_SOURCES += \ libknot/xdp/bpf-user.c \ libknot/xdp/bpf-user.h \ libknot/xdp/eth.c \ + libknot/xdp/msg_init.h \ + libknot/xdp/protocols.h \ libknot/xdp/xdp.c endif ENABLE_XDP diff --git a/src/libknot/xdp/msg.h b/src/libknot/xdp/msg.h new file mode 100644 index 0000000000000000000000000000000000000000..179abf86299f82407e29afae65a08882d9c20d70 --- /dev/null +++ b/src/libknot/xdp/msg.h @@ -0,0 +1,45 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <linux/if_ether.h> +#include <linux/ipv6.h> +#include <sys/uio.h> + +/*! \brief Message flags. */ +typedef enum { + KNOT_XDP_MSG_IPV6 = (1 << 0), /*!< This packet is a IPv6 (IPv4 otherwise). */ + KNOT_XDP_MSG_TCP = (1 << 1), /*!< This packet is a TCP (UDP otherwise). */ + KNOT_XDP_MSG_SYN = (1 << 2), /*!< SYN flag set (TCP only). */ + KNOT_XDP_MSG_ACK = (1 << 3), /*!< ACK flag set (TCP only). */ + KNOT_XDP_MSG_FIN = (1 << 4), /*!< FIN flag set (TCP only). */ + KNOT_XDP_MSG_RST = (1 << 5), /*!< RST flag set (TCP only). */ + KNOT_XDP_MSG_MSS = (1 << 6), /*!< MSS option in TCP header (TCP only). */ +} knot_xdp_msg_flag_t; + +/*! \brief Packet description with src & dst MAC & IP addrs + DNS payload. */ +typedef struct knot_xdp_msg { + struct sockaddr_in6 ip_from; + struct sockaddr_in6 ip_to; + uint8_t eth_from[ETH_ALEN]; + uint8_t eth_to[ETH_ALEN]; + knot_xdp_msg_flag_t flags; + uint32_t seqno; + uint32_t ackno; + struct iovec payload; +} knot_xdp_msg_t; diff --git a/src/libknot/xdp/msg_init.h b/src/libknot/xdp/msg_init.h new file mode 100644 index 0000000000000000000000000000000000000000..5aff3cb348477a14aaf83535882c76da9605c6a1 --- /dev/null +++ b/src/libknot/xdp/msg_init.h @@ -0,0 +1,79 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <string.h> + +#include "libknot/xdp/msg.h" + +inline static bool empty_msg(const knot_xdp_msg_t *msg) +{ + const unsigned tcp_flags = KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_ACK | + KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_RST; + + return (msg->payload.iov_len == 0 && !(msg->flags & tcp_flags)); +} + +// FIXME do we care for better random? +inline static uint32_t rnd_uint32(void) +{ + uint32_t res = rand() & 0xffff; + res <<= 16; + res |= rand() & 0xffff; + return res; +} + +inline static void msg_init_base(knot_xdp_msg_t *msg, knot_xdp_msg_flag_t flags) +{ + memset(msg, 0, sizeof(*msg)); + + msg->flags = flags; +} + +inline static void msg_init(knot_xdp_msg_t *msg, knot_xdp_msg_flag_t flags) +{ + msg_init_base(msg, flags); + + if (flags & KNOT_XDP_MSG_TCP) { + msg->ackno = 0; + msg->seqno = rnd_uint32(); + } +} + +inline static void msg_init_reply(knot_xdp_msg_t *msg, const knot_xdp_msg_t *query) +{ + msg_init_base(msg, query->flags & (KNOT_XDP_MSG_IPV6 | KNOT_XDP_MSG_TCP)); + + memcpy(msg->eth_from, query->eth_to, ETH_ALEN); + memcpy(msg->eth_to, query->eth_from, ETH_ALEN); + + memcpy(&msg->ip_from, &query->ip_to, sizeof(msg->ip_from)); + memcpy(&msg->ip_to, &query->ip_from, sizeof(msg->ip_to)); + + if (msg->flags & KNOT_XDP_MSG_TCP) { + msg->ackno = query->seqno; + msg->ackno += query->payload.iov_len; + if (query->flags & (KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_FIN)) { + msg->ackno++; + } + msg->seqno = query->ackno; + if (msg->seqno == 0) { + msg->seqno = rnd_uint32(); + } + } +} diff --git a/src/libknot/xdp/protocols.h b/src/libknot/xdp/protocols.h new file mode 100644 index 0000000000000000000000000000000000000000..2914601ad14cba583016c06b4491b50e36337021 --- /dev/null +++ b/src/libknot/xdp/protocols.h @@ -0,0 +1,394 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <assert.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/tcp.h> +#include <linux/udp.h> +#include <netinet/in.h> +#include <string.h> + +#include "libknot/xdp/msg.h" + +/* Don't fragment flag. */ +#define IP_DF 0x4000 + +/* + * Following prot_read_*() functions do not check sanity of parsed packet. + * Broken packets have to be dropped by BPF filter prior getting here. + */ + +inline static void *prot_read_udp(void *data, uint16_t *src_port, uint16_t *dst_port) +{ + const struct udphdr *udp = data; + + *src_port = udp->source; + *dst_port = udp->dest; + + return data + sizeof(*udp); +} + +enum { + PROT_TCP_OPT_ENDOP = 0, + PROT_TCP_OPT_NOOP = 1, + PROT_TCP_OPT_MSS = 2, + + PROT_TCP_OPT_LEN_MSS = 4, +}; + +inline static void *prot_read_tcp(void *data, knot_xdp_msg_t *msg, uint16_t *src_port, uint16_t *dst_port) +{ + const struct tcphdr *tcp = data; + + msg->flags |= KNOT_XDP_MSG_TCP; + + if (tcp->syn) { + msg->flags |= KNOT_XDP_MSG_SYN; + } + if (tcp->ack) { + msg->flags |= KNOT_XDP_MSG_ACK; + } + if (tcp->fin) { + msg->flags |= KNOT_XDP_MSG_FIN; + } + if (tcp->rst) { + msg->flags |= KNOT_XDP_MSG_RST; + } + + msg->seqno = be32toh(tcp->seq); + msg->ackno = be32toh(tcp->ack_seq); + + *src_port = tcp->source; + *dst_port = tcp->dest; + + uint8_t *opts = data + sizeof(*tcp), *hdr_end = data + tcp->doff * 4; + while (opts < hdr_end) { + if (opts[0] == PROT_TCP_OPT_ENDOP || opts[0] == PROT_TCP_OPT_NOOP) { + opts++; + continue; + } + + if (opts + 1 > hdr_end || opts + opts[1] > hdr_end) { + // Malformed option. + break; + } + + if (opts[0] == PROT_TCP_OPT_MSS && opts[1] == PROT_TCP_OPT_LEN_MSS) { + msg->flags |= KNOT_XDP_MSG_MSS; + // TODO read MSS value + } + + opts += opts[1]; + } + + return hdr_end; +} + +inline static void *prot_read_ipv4(void *data, knot_xdp_msg_t *msg, void **data_end) +{ + const struct iphdr *ip4 = data; + + // Conditions ensured by the BPF filter. + assert(ip4->version == 4); + assert(ip4->frag_off == 0 || ip4->frag_off == __constant_htons(IP_DF)); + // IPv4 header checksum is not verified! + + struct sockaddr_in *src = (struct sockaddr_in *)&msg->ip_from; + struct sockaddr_in *dst = (struct sockaddr_in *)&msg->ip_to; + memcpy(&src->sin_addr, &ip4->saddr, sizeof(src->sin_addr)); + memcpy(&dst->sin_addr, &ip4->daddr, sizeof(dst->sin_addr)); + src->sin_family = AF_INET; + dst->sin_family = AF_INET; + + *data_end = data + be16toh(ip4->tot_len); + data += ip4->ihl * 4; + + if (ip4->protocol == IPPROTO_TCP) { + return prot_read_tcp(data, msg, &src->sin_port, &dst->sin_port); + } else { + assert(ip4->protocol == IPPROTO_UDP); + return prot_read_udp(data, &src->sin_port, &dst->sin_port); + } +} + +inline static void *prot_read_ipv6(void *data, knot_xdp_msg_t *msg, void **data_end) +{ + const struct ipv6hdr *ip6 = data; + + msg->flags |= KNOT_XDP_MSG_IPV6; + + // Conditions ensured by the BPF filter. + assert(ip6->version == 6); + + struct sockaddr_in6 *src = (struct sockaddr_in6 *)&msg->ip_from; + struct sockaddr_in6 *dst = (struct sockaddr_in6 *)&msg->ip_to; + memcpy(&src->sin6_addr, &ip6->saddr, sizeof(src->sin6_addr)); + memcpy(&dst->sin6_addr, &ip6->daddr, sizeof(dst->sin6_addr)); + src->sin6_family = AF_INET6; + dst->sin6_family = AF_INET6; + // Flow label is ignored. + + data += sizeof(*ip6); + *data_end = data + be16toh(ip6->payload_len); + + if (ip6->nexthdr == IPPROTO_TCP) { + return prot_read_tcp(data, msg, &src->sin6_port, &dst->sin6_port); + } else { + assert(ip6->nexthdr == IPPROTO_UDP); + return prot_read_udp(data, &src->sin6_port, &dst->sin6_port); + } +} + +inline static void *prot_read_eth(void *data, knot_xdp_msg_t *msg, void **data_end) +{ + const struct ethhdr *eth = data; + + memcpy(msg->eth_from, eth->h_source, ETH_ALEN); + memcpy(msg->eth_to, eth->h_dest, ETH_ALEN); + msg->flags = 0; + + data += sizeof(*eth); + + if (eth->h_proto == __constant_htons(ETH_P_IPV6)) { + return prot_read_ipv6(data, msg, data_end); + } else { + assert(eth->h_proto == __constant_htons(ETH_P_IP)); + return prot_read_ipv4(data, msg, data_end); + } +} + +inline static size_t prot_write_hdrs_len(const knot_xdp_msg_t *msg) +{ + size_t res = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr); + + if (msg->flags & KNOT_XDP_MSG_IPV6) { + res += sizeof(struct ipv6hdr) - sizeof(struct iphdr); + } + + if (msg->flags & KNOT_XDP_MSG_TCP) { + res += sizeof(struct tcphdr) - sizeof(struct udphdr); + + if (msg->flags & KNOT_XDP_MSG_MSS) { + res += PROT_TCP_OPT_LEN_MSS; + } + } + + return res; +} + +/* Checksum endianness implementation notes for ipv4_checksum() and checksum(). + * + * The basis for checksum is addition on big-endian 16-bit words, with bit 16 carrying + * over to bit 0. That can be viewed as first byte carrying to the second and the + * second one carrying back to the first one, i.e. a symmetrical situation. + * Therefore the result is the same even when arithmetics is done on litte-endian (!) + */ + +inline static void checksum(uint32_t *result, const void *_data, uint32_t _data_len) +{ + assert(!(_data_len & 1)); + const uint16_t *data = _data; + uint32_t len = _data_len / 2; + while (len-- > 0) { + *result += *data++; + } +} + +inline static void checksum_uint16(uint32_t *result, uint16_t x) +{ + checksum(result, &x, sizeof(x)); +} + +inline static void checksum_payload(uint32_t *result, void *payload, size_t pay_len) +{ + if (pay_len & 1) { + ((uint8_t *)payload)[pay_len++] = 0; + } + checksum(result, payload, pay_len); +} + +inline static uint16_t checksum_finish(uint32_t result, bool nonzero) +{ + while (result > 0xffff) { + result = (result & 0xffff) + (result >> 16); + } + if (!nonzero || result != 0xffff) { + result = ~result; + } + return result; +} + +inline static void prot_write_udp(void *data, const knot_xdp_msg_t *msg, void *data_end, + uint16_t src_port, uint16_t dst_port, uint32_t chksum) +{ + struct udphdr *udp = data; + + udp->len = htobe16(data_end - data); + udp->source = src_port; + udp->dest = dst_port; + + if (msg->flags & KNOT_XDP_MSG_IPV6) { + udp->check = 0; + checksum(&chksum, &udp->len, sizeof(udp->len)); + checksum_uint16(&chksum, htobe16(IPPROTO_UDP)); + checksum_payload(&chksum, data, data_end - data); + udp->check = checksum_finish(chksum, true); + } else { + udp->check = 0; // UDP over IPv4 doesn't require checksum. + } + + assert(data + sizeof(*udp) == msg->payload.iov_base); +} + +inline static void prot_write_tcp(void *data, const knot_xdp_msg_t *msg, void *data_end, + uint16_t src_port, uint16_t dst_port, uint32_t chksum) +{ + struct tcphdr *tcp = data; + + tcp->source = src_port; + tcp->dest = dst_port; + tcp->seq = htobe32(msg->seqno); + tcp->ack_seq = htobe32(msg->ackno); + tcp->window = htobe16(0x8000); // TODO proper window size handling in TCP streams + tcp->check = 0; // Temporarily initialize before checksum calculation. + + tcp->syn = ((msg->flags & KNOT_XDP_MSG_SYN) ? 1 : 0); + tcp->ack = ((msg->flags & KNOT_XDP_MSG_ACK) ? 1 : 0); + tcp->fin = ((msg->flags & KNOT_XDP_MSG_FIN) ? 1 : 0); + tcp->rst = ((msg->flags & KNOT_XDP_MSG_RST) ? 1 : 0); + + uint8_t *hdr_end = data + sizeof(*tcp); + if (msg->flags & KNOT_XDP_MSG_MSS) { + uint16_t mss = htobe16(1460); // TODO: set proper MSS value + hdr_end[0] = PROT_TCP_OPT_MSS; + hdr_end[1] = PROT_TCP_OPT_LEN_MSS; + memcpy(&hdr_end[2], &mss, sizeof(mss)); + hdr_end += PROT_TCP_OPT_LEN_MSS; + } + + tcp->psh = ((data_end - (void *)hdr_end > 0) ? 1 : 0); + tcp->doff = (hdr_end - (uint8_t *)tcp) / 4; + assert((hdr_end - (uint8_t *)tcp) % 4 == 0); + + checksum_uint16(&chksum, htobe16(IPPROTO_TCP)); + checksum_uint16(&chksum, htobe16(data_end - data)); + checksum_payload(&chksum, data, data_end - data); + tcp->check = checksum_finish(chksum, false); + + assert(hdr_end == msg->payload.iov_base); +} + +inline static uint16_t from32to16(uint32_t sum) +{ + sum = (sum & 0xffff) + (sum >> 16); + sum = (sum & 0xffff) + (sum >> 16); + return sum; +} + +inline static uint16_t ipv4_checksum(const uint16_t *ipv4_hdr) +{ + uint32_t sum32 = 0; + for (int i = 0; i < 10; ++i) { + if (i != 5) { + sum32 += ipv4_hdr[i]; + } + } + return ~from32to16(sum32); +} + +inline static void prot_write_ipv4(void *data, const knot_xdp_msg_t *msg, void *data_end) +{ + struct iphdr *ip4 = data; + + ip4->version = 4; + ip4->ihl = sizeof(*ip4) / 4; + ip4->tos = 0; + ip4->tot_len = htobe16(data_end - data); + ip4->id = 0; + ip4->frag_off = 0; + ip4->ttl = IPDEFTTL; + ip4->protocol = ((msg->flags & KNOT_XDP_MSG_TCP) ? IPPROTO_TCP : IPPROTO_UDP); + + const struct sockaddr_in *src = (const struct sockaddr_in *)&msg->ip_from; + const struct sockaddr_in *dst = (const struct sockaddr_in *)&msg->ip_to; + memcpy(&ip4->saddr, &src->sin_addr, sizeof(src->sin_addr)); + memcpy(&ip4->daddr, &dst->sin_addr, sizeof(dst->sin_addr)); + + ip4->check = ipv4_checksum(data); + + data += sizeof(*ip4); + + if (msg->flags & KNOT_XDP_MSG_TCP) { + uint32_t chk = 0; + checksum(&chk, &src->sin_addr, sizeof(src->sin_addr)); + checksum(&chk, &dst->sin_addr, sizeof(dst->sin_addr)); + + prot_write_tcp(data, msg, data_end, src->sin_port, dst->sin_port, chk); + } else { + prot_write_udp(data, msg, data_end, src->sin_port, dst->sin_port, 0); // IPv4/UDP requires no checksum + } +} + +inline static void prot_write_ipv6(void *data, const knot_xdp_msg_t *msg, void *data_end) +{ + struct ipv6hdr *ip6 = data; + + ip6->version = 6; + ip6->priority = 0; + ip6->payload_len = htobe16(data_end - data - sizeof(*ip6)); + ip6->nexthdr = ((msg->flags & KNOT_XDP_MSG_TCP) ? IPPROTO_TCP : IPPROTO_UDP); + ip6->hop_limit = IPDEFTTL; + + memset(ip6->flow_lbl, 0, sizeof(ip6->flow_lbl)); + + const struct sockaddr_in6 *src = (const struct sockaddr_in6 *)&msg->ip_from; + const struct sockaddr_in6 *dst = (const struct sockaddr_in6 *)&msg->ip_to; + memcpy(&ip6->saddr, &src->sin6_addr, sizeof(src->sin6_addr)); + memcpy(&ip6->daddr, &dst->sin6_addr, sizeof(dst->sin6_addr)); + + data += sizeof(*ip6); + + uint32_t chk = 0; + checksum(&chk, &src->sin6_addr, sizeof(src->sin6_addr)); + checksum(&chk, &dst->sin6_addr, sizeof(dst->sin6_addr)); + + if (msg->flags & KNOT_XDP_MSG_TCP) { + prot_write_tcp(data, msg, data_end, src->sin6_port, dst->sin6_port, chk); + } else { + prot_write_udp(data, msg, data_end, src->sin6_port, dst->sin6_port, chk); + } +} + +inline static void prot_write_eth(void *data, const knot_xdp_msg_t *msg, void *data_end) +{ + struct ethhdr *eth = data; + + memcpy(eth->h_source, msg->eth_from, ETH_ALEN); + memcpy(eth->h_dest, msg->eth_to, ETH_ALEN); + + data += sizeof(*eth); + + if (msg->flags & KNOT_XDP_MSG_IPV6) { + eth->h_proto = __constant_htons(ETH_P_IPV6); + prot_write_ipv6(data, msg, data_end); + } else { + eth->h_proto = __constant_htons(ETH_P_IP); + prot_write_ipv4(data, msg, data_end); + } +} diff --git a/src/libknot/xdp/xdp.c b/src/libknot/xdp/xdp.c index 29dfe619f1b158cdd0784c332f7a9bb9793f95e4..a81e718cf1e9aa5fe61b0dfbb772c17fb4689985 100644 --- a/src/libknot/xdp/xdp.c +++ b/src/libknot/xdp/xdp.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -29,12 +29,11 @@ #include "libknot/endian.h" #include "libknot/errcode.h" #include "libknot/xdp/bpf-user.h" +#include "libknot/xdp/msg_init.h" +#include "libknot/xdp/protocols.h" #include "libknot/xdp/xdp.h" #include "contrib/macros.h" -/* Don't fragment flag. */ -#define IP_DF 0x4000 - #define FRAME_SIZE 2048 #define UMEM_FRAME_COUNT_RX 4096 #define UMEM_FRAME_COUNT_TX UMEM_FRAME_COUNT_RX // No reason to differ so far. @@ -56,48 +55,10 @@ _Static_assert((FRAME_SIZE == 4096 || FRAME_SIZE == 2048) , "Incorrect #define combination for AF_XDP."); #endif -/*! \brief The memory layout of IPv4 umem frame. */ -struct udpv4 { - union { - uint8_t bytes[1]; - struct { - struct ethhdr eth; // No VLAN support; CRC at the "end" of .data! - struct iphdr ipv4; - struct udphdr udp; - uint8_t data[]; - } __attribute__((packed)); - }; -}; - -/*! \brief The memory layout of IPv6 umem frame. */ -struct udpv6 { - union { - uint8_t bytes[1]; - struct { - struct ethhdr eth; // No VLAN support; CRC at the "end" of .data! - struct ipv6hdr ipv6; - struct udphdr udp; - uint8_t data[]; - } __attribute__((packed)); - }; -}; - -/*! \brief The memory layout of each umem frame. */ struct umem_frame { - union { - uint8_t bytes[FRAME_SIZE]; - union { - struct udpv4 udpv4; - struct udpv6 udpv6; - }; - }; + uint8_t bytes[FRAME_SIZE]; }; -_public_ -const size_t KNOT_XDP_PAYLOAD_OFFSET4 = offsetof(struct udpv4, data) + offsetof(struct umem_frame, udpv4); -_public_ -const size_t KNOT_XDP_PAYLOAD_OFFSET6 = offsetof(struct udpv6, data) + offsetof(struct umem_frame, udpv6); - static int configure_xsk_umem(struct kxsk_umem **out_umem) { /* Allocate memory and call driver to create the UMEM. */ @@ -299,191 +260,48 @@ static struct umem_frame *alloc_tx_frame(struct kxsk_umem *umem) return umem->frames + index; } +static void prepare_payload(knot_xdp_msg_t *msg, void *uframe) +{ + size_t hdr_len = prot_write_hdrs_len(msg); + msg->payload.iov_base = uframe + hdr_len; + msg->payload.iov_len = FRAME_SIZE - hdr_len; +} + _public_ -int knot_xdp_send_alloc(knot_xdp_socket_t *socket, bool ipv6, knot_xdp_msg_t *out, - const knot_xdp_msg_t *in_reply_to) +int knot_xdp_send_alloc(knot_xdp_socket_t *socket, knot_xdp_msg_flag_t flags, + knot_xdp_msg_t *out) { if (socket == NULL || out == NULL) { return KNOT_EINVAL; } - size_t ofs = ipv6 ? KNOT_XDP_PAYLOAD_OFFSET6 : KNOT_XDP_PAYLOAD_OFFSET4; - struct umem_frame *uframe = alloc_tx_frame(socket->umem); if (uframe == NULL) { return KNOT_ENOMEM; } - memset(out, 0, sizeof(*out)); - - out->payload.iov_base = ipv6 ? uframe->udpv6.data : uframe->udpv4.data; - out->payload.iov_len = MIN(UINT16_MAX, FRAME_SIZE - ofs); - - const struct ethhdr *eth = (struct ethhdr *)uframe; - out->eth_from = (void *)ð->h_source; - out->eth_to = (void *)ð->h_dest; - - if (in_reply_to != NULL) { - memcpy(out->eth_from, in_reply_to->eth_to, ETH_ALEN); - memcpy(out->eth_to, in_reply_to->eth_from, ETH_ALEN); - - memcpy(&out->ip_from, &in_reply_to->ip_to, sizeof(out->ip_from)); - memcpy(&out->ip_to, &in_reply_to->ip_from, sizeof(out->ip_to)); - } + msg_init(out, flags); + prepare_payload(out, uframe); return KNOT_EOK; } -static uint16_t from32to16(uint32_t sum) -{ - sum = (sum & 0xffff) + (sum >> 16); - sum = (sum & 0xffff) + (sum >> 16); - return sum; -} - -static uint16_t ipv4_checksum(const uint8_t *ipv4_hdr) -{ - const uint16_t *h = (const uint16_t *)ipv4_hdr; - uint32_t sum32 = 0; - for (int i = 0; i < 10; ++i) { - if (i != 5) { - sum32 += h[i]; - } - } - return ~from32to16(sum32); -} - -/* Checksum endianness implementation notes for ipv4_checksum() and udp_checksum_step(). - * - * The basis for checksum is addition on big-endian 16-bit words, with bit 16 carrying - * over to bit 0. That can be viewed as first byte carrying to the second and the - * second one carrying back to the first one, i.e. a symmetrical situation. - * Therefore the result is the same even when arithmetics is done on litte-endian (!) - */ - -static void udp_checksum_step(size_t *result, const void *_data, size_t _data_len) +_public_ +int knot_xdp_reply_alloc(knot_xdp_socket_t *socket, const knot_xdp_msg_t *query, + knot_xdp_msg_t *out) { - assert(!(_data_len & 1)); - const uint16_t *data = _data; - size_t len = _data_len / 2; - while (len-- > 0) { - *result += *data++; + if (socket == NULL || query == NULL || out == NULL) { + return KNOT_EINVAL; } -} -static void udp_checksum_finish(size_t *result) -{ - while (*result > 0xffff) { - *result = (*result & 0xffff) + (*result >> 16); - } - if (*result != 0xffff) { - *result = ~*result; + struct umem_frame *uframe = alloc_tx_frame(socket->umem); + if (uframe == NULL) { + return KNOT_ENOMEM; } -} - -static uint8_t *msg_uframe_ptr(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg, - /* Next parameters are just for debugging. */ - bool ipv6) -{ - uint8_t *uNULL = NULL; - uint8_t *uframe_p = uNULL + ((msg->payload.iov_base - NULL) & ~(FRAME_SIZE - 1)); - -#ifndef NDEBUG - intptr_t pd = (uint8_t *)msg->payload.iov_base - uframe_p - - (ipv6 ? KNOT_XDP_PAYLOAD_OFFSET6 : KNOT_XDP_PAYLOAD_OFFSET4); - /* This assertion might fire in some OK cases. For example, the second branch - * had to be added for cases with "emulated" AF_XDP support. */ - assert(pd == XDP_PACKET_HEADROOM || pd == 0); - - const uint8_t *umem_mem_start = socket->umem->frames->bytes; - const uint8_t *umem_mem_end = umem_mem_start + FRAME_SIZE * UMEM_FRAME_COUNT; - assert(umem_mem_start <= uframe_p && uframe_p < umem_mem_end); -#endif - return uframe_p; -} - -static void xsk_sendmsg_ipv4(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg, - uint32_t index) -{ - uint8_t *uframe_p = msg_uframe_ptr(socket, msg, false); - struct umem_frame *uframe = (struct umem_frame *)uframe_p; - struct udpv4 *h = &uframe->udpv4; - - const struct sockaddr_in *src_v4 = (const struct sockaddr_in *)&msg->ip_from; - const struct sockaddr_in *dst_v4 = (const struct sockaddr_in *)&msg->ip_to; - const uint16_t udp_len = sizeof(h->udp) + msg->payload.iov_len; - - h->eth.h_proto = __constant_htons(ETH_P_IP); - - h->ipv4.version = IPVERSION; - h->ipv4.ihl = 5; - h->ipv4.tos = 0; - h->ipv4.tot_len = htobe16(5 * 4 + udp_len); - h->ipv4.id = 0; - h->ipv4.frag_off = 0; - h->ipv4.ttl = IPDEFTTL; - h->ipv4.protocol = IPPROTO_UDP; - memcpy(&h->ipv4.saddr, &src_v4->sin_addr, sizeof(src_v4->sin_addr)); - memcpy(&h->ipv4.daddr, &dst_v4->sin_addr, sizeof(dst_v4->sin_addr)); - h->ipv4.check = ipv4_checksum(h->bytes + sizeof(struct ethhdr)); - - h->udp.len = htobe16(udp_len); - h->udp.source = src_v4->sin_port; - h->udp.dest = dst_v4->sin_port; - h->udp.check = 0; // Optional for IPv4 - not computed. - - *xsk_ring_prod__tx_desc(&socket->tx, index) = (struct xdp_desc){ - .addr = h->bytes - socket->umem->frames->bytes, - .len = KNOT_XDP_PAYLOAD_OFFSET4 + msg->payload.iov_len - }; -} -static void xsk_sendmsg_ipv6(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg, - uint32_t index) -{ - uint8_t *uframe_p = msg_uframe_ptr(socket, msg, true); - struct umem_frame *uframe = (struct umem_frame *)uframe_p; - struct udpv6 *h = &uframe->udpv6; - - const struct sockaddr_in6 *src_v6 = (const struct sockaddr_in6 *)&msg->ip_from; - const struct sockaddr_in6 *dst_v6 = (const struct sockaddr_in6 *)&msg->ip_to; - const uint16_t udp_len = sizeof(h->udp) + msg->payload.iov_len; - - h->eth.h_proto = __constant_htons(ETH_P_IPV6); - - h->ipv6.version = 6; - h->ipv6.priority = 0; - memset(h->ipv6.flow_lbl, 0, sizeof(h->ipv6.flow_lbl)); - h->ipv6.payload_len = htobe16(udp_len); - h->ipv6.nexthdr = IPPROTO_UDP; - h->ipv6.hop_limit = IPDEFTTL; - memcpy(&h->ipv6.saddr, &src_v6->sin6_addr, sizeof(src_v6->sin6_addr)); - memcpy(&h->ipv6.daddr, &dst_v6->sin6_addr, sizeof(dst_v6->sin6_addr)); - - h->udp.len = htobe16(udp_len); - h->udp.source = src_v6->sin6_port; - h->udp.dest = dst_v6->sin6_port; - h->udp.check = 0; // Mandatory for IPv6 - computed afterwards. - - size_t chk = 0; - udp_checksum_step(&chk, &h->ipv6.saddr, sizeof(h->ipv6.saddr)); - udp_checksum_step(&chk, &h->ipv6.daddr, sizeof(h->ipv6.daddr)); - udp_checksum_step(&chk, &h->udp.len, sizeof(h->udp.len)); - __be16 version = htobe16(h->ipv6.nexthdr); - udp_checksum_step(&chk, &version, sizeof(version)); - udp_checksum_step(&chk, &h->udp, sizeof(h->udp)); - size_t padded_len = msg->payload.iov_len; - if (padded_len & 1) { - ((uint8_t *)msg->payload.iov_base)[padded_len++] = 0; - } - udp_checksum_step(&chk, msg->payload.iov_base, padded_len); - udp_checksum_finish(&chk); - h->udp.check = chk; - - *xsk_ring_prod__tx_desc(&socket->tx, index) = (struct xdp_desc){ - .addr = h->bytes - socket->umem->frames->bytes, - .len = KNOT_XDP_PAYLOAD_OFFSET6 + msg->payload.iov_len - }; + msg_init_reply(out, query); + prepare_payload(out, uframe); + return KNOT_EOK; } _public_ @@ -507,15 +325,20 @@ int knot_xdp_send(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[], for (uint32_t i = 0; i < count; ++i) { const knot_xdp_msg_t *msg = &msgs[i]; - if (msg->payload.iov_len && msg->ip_from.sin6_family == AF_INET) { - xsk_sendmsg_ipv4(socket, msg, idx++); - } else if (msg->payload.iov_len && msg->ip_from.sin6_family == AF_INET6) { - xsk_sendmsg_ipv6(socket, msg, idx++); - } else { - /* Some problem; we just ignore this message. */ + if (empty_msg(msg)) { uint64_t addr_relative = (uint8_t *)msg->payload.iov_base - socket->umem->frames->bytes; tx_free_relative(socket->umem, addr_relative); + } else { + size_t hdr_len = prot_write_hdrs_len(msg); + size_t tot_len = hdr_len + msg->payload.iov_len; + uint8_t *msg_beg = msg->payload.iov_base - hdr_len; + prot_write_eth(msg_beg, msg, msg_beg + tot_len); + + *xsk_ring_prod__tx_desc(&socket->tx, idx++) = (struct xdp_desc) { + .addr = msg_beg - socket->umem->frames->bytes, + .len = tot_len, + }; } } @@ -565,78 +388,9 @@ int knot_xdp_send_finish(knot_xdp_socket_t *socket) */ } -static void rx_desc(knot_xdp_socket_t *socket, const struct xdp_desc *desc, - knot_xdp_msg_t *msg) -{ - uint8_t *uframe_p = socket->umem->frames->bytes + desc->addr; - const struct ethhdr *eth = (struct ethhdr *)uframe_p; - const struct iphdr *ip4 = NULL; - const struct ipv6hdr *ip6 = NULL; - const struct udphdr *udp = NULL; - - switch (eth->h_proto) { - case __constant_htons(ETH_P_IP): - ip4 = (struct iphdr *)(uframe_p + sizeof(struct ethhdr)); - // Next conditions are ensured by the BPF filter. - assert(ip4->version == 4); - assert(ip4->frag_off == 0 || - ip4->frag_off == __constant_htons(IP_DF)); - assert(ip4->protocol == IPPROTO_UDP); - // IPv4 header checksum is not verified! - udp = (struct udphdr *)(uframe_p + sizeof(struct ethhdr) + - ip4->ihl * 4); - break; - case __constant_htons(ETH_P_IPV6): - ip6 = (struct ipv6hdr *)(uframe_p + sizeof(struct ethhdr)); - // Next conditions are ensured by the BPF filter. - assert(ip6->version == 6); - assert(ip6->nexthdr == IPPROTO_UDP); - udp = (struct udphdr *)(uframe_p + sizeof(struct ethhdr) + - sizeof(struct ipv6hdr)); - break; - default: - assert(0); - msg->payload.iov_len = 0; - return; - } - // UDP checksum is not verified! - - assert(eth && (!!ip4 != !!ip6) && udp); - - // Process the packet; ownership is passed on, beware of holding frames. - - msg->payload.iov_base = (uint8_t *)udp + sizeof(struct udphdr); - msg->payload.iov_len = be16toh(udp->len) - sizeof(struct udphdr); - - msg->eth_from = (void *)ð->h_source; - msg->eth_to = (void *)ð->h_dest; - - if (ip4 != NULL) { - struct sockaddr_in *src_v4 = (struct sockaddr_in *)&msg->ip_from; - struct sockaddr_in *dst_v4 = (struct sockaddr_in *)&msg->ip_to; - memcpy(&src_v4->sin_addr, &ip4->saddr, sizeof(src_v4->sin_addr)); - memcpy(&dst_v4->sin_addr, &ip4->daddr, sizeof(dst_v4->sin_addr)); - src_v4->sin_port = udp->source; - dst_v4->sin_port = udp->dest; - src_v4->sin_family = AF_INET; - dst_v4->sin_family = AF_INET; - } else { - assert(ip6); - struct sockaddr_in6 *src_v6 = (struct sockaddr_in6 *)&msg->ip_from; - struct sockaddr_in6 *dst_v6 = (struct sockaddr_in6 *)&msg->ip_to; - memcpy(&src_v6->sin6_addr, &ip6->saddr, sizeof(src_v6->sin6_addr)); - memcpy(&dst_v6->sin6_addr, &ip6->daddr, sizeof(dst_v6->sin6_addr)); - src_v6->sin6_port = udp->source; - dst_v6->sin6_port = udp->dest; - src_v6->sin6_family = AF_INET6; - dst_v6->sin6_family = AF_INET6; - // Flow label is ignored. - } -} - _public_ int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[], - uint32_t max_count, uint32_t *count) + uint32_t max_count, uint32_t *count, size_t *wire_size) { if (socket == NULL || msgs == NULL || count == NULL) { return KNOT_EINVAL; @@ -651,7 +405,18 @@ int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[], assert(available <= max_count); for (uint32_t i = 0; i < available; ++i) { - rx_desc(socket, xsk_ring_cons__rx_desc(&socket->rx, idx++), &msgs[i]); + knot_xdp_msg_t *msg = &msgs[i]; + const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&socket->rx, idx++); + uint8_t *uframe_p = socket->umem->frames->bytes + desc->addr; + + void *payl_end, *payl_start = prot_read_eth(uframe_p, msg, &payl_end); + + msg->payload.iov_base = payl_start; + msg->payload.iov_len = payl_end - payl_start; + + if (wire_size != NULL) { + (*wire_size) += desc->len; + } } xsk_ring_cons__release(&socket->rx, available); @@ -660,6 +425,11 @@ int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[], return KNOT_EOK; } +static uint8_t *msg_uframe_ptr(const knot_xdp_msg_t *msg) +{ + return NULL + ((msg->payload.iov_base - NULL) & ~(FRAME_SIZE - 1)); +} + _public_ void knot_xdp_recv_finish(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[], uint32_t count) @@ -676,8 +446,7 @@ void knot_xdp_recv_finish(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[] assert(reserved == count); for (uint32_t i = 0; i < reserved; ++i) { - uint8_t *uframe_p = msg_uframe_ptr(socket, &msgs[i], - msgs[i].ip_from.sin6_family == AF_INET6); + uint8_t *uframe_p = msg_uframe_ptr(&msgs[i]); uint64_t offset = uframe_p - umem->frames->bytes; *xsk_ring_prod__fill_addr(fq, idx++) = offset; } diff --git a/src/libknot/xdp/xdp.h b/src/libknot/xdp/xdp.h index 74a845a1ae71f4f89313a054226acd30197038e7..8e8f02fac9a18808c52ede47d82057a5c2e68583 100644 --- a/src/libknot/xdp/xdp.h +++ b/src/libknot/xdp/xdp.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,21 +22,12 @@ #include <netinet/in.h> #include "libknot/xdp/bpf-consts.h" +#include "libknot/xdp/msg.h" #ifdef ENABLE_XDP #define KNOT_XDP_AVAILABLE 1 #endif -/*! \brief A packet with src & dst MAC & IP addrs + UDP payload. */ -typedef struct knot_xdp_msg knot_xdp_msg_t; -struct knot_xdp_msg { - struct sockaddr_in6 ip_from; - struct sockaddr_in6 ip_to; - uint8_t *eth_from; - uint8_t *eth_to; - struct iovec payload; -}; - /*! * \brief Styles of loading BPF program. * @@ -55,10 +46,6 @@ typedef enum { /*! \brief Context structure for one XDP socket. */ typedef struct knot_xdp_socket knot_xdp_socket_t; -/*! \brief Offset of DNS payload inside ethernet frame (IPv4 and v6 variants). */ -extern const size_t KNOT_XDP_PAYLOAD_OFFSET4; -extern const size_t KNOT_XDP_PAYLOAD_OFFSET6; - /*! * \brief Initialize XDP socket. * @@ -100,20 +87,31 @@ void knot_xdp_send_prepare(knot_xdp_socket_t *socket); * \brief Allocate one buffer for an outgoing packet. * * \param socket XDP socket. - * \param ipv6 The packet will use IPv6 (IPv4 otherwise). + * \param flags Flags for new message. + * \param out Out: the allocated packet buffer. + * + * \return KNOT_E* + */ +int knot_xdp_send_alloc(knot_xdp_socket_t *socket, knot_xdp_msg_flag_t flags, + knot_xdp_msg_t *out); + +/*! + * \brief Allocate one buffer for a reply packet. + * + * \param socket XDP socket. + * \param query The packet to be replied to. * \param out Out: the allocated packet buffer. - * \param in_reply_to Optional: fill in addresses from this query. * * \return KNOT_E* */ -int knot_xdp_send_alloc(knot_xdp_socket_t *socket, bool ipv6, knot_xdp_msg_t *out, - const knot_xdp_msg_t *in_reply_to); +int knot_xdp_reply_alloc(knot_xdp_socket_t *socket, const knot_xdp_msg_t *query, + knot_xdp_msg_t *out); /*! * \brief Send multiple packets thru XDP. * * \note The packets all must have been allocated by knot_xdp_send_alloc()! - * \note Do not free the packets payloads afterwards. + * \note Do not free the packet payloads afterwards. * \note Packets with zero length will be skipped. * * \param socket XDP socket. @@ -142,11 +140,12 @@ int knot_xdp_send_finish(knot_xdp_socket_t *socket); * \param msgs Out: buffers to be filled in with incomming packets. * \param max_count Limit for number of packets received at once. * \param count Out: real number of received packets. + * \param wire_size Out: (optional) total wire size of received packets. * * \return KNOT_E* */ int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[], - uint32_t max_count, uint32_t *count); + uint32_t max_count, uint32_t *count, size_t *wire_size); /*! * \brief Free buffers with received packets. diff --git a/src/utils/kxdpgun/main.c b/src/utils/kxdpgun/main.c index f3c06dcaa7109e4c8148fb5a8f4318cf85f8dea3..05417c02d8059bd169dc7e6ada94728ee4665717 100644 --- a/src/utils/kxdpgun/main.c +++ b/src/utils/kxdpgun/main.c @@ -51,6 +51,7 @@ pthread_mutex_t global_mutex; uint64_t global_pkts_sent = 0; uint64_t global_pkts_recv = 0; uint64_t global_size_recv = 0; +uint64_t global_wire_recv = 0; unsigned global_cpu_aff_start = 0; unsigned global_cpu_aff_step = 1; @@ -142,7 +143,7 @@ static int alloc_pkts(knot_xdp_msg_t *pkts, int npkts, struct knot_xdp_socket *x uint64_t unique = (tick * ctx->n_threads + ctx->thread_id) * ctx->at_once; for (int i = 0; i < npkts; i++) { - int ret = knot_xdp_send_alloc(xsk, ctx->ipv6, &pkts[i], NULL); + int ret = knot_xdp_send_alloc(xsk, ctx->ipv6, &pkts[i]); if (ret != KNOT_EOK) { return ret; } @@ -178,7 +179,7 @@ void *xdp_gun_thread(void *_ctx) struct knot_xdp_socket *xsk; struct timespec timer; knot_xdp_msg_t pkts[ctx->at_once]; - uint64_t tot_sent = 0, tot_recv = 0, tot_size = 0, errors = 0; + uint64_t tot_sent = 0, tot_recv = 0, tot_size = 0, tot_wire = 0, errors = 0; uint64_t duration = 0; knot_xdp_load_bpf_t mode = (ctx->thread_id == 0 ? @@ -248,7 +249,8 @@ void *xdp_gun_thread(void *_ctx) } uint32_t recvd = 0; - ret = knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd); + size_t wire = 0; + ret = knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd, &wire); if (ret != KNOT_EOK) { errors++; break; @@ -262,6 +264,7 @@ void *xdp_gun_thread(void *_ctx) tot_size += pkts[i].payload.iov_len; tot_recv++; } + tot_wire += wire; knot_xdp_recv_finish(xsk, pkts, recvd); pfd.revents = 0; } @@ -287,6 +290,7 @@ void *xdp_gun_thread(void *_ctx) global_pkts_sent += tot_sent; global_pkts_recv += tot_recv; global_size_recv += tot_size; + global_wire_recv += tot_wire; pthread_mutex_unlock(&global_mutex); return NULL; @@ -724,8 +728,7 @@ int main(int argc, char *argv[]) printf("total replies: %lu (%lu pps) (%lu%%)\n", global_pkts_recv, global_pkts_recv * 1000 / (ctx.duration / 1000), global_pkts_recv * 100 / global_pkts_sent); printf("average DNS reply size: %lu B\n", global_pkts_recv > 0 ? global_size_recv / global_pkts_recv : 0); - size_t bytes_recv = global_size_recv + (ctx.ipv6 ? KNOT_XDP_PAYLOAD_OFFSET6 : KNOT_XDP_PAYLOAD_OFFSET4) * global_pkts_recv; - printf("average Ethernet reply rate: %lu bps\n", bytes_recv * 8 * 1000 / (ctx.duration / 1000)); + printf("average Ethernet reply rate: %lu bps\n", global_wire_recv * 8 * 1000 / (ctx.duration / 1000)); for (int i = 0; i < KNOWN_RCODE_MAX; i++) { uint64_t rcode_count = 0; for (size_t j = 0; j < ctx.n_threads; j++) {