summaryrefslogtreecommitdiffstats
path: root/src/utils/kxdpgun
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils/kxdpgun')
-rw-r--r--src/utils/kxdpgun/ip_route.c358
-rw-r--r--src/utils/kxdpgun/ip_route.h46
-rw-r--r--src/utils/kxdpgun/load_queries.c159
-rw-r--r--src/utils/kxdpgun/load_queries.h32
-rw-r--r--src/utils/kxdpgun/main.c1302
5 files changed, 1897 insertions, 0 deletions
diff --git a/src/utils/kxdpgun/ip_route.c b/src/utils/kxdpgun/ip_route.c
new file mode 100644
index 0000000..003c764
--- /dev/null
+++ b/src/utils/kxdpgun/ip_route.c
@@ -0,0 +1,358 @@
+/* Copyright (C) 2022 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/>.
+ */
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <arpa/inet.h>
+#include <libmnl/libmnl.h>
+#include <linux/if_ether.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+#include <net/if.h>
+
+#include "utils/kxdpgun/ip_route.h"
+#include "contrib/sockaddr.h"
+
+#define ROUTE_LOOKUP_LOOP_LIMIT 10000
+
+static size_t addr_len(int family)
+{
+ switch (family) {
+ case AF_INET:
+ return sizeof(struct in_addr);
+ case AF_INET6:
+ return sizeof(struct in6_addr);
+ default:
+ return 0;
+ }
+}
+
+static int send_dummy_pkt(const struct sockaddr_storage *ip)
+{
+ static const uint8_t dummy_pkt[] = {
+ // dummy data
+ 0x08, 0x00, 0xec, 0x72, 0x0b, 0x87, 0x00, 0x06,
+
+ //padding
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+
+ int fd = socket(ip->ss_family, SOCK_RAW,
+ ip->ss_family == AF_INET6 ? IPPROTO_ICMPV6 : IPPROTO_ICMP);
+ if (fd < 0) {
+ return -errno;
+ }
+ int ret = sendto(fd, dummy_pkt, sizeof(dummy_pkt), 0, (const struct sockaddr *)ip,
+ ip->ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) :
+ sizeof(struct sockaddr_in));
+ if (ret < 0) {
+ ret = -errno;
+ }
+ close(fd);
+ return ret;
+}
+
+static int netlink_query(int family, uint16_t type, mnl_cb_t cb, void *data,
+ void *qextra, size_t qextra_len, uint16_t qextra_type)
+{
+ // open and bind NETLINK socket
+ struct mnl_socket *nl = mnl_socket_open(NETLINK_ROUTE);
+ if (nl == NULL) {
+ return -errno;
+ }
+ if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
+ mnl_socket_close(nl);
+ return -errno;
+ }
+ unsigned portid = mnl_socket_get_portid(nl);
+ int ret = 0;
+
+ // allocate request
+ char buf[MNL_SOCKET_BUFFER_SIZE];
+ struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
+ if (nlh == NULL) {
+ ret = -ENOMEM;
+ goto end;
+ }
+ unsigned seq = time(NULL);
+ nlh->nlmsg_seq = seq;
+ nlh->nlmsg_type = type;
+ nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+ struct rtmsg *rtm = mnl_nlmsg_put_extra_header(nlh, sizeof(*rtm));
+ if (rtm == NULL) {
+ ret = -ENOMEM;
+ goto end;
+ }
+
+ if (qextra_len > 0) {
+ nlh->nlmsg_flags = NLM_F_REQUEST;
+ rtm->rtm_dst_len = qextra_len * 8; // 8 bits per byte
+ mnl_attr_put(nlh, qextra_type, qextra_len, qextra);
+ }
+
+ // send request
+ rtm->rtm_family = family;
+ ret = mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);
+ if (ret < 0) {
+ ret = -errno;
+ goto end;
+ }
+
+ // collect replies with callback
+ while ((ret = mnl_socket_recvfrom(nl, buf, sizeof(buf))) > 0) {
+ ret = mnl_cb_run(buf, ret, seq, portid, cb, data);
+ if (ret <= MNL_CB_STOP) {
+ break;
+ }
+ if (qextra_len > 0) {
+ break;
+ }
+ }
+ ret = ret < 0 ? -errno : 0;
+
+end:
+ mnl_socket_close(nl);
+ return ret;
+}
+
+typedef struct {
+ const struct sockaddr_storage *ip;
+ struct sockaddr_storage *via;
+ struct sockaddr_storage *src;
+ char *dev;
+ uint64_t priority; // top 32 bits: unmatched address bits; bottom 32 bits: route metric priority
+ unsigned match;
+
+ // intermediate callback data
+ const struct nlattr *tb[RTA_MAX+1];
+} ip_route_get_ctx_t;
+
+static int validate_attr_route(const struct nlattr *attr, void *data)
+{
+ /* skip unsupported attribute in user-space */
+ if (mnl_attr_type_valid(attr, RTA_MAX) < 0) {
+ return MNL_CB_OK;
+ }
+
+ ip_route_get_ctx_t *ctx = data;
+
+ int type = mnl_attr_get_type(attr);
+ switch(type) {
+ case RTA_TABLE:
+ case RTA_OIF:
+ case RTA_FLOW:
+ case RTA_PRIORITY:
+ if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ case RTA_DST:
+ case RTA_SRC:
+ case RTA_PREFSRC:
+ case RTA_GATEWAY:
+ if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, addr_len(ctx->ip->ss_family)) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ case RTA_METRICS:
+ if (mnl_attr_validate(attr, MNL_TYPE_NESTED) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ }
+ ctx->tb[type] = attr;
+ return MNL_CB_OK;
+}
+
+static void attr2addr(const struct nlattr *attr, int family, struct sockaddr_storage *out)
+{
+ if (attr == NULL) {
+ out->ss_family = AF_UNSPEC;
+ return;
+ }
+ out->ss_family = family;
+ if (family == AF_INET6) {
+ struct in6_addr *addr = mnl_attr_get_payload(attr);
+ memcpy(&((struct sockaddr_in6 *)out)->sin6_addr, addr, sizeof(*addr));
+ } else {
+ struct in_addr *addr = mnl_attr_get_payload(attr);
+ memcpy(&((struct sockaddr_in *)out)->sin_addr, addr, sizeof(*addr));
+ }
+}
+
+static void attr2dev(const struct nlattr *attr, char *out) // out must have IFNAMSIZ length
+{
+ *out = '\0';
+
+ if (attr != NULL) {
+ if_indextoname(mnl_attr_get_u32(attr), out);
+ }
+}
+
+static uint32_t attr2prio(const struct nlattr *attr)
+{
+ if (attr == NULL) {
+ return 0; // 0 is the default metric priority in linux
+ }
+ return mnl_attr_get_u32(attr);
+}
+
+static int ip_route_get_cb(const struct nlmsghdr *nlh, void *data)
+{
+ ip_route_get_ctx_t *ctx = data;
+ struct rtmsg *rm = mnl_nlmsg_get_payload(nlh);
+ if (rm->rtm_family != ctx->ip->ss_family) {
+ return MNL_CB_ERROR;
+ }
+
+ mnl_attr_parse(nlh, sizeof(*rm), validate_attr_route, data);
+
+ uint64_t new_metric = addr_len(rm->rtm_family) * 8 - rm->rtm_dst_len;
+ new_metric = (new_metric << 32) + attr2prio(ctx->tb[RTA_PRIORITY]);
+ if (new_metric >= ctx->priority) {
+ return MNL_CB_OK;
+ }
+
+ struct sockaddr_storage dst;
+ attr2addr(ctx->tb[RTA_DST], rm->rtm_family, &dst);
+
+ if (rm->rtm_dst_len == 0 ||
+ sockaddr_net_match(&dst, ctx->ip, rm->rtm_dst_len)) {
+ attr2addr(ctx->tb[RTA_PREFSRC], rm->rtm_family, ctx->src);
+ attr2addr(ctx->tb[RTA_GATEWAY], rm->rtm_family, ctx->via);
+ attr2dev(ctx->tb[RTA_OIF], ctx->dev);
+ ctx->match++;
+ ctx->priority = new_metric;
+ }
+
+ memset(ctx->tb, 0, sizeof(void *) * (RTA_MAX+1));
+ return MNL_CB_OK;
+}
+
+int ip_route_get(const struct sockaddr_storage *ip,
+ struct sockaddr_storage *via,
+ struct sockaddr_storage *src,
+ char *dev)
+{
+ struct sockaddr_storage last_via = { 0 };
+ ip_route_get_ctx_t ctx = { ip, &last_via, src, dev, 0, 0 };
+ do {
+ ctx.priority = UINT64_MAX;
+
+ size_t qextra_len;
+ void *qextra = sockaddr_raw(ip, &qextra_len);
+ int ret = netlink_query(ip->ss_family, RTM_GETROUTE,
+ ip_route_get_cb, &ctx, qextra,
+ qextra_len, IFA_ADDRESS);
+ if (ret != 0) {
+ return ret;
+ }
+ if (last_via.ss_family == ip->ss_family) { // not AF_UNSPEC
+ memcpy(via, &last_via, sizeof(*via));
+ }
+
+ // next loop will search for path to "via"
+ ctx.ip = via;
+ } while (last_via.ss_family != AF_UNSPEC &&
+ ctx.priority != UINT64_MAX && // avoid loop when nothing found
+ ctx.match < ROUTE_LOOKUP_LOOP_LIMIT); // avoid loop when looped route
+
+ return src->ss_family == ip->ss_family ? 0 : -ENOENT;
+}
+
+typedef struct {
+ const struct sockaddr_storage *ip;
+ uint8_t *mac;
+ unsigned match;
+
+ // intermediate callback data
+ const struct nlattr *tb[RTA_MAX+1];
+} ip_neigh_ctx_t;
+
+static int validate_attr_neigh(const struct nlattr *attr, void *data)
+{
+ /* skip unsupported attribute in user-space */
+ if (mnl_attr_type_valid(attr, NDA_MAX) < 0) {
+ return MNL_CB_OK;
+ }
+
+ ip_neigh_ctx_t *ctx = data;
+
+ int type = mnl_attr_get_type(attr);
+ switch (type) {
+ case NDA_DST:
+ if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, addr_len(ctx->ip->ss_family)) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ case NDA_LLADDR:
+ if (mnl_attr_validate2(attr, MNL_TYPE_BINARY, ETH_ALEN) < 0) {
+ return MNL_CB_ERROR;
+ }
+ break;
+ }
+
+ ctx->tb[type] = attr;
+ return MNL_CB_OK;
+}
+
+static int ip_neigh_cb(const struct nlmsghdr *nlh, void *data)
+{
+ ip_neigh_ctx_t *ctx = data;
+ struct rtmsg *rm = mnl_nlmsg_get_payload(nlh);
+ if (rm->rtm_family != ctx->ip->ss_family) {
+ return MNL_CB_ERROR;
+ }
+
+ mnl_attr_parse(nlh, sizeof(*rm), validate_attr_neigh, data);
+
+ struct sockaddr_storage dst;
+ attr2addr(ctx->tb[NDA_DST], rm->rtm_family, &dst);
+
+ if (sockaddr_cmp((struct sockaddr_storage *)&dst, (struct sockaddr_storage *)ctx->ip, true) == 0 &&
+ ctx->tb[NDA_LLADDR] != NULL) {
+ memcpy(ctx->mac, mnl_attr_get_payload(ctx->tb[NDA_LLADDR]), ETH_ALEN);
+ ctx->match++;
+ }
+
+ memset(ctx->tb, 0, sizeof(void *) * (RTA_MAX+1));
+ return MNL_CB_OK;
+}
+
+int ip_neigh_get(const struct sockaddr_storage *ip, bool dummy_sendto, uint8_t *mac)
+{
+ if (dummy_sendto) {
+ int ret = send_dummy_pkt(ip);
+ if (ret < 0) {
+ return ret;
+ }
+ usleep(10000);
+ }
+ ip_neigh_ctx_t ctx = { ip, mac, 0 };
+ int ret = netlink_query(ip->ss_family, RTM_GETNEIGH, ip_neigh_cb, &ctx,
+ NULL, 0, 0);
+ if (ret == 0 && ctx.match == 0) {
+ return -ENOENT;
+ }
+ return ret;
+}
diff --git a/src/utils/kxdpgun/ip_route.h b/src/utils/kxdpgun/ip_route.h
new file mode 100644
index 0000000..c35c25b
--- /dev/null
+++ b/src/utils/kxdpgun/ip_route.h
@@ -0,0 +1,46 @@
+/* Copyright (C) 2022 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 <sys/socket.h>
+
+/*!
+ * \brief Get route to an IPv4/6 from system routing table.
+ *
+ * \param ip IPv4 or IPv6 to search route to.
+ * \param via Out: gateway (first hop) on the route, or AF_UNSPEC if same subnet.
+ * \param src Out: local outgoing IP address on the route.
+ * \param dev Out: local network interface on the route (you must pre-allocate IFNAMSIZ bytes!).
+ *
+ * \return 0 on success, negative errno otherwise.
+ */
+int ip_route_get(const struct sockaddr_storage *ip,
+ struct sockaddr_storage *via,
+ struct sockaddr_storage *src,
+ char *dev);
+
+/*!
+ * \brief Obtain neighbour's MAC addr from system neighbour table.
+ *
+ * \param ip IPv4 or IPv6 of the neighbour in question.
+ * \param dummy_sendto Attempt sendto() to target IP in order to let the system fill the neighbour table.
+ * \param mac Out: MAC address of the neighbour (you must pre-allocate ETH_ALEN bytes!).
+ *
+ * \return 0 on success, -ENOENT if neighbour not found, negative errno otherwise.
+ */
+int ip_neigh_get(const struct sockaddr_storage *ip, bool dummy_sendto, uint8_t *mac);
diff --git a/src/utils/kxdpgun/load_queries.c b/src/utils/kxdpgun/load_queries.c
new file mode 100644
index 0000000..fb421f9
--- /dev/null
+++ b/src/utils/kxdpgun/load_queries.c
@@ -0,0 +1,159 @@
+/* Copyright (C) 2022 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/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "load_queries.h"
+#include "libknot/libknot.h"
+#include "utils/common/msg.h"
+
+#define ERR_PREFIX "failed loading queries "
+
+enum qflags {
+ QFLAG_EDNS = 1,
+ QFLAG_DO = 2,
+};
+
+struct pkt_payload *global_payloads = NULL;
+
+void free_global_payloads()
+{
+ struct pkt_payload *g_payloads_p = global_payloads, *tmp;
+ while (g_payloads_p != NULL) {
+ tmp = g_payloads_p;
+ g_payloads_p = tmp->next;
+ free(tmp);
+ }
+ global_payloads = NULL;
+}
+
+bool load_queries(const char *filename, uint16_t edns_size, uint16_t msgid)
+{
+ FILE *f = fopen(filename, "r");
+ if (f == NULL) {
+ ERR2(ERR_PREFIX "file '%s' (%s)", filename, strerror(errno));
+ return false;
+ }
+ struct pkt_payload *g_payloads_top = NULL;
+
+ struct {
+ char line[KNOT_DNAME_TXT_MAXLEN + 256];
+ char dname_txt[KNOT_DNAME_TXT_MAXLEN + 1];
+ uint8_t dname[KNOT_DNAME_MAXLEN];
+ char type_txt[128];
+ char flags_txt[128];
+ } *bufs;
+ bufs = malloc(sizeof(*bufs)); // avoiding too much stuff on stack
+ if (bufs == NULL) {
+ ERR2(ERR_PREFIX "(out of memory)");
+ goto fail;
+ }
+
+ while (fgets(bufs->line, sizeof(bufs->line), f) != NULL) {
+ bufs->flags_txt[0] = '\0';
+ int ret = sscanf(bufs->line, "%s%s%s", bufs->dname_txt, bufs->type_txt, bufs->flags_txt);
+ if (ret < 2) {
+ ERR2(ERR_PREFIX "(faulty line): '%.*s'",
+ (int)strcspn(bufs->line, "\n"), bufs->line);
+ goto fail;
+ }
+
+ void *pret = knot_dname_from_str(bufs->dname, bufs->dname_txt, sizeof(bufs->dname));
+ if (pret == NULL) {
+ ERR2(ERR_PREFIX "(faulty dname): '%s'", bufs->dname_txt);
+ goto fail;
+ }
+
+ uint16_t type;
+ ret = knot_rrtype_from_string(bufs->type_txt, &type);
+ if (ret < 0) {
+ ERR2(ERR_PREFIX "(faulty type): '%s'", bufs->type_txt);
+ goto fail;
+ }
+
+ enum qflags flags = 0;
+ switch (bufs->flags_txt[0]) {
+ case '\0':
+ break;
+ case 'e':
+ case 'E':
+ flags |= QFLAG_EDNS;
+ break;
+ case 'd':
+ case 'D':
+ flags |= QFLAG_EDNS | QFLAG_DO;
+ break;
+ default:
+ ERR2(ERR_PREFIX "(faulty flag): '%s'", bufs->flags_txt);
+ goto fail;
+ }
+
+ size_t dname_len = knot_dname_size(bufs->dname);
+ size_t pkt_len = KNOT_WIRE_HEADER_SIZE + 2 * sizeof(uint16_t) + dname_len;
+ if (flags & QFLAG_EDNS) {
+ pkt_len += KNOT_EDNS_MIN_SIZE;
+ }
+
+ struct pkt_payload *pkt = calloc(1, sizeof(struct pkt_payload) + pkt_len);
+ if (pkt == NULL) {
+ ERR2(ERR_PREFIX "(out of memory)");
+ goto fail;
+ }
+ pkt->len = pkt_len;
+ memcpy(pkt->payload, &msgid, sizeof(msgid));
+ pkt->payload[2] = 0x01; // QR bit
+ pkt->payload[5] = 0x01; // 1 question
+ pkt->payload[11] = (flags & QFLAG_EDNS) ? 0x01 : 0x00;
+ memcpy(pkt->payload + 12, bufs->dname, dname_len);
+ pkt->payload[dname_len + 12] = type >> 8;
+ pkt->payload[dname_len + 13] = type & 0xff;
+ pkt->payload[dname_len + 15] = KNOT_CLASS_IN;
+ if (flags & QFLAG_EDNS) {
+ pkt->payload[dname_len + 18] = KNOT_RRTYPE_OPT;
+ pkt->payload[dname_len + 19] = edns_size >> 8;
+ pkt->payload[dname_len + 20] = edns_size & 0xff;
+ pkt->payload[dname_len + 23] = (flags & QFLAG_DO) ? 0x80 : 0x00;
+ }
+
+ // add pkt to list global_payloads
+ if (g_payloads_top == NULL) {
+ global_payloads = pkt;
+ g_payloads_top = pkt;
+ } else {
+ g_payloads_top->next = pkt;
+ g_payloads_top = pkt;
+ }
+ }
+
+ if (global_payloads == NULL) {
+ ERR2(ERR_PREFIX "(no queries in file)");
+ goto fail;
+ }
+
+ free(bufs);
+ fclose(f);
+ return true;
+
+fail:
+ free_global_payloads();
+ free(bufs);
+ fclose(f);
+ return false;
+}
diff --git a/src/utils/kxdpgun/load_queries.h b/src/utils/kxdpgun/load_queries.h
new file mode 100644
index 0000000..368686b
--- /dev/null
+++ b/src/utils/kxdpgun/load_queries.h
@@ -0,0 +1,32 @@
+/* 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 <stdint.h>
+
+struct pkt_payload {
+ struct pkt_payload *next;
+ size_t len;
+ uint8_t payload[];
+};
+
+extern struct pkt_payload *global_payloads;
+
+bool load_queries(const char *filename, uint16_t edns_size, uint16_t msgid);
+
+void free_global_payloads(void);
diff --git a/src/utils/kxdpgun/main.c b/src/utils/kxdpgun/main.c
new file mode 100644
index 0000000..2ec0c0c
--- /dev/null
+++ b/src/utils/kxdpgun/main.c
@@ -0,0 +1,1302 @@
+/* Copyright (C) 2023 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/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <getopt.h>
+#include <ifaddrs.h>
+#include <inttypes.h>
+#include <net/if.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/resource.h>
+
+#include "libknot/libknot.h"
+#include "libknot/xdp.h"
+#include "libknot/xdp/tcp_iobuf.h"
+#ifdef ENABLE_QUIC
+#include <gnutls/gnutls.h>
+#include "libknot/xdp/quic.h"
+#endif // ENABLE_QUIC
+#include "contrib/macros.h"
+#include "contrib/mempattern.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/openbsd/strlcpy.h"
+#include "contrib/os.h"
+#include "contrib/sockaddr.h"
+#include "contrib/toeplitz.h"
+#include "contrib/ucw/mempool.h"
+#include "utils/common/msg.h"
+#include "utils/common/params.h"
+#include "utils/kxdpgun/ip_route.h"
+#include "utils/kxdpgun/load_queries.h"
+
+#define PROGRAM_NAME "kxdpgun"
+#define SPACE " "
+
+enum {
+ KXDPGUN_WAIT,
+ KXDPGUN_START,
+ KXDPGUN_STOP,
+};
+
+volatile int xdp_trigger = KXDPGUN_WAIT;
+
+volatile unsigned stats_trigger = 0;
+
+unsigned global_cpu_aff_start = 0;
+unsigned global_cpu_aff_step = 1;
+
+#define REMOTE_PORT_DEFAULT 53
+#define REMOTE_PORT_DOQ_DEFAULT 853
+#define LOCAL_PORT_MIN 2000
+#define LOCAL_PORT_MAX 65535
+#define QUIC_THREAD_PORTS 100
+
+#define RCODE_MAX (0x0F + 1)
+
+typedef struct {
+ size_t collected;
+ uint64_t duration;
+ uint64_t qry_sent;
+ uint64_t synack_recv;
+ uint64_t ans_recv;
+ uint64_t finack_recv;
+ uint64_t rst_recv;
+ uint64_t size_recv;
+ uint64_t wire_recv;
+ uint64_t rcodes_recv[RCODE_MAX];
+ pthread_mutex_t mutex;
+} kxdpgun_stats_t;
+
+static kxdpgun_stats_t global_stats = { 0 };
+
+typedef enum {
+ KXDPGUN_IGNORE_NONE = 0,
+ KXDPGUN_IGNORE_QUERY = (1 << 0),
+ KXDPGUN_IGNORE_LASTBYTE = (1 << 1),
+ KXDPGUN_IGNORE_CLOSE = (1 << 2),
+} xdp_gun_ignore_t;
+
+typedef struct {
+ union {
+ struct sockaddr_in local_ip4;
+ struct sockaddr_in6 local_ip;
+ struct sockaddr_storage local_ip_ss;
+ };
+ union {
+ struct sockaddr_in target_ip4;
+ struct sockaddr_in6 target_ip;
+ struct sockaddr_storage target_ip_ss;
+ };
+ char dev[IFNAMSIZ];
+ uint64_t qps, duration;
+ unsigned at_once;
+ uint16_t msgid;
+ uint16_t edns_size;
+ uint16_t vlan_tci;
+ uint8_t local_mac[6], target_mac[6];
+ uint8_t local_ip_range;
+ bool ipv6;
+ bool tcp;
+ bool quic;
+ bool quic_full_handshake;
+ const char *sending_mode;
+ xdp_gun_ignore_t ignore1;
+ knot_tcp_ignore_t ignore2;
+ uint16_t target_port;
+ knot_xdp_filter_flag_t flags;
+ unsigned n_threads, thread_id;
+ knot_eth_rss_conf_t *rss_conf;
+} xdp_gun_ctx_t;
+
+const static xdp_gun_ctx_t ctx_defaults = {
+ .dev[0] = '\0',
+ .edns_size = 1232,
+ .qps = 1000,
+ .duration = 5000000UL, // usecs
+ .at_once = 10,
+ .sending_mode = "",
+ .target_port = 0,
+ .flags = KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_PASS,
+};
+
+static void sigterm_handler(int signo)
+{
+ assert(signo == SIGTERM || signo == SIGINT);
+ xdp_trigger = KXDPGUN_STOP;
+}
+
+static void sigusr_handler(int signo)
+{
+ assert(signo == SIGUSR1);
+ if (global_stats.collected == 0) {
+ stats_trigger++;
+ }
+}
+
+static void clear_stats(kxdpgun_stats_t *st)
+{
+ pthread_mutex_lock(&st->mutex);
+ st->duration = 0;
+ st->qry_sent = 0;
+ st->synack_recv = 0;
+ st->ans_recv = 0;
+ st->finack_recv = 0;
+ st->rst_recv = 0;
+ st->size_recv = 0;
+ st->wire_recv = 0;
+ st->collected = 0;
+ memset(st->rcodes_recv, 0, sizeof(st->rcodes_recv));
+ pthread_mutex_unlock(&st->mutex);
+}
+
+static size_t collect_stats(kxdpgun_stats_t *into, const kxdpgun_stats_t *what)
+{
+ pthread_mutex_lock(&into->mutex);
+ into->duration = MAX(into->duration, what->duration);
+ into->qry_sent += what->qry_sent;
+ into->synack_recv += what->synack_recv;
+ into->ans_recv += what->ans_recv;
+ into->finack_recv += what->finack_recv;
+ into->rst_recv += what->rst_recv;
+ into->size_recv += what->size_recv;
+ into->wire_recv += what->wire_recv;
+ for (int i = 0; i < RCODE_MAX; i++) {
+ into->rcodes_recv[i] += what->rcodes_recv[i];
+ }
+ size_t res = ++into->collected;
+ pthread_mutex_unlock(&into->mutex);
+ return res;
+}
+
+static void print_stats(kxdpgun_stats_t *st, bool tcp, bool quic, bool recv)
+{
+ pthread_mutex_lock(&st->mutex);
+
+#define ps(counter) ((counter) * 1000 / (st->duration / 1000))
+#define pct(counter) ((counter) * 100 / st->qry_sent)
+
+ const char *name = tcp ? "SYNs: " : quic ? "initials:" : "queries: ";
+ printf("total %s %"PRIu64" (%"PRIu64" pps)\n", name,
+ st->qry_sent, ps(st->qry_sent));
+ if (st->qry_sent > 0 && recv) {
+ if (tcp || quic) {
+ name = tcp ? "established:" : "handshakes: ";
+ printf("total %s %"PRIu64" (%"PRIu64" pps) (%"PRIu64"%%)\n", name,
+ st->synack_recv, ps(st->synack_recv), pct(st->synack_recv));
+ }
+ printf("total replies: %"PRIu64" (%"PRIu64" pps) (%"PRIu64"%%)\n",
+ st->ans_recv, ps(st->ans_recv), pct(st->ans_recv));
+ if (tcp) {
+ printf("total closed: %"PRIu64" (%"PRIu64" pps) (%"PRIu64"%%)\n",
+ st->finack_recv, ps(st->finack_recv), pct(st->finack_recv));
+ }
+ if (st->rst_recv > 0) {
+ printf("total reset: %"PRIu64" (%"PRIu64" pps) (%"PRIu64"%%)\n",
+ st->rst_recv, ps(st->rst_recv), pct(st->rst_recv));
+ }
+ printf("average DNS reply size: %"PRIu64" B\n",
+ st->ans_recv > 0 ? st->size_recv / st->ans_recv : 0);
+ printf("average Ethernet reply rate: %"PRIu64" bps (%.2f Mbps)\n",
+ ps(st->wire_recv * 8), ps((float)st->wire_recv * 8 / (1000 * 1000)));
+
+ for (int i = 0; i < RCODE_MAX; i++) {
+ if (st->rcodes_recv[i] > 0) {
+ const knot_lookup_t *rcode = knot_lookup_by_id(knot_rcode_names, i);
+ const char *rcname = rcode == NULL ? "unknown" : rcode->name;
+ int space = MAX(9 - strlen(rcname), 0);
+ printf("responded %s: %.*s%"PRIu64"\n",
+ rcname, space, " ", st->rcodes_recv[i]);
+ }
+ }
+ }
+ printf("duration: %"PRIu64" s\n", (st->duration / (1000 * 1000)));
+
+ pthread_mutex_unlock(&st->mutex);
+}
+
+inline static void timer_start(struct timespec *timesp)
+{
+ clock_gettime(CLOCK_MONOTONIC, timesp);
+}
+
+inline static uint64_t timer_end(struct timespec *timesp)
+{
+ struct timespec end;
+ clock_gettime(CLOCK_MONOTONIC, &end);
+ uint64_t res = (end.tv_sec - timesp->tv_sec) * (uint64_t)1000000;
+ res += ((int64_t)end.tv_nsec - timesp->tv_nsec) / 1000;
+ return res;
+}
+
+static unsigned addr_bits(bool ipv6)
+{
+ return ipv6 ? 128 : 32;
+}
+
+static void shuffle_sockaddr4(struct sockaddr_in *dst, struct sockaddr_in *src, uint64_t increment)
+{
+ memcpy(&dst->sin_addr, &src->sin_addr, sizeof(dst->sin_addr));
+ if (increment > 0) {
+ dst->sin_addr.s_addr = htobe32(be32toh(src->sin_addr.s_addr) + increment);
+ }
+}
+
+static void shuffle_sockaddr6(struct sockaddr_in6 *dst, struct sockaddr_in6 *src, uint64_t increment)
+{
+ memcpy(&dst->sin6_addr, &src->sin6_addr, sizeof(dst->sin6_addr));
+ if (increment > 0) {
+ uint64_t *dst_addr = (uint64_t *)&dst->sin6_addr;
+ uint64_t *src_addr = (uint64_t *)&src->sin6_addr;
+ dst_addr[1] = htobe64(be64toh(src_addr[1]) + increment);
+ }
+}
+
+static void shuffle_sockaddr(struct sockaddr_in6 *dst, struct sockaddr_in6 *src,
+ uint16_t port, uint64_t increment)
+{
+ dst->sin6_family = src->sin6_family;
+ dst->sin6_port = htobe16(port);
+ if (src->sin6_family == AF_INET6) {
+ shuffle_sockaddr6(dst, src, increment);
+ } else {
+ shuffle_sockaddr4((struct sockaddr_in *)dst, (struct sockaddr_in *)src, increment);
+ }
+}
+
+static void next_payload(struct pkt_payload **payload, int increment)
+{
+ if (*payload == NULL) {
+ *payload = global_payloads;
+ }
+ for (int i = 0; i < increment; i++) {
+ if ((*payload)->next == NULL) {
+ *payload = global_payloads;
+ } else {
+ *payload = (*payload)->next;
+ }
+ }
+}
+
+static void put_dns_payload(struct iovec *put_into, bool zero_copy, xdp_gun_ctx_t *ctx, struct pkt_payload **payl)
+{
+ if (zero_copy) {
+ put_into->iov_base = (*payl)->payload;
+ } else {
+ memcpy(put_into->iov_base, (*payl)->payload, (*payl)->len);
+ }
+ put_into->iov_len = (*payl)->len;
+ next_payload(payl, ctx->n_threads);
+}
+
+#ifdef ENABLE_QUIC
+static uint16_t get_rss_id(xdp_gun_ctx_t *ctx, uint16_t local_port)
+{
+ assert(ctx->rss_conf);
+
+ const uint8_t *key = (const uint8_t *)&(ctx->rss_conf->data[ctx->rss_conf->table_size]);
+ const size_t key_len = ctx->rss_conf->key_size;
+ uint8_t data[2 * sizeof(struct in6_addr) + 2 * sizeof(uint16_t)];
+
+ size_t addr_len;
+ if (ctx->ipv6) {
+ addr_len = sizeof(struct in6_addr);
+ memcpy(data, &ctx->target_ip.sin6_addr, addr_len);
+ memcpy(data + addr_len, &ctx->local_ip.sin6_addr, addr_len);
+ } else {
+ addr_len = sizeof(struct in_addr);
+ memcpy(data, &ctx->target_ip4.sin_addr, addr_len);
+ memcpy(data + addr_len, &ctx->local_ip4.sin_addr, addr_len);
+ }
+
+ uint16_t src_port = htobe16(ctx->target_port);
+ memcpy(data + 2 * addr_len, &src_port, sizeof(src_port));
+ uint16_t dst_port = htobe16(local_port);
+ memcpy(data + 2 * addr_len + sizeof(uint16_t), &dst_port, sizeof(dst_port));
+
+ size_t data_len = 2 * addr_len + 2 * sizeof(uint16_t);
+ uint16_t hash = toeplitz_hash(key, key_len, data, data_len);
+
+ return ctx->rss_conf->data[hash & ctx->rss_conf->mask];
+}
+
+static uint16_t adjust_port(xdp_gun_ctx_t *ctx, uint16_t local_port)
+{
+ assert(UINT16_MAX == LOCAL_PORT_MAX);
+
+ if (local_port < LOCAL_PORT_MIN) {
+ local_port = LOCAL_PORT_MIN;
+ }
+
+ if (ctx->rss_conf == NULL) {
+ return local_port;
+ }
+
+ for (int i = 0; i < UINT16_MAX; i++) {
+ if (ctx->thread_id == get_rss_id(ctx, local_port)) {
+ break;
+ }
+ local_port++;
+ if (local_port < LOCAL_PORT_MIN) {
+ local_port = LOCAL_PORT_MIN;
+ }
+ }
+
+ return local_port;
+}
+#endif // ENABLE_QUIC
+
+static unsigned alloc_pkts(knot_xdp_msg_t *pkts, struct knot_xdp_socket *xsk,
+ xdp_gun_ctx_t *ctx, uint64_t tick)
+{
+ uint64_t unique = (tick * ctx->n_threads + ctx->thread_id) * ctx->at_once;
+
+ knot_xdp_msg_flag_t flags = ctx->ipv6 ? KNOT_XDP_MSG_IPV6 : 0;
+ if (ctx->tcp) {
+ flags |= (KNOT_XDP_MSG_TCP | KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_MSS);
+ } else if (ctx->quic) {
+ return ctx->at_once; // NOOP
+ }
+ if (ctx->vlan_tci != 0) {
+ flags |= KNOT_XDP_MSG_VLAN;
+ }
+
+ for (unsigned i = 0; i < ctx->at_once; i++) {
+ int ret = knot_xdp_send_alloc(xsk, flags, &pkts[i]);
+ if (ret != KNOT_EOK) {
+ return i;
+ }
+
+ uint16_t port_range = LOCAL_PORT_MAX - LOCAL_PORT_MIN + 1;
+ uint16_t local_port = LOCAL_PORT_MIN + unique % port_range;
+ uint64_t ip_incr = (unique / port_range) % (1 << (addr_bits(ctx->ipv6) - ctx->local_ip_range));
+ shuffle_sockaddr(&pkts[i].ip_from, &ctx->local_ip, local_port, ip_incr);
+ shuffle_sockaddr(&pkts[i].ip_to, &ctx->target_ip, ctx->target_port, 0);
+
+ memcpy(pkts[i].eth_from, ctx->local_mac, 6);
+ memcpy(pkts[i].eth_to, ctx->target_mac, 6);
+
+ pkts[i].vlan_tci = ctx->vlan_tci;
+
+ unique++;
+ }
+ return ctx->at_once;
+}
+
+inline static bool check_dns_payload(struct iovec *payl, xdp_gun_ctx_t *ctx,
+ kxdpgun_stats_t *st)
+{
+ if (payl->iov_len < KNOT_WIRE_HEADER_SIZE ||
+ memcmp(payl->iov_base, &ctx->msgid, sizeof(ctx->msgid)) != 0) {
+ return false;
+ }
+ st->rcodes_recv[((uint8_t *)payl->iov_base)[3] & 0x0F]++;
+ st->size_recv += payl->iov_len;
+ st->ans_recv++;
+ return true;
+}
+
+void *xdp_gun_thread(void *_ctx)
+{
+ xdp_gun_ctx_t *ctx = _ctx;
+ struct knot_xdp_socket *xsk;
+ struct timespec timer;
+ knot_xdp_msg_t pkts[ctx->at_once];
+ uint64_t errors = 0, lost = 0, duration = 0;
+ kxdpgun_stats_t local_stats = { 0 };
+ unsigned stats_triggered = 0;
+ knot_tcp_table_t *tcp_table = NULL;
+#ifdef ENABLE_QUIC
+ knot_xquic_table_t *quic_table = NULL;
+ struct knot_quic_creds *quic_creds = NULL;
+ knot_xdp_msg_t quic_fake_req = { 0 };
+ list_t quic_sessions;
+ init_list(&quic_sessions);
+#endif // ENABLE_QUIC
+ const uint64_t extra_wait = ctx->quic ? 4000000 : 1000000;
+
+ if (ctx->tcp) {
+ tcp_table = knot_tcp_table_new(ctx->qps, NULL);
+ if (tcp_table == NULL) {
+ ERR2("failed to allocate TCP connection table");
+ return NULL;
+ }
+ }
+ if (ctx->quic) {
+#ifdef ENABLE_QUIC
+ quic_creds = knot_xquic_init_creds(false, NULL, NULL);
+ if (quic_creds == NULL) {
+ ERR2("failed to initialize QUIC context");
+ return NULL;
+ }
+ quic_table = knot_xquic_table_new(ctx->qps * 100, SIZE_MAX, SIZE_MAX, 1232, quic_creds);
+ if (quic_table == NULL) {
+ ERR2("failed to allocate QUIC connection table");
+ return NULL;
+ }
+ ctx->target_ip.sin6_port = htobe16(ctx->target_port);
+
+ memcpy(quic_fake_req.eth_from, ctx->target_mac, sizeof(ctx->target_mac));
+ memcpy(quic_fake_req.eth_to, ctx->local_mac, sizeof(ctx->local_mac));
+ memcpy(&quic_fake_req.ip_from, &ctx->target_ip, sizeof(quic_fake_req.ip_from));
+ memcpy(&quic_fake_req.ip_to, &ctx->local_ip, sizeof(quic_fake_req.ip_to));
+ quic_fake_req.flags = ctx->ipv6 ? KNOT_XDP_MSG_IPV6 : 0;
+#else
+ assert(0);
+#endif // ENABLE_QUIC
+ }
+
+ knot_xdp_load_bpf_t mode = (ctx->thread_id == 0 ?
+ KNOT_XDP_LOAD_BPF_ALWAYS : KNOT_XDP_LOAD_BPF_NEVER);
+ /*
+ * This mutex prevents libbpf from logging:
+ * 'libbpf: can't get link by id (5535): Resource temporarily unavailable'
+ */
+ pthread_mutex_lock(&global_stats.mutex);
+ int ret = knot_xdp_init(&xsk, ctx->dev, ctx->thread_id, ctx->flags,
+ LOCAL_PORT_MIN, LOCAL_PORT_MIN, mode, NULL);
+ pthread_mutex_unlock(&global_stats.mutex);
+ if (ret != KNOT_EOK) {
+ ERR2("failed to initialize XDP socket#%u (%s)",
+ ctx->thread_id, knot_strerror(ret));
+ knot_tcp_table_free(tcp_table);
+ return NULL;
+ }
+
+ if (ctx->thread_id == 0) {
+ INFO2("using interface %s, XDP threads %u, %s%s%s, %s mode",
+ ctx->dev, ctx->n_threads,
+ (ctx->tcp ? "TCP" : ctx->quic ? "QUIC" : "UDP"),
+ (ctx->sending_mode[0] != '\0' ? " mode " : ""),
+ (ctx->sending_mode[0] != '\0' ? ctx->sending_mode : ""),
+ (knot_eth_xdp_mode(if_nametoindex(ctx->dev)) == KNOT_XDP_MODE_FULL ?
+ "native" : "emulated"));
+ }
+
+ struct pollfd pfd = { knot_xdp_socket_fd(xsk), POLLIN, 0 };
+
+ while (xdp_trigger == KXDPGUN_WAIT) {
+ usleep(1000);
+ }
+
+ uint64_t tick = 0;
+ struct pkt_payload *payload_ptr = NULL;
+ next_payload(&payload_ptr, ctx->thread_id);
+
+#ifdef ENABLE_QUIC
+ knot_sweep_stats_t sweep_stats = { 0 };
+ uint16_t local_ports[QUIC_THREAD_PORTS];
+ uint16_t port = LOCAL_PORT_MIN;
+ for (int i = 0; i < QUIC_THREAD_PORTS; ++i) {
+ local_ports[i] = adjust_port(ctx, port);
+ port = local_ports[i] + 1;
+ assert(port >= LOCAL_PORT_MIN);
+ }
+ size_t local_ports_it = 0;
+#endif // ENABLE_QUIC
+
+ timer_start(&timer);
+
+ while (duration < ctx->duration + extra_wait) {
+
+ // sending part
+ if (duration < ctx->duration) {
+ while (1) {
+ knot_xdp_send_prepare(xsk);
+ unsigned alloced = alloc_pkts(pkts, xsk, ctx, tick);
+ if (alloced < ctx->at_once) {
+ lost += ctx->at_once - alloced;
+ if (alloced == 0) {
+ break;
+ }
+ }
+
+ if (ctx->tcp) {
+ for (int i = 0; i < alloced; i++) {
+ pkts[i].payload.iov_len = 0;
+ }
+ } else if (ctx->quic) {
+#ifdef ENABLE_QUIC
+ uint16_t local_port = local_ports[local_ports_it++ % QUIC_THREAD_PORTS];
+ for (unsigned i = 0; i < ctx->at_once; i++) {
+ knot_xquic_conn_t *newconn = NULL;
+ ctx->local_ip.sin6_port = htobe16(local_port);
+ ret = knot_xquic_client(quic_table, &ctx->target_ip, &ctx->local_ip, &newconn);
+ if (ret == KNOT_EOK) {
+ struct iovec tmp = { knot_xquic_stream_add_data(newconn, 0, NULL, payload_ptr->len), 0 };
+ put_dns_payload(&tmp, false, ctx, &payload_ptr);
+ if (EMPTY_LIST(quic_sessions)) {
+ newconn->streams_count = -1;
+ } else {
+ void *session = HEAD(quic_sessions);
+ rem_node(session);
+ (void)knot_xquic_session_load(newconn, session);
+ }
+ quic_fake_req.ip_to.sin6_port = htobe16(local_port);
+ ret = knot_xquic_send(quic_table, newconn, xsk, &quic_fake_req, KNOT_EOK, 1, (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE));
+ }
+ if (ret == KNOT_EOK) {
+ local_stats.qry_sent++;
+ }
+ }
+ (void)knot_xdp_send_finish(xsk);
+#endif // ENABLE_QUIC
+ break;
+ } else {
+ for (int i = 0; i < alloced; i++) {
+ put_dns_payload(&pkts[i].payload, false,
+ ctx, &payload_ptr);
+ }
+ }
+
+ uint32_t really_sent = 0;
+ if (knot_xdp_send(xsk, pkts, alloced, &really_sent) != KNOT_EOK) {
+ lost += alloced;
+ }
+ local_stats.qry_sent += really_sent;
+ (void)knot_xdp_send_finish(xsk);
+
+ break;
+ }
+ }
+
+ // receiving part
+ if (!(ctx->flags & KNOT_XDP_FILTER_DROP)) {
+ while (1) {
+ ret = poll(&pfd, 1, 0);
+ if (ret < 0) {
+ errors++;
+ break;
+ }
+ if (!pfd.revents) {
+ break;
+ }
+
+ uint32_t recvd = 0;
+ size_t wire = 0;
+ (void)knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd, &wire);
+ if (recvd == 0) {
+ break;
+ }
+ if (ctx->tcp) {
+ knot_tcp_relay_t relays[recvd];
+ ret = knot_tcp_recv(relays, pkts, recvd, tcp_table, NULL, ctx->ignore2);
+ if (ret != KNOT_EOK) {
+ errors++;
+ break;
+ }
+
+ for (size_t i = 0; i < recvd; i++) {
+ knot_tcp_relay_t *rl = &relays[i];
+ struct iovec payl;
+ switch (rl->action) {
+ case XDP_TCP_ESTABLISH:
+ local_stats.synack_recv++;
+ if (ctx->ignore1 & KXDPGUN_IGNORE_QUERY) {
+ break;
+ }
+ put_dns_payload(&payl, true, ctx, &payload_ptr);
+ ret = knot_tcp_reply_data(rl, tcp_table,
+ (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE),
+ payl.iov_base, payl.iov_len);
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+ break;
+ case XDP_TCP_CLOSE:
+ local_stats.finack_recv++;
+ break;
+ case XDP_TCP_RESET:
+ local_stats.rst_recv++;
+ break;
+ default:
+ break;
+ }
+ for (size_t j = 0; j < rl->inbufs_count; j++) {
+ if (check_dns_payload(&rl->inbufs[j], ctx, &local_stats)) {
+ if (!(ctx->ignore1 & KXDPGUN_IGNORE_CLOSE)) {
+ rl->answer = XDP_TCP_CLOSE;
+ }
+ }
+ }
+ }
+
+ ret = knot_tcp_send(xsk, relays, recvd, ctx->at_once);
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+ (void)knot_xdp_send_finish(xsk);
+
+ knot_tcp_cleanup(tcp_table, relays, recvd);
+ } else if (ctx->quic) {
+#ifdef ENABLE_QUIC
+ knot_xquic_conn_t *relays[recvd];
+ for (size_t i = 0; i < recvd; i++) {
+ ret = knot_xquic_handle(quic_table, &pkts[i], 5000000000L, &relays[i]);
+ if (ret == KNOT_ECONN) {
+ local_stats.rst_recv++;
+ continue;
+ } else if (ret != 0) {
+ errors++;
+ break;
+ }
+
+ knot_xquic_conn_t *rl = relays[i];
+ if (rl == NULL) {
+ continue;
+ }
+
+ bool sess_ticket = (gnutls_session_get_flags(rl->tls_session) & GNUTLS_SFLAGS_SESSION_TICKET);
+
+ if (sess_ticket && !rl->session_taken && !ctx->quic_full_handshake) {
+ rl->session_taken = true;
+ void *session = knot_xquic_session_save(rl);
+ if (session != NULL) {
+ add_tail(&quic_sessions, session);
+ }
+ }
+
+ if (rl->handshake_done && rl->streams_count == -1) {
+ rl->streams_count = 1;
+
+ local_stats.synack_recv++;
+ if ((ctx->ignore1 & KXDPGUN_IGNORE_QUERY)) {
+ knot_xquic_table_rem(relays[i], quic_table);
+ knot_xquic_cleanup(&relays[i], 1);
+ relays[i] = NULL;
+ continue;
+ }
+ }
+ if (!rl->handshake_done && rl->streams_count == -1) {
+ continue;
+ }
+
+ knot_xquic_stream_t *stream0 = knot_xquic_conn_get_stream(rl, 0, false);
+ assert(stream0 != NULL);
+
+ if ((ctx->ignore2 & XDP_TCP_IGNORE_ESTABLISH)) {
+ knot_xquic_table_rem(relays[i], quic_table);
+ knot_xquic_cleanup(&relays[i], 1);
+ relays[i] = NULL;
+ local_stats.synack_recv++;
+ continue;
+ }
+
+ stream0 = knot_xquic_conn_get_stream(rl, 0, false);
+ if (stream0 != NULL && stream0->inbuf_fin != NULL) {
+ check_dns_payload(stream0->inbuf_fin, ctx, &local_stats);
+ free(stream0->inbuf_fin);
+ stream0->inbuf_fin = NULL;
+
+ if ((ctx->ignore2 & XDP_TCP_IGNORE_DATA_ACK)) {
+ knot_xquic_table_rem(relays[i], quic_table);
+ knot_xquic_cleanup(&relays[i], 1);
+ relays[i] = NULL;
+ continue;
+ }
+ }
+ ret = knot_xquic_send(quic_table, rl, xsk, &pkts[i], KNOT_EOK, 4, (ctx->ignore1 & KXDPGUN_IGNORE_LASTBYTE));
+ if (ret != KNOT_EOK) {
+ errors++;
+ }
+ }
+ knot_xquic_cleanup(relays, recvd);
+ (void)knot_xdp_send_finish(xsk);
+#endif // ENABLE_QUIC
+ } else {
+ for (int i = 0; i < recvd; i++) {
+ (void)check_dns_payload(&pkts[i].payload, ctx,
+ &local_stats);
+ }
+ }
+ local_stats.wire_recv += wire;
+ knot_xdp_recv_finish(xsk, pkts, recvd);
+ pfd.revents = 0;
+ }
+ }
+
+#ifdef ENABLE_QUIC
+ if (ctx->quic) {
+ (void)knot_xquic_table_sweep(quic_table, &sweep_stats);
+ }
+#endif // ENABLE_QUIC
+
+ // speed and signal part
+ uint64_t dura_exp = (local_stats.qry_sent * 1000000) / ctx->qps;
+ duration = timer_end(&timer);
+ if (xdp_trigger == KXDPGUN_STOP && ctx->duration > duration) {
+ ctx->duration = duration;
+ }
+ if (stats_trigger > stats_triggered) {
+ assert(stats_trigger == stats_triggered + 1);
+ stats_triggered++;
+
+ local_stats.duration = duration;
+ size_t collected = collect_stats(&global_stats, &local_stats);
+ assert(collected <= ctx->n_threads);
+ if (collected == ctx->n_threads) {
+ print_stats(&global_stats, ctx->tcp, ctx->quic,
+ !(ctx->flags & KNOT_XDP_FILTER_DROP));
+ clear_stats(&global_stats);
+ }
+ }
+ if (dura_exp > duration) {
+ usleep(dura_exp - duration);
+ }
+ if (duration > ctx->duration) {
+ usleep(1000);
+ }
+ tick++;
+ }
+
+ knot_xdp_deinit(xsk);
+
+ knot_tcp_table_free(tcp_table);
+#ifdef ENABLE_QUIC
+ knot_xquic_table_free(quic_table);
+ struct knot_quic_session *n, *nxt;
+ WALK_LIST_DELSAFE(n, nxt, quic_sessions) {
+ knot_xquic_session_load(NULL, n);
+ }
+ knot_xquic_free_creds(quic_creds);
+#endif // ENABLE_QUIC
+
+ char recv_str[40] = "", lost_str[40] = "", err_str[40] = "";
+ if (!(ctx->flags & KNOT_XDP_FILTER_DROP)) {
+ (void)snprintf(recv_str, sizeof(recv_str), ", received %"PRIu64, local_stats.ans_recv);
+ }
+ if (lost > 0) {
+ (void)snprintf(lost_str, sizeof(lost_str), ", lost %"PRIu64, lost);
+ }
+ if (errors > 0) {
+ (void)snprintf(err_str, sizeof(err_str), ", errors %"PRIu64, errors);
+ }
+ INFO2("thread#%02u: sent %"PRIu64"%s%s%s",
+ ctx->thread_id, local_stats.qry_sent, recv_str, lost_str, err_str);
+ local_stats.duration = ctx->duration;
+ collect_stats(&global_stats, &local_stats);
+
+ return NULL;
+}
+
+static int dev2mac(const char *dev, uint8_t *mac)
+{
+ struct ifreq ifr;
+ memset(&ifr, 0, sizeof(ifr));
+ int fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ return -errno;
+ }
+ strlcpy(ifr.ifr_name, dev, IFNAMSIZ);
+
+ int ret = ioctl(fd, SIOCGIFHWADDR, &ifr);
+ if (ret >= 0) {
+ memcpy(mac, ifr.ifr_hwaddr.sa_data, 6);
+ } else {
+ ret = -errno;
+ }
+ close(fd);
+ return ret;
+}
+
+static bool mac_empty(const uint8_t *mac)
+{
+ static const uint8_t unset_mac[6] = { 0 };
+ return (memcmp(mac, unset_mac, sizeof(unset_mac)) == 0);
+}
+
+static int mac_sscan(const char *src, uint8_t *dst)
+{
+ int tmp[6];
+ if (6 != sscanf(src, "%2x:%2x:%2x:%2x:%2x:%2x",
+ &tmp[0], &tmp[1], &tmp[2], &tmp[3], &tmp[4], &tmp[5])) {
+ return KNOT_EINVAL;
+ }
+
+ for (int i = 0; i < 6; i++) {
+ dst[i] = (uint8_t)tmp[i];
+ }
+
+ return KNOT_EOK;
+}
+
+static bool configure_target(char *target_str, char *local_ip, xdp_gun_ctx_t *ctx)
+{
+ int val;
+ char *at = strrchr(target_str, '@');
+ if (at != NULL && (val = atoi(at + 1)) > 0 && val <= 0xffff) {
+ ctx->target_port = val;
+ *at = '\0';
+ }
+
+ ctx->ipv6 = false;
+ if (inet_pton(AF_INET, target_str, &ctx->target_ip4.sin_addr) <= 0) {
+ ctx->ipv6 = true;
+ ctx->target_ip.sin6_family = AF_INET6;
+ if (inet_pton(AF_INET6, target_str, &ctx->target_ip.sin6_addr) <= 0) {
+ ERR2("invalid target IP");
+ return false;
+ }
+ } else {
+ ctx->target_ip.sin6_family = AF_INET;
+ }
+
+ struct sockaddr_storage via = { 0 };
+ if (local_ip == NULL || ctx->dev[0] == '\0' || mac_empty(ctx->target_mac)) {
+ char auto_dev[IFNAMSIZ];
+ int ret = ip_route_get(&ctx->target_ip_ss,
+ &via,
+ &ctx->local_ip_ss,
+ (ctx->dev[0] == '\0') ? ctx->dev : auto_dev);
+ if (ret < 0) {
+ ERR2("can't find route to '%s' (%s)", target_str, strerror(-ret));
+ return false;
+ }
+ }
+
+ ctx->local_ip_range = addr_bits(ctx->ipv6); // by default use one IP
+ if (local_ip != NULL) {
+ at = strrchr(local_ip, '/');
+ if (at != NULL && (val = atoi(at + 1)) > 0 && val <= ctx->local_ip_range) {
+ ctx->local_ip_range = val;
+ *at = '\0';
+ }
+ if (ctx->ipv6) {
+ if (ctx->local_ip_range < 64 ||
+ inet_pton(AF_INET6, local_ip, &ctx->local_ip.sin6_addr) <= 0) {
+ ERR2("invalid local IPv6 or unsupported prefix length");
+ return false;
+ }
+ } else {
+ if (inet_pton(AF_INET, local_ip, &ctx->local_ip4.sin_addr) <= 0) {
+ ERR2("invalid local IPv4");
+ return false;
+ }
+ }
+ }
+
+ if (mac_empty(ctx->target_mac)) {
+ const struct sockaddr_storage *neigh = (via.ss_family == AF_UNSPEC) ?
+ &ctx->target_ip_ss : &via;
+ int ret = ip_neigh_get(neigh, true, ctx->target_mac);
+ if (ret < 0) {
+ char neigh_str[256] = { 0 };
+ sockaddr_tostr(neigh_str, sizeof(neigh_str), (struct sockaddr_storage *)neigh);
+ ERR2("failed to get remote MAC of target/gateway '%s' (%s)",
+ neigh_str, strerror(-ret));
+ return false;
+ }
+ }
+
+ if (mac_empty(ctx->local_mac)) {
+ int ret = dev2mac(ctx->dev, ctx->local_mac);
+ if (ret < 0) {
+ ERR2("failed to get MAC of device '%s' (%s)", ctx->dev, strerror(-ret));
+ return false;
+ }
+ }
+
+ int ret = knot_eth_queues(ctx->dev);
+ if (ret >= 0) {
+ ctx->n_threads = ret;
+ } else {
+ ERR2("unable to get number of queues for '%s' (%s)", ctx->dev,
+ knot_strerror(ret));
+ return false;
+ }
+
+ if (ctx->n_threads > 1 && ctx->quic) {
+ ret = knot_eth_rss(ctx->dev, &ctx->rss_conf);
+ if (ret != 0) {
+ WARN2("unable to read NIC RSS configuration for '%s' (%s)",
+ ctx->dev, knot_strerror(ret));
+ }
+ }
+
+ return true;
+}
+
+static void print_help(void)
+{
+ printf("Usage: %s [parameters] -i <queries_file> <dest_ip>\n"
+ "\n"
+ "Parameters:\n"
+ " -t, --duration <sec> "SPACE"Duration of traffic generation.\n"
+ " "SPACE" (default is %"PRIu64" seconds)\n"
+ " -T, --tcp[=debug_mode] "SPACE"Send queries over TCP.\n"
+ " -U, --quic[=debug_mode] "SPACE"Send queries over QUIC.\n"
+ " -Q, --qps <qps> "SPACE"Number of queries-per-second (approximately) to be sent.\n"
+ " "SPACE" (default is %"PRIu64" qps)\n"
+ " -b, --batch <size> "SPACE"Send queries in a batch of defined size.\n"
+ " "SPACE" (default is %d for UDP, %d for TCP)\n"
+ " -r, --drop "SPACE"Drop incoming responses (disables response statistics).\n"
+ " -p, --port <port> "SPACE"Remote destination port.\n"
+ " "SPACE" (default is %d for UDP/TCP, %u for QUIC)\n"
+ " -F, --affinity <spec> "SPACE"CPU affinity in the format [<cpu_start>][s<cpu_step>].\n"
+ " "SPACE" (default is %s)\n"
+ " -i, --infile <file> "SPACE"Path to a file with query templates.\n"
+ " -I, --interface <ifname> "SPACE"Override auto-detected interface for outgoing communication.\n"
+ " -l, --local <ip[/prefix]>"SPACE"Override auto-detected source IP address or subnet.\n"
+ " -L, --local-mac <MAC> "SPACE"Override auto-detected local MAC address.\n"
+ " -R, --remote-mac <MAC> "SPACE"Override auto-detected remote MAC address.\n"
+ " -v, --vlan <id> "SPACE"Add VLAN 802.1Q header with the given id.\n"
+ " -h, --help "SPACE"Print the program help.\n"
+ " -V, --version "SPACE"Print the program version.\n"
+ "\n"
+ "Arguments:\n"
+ " <dest_ip> "SPACE"IPv4 or IPv6 address of the remote destination.\n",
+ PROGRAM_NAME, ctx_defaults.duration / 1000000, ctx_defaults.qps,
+ ctx_defaults.at_once, 1, REMOTE_PORT_DEFAULT, REMOTE_PORT_DOQ_DEFAULT, "0s1");
+}
+
+static bool sending_mode(const char *arg, xdp_gun_ctx_t *ctx)
+{
+ if (arg == NULL) {
+ ctx->sending_mode = "";
+ return true;
+ } else if (strlen(arg) != 1) {
+ goto mode_invalid;
+ }
+ ctx->sending_mode = arg;
+
+ switch (ctx->sending_mode[0]) {
+ case '0':
+ if (!ctx->quic) {
+ goto mode_unavailable;
+ }
+ ctx->quic_full_handshake = true;
+ break;
+ case '1':
+ ctx->ignore1 = KXDPGUN_IGNORE_QUERY;
+ ctx->ignore2 = XDP_TCP_IGNORE_ESTABLISH | XDP_TCP_IGNORE_FIN;
+ break;
+ case '2':
+ ctx->ignore1 = KXDPGUN_IGNORE_QUERY;
+ break;
+ case '3':
+ ctx->ignore1 = KXDPGUN_IGNORE_QUERY;
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case '5':
+ ctx->ignore1 = KXDPGUN_IGNORE_LASTBYTE;
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case '7':
+ ctx->ignore1 = KXDPGUN_IGNORE_CLOSE;
+ ctx->ignore2 = XDP_TCP_IGNORE_DATA_ACK | XDP_TCP_IGNORE_FIN;
+ break;
+ case '8':
+ if (!ctx->tcp) {
+ goto mode_unavailable;
+ }
+ ctx->ignore1 = KXDPGUN_IGNORE_CLOSE;
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ case '9':
+ if (!ctx->tcp) {
+ goto mode_unavailable;
+ }
+ ctx->ignore2 = XDP_TCP_IGNORE_FIN;
+ break;
+ default:
+ goto mode_invalid;
+ }
+
+ return true;
+mode_unavailable:
+ ERR2("mode '%s' not available", optarg);
+ return false;
+mode_invalid:
+ ERR2("invalid mode '%s'", optarg);
+ return false;
+}
+
+static bool get_opts(int argc, char *argv[], xdp_gun_ctx_t *ctx)
+{
+ struct option opts[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { "duration", required_argument, NULL, 't' },
+ { "qps", required_argument, NULL, 'Q' },
+ { "batch", required_argument, NULL, 'b' },
+ { "drop", no_argument, NULL, 'r' },
+ { "port", required_argument, NULL, 'p' },
+ { "tcp", optional_argument, NULL, 'T' },
+ { "quic", optional_argument, NULL, 'U' },
+ { "affinity", required_argument, NULL, 'F' },
+ { "interface", required_argument, NULL, 'I' },
+ { "local", required_argument, NULL, 'l' },
+ { "infile", required_argument, NULL, 'i' },
+ { "local-mac", required_argument, NULL, 'L' },
+ { "remote-mac", required_argument, NULL, 'R' },
+ { "vlan", required_argument, NULL, 'v' },
+ { NULL }
+ };
+
+ int opt = 0, arg;
+ bool default_at_once = true;
+ double argf;
+ char *argcp, *local_ip = NULL;
+ while ((opt = getopt_long(argc, argv, "hVt:Q:b:rp:T::U::F:I:l:i:L:R:v:", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'h':
+ print_help();
+ exit(EXIT_SUCCESS);
+ case 'V':
+ print_version(PROGRAM_NAME);
+ exit(EXIT_SUCCESS);
+ case 't':
+ assert(optarg);
+ argf = atof(optarg);
+ if (argf > 0) {
+ ctx->duration = argf * 1000000.0;
+ assert(ctx->duration >= 1000);
+ } else {
+ ERR2("invalid duration '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'Q':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0) {
+ ctx->qps = arg;
+ } else {
+ ERR2("invalid QPS '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'b':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0) {
+ default_at_once = false;
+ ctx->at_once = arg;
+ } else {
+ ERR2("invalid batch size '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'r':
+ ctx->flags &= ~KNOT_XDP_FILTER_PASS;
+ ctx->flags |= KNOT_XDP_FILTER_DROP;
+ break;
+ case 'p':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0 && arg <= 0xffff) {
+ ctx->target_port = arg;
+ } else {
+ ERR2("invalid port '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'T':
+ ctx->tcp = true;
+ ctx->quic = false;
+ ctx->flags &= ~(KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_QUIC);
+ ctx->flags |= KNOT_XDP_FILTER_TCP;
+ if (default_at_once) {
+ ctx->at_once = 1;
+ }
+ if (!sending_mode(optarg, ctx)) {
+ return false;
+ }
+ break;
+ case 'U':
+#ifdef ENABLE_QUIC
+ ctx->quic = true;
+ ctx->tcp = false;
+ ctx->flags &= ~(KNOT_XDP_FILTER_UDP | KNOT_XDP_FILTER_TCP);
+ ctx->flags |= KNOT_XDP_FILTER_QUIC;
+ if (ctx->target_port == 0) {
+ ctx->target_port = REMOTE_PORT_DOQ_DEFAULT;
+ }
+ if (default_at_once) {
+ ctx->at_once = 1;
+ }
+ if (!sending_mode(optarg, ctx)) {
+ return false;
+ }
+#else
+ ERR2("QUIC not available");
+ return false;
+#endif // ENABLE_QUIC
+ break;
+ case 'F':
+ assert(optarg);
+ if ((arg = atoi(optarg)) > 0) {
+ global_cpu_aff_start = arg;
+ }
+ argcp = strchr(optarg, 's');
+ if (argcp != NULL && (arg = atoi(argcp + 1)) > 0) {
+ global_cpu_aff_step = arg;
+ }
+ break;
+ case 'I':
+ strlcpy(ctx->dev, optarg, IFNAMSIZ);
+ break;
+ case 'l':
+ local_ip = optarg;
+ break;
+ case 'i':
+ if (!load_queries(optarg, ctx->edns_size, ctx->msgid)) {
+ return false;
+ }
+ break;
+ case 'L':
+ if (mac_sscan(optarg, ctx->local_mac) != KNOT_EOK) {
+ ERR2("invalid local MAC address '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'R':
+ if (mac_sscan(optarg, ctx->target_mac) != KNOT_EOK) {
+ ERR2("invalid remote MAC address '%s'", optarg);
+ return false;
+ }
+ break;
+ case 'v':
+ assert(optarg);
+ arg = atoi(optarg);
+ if (arg > 0 && arg < 4095) {
+ uint16_t id = arg;
+ ctx->vlan_tci = htobe16(id);
+ } else {
+ ERR2("invalid VLAN id '%s'", optarg);
+ return false;
+ }
+ break;
+ default:
+ print_help();
+ return false;
+ }
+ }
+ if (global_payloads == NULL || argc - optind != 1) {
+ print_help();
+ return false;
+ }
+
+ if (ctx->target_port == 0) {
+ ctx->target_port = REMOTE_PORT_DEFAULT;
+ }
+
+ if (!configure_target(argv[optind], local_ip, ctx)) {
+ return false;
+ }
+
+ if (ctx->qps < ctx->n_threads) {
+ WARN2("QPS increased to the number of threads/queues: %u", ctx->n_threads);
+ ctx->qps = ctx->n_threads;
+ }
+ ctx->qps /= ctx->n_threads;
+
+ return true;
+}
+
+int main(int argc, char *argv[])
+{
+ xdp_gun_ctx_t ctx = ctx_defaults, *thread_ctxs = NULL;
+ ctx.msgid = time(NULL) % UINT16_MAX;
+ pthread_t *threads = NULL;
+
+ if (!get_opts(argc, argv, &ctx)) {
+ free_global_payloads();
+ return EXIT_FAILURE;
+ }
+
+ thread_ctxs = calloc(ctx.n_threads, sizeof(*thread_ctxs));
+ threads = calloc(ctx.n_threads, sizeof(*threads));
+ if (thread_ctxs == NULL || threads == NULL) {
+ ERR2("out of memory");
+ free(thread_ctxs);
+ free(threads);
+ free_global_payloads();
+ return EXIT_FAILURE;
+ }
+ for (int i = 0; i < ctx.n_threads; i++) {
+ thread_ctxs[i] = ctx;
+ thread_ctxs[i].thread_id = i;
+ }
+
+ if (!linux_at_least(5, 11)) {
+ struct rlimit min_limit = { RLIM_INFINITY, RLIM_INFINITY }, cur_limit = { 0 };
+ if (getrlimit(RLIMIT_MEMLOCK, &cur_limit) != 0 ||
+ cur_limit.rlim_cur != min_limit.rlim_cur ||
+ cur_limit.rlim_max != min_limit.rlim_max) {
+ int ret = setrlimit(RLIMIT_MEMLOCK, &min_limit);
+ if (ret != 0) {
+ WARN2("unable to increase RLIMIT_MEMLOCK: %s",
+ strerror(errno));
+ }
+ }
+ }
+
+ pthread_mutex_init(&global_stats.mutex, NULL);
+
+ struct sigaction stop_action = { .sa_handler = sigterm_handler };
+ struct sigaction stats_action = { .sa_handler = sigusr_handler };
+ sigaction(SIGINT, &stop_action, NULL);
+ sigaction(SIGTERM, &stop_action, NULL);
+ sigaction(SIGUSR1, &stats_action, NULL);
+
+ for (size_t i = 0; i < ctx.n_threads; i++) {
+ unsigned affinity = global_cpu_aff_start + i * global_cpu_aff_step;
+ cpu_set_t set;
+ CPU_ZERO(&set);
+ CPU_SET(affinity, &set);
+ (void)pthread_create(&threads[i], NULL, xdp_gun_thread, &thread_ctxs[i]);
+ int ret = pthread_setaffinity_np(threads[i], sizeof(cpu_set_t), &set);
+ if (ret != 0) {
+ WARN2("failed to set affinity of thread#%zu to CPU#%u", i, affinity);
+ }
+ usleep(20000);
+ }
+ usleep(1000000);
+
+ xdp_trigger = KXDPGUN_START;
+ usleep(1000000);
+
+ for (size_t i = 0; i < ctx.n_threads; i++) {
+ pthread_join(threads[i], NULL);
+ }
+ if (global_stats.duration > 0 && global_stats.qry_sent > 0) {
+ print_stats(&global_stats, ctx.tcp, ctx.quic, !(ctx.flags & KNOT_XDP_FILTER_DROP));
+ }
+ pthread_mutex_destroy(&global_stats.mutex);
+
+ free(ctx.rss_conf);
+ free(thread_ctxs);
+ free(threads);
+ free_global_payloads();
+
+ return EXIT_SUCCESS;
+}