From 8420f93bd3f4b1a32bb3df573016597550492024 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Tue, 14 Jul 2026 22:26:07 +0000 Subject: [PATCH 01/14] tc: make netlink_qdisc_add idempotent Remove NLM_F_EXCL so adding the clsact qdisc succeeds even when it already exists, e.g. from a previous endpoint run or a third-party tool. --- non-GPL/HostIsolation/Lib/TcLoader.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index f984440a..9cf7d656 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -414,7 +414,7 @@ static int netlink_qdisc(int cmd, unsigned int flags, const char *ifname) int netlink_qdisc_add(const char *ifname) { - return netlink_qdisc(RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE, ifname); + return netlink_qdisc(RTM_NEWQDISC, NLM_F_CREATE, ifname); } int netlink_qdisc_del(const char *ifname) From fe545e8ce30a02abd38cefb2d05bcc2069f74a18 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Tue, 14 Jul 2026 22:26:34 +0000 Subject: [PATCH 02/14] tc: add netlink_filter_exists/del for selective filter removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ELASTIC_TC_FILTER_MARKER ("el-endpo") and two new functions: - netlink_filter_exists: checks if an interface has our BPF tc filter - netlink_filter_del: removes only our BPF tc filter by marker match Both scan RTM_GETTFILTER dumps on ingress and egress parents and match by TCA_BPF_NAME substring. TCA_KIND is intentionally omitted from the dump request — some Linux 6.x kernels silently drop dumps that carry TCA_KIND, causing recvmsg to block forever. --- non-GPL/HostIsolation/Lib/TcLoader.c | 348 +++++++++++++++++++++++++++ non-GPL/HostIsolation/Lib/TcLoader.h | 19 ++ 2 files changed, 367 insertions(+) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 9cf7d656..7188958b 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -37,6 +37,7 @@ #define TC_H_MAKE(maj, min) (((maj)&TC_H_MAJ_MASK) | ((min)&TC_H_MIN_MASK)) #define TC_H_INGRESS (0xFFFFFFF1U) #define TC_H_CLSACT TC_H_INGRESS +#define TC_H_MIN_INGRESS 0xFFF2U #define TC_H_MIN_EGRESS 0xFFF3U #define TCA_BPF_FLAG_ACT_DIRECT (1 << 0) @@ -58,6 +59,22 @@ enum { #define NLMSG_TAIL(nmsg) ((struct rtattr *)(((char *)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))) +static void parse_rtattr(struct rtattr **tb, int max, struct rtattr *rta, int len) +{ + memset(tb, 0, sizeof(struct rtattr *) * (max + 1)); + while (RTA_OK(rta, len)) { + if (rta->rta_type <= max) { + tb[rta->rta_type] = rta; + } + rta = RTA_NEXT(rta, len); + } +} + +static void parse_rtattr_nested(struct rtattr **tb, int max, struct rtattr *rta) +{ + parse_rtattr(tb, max, (struct rtattr *)RTA_DATA(rta), RTA_PAYLOAD(rta)); +} + static int attr_put(struct nlmsghdr *n, size_t max, int type, const void *buf, size_t attr_len) { size_t len = RTA_LENGTH(attr_len); @@ -522,3 +539,334 @@ int netlink_filter_add_end(int fd, struct netlink_ctx *ctx) } return rv; } + +static int netlink_filter_exists_on_parent( + const char *ifname, + __u32 parent, + const char *marker_name) +{ + int rv = -1; + int found = 0; + struct rtnetlink_handle filter_rth = {.fd = -1}; + struct netlink_msg req = { + .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), + .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .n.nlmsg_type = RTM_GETTFILTER, + .t.tcm_family = AF_UNSPEC, + }; + struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; + struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + unsigned int seq = 0; + int done = 0; + + if (!ifname || !marker_name) { + ebpf_log("netlink_filter_exists_on_parent error: NULL parameter\n"); + rv = -1; + goto out; + } + + if (rtnetlink_open(&filter_rth) < 0) { + ebpf_log("failed to open netlink\n"); + rv = -1; + goto out; + } + + req.t.tcm_ifindex = if_nametoindex(ifname); + if (0 == req.t.tcm_ifindex) { + ebpf_log("failed to find device %s\n", ifname); + rv = -1; + goto out; + } + req.t.tcm_parent = parent; + + /* Do not filter by TCA_KIND in the dump request; enumerate all filters + * and match by name in userspace. Specifying TCA_KIND on a dump can + * cause some kernels to silently drop the request. */ + + req.n.nlmsg_seq = seq = ++filter_rth.seq; + if (sendmsg(filter_rth.fd, &msg, 0) < 0) { + ebpf_log("failure talking to rtnetlink\n"); + rv = -1; + goto out; + } + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + while (!done) { + char *buf = NULL; + ssize_t recv_len = rtnetlink_recv(filter_rth.fd, &msg, &buf); + + if (recv_len <= 0) { + rv = -1; + goto out; + } + + for (struct nlmsghdr *h = (struct nlmsghdr *)buf; + NLMSG_OK(h, (unsigned int)recv_len); + h = NLMSG_NEXT(h, recv_len)) { + if (h->nlmsg_seq != seq || h->nlmsg_pid != filter_rth.local.nl_pid) { + continue; + } + + if (h->nlmsg_type == NLMSG_DONE) { + done = 1; + break; + } + + if (h->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(h); + if (err->error) { + rtnetlink_send_error(err); + free(buf); + rv = -1; + goto out; + } + continue; + } + + if (h->nlmsg_type != RTM_NEWTFILTER && h->nlmsg_type != RTM_GETTFILTER) { + continue; + } + + struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); + int len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); + struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); + const char *kind = NULL; + struct rtattr *options = NULL; + + for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { + if (rta->rta_type == TCA_KIND) { + kind = (const char *)RTA_DATA(rta); + } else if (rta->rta_type == TCA_OPTIONS) { + options = rta; + } + } + + if (kind && options && !strcmp(kind, "bpf")) { + struct rtattr *tb[__TCA_BPF_MAX + 1]; + parse_rtattr_nested(tb, __TCA_BPF_MAX, options); + if (tb[TCA_BPF_NAME]) { + const char *name = (const char *)RTA_DATA(tb[TCA_BPF_NAME]); + if (name && strstr(name, marker_name)) { + found = 1; + free(buf); + rv = 1; + goto out; + } + } + } + } + free(buf); + } + + rv = found ? 1 : 0; +out: + rtnetlink_close(&filter_rth); + return rv; +} + +int netlink_filter_exists(const char *ifname, const char *marker_name) +{ + int rv_ingress = netlink_filter_exists_on_parent( + ifname, + TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS), + marker_name); + if (rv_ingress == 1) { + return 1; + } + int rv_egress = netlink_filter_exists_on_parent( + ifname, + TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS), + marker_name); + if (rv_egress == 1) { + return 1; + } + if (rv_ingress < 0 || rv_egress < 0) { + return -1; + } + return 0; +} + +static int netlink_filter_del_on_parent( + const char *ifname, + __u32 parent, + const char *marker_name) +{ + int rv = -1; + struct rtnetlink_handle filter_rth = {.fd = -1}; + struct rtnetlink_handle del_rth = {.fd = -1}; + struct netlink_msg req = { + .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), + .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .n.nlmsg_type = RTM_GETTFILTER, + .t.tcm_family = AF_UNSPEC, + }; + struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; + struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + unsigned int seq = 0; + int done = 0; + unsigned int ifindex = 0; + + if (!ifname || !marker_name) { + ebpf_log("netlink_filter_del_on_parent error: NULL parameter\n"); + rv = -1; + goto out; + } + + if (rtnetlink_open(&filter_rth) < 0) { + ebpf_log("failed to open netlink for listing\n"); + rv = -1; + goto out; + } + + ifindex = if_nametoindex(ifname); + if (0 == ifindex) { + ebpf_log("failed to find device %s\n", ifname); + rv = -1; + goto out; + } + + req.t.tcm_ifindex = ifindex; + req.t.tcm_parent = parent; + + /* Do not filter by TCA_KIND in the dump; enumerate all and match by name. */ + + req.n.nlmsg_seq = seq = ++filter_rth.seq; + if (sendmsg(filter_rth.fd, &msg, 0) < 0) { + ebpf_log("failure talking to rtnetlink\n"); + rv = -1; + goto out; + } + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + while (!done) { + char *buf = NULL; + ssize_t recv_len = rtnetlink_recv(filter_rth.fd, &msg, &buf); + + if (recv_len <= 0) { + rv = -1; + goto out; + } + + for (struct nlmsghdr *h = (struct nlmsghdr *)buf; + NLMSG_OK(h, (unsigned int)recv_len); + h = NLMSG_NEXT(h, recv_len)) { + /* kernel dump responses may have nlmsg_pid == 0 or our portid */ + if (h->nlmsg_seq != seq || + (h->nlmsg_pid != 0 && h->nlmsg_pid != filter_rth.local.nl_pid)) { + continue; + } + + if (h->nlmsg_type == NLMSG_DONE) { + done = 1; + break; + } + + if (h->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(h); + if (err->error) { + rtnetlink_send_error(err); + free(buf); + rv = -1; + goto out; + } + continue; + } + + if (h->nlmsg_type != RTM_NEWTFILTER && h->nlmsg_type != RTM_GETTFILTER) { + continue; + } + + struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); + int len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); + struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); + const char *kind = NULL; + struct rtattr *options = NULL; + + for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { + if (rta->rta_type == TCA_KIND) { + kind = (const char *)RTA_DATA(rta); + } else if (rta->rta_type == TCA_OPTIONS) { + options = rta; + } + } + + if (kind && options && !strcmp(kind, "bpf")) { + struct rtattr *tb[__TCA_BPF_MAX + 1]; + parse_rtattr_nested(tb, __TCA_BPF_MAX, options); + if (tb[TCA_BPF_NAME]) { + const char *name = (const char *)RTA_DATA(tb[TCA_BPF_NAME]); + if (name && strstr(name, marker_name)) { + /* Found our filter - delete it using RTM_DELTFILTER */ + struct netlink_msg del_req = { + .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), + .n.nlmsg_flags = NLM_F_REQUEST, + .n.nlmsg_type = RTM_DELTFILTER, + .t.tcm_family = AF_UNSPEC, + .t.tcm_ifindex = ifindex, + .t.tcm_parent = parent, + .t.tcm_handle = t->tcm_handle, + .t.tcm_info = t->tcm_info, + }; + attr_put(&del_req.n, sizeof(del_req), TCA_KIND, "bpf", + strlen("bpf") + 1); + free(buf); + buf = NULL; + + if (rtnetlink_open(&del_rth) < 0) { + ebpf_log("failed to open netlink for delete\n"); + rv = -1; + goto out; + } + if (rtnetlink_send(&del_rth, &del_req.n) < 0) { + ebpf_log("failed to delete tc filter\n"); + rv = -1; + goto out; + } + rv = 0; + goto out; + } + } + } + } + free(buf); + } + + /* Filter not found - not an error, nothing to delete */ + rv = 0; +out: + rtnetlink_close(&filter_rth); + rtnetlink_close(&del_rth); + return rv; +} + +int netlink_filter_del(const char *ifname, const char *marker_name) +{ + int rv_egress = netlink_filter_del_on_parent( + ifname, + TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS), + marker_name); + int rv_ingress = netlink_filter_del_on_parent( + ifname, + TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS), + marker_name); + if (rv_egress < 0 || rv_ingress < 0) { + return -1; + } + return 0; +} diff --git a/non-GPL/HostIsolation/Lib/TcLoader.h b/non-GPL/HostIsolation/Lib/TcLoader.h index 1ae2f6d1..2ff36af8 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.h +++ b/non-GPL/HostIsolation/Lib/TcLoader.h @@ -18,6 +18,7 @@ /* maximum netlink message size */ #define MAX_MSG 16384 +#define ELASTIC_TC_FILTER_MARKER "el-endpo" struct rtnetlink_handle { int fd; @@ -80,4 +81,22 @@ int netlink_filter_add_begin(struct netlink_ctx *ctx, const char *ifname); * @return Error value (0 for success) */ int netlink_filter_add_end(int fd, struct netlink_ctx *ctx); + +/** + * @brief Check if an interface has our eBPF tc filter attached + * + * @param[in] ifname Network interface name + * @param[in] marker_name Substring to match against TCA_BPF_NAME + * @return 1 if found, 0 if not found, -1 on error + */ +int netlink_filter_exists(const char *ifname, const char *marker_name); + +/** + * @brief Delete our eBPF tc filter from a network interface + * + * @param[in] ifname Network interface name + * @param[in] marker_name Substring to match against TCA_BPF_NAME + * @return 0 on success, -1 on error + */ +int netlink_filter_del(const char *ifname, const char *marker_name); #endif From d18394d467776c6a5ae84091b8f9be5f6a5a4761 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Wed, 15 Jul 2026 16:14:48 +0200 Subject: [PATCH 03/14] clang-format TcLoader.c Co-Authored-By: Claude Sonnet 4.6 --- non-GPL/HostIsolation/Lib/TcLoader.c | 128 ++++++++++++--------------- 1 file changed, 56 insertions(+), 72 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 7188958b..541fa5cd 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -32,9 +32,9 @@ #define TC_H_MAJ_MASK (0xFFFF0000U) #define TC_H_MIN_MASK (0x0000FFFFU) -#define TC_H_MAJ(h) ((h)&TC_H_MAJ_MASK) -#define TC_H_MIN(h) ((h)&TC_H_MIN_MASK) -#define TC_H_MAKE(maj, min) (((maj)&TC_H_MAJ_MASK) | ((min)&TC_H_MIN_MASK)) +#define TC_H_MAJ(h) ((h) & TC_H_MAJ_MASK) +#define TC_H_MIN(h) ((h) & TC_H_MIN_MASK) +#define TC_H_MAKE(maj, min) (((maj) & TC_H_MAJ_MASK) | ((min) & TC_H_MIN_MASK)) #define TC_H_INGRESS (0xFFFFFFF1U) #define TC_H_CLSACT TC_H_INGRESS #define TC_H_MIN_INGRESS 0xFFF2U @@ -540,27 +540,25 @@ int netlink_filter_add_end(int fd, struct netlink_ctx *ctx) return rv; } -static int netlink_filter_exists_on_parent( - const char *ifname, - __u32 parent, - const char *marker_name) +static int +netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *marker_name) { - int rv = -1; - int found = 0; + int rv = -1; + int found = 0; struct rtnetlink_handle filter_rth = {.fd = -1}; - struct netlink_msg req = { - .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), - .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, - .n.nlmsg_type = RTM_GETTFILTER, - .t.tcm_family = AF_UNSPEC, + struct netlink_msg req = { + .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), + .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .n.nlmsg_type = RTM_GETTFILTER, + .t.tcm_family = AF_UNSPEC, }; struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; - struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; - struct msghdr msg = { - .msg_name = &nladdr, - .msg_namelen = sizeof(nladdr), - .msg_iov = &iov, - .msg_iovlen = 1, + struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = &iov, + .msg_iovlen = 1, }; unsigned int seq = 0; int done = 0; @@ -596,21 +594,20 @@ static int netlink_filter_exists_on_parent( goto out; } - msg.msg_iov = &iov; + msg.msg_iov = &iov; msg.msg_iovlen = 1; while (!done) { - char *buf = NULL; - ssize_t recv_len = rtnetlink_recv(filter_rth.fd, &msg, &buf); + char *buf = NULL; + ssize_t recv_len = rtnetlink_recv(filter_rth.fd, &msg, &buf); if (recv_len <= 0) { rv = -1; goto out; } - for (struct nlmsghdr *h = (struct nlmsghdr *)buf; - NLMSG_OK(h, (unsigned int)recv_len); - h = NLMSG_NEXT(h, recv_len)) { + for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, (unsigned int)recv_len); + h = NLMSG_NEXT(h, recv_len)) { if (h->nlmsg_seq != seq || h->nlmsg_pid != filter_rth.local.nl_pid) { continue; } @@ -635,10 +632,10 @@ static int netlink_filter_exists_on_parent( continue; } - struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); - int len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); - struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); - const char *kind = NULL; + struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); + int len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); + struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); + const char *kind = NULL; struct rtattr *options = NULL; for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { @@ -675,16 +672,12 @@ static int netlink_filter_exists_on_parent( int netlink_filter_exists(const char *ifname, const char *marker_name) { int rv_ingress = netlink_filter_exists_on_parent( - ifname, - TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS), - marker_name); + ifname, TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS), marker_name); if (rv_ingress == 1) { return 1; } - int rv_egress = netlink_filter_exists_on_parent( - ifname, - TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS), - marker_name); + int rv_egress = netlink_filter_exists_on_parent(ifname, TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS), + marker_name); if (rv_egress == 1) { return 1; } @@ -694,30 +687,27 @@ int netlink_filter_exists(const char *ifname, const char *marker_name) return 0; } -static int netlink_filter_del_on_parent( - const char *ifname, - __u32 parent, - const char *marker_name) +static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const char *marker_name) { - int rv = -1; + int rv = -1; struct rtnetlink_handle filter_rth = {.fd = -1}; struct rtnetlink_handle del_rth = {.fd = -1}; - struct netlink_msg req = { - .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), - .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, - .n.nlmsg_type = RTM_GETTFILTER, - .t.tcm_family = AF_UNSPEC, + struct netlink_msg req = { + .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), + .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .n.nlmsg_type = RTM_GETTFILTER, + .t.tcm_family = AF_UNSPEC, }; struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; - struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; - struct msghdr msg = { - .msg_name = &nladdr, - .msg_namelen = sizeof(nladdr), - .msg_iov = &iov, - .msg_iovlen = 1, + struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = &iov, + .msg_iovlen = 1, }; - unsigned int seq = 0; - int done = 0; + unsigned int seq = 0; + int done = 0; unsigned int ifindex = 0; if (!ifname || !marker_name) { @@ -763,9 +753,8 @@ static int netlink_filter_del_on_parent( goto out; } - for (struct nlmsghdr *h = (struct nlmsghdr *)buf; - NLMSG_OK(h, (unsigned int)recv_len); - h = NLMSG_NEXT(h, recv_len)) { + for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, (unsigned int)recv_len); + h = NLMSG_NEXT(h, recv_len)) { /* kernel dump responses may have nlmsg_pid == 0 or our portid */ if (h->nlmsg_seq != seq || (h->nlmsg_pid != 0 && h->nlmsg_pid != filter_rth.local.nl_pid)) { @@ -792,10 +781,10 @@ static int netlink_filter_del_on_parent( continue; } - struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); - int len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); - struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); - const char *kind = NULL; + struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); + int len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); + struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); + const char *kind = NULL; struct rtattr *options = NULL; for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { @@ -823,8 +812,7 @@ static int netlink_filter_del_on_parent( .t.tcm_handle = t->tcm_handle, .t.tcm_info = t->tcm_info, }; - attr_put(&del_req.n, sizeof(del_req), TCA_KIND, "bpf", - strlen("bpf") + 1); + attr_put(&del_req.n, sizeof(del_req), TCA_KIND, "bpf", strlen("bpf") + 1); free(buf); buf = NULL; @@ -857,14 +845,10 @@ static int netlink_filter_del_on_parent( int netlink_filter_del(const char *ifname, const char *marker_name) { - int rv_egress = netlink_filter_del_on_parent( - ifname, - TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS), - marker_name); - int rv_ingress = netlink_filter_del_on_parent( - ifname, - TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS), - marker_name); + int rv_egress = + netlink_filter_del_on_parent(ifname, TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_EGRESS), marker_name); + int rv_ingress = + netlink_filter_del_on_parent(ifname, TC_H_MAKE(TC_H_CLSACT, TC_H_MIN_INGRESS), marker_name); if (rv_egress < 0 || rv_ingress < 0) { return -1; } From df3fb0184099be42a9c7896b7b95b9da4cc3278c Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Thu, 16 Jul 2026 21:04:46 +0200 Subject: [PATCH 04/14] Fix clang-format for CI: use version 14.0.6 (not 21) CI uses clang-format 14.0.6 via pip. The previous formatting commit used v21 which formats macro binary-op spacing differently. Co-Authored-By: Claude Sonnet 4.6 --- non-GPL/HostIsolation/Lib/TcLoader.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 541fa5cd..23147ed7 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -32,9 +32,9 @@ #define TC_H_MAJ_MASK (0xFFFF0000U) #define TC_H_MIN_MASK (0x0000FFFFU) -#define TC_H_MAJ(h) ((h) & TC_H_MAJ_MASK) -#define TC_H_MIN(h) ((h) & TC_H_MIN_MASK) -#define TC_H_MAKE(maj, min) (((maj) & TC_H_MAJ_MASK) | ((min) & TC_H_MIN_MASK)) +#define TC_H_MAJ(h) ((h)&TC_H_MAJ_MASK) +#define TC_H_MIN(h) ((h)&TC_H_MIN_MASK) +#define TC_H_MAKE(maj, min) (((maj)&TC_H_MAJ_MASK) | ((min)&TC_H_MIN_MASK)) #define TC_H_INGRESS (0xFFFFFFF1U) #define TC_H_CLSACT TC_H_INGRESS #define TC_H_MIN_INGRESS 0xFFF2U From 9c153211f42d0d3731a074448e001edf002b2718 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Thu, 16 Jul 2026 22:53:12 +0200 Subject: [PATCH 05/14] tc: fix correctness issues in netlink_filter_{exists,del}_on_parent - netlink_filter_del_on_parent: stop on first match bug - open del_rth once before the loop and continue scanning after each delete instead of goto out, so all matching filters are removed - Both functions: check NLM_F_DUMP_INTR and NLMSG_DONE error payload to detect interrupted or failed netlink dumps - Both functions: reject empty marker_name to prevent strstr("") matching every BPF filter and causing mass deletion - netlink_filter_exists_on_parent: accept nlmsg_pid == 0 to match del's existing behaviour (some kernels send pid=0 in dump responses) - netlink_filter_exists_on_parent: remove dead found variable --- non-GPL/HostIsolation/Lib/TcLoader.c | 65 +++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 23147ed7..c70a7766 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -544,7 +544,6 @@ static int netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *marker_name) { int rv = -1; - int found = 0; struct rtnetlink_handle filter_rth = {.fd = -1}; struct netlink_msg req = { .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), @@ -569,6 +568,12 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma goto out; } + if (marker_name[0] == '\0') { + ebpf_log("netlink_filter_exists_on_parent error: empty marker_name\n"); + rv = -1; + goto out; + } + if (rtnetlink_open(&filter_rth) < 0) { ebpf_log("failed to open netlink\n"); rv = -1; @@ -608,11 +613,27 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, (unsigned int)recv_len); h = NLMSG_NEXT(h, recv_len)) { - if (h->nlmsg_seq != seq || h->nlmsg_pid != filter_rth.local.nl_pid) { + if (h->nlmsg_seq != seq || + (h->nlmsg_pid != 0 && h->nlmsg_pid != filter_rth.local.nl_pid)) { continue; } if (h->nlmsg_type == NLMSG_DONE) { + if (h->nlmsg_flags & NLM_F_DUMP_INTR) { + ebpf_log("netlink dump was interrupted\n"); + free(buf); + rv = -1; + goto out; + } + if (h->nlmsg_len >= NLMSG_LENGTH(sizeof(int))) { + int done_err = *(int *)NLMSG_DATA(h); + if (done_err) { + ebpf_log("netlink NLMSG_DONE error: %s\n", strerror(-done_err)); + free(buf); + rv = -1; + goto out; + } + } done = 1; break; } @@ -652,7 +673,6 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma if (tb[TCA_BPF_NAME]) { const char *name = (const char *)RTA_DATA(tb[TCA_BPF_NAME]); if (name && strstr(name, marker_name)) { - found = 1; free(buf); rv = 1; goto out; @@ -663,7 +683,7 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma free(buf); } - rv = found ? 1 : 0; + rv = 0; out: rtnetlink_close(&filter_rth); return rv; @@ -716,12 +736,24 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const goto out; } + if (marker_name[0] == '\0') { + ebpf_log("netlink_filter_del_on_parent error: empty marker_name\n"); + rv = -1; + goto out; + } + if (rtnetlink_open(&filter_rth) < 0) { ebpf_log("failed to open netlink for listing\n"); rv = -1; goto out; } + if (rtnetlink_open(&del_rth) < 0) { + ebpf_log("failed to open netlink for delete\n"); + rv = -1; + goto out; + } + ifindex = if_nametoindex(ifname); if (0 == ifindex) { ebpf_log("failed to find device %s\n", ifname); @@ -762,6 +794,21 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const } if (h->nlmsg_type == NLMSG_DONE) { + if (h->nlmsg_flags & NLM_F_DUMP_INTR) { + ebpf_log("netlink dump was interrupted\n"); + free(buf); + rv = -1; + goto out; + } + if (h->nlmsg_len >= NLMSG_LENGTH(sizeof(int))) { + int done_err = *(int *)NLMSG_DATA(h); + if (done_err) { + ebpf_log("netlink NLMSG_DONE error: %s\n", strerror(-done_err)); + free(buf); + rv = -1; + goto out; + } + } done = 1; break; } @@ -813,21 +860,15 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const .t.tcm_info = t->tcm_info, }; attr_put(&del_req.n, sizeof(del_req), TCA_KIND, "bpf", strlen("bpf") + 1); - free(buf); - buf = NULL; - if (rtnetlink_open(&del_rth) < 0) { - ebpf_log("failed to open netlink for delete\n"); - rv = -1; - goto out; - } if (rtnetlink_send(&del_rth, &del_req.n) < 0) { ebpf_log("failed to delete tc filter\n"); + free(buf); rv = -1; goto out; } rv = 0; - goto out; + /* continue scanning to delete all matching filters */ } } } From 362eb37d927e7744dc55fdd1d7851b719fe8a296 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Fri, 17 Jul 2026 04:17:19 +0200 Subject: [PATCH 06/14] tc: ignore NETLINK_EXT_ACK setsockopt failure on older kernels NETLINK_EXT_ACK was added in kernel 4.12; failing the socket open on ENOPROTOOPT breaks qdisc/filter operations on 4.5-4.11 kernels. Mirror iproute2 libnetlink.c which deliberately ignores the return value. Co-Authored-By: Claude Sonnet 4.6 --- non-GPL/HostIsolation/Lib/TcLoader.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index c70a7766..2f51284e 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -160,11 +160,8 @@ static int rtnetlink_open(struct rtnetlink_handle *rth) goto out; } - if (setsockopt(rth->fd, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one))) { - ebpf_log("error setsockopt netlink\n"); - rv = -1; - goto out; - } + /* Older kernels (< 4.12) may not support NETLINK_EXT_ACK; ignore failure */ + setsockopt(rth->fd, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)); memset(&rth->local, 0, sizeof(rth->local)); From 646dec18647063b38477a628f768ab8b471eec1d Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Fri, 17 Jul 2026 05:04:01 +0200 Subject: [PATCH 07/14] tc: ignore filters outside the default chain Endpoint attaches its tc BPF filters only to chain 0. Parse TCA_CHAIN from filter dumps and ignore entries from other chains so a nonzero-chain entry cannot drive a chain-0 deletion. --- non-GPL/HostIsolation/Lib/TcLoader.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 2f51284e..46708812 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -655,16 +655,25 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); const char *kind = NULL; struct rtattr *options = NULL; + __u32 chain_index = 0; + int chain_valid = 1; for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { if (rta->rta_type == TCA_KIND) { kind = (const char *)RTA_DATA(rta); } else if (rta->rta_type == TCA_OPTIONS) { options = rta; + } else if (rta->rta_type == TCA_CHAIN) { + if (RTA_PAYLOAD(rta) != sizeof(chain_index)) { + chain_valid = 0; + } else { + memcpy(&chain_index, RTA_DATA(rta), sizeof(chain_index)); + } } } - if (kind && options && !strcmp(kind, "bpf")) { + /* Endpoint attaches filters only to the default (chain 0) chain. */ + if (chain_valid && chain_index == 0 && kind && options && !strcmp(kind, "bpf")) { struct rtattr *tb[__TCA_BPF_MAX + 1]; parse_rtattr_nested(tb, __TCA_BPF_MAX, options); if (tb[TCA_BPF_NAME]) { @@ -830,16 +839,25 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); const char *kind = NULL; struct rtattr *options = NULL; + __u32 chain_index = 0; + int chain_valid = 1; for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { if (rta->rta_type == TCA_KIND) { kind = (const char *)RTA_DATA(rta); } else if (rta->rta_type == TCA_OPTIONS) { options = rta; + } else if (rta->rta_type == TCA_CHAIN) { + if (RTA_PAYLOAD(rta) != sizeof(chain_index)) { + chain_valid = 0; + } else { + memcpy(&chain_index, RTA_DATA(rta), sizeof(chain_index)); + } } } - if (kind && options && !strcmp(kind, "bpf")) { + /* Endpoint attaches filters only to the default (chain 0) chain. */ + if (chain_valid && chain_index == 0 && kind && options && !strcmp(kind, "bpf")) { struct rtattr *tb[__TCA_BPF_MAX + 1]; parse_rtattr_nested(tb, __TCA_BPF_MAX, options); if (tb[TCA_BPF_NAME]) { From ab031cfd70350ba6ed0439cfdb8f3b4828573b90 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Fri, 17 Jul 2026 05:06:48 +0200 Subject: [PATCH 08/14] tc: restart filter dumps after each deletion TC filter dumps use live cursors rather than snapshots. Delete only one matching filter per dump, then close the listing socket and start a fresh dump so stale Endpoint filters cannot be skipped. --- non-GPL/HostIsolation/Lib/TcLoader.c | 32 ++++++++++++++++++---------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 46708812..18e42b0c 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -748,12 +748,6 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const goto out; } - if (rtnetlink_open(&filter_rth) < 0) { - ebpf_log("failed to open netlink for listing\n"); - rv = -1; - goto out; - } - if (rtnetlink_open(&del_rth) < 0) { ebpf_log("failed to open netlink for delete\n"); rv = -1; @@ -772,6 +766,23 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const /* Do not filter by TCA_KIND in the dump; enumerate all and match by name. */ +restart_dump: + done = 0; + + if (rtnetlink_open(&filter_rth) < 0) { + ebpf_log("failed to open netlink for listing\n"); + rv = -1; + goto out; + } + + memset(&nladdr, 0, sizeof(nladdr)); + nladdr.nl_family = AF_NETLINK; + iov.iov_base = &req.n; + iov.iov_len = req.n.nlmsg_len; + msg.msg_namelen = sizeof(nladdr); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + req.n.nlmsg_seq = seq = ++filter_rth.seq; if (sendmsg(filter_rth.fd, &msg, 0) < 0) { ebpf_log("failure talking to rtnetlink\n"); @@ -779,9 +790,6 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const goto out; } - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - while (!done) { char *buf = NULL; ssize_t recv_len = rtnetlink_recv(filter_rth.fd, &msg, &buf); @@ -882,8 +890,10 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const rv = -1; goto out; } - rv = 0; - /* continue scanning to delete all matching filters */ + /* The dump cursor is no longer valid after deletion. */ + free(buf); + rtnetlink_close(&filter_rth); + goto restart_dump; } } } From 843189800fe52c0575c1c58dd3e8c269f22a4b7e Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Fri, 17 Jul 2026 05:07:41 +0200 Subject: [PATCH 09/14] tc: match Endpoint filter names by prefix Use one canonical el-endpo_ prefix when creating and identifying Endpoint tc filters. Prefix matching avoids deleting unrelated BPF filters that merely contain the shorter marker elsewhere in their names. --- non-GPL/HostIsolation/Lib/TcLoader.c | 6 +++--- non-GPL/HostIsolation/Lib/TcLoader.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 18e42b0c..e4a26574 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -509,7 +509,7 @@ int netlink_filter_add_end(int fd, struct netlink_ctx *ctx) nl = &ctx->msg.n; memset(buf, 0, sizeof(buf)); - len = snprintf(buf, sizeof(buf), "el-endpo_%s:[%u]", info.name, info.id); + len = snprintf(buf, sizeof(buf), ELASTIC_TC_FILTER_MARKER "%s:[%u]", info.name, info.id); if (len < 0 || len >= (int)sizeof(buf)) { ebpf_log("netlink_filter_add_end error: name too long\n"); rv = -1; @@ -678,7 +678,7 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma parse_rtattr_nested(tb, __TCA_BPF_MAX, options); if (tb[TCA_BPF_NAME]) { const char *name = (const char *)RTA_DATA(tb[TCA_BPF_NAME]); - if (name && strstr(name, marker_name)) { + if (name && !strncmp(name, marker_name, strlen(marker_name))) { free(buf); rv = 1; goto out; @@ -870,7 +870,7 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const parse_rtattr_nested(tb, __TCA_BPF_MAX, options); if (tb[TCA_BPF_NAME]) { const char *name = (const char *)RTA_DATA(tb[TCA_BPF_NAME]); - if (name && strstr(name, marker_name)) { + if (name && !strncmp(name, marker_name, strlen(marker_name))) { /* Found our filter - delete it using RTM_DELTFILTER */ struct netlink_msg del_req = { .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), diff --git a/non-GPL/HostIsolation/Lib/TcLoader.h b/non-GPL/HostIsolation/Lib/TcLoader.h index 2ff36af8..84a4d625 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.h +++ b/non-GPL/HostIsolation/Lib/TcLoader.h @@ -18,7 +18,7 @@ /* maximum netlink message size */ #define MAX_MSG 16384 -#define ELASTIC_TC_FILTER_MARKER "el-endpo" +#define ELASTIC_TC_FILTER_MARKER "el-endpo_" struct rtnetlink_handle { int fd; @@ -86,7 +86,7 @@ int netlink_filter_add_end(int fd, struct netlink_ctx *ctx); * @brief Check if an interface has our eBPF tc filter attached * * @param[in] ifname Network interface name - * @param[in] marker_name Substring to match against TCA_BPF_NAME + * @param[in] marker_name Prefix to match against TCA_BPF_NAME * @return 1 if found, 0 if not found, -1 on error */ int netlink_filter_exists(const char *ifname, const char *marker_name); @@ -95,7 +95,7 @@ int netlink_filter_exists(const char *ifname, const char *marker_name); * @brief Delete our eBPF tc filter from a network interface * * @param[in] ifname Network interface name - * @param[in] marker_name Substring to match against TCA_BPF_NAME + * @param[in] marker_name Prefix to match against TCA_BPF_NAME * @return 0 on success, -1 on error */ int netlink_filter_del(const char *ifname, const char *marker_name); From 92af611a4112c02f6ccb28f3cc87e269185facaa Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Fri, 17 Jul 2026 05:09:17 +0200 Subject: [PATCH 10/14] tc: treat an existing clsact qdisc as success Restore exclusive qdisc creation so repeated setup cannot modify existing qdisc state. Preserve the kernel errno and accept EEXIST as the idempotent success case without logging it as an error. --- non-GPL/HostIsolation/Lib/TcLoader.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index e4a26574..4cb9f466 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -259,7 +259,7 @@ static int rtnetlink_recv(int fd, struct msghdr *msg, char **answer) return rv; } -static int rtnetlink_send(struct rtnetlink_handle *rtnl, struct nlmsghdr *nlmsg) +static int rtnetlink_send(struct rtnetlink_handle *rtnl, struct nlmsghdr *nlmsg, int expected_error) { struct iovec iov = {.iov_base = nlmsg, .iov_len = nlmsg->nlmsg_len}; struct iovec riov = {0}; @@ -348,11 +348,11 @@ static int rtnetlink_send(struct rtnetlink_handle *rtnl, struct nlmsghdr *nlmsg) goto out; } - if (error) { + if (error && error != expected_error) { rtnetlink_send_error(err); } - rv = error ? -1 : 0; + rv = error; goto out; } @@ -384,6 +384,7 @@ static int rtnetlink_send(struct rtnetlink_handle *rtnl, struct nlmsghdr *nlmsg) static int netlink_qdisc(int cmd, unsigned int flags, const char *ifname) { int rv = -1; + int expected_error = cmd == RTM_NEWQDISC ? -EEXIST : 0; struct rtnetlink_handle qdisc_rth = {.fd = -1}; struct netlink_msg qdisc_req = { .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), @@ -414,9 +415,11 @@ static int netlink_qdisc(int cmd, unsigned int flags, const char *ifname) goto out; } /* talk to netlink */ - if (rtnetlink_send(&qdisc_rth, &qdisc_req.n) < 0) { - ebpf_log("error talking to the kernel (rtnetlink_send)\n"); - rv = -1; + rv = rtnetlink_send(&qdisc_rth, &qdisc_req.n, expected_error); + if (rv < 0) { + if (rv != expected_error) { + ebpf_log("error talking to the kernel (rtnetlink_send)\n"); + } goto out; } @@ -428,7 +431,9 @@ static int netlink_qdisc(int cmd, unsigned int flags, const char *ifname) int netlink_qdisc_add(const char *ifname) { - return netlink_qdisc(RTM_NEWQDISC, NLM_F_CREATE, ifname); + int rv = netlink_qdisc(RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE, ifname); + + return rv == -EEXIST ? 0 : rv; } int netlink_qdisc_del(const char *ifname) @@ -523,7 +528,7 @@ int netlink_filter_add_end(int fd, struct netlink_ctx *ctx) ctx->tail->rta_len = (((char *)nl) + nl->nlmsg_len) - (char *)ctx->tail; /* talk to netlink */ - if (rtnetlink_send(&ctx->filter_rth, &ctx->msg.n) < 0) { + if (rtnetlink_send(&ctx->filter_rth, &ctx->msg.n, 0) < 0) { ebpf_log("error talking to the kernel (rtnetlink_send)\n"); rv = -1; goto out; @@ -884,7 +889,7 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const }; attr_put(&del_req.n, sizeof(del_req), TCA_KIND, "bpf", strlen("bpf") + 1); - if (rtnetlink_send(&del_rth, &del_req.n) < 0) { + if (rtnetlink_send(&del_rth, &del_req.n, 0) < 0) { ebpf_log("failed to delete tc filter\n"); free(buf); rv = -1; From a2ce1d614228d74082f52145466874eebe9533ac Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Fri, 17 Jul 2026 19:56:05 +0200 Subject: [PATCH 11/14] tc: verify existing qdisc is clsact on EEXIST When RTM_NEWQDISC returns EEXIST, TC_H_CLSACT == TC_H_INGRESS (0xFFFFFFF1), so the occupied handle may belong to an ingress qdisc instead of clsact. Silent success in that case would leave us with no clsact attached and BPF filters would not be installed. Add netlink_qdisc_verify_clsact() which dumps qdiscs on the interface and checks TCA_KIND of the entry at TC_H_MAKE(TC_H_CLSACT, 0). Returns 0 only when the kind is "clsact"; returns -EINVAL otherwise. Co-Authored-By: Claude Sonnet 4.6 --- non-GPL/HostIsolation/Lib/TcLoader.c | 116 ++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index 4cb9f466..d906b531 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -429,11 +429,125 @@ static int netlink_qdisc(int cmd, unsigned int flags, const char *ifname) return rv; } +/* When RTM_NEWQDISC returns EEXIST, TC_H_CLSACT == TC_H_INGRESS == 0xFFFFFFF1 + * so the slot may be held by an ingress qdisc rather than clsact. Verify by + * dumping qdiscs on the interface and checking TCA_KIND of the entry at the + * TC_H_MAKE(TC_H_CLSACT, 0) handle. Returns 0 if it is "clsact", -EINVAL if + * it is a different kind, or -1 on any netlink error. */ +static int netlink_qdisc_verify_clsact(const char *ifname) +{ + int rv = -1; + int done = 0; + unsigned int seq = 0; + int ifindex = 0; + struct rtnetlink_handle rth = {.fd = -1}; + struct netlink_msg req = { + .n.nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg)), + .n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .n.nlmsg_type = RTM_GETQDISC, + .t.tcm_family = AF_UNSPEC, + }; + struct iovec iov = {.iov_base = &req.n, .iov_len = req.n.nlmsg_len}; + struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK}; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + struct iovec riov = {0}; + + ifindex = (int)if_nametoindex(ifname); + if (!ifindex) { + ebpf_log("netlink_qdisc_verify_clsact: failed to find device %s\n", ifname); + goto out; + } + + if (rtnetlink_open(&rth) < 0) { + ebpf_log("netlink_qdisc_verify_clsact: failed to open netlink\n"); + goto out; + } + + req.t.tcm_ifindex = ifindex; + req.n.nlmsg_seq = seq = ++rth.seq; + + if (sendmsg(rth.fd, &msg, 0) < 0) { + ebpf_log("netlink_qdisc_verify_clsact: failed to send request\n"); + goto out; + } + + msg.msg_iov = &riov; + msg.msg_iovlen = 1; + + while (!done) { + char *buf = NULL; + ssize_t recv_len = rtnetlink_recv(rth.fd, &msg, &buf); + + if (recv_len <= 0) { + goto out; + } + + for (struct nlmsghdr *h = (struct nlmsghdr *)buf; NLMSG_OK(h, (unsigned int)recv_len); + h = NLMSG_NEXT(h, recv_len)) { + + if (h->nlmsg_seq != seq) + continue; + + if (h->nlmsg_type == NLMSG_DONE) { + done = 1; + break; + } + + if (h->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(h); + if (err->error) { + rtnetlink_send_error(err); + free(buf); + goto out; + } + continue; + } + + if (h->nlmsg_type != RTM_NEWQDISC) + continue; + + struct tcmsg *t = (struct tcmsg *)NLMSG_DATA(h); + if (t->tcm_ifindex != ifindex || t->tcm_handle != TC_H_MAKE(TC_H_CLSACT, 0)) + continue; + + int attr_len = h->nlmsg_len - NLMSG_LENGTH(sizeof(*t)); + struct rtattr *rta = (struct rtattr *)((char *)t + NLMSG_ALIGN(sizeof(*t))); + for (; RTA_OK(rta, attr_len); rta = RTA_NEXT(rta, attr_len)) { + if (rta->rta_type != TCA_KIND) + continue; + const char *kind = (const char *)RTA_DATA(rta); + if (strcmp(kind, "clsact") == 0) { + rv = 0; + } else { + ebpf_log("existing qdisc at TC_H_CLSACT handle is '%s', not 'clsact'\n", kind); + rv = -EINVAL; + } + free(buf); + goto out; + } + } + free(buf); + } + + /* qdisc at TC_H_CLSACT handle not found in dump — treat as error */ + rv = -1; +out: + rtnetlink_close(&rth); + return rv; +} + int netlink_qdisc_add(const char *ifname) { int rv = netlink_qdisc(RTM_NEWQDISC, NLM_F_EXCL | NLM_F_CREATE, ifname); - return rv == -EEXIST ? 0 : rv; + if (rv == -EEXIST) + rv = netlink_qdisc_verify_clsact(ifname); + return rv; } int netlink_qdisc_del(const char *ifname) From 5cc94d7fdd2dd90eec9cbaf325dba3933c7b0f25 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Sat, 18 Jul 2026 01:25:09 +0200 Subject: [PATCH 12/14] tc: report ingress qdisc conflict clearly --- non-GPL/HostIsolation/Lib/TcLoader.c | 8 +++++--- non-GPL/HostIsolation/Lib/TcLoader.h | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index d906b531..f8b3409e 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -432,7 +432,7 @@ static int netlink_qdisc(int cmd, unsigned int flags, const char *ifname) /* When RTM_NEWQDISC returns EEXIST, TC_H_CLSACT == TC_H_INGRESS == 0xFFFFFFF1 * so the slot may be held by an ingress qdisc rather than clsact. Verify by * dumping qdiscs on the interface and checking TCA_KIND of the entry at the - * TC_H_MAKE(TC_H_CLSACT, 0) handle. Returns 0 if it is "clsact", -EINVAL if + * TC_H_MAKE(TC_H_CLSACT, 0) handle. Returns 0 if it is "clsact", -EBUSY if * it is a different kind, or -1 on any netlink error. */ static int netlink_qdisc_verify_clsact(const char *ifname) { @@ -524,8 +524,10 @@ static int netlink_qdisc_verify_clsact(const char *ifname) if (strcmp(kind, "clsact") == 0) { rv = 0; } else { - ebpf_log("existing qdisc at TC_H_CLSACT handle is '%s', not 'clsact'\n", kind); - rv = -EINVAL; + ebpf_log("cannot add clsact qdisc on %s: existing '%s' qdisc occupies the " + "ingress/clsact slot; leaving it untouched\n", + ifname, kind); + rv = -EBUSY; } free(buf); goto out; diff --git a/non-GPL/HostIsolation/Lib/TcLoader.h b/non-GPL/HostIsolation/Lib/TcLoader.h index 84a4d625..77ec0179 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.h +++ b/non-GPL/HostIsolation/Lib/TcLoader.h @@ -50,7 +50,8 @@ struct netlink_ctx { * @brief Add qdisc to a network interface * * @param[in] ifname Network interface name - * @return Error value (0 for success) + * @return 0 on success, -EBUSY if a non-clsact qdisc occupies the + * ingress/clsact slot, or another negative error value on failure */ int netlink_qdisc_add(const char *ifname); From 5aef500f7206536ab333685078a486ab5d7b91d6 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Sat, 18 Jul 2026 02:17:09 +0200 Subject: [PATCH 13/14] tc: validate clsact qdisc dump completion --- non-GPL/HostIsolation/Lib/TcLoader.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index f8b3409e..d5fb258c 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -438,6 +438,7 @@ static int netlink_qdisc_verify_clsact(const char *ifname) { int rv = -1; int done = 0; + int dump_intr = 0; unsigned int seq = 0; int ifindex = 0; struct rtnetlink_handle rth = {.fd = -1}; @@ -493,7 +494,25 @@ static int netlink_qdisc_verify_clsact(const char *ifname) if (h->nlmsg_seq != seq) continue; + if (h->nlmsg_flags & NLM_F_DUMP_INTR) + dump_intr = 1; + if (h->nlmsg_type == NLMSG_DONE) { + if (dump_intr) { + ebpf_log("netlink qdisc dump was interrupted\n"); + free(buf); + rv = -1; + goto out; + } + if (h->nlmsg_len >= NLMSG_LENGTH(sizeof(int))) { + int done_err = *(int *)NLMSG_DATA(h); + if (done_err) { + ebpf_log("netlink qdisc NLMSG_DONE error: %s\n", strerror(-done_err)); + free(buf); + rv = -1; + goto out; + } + } done = 1; break; } @@ -529,15 +548,12 @@ static int netlink_qdisc_verify_clsact(const char *ifname) ifname, kind); rv = -EBUSY; } - free(buf); - goto out; + break; } } free(buf); } - /* qdisc at TC_H_CLSACT handle not found in dump — treat as error */ - rv = -1; out: rtnetlink_close(&rth); return rv; From fcd611e0e962d23c20d86727f421786d67625871 Mon Sep 17 00:00:00 2001 From: Michal Stanek Date: Thu, 23 Jul 2026 04:20:15 +0200 Subject: [PATCH 14/14] tc: accumulate NLM_F_DUMP_INTR across all messages in filter dumps The kernel may set NLM_F_DUMP_INTR on any message in a dump, not necessarily on the final NLMSG_DONE. Track it with a persistent flag checked at NLMSG_DONE, matching netlink_qdisc_verify_clsact, so an interrupted dump cannot be reported as a complete successful scan. Reset the flag when netlink_filter_del_on_parent restarts its dump. Co-Authored-By: Claude Fable 5 --- non-GPL/HostIsolation/Lib/TcLoader.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/non-GPL/HostIsolation/Lib/TcLoader.c b/non-GPL/HostIsolation/Lib/TcLoader.c index d5fb258c..45b53ea0 100644 --- a/non-GPL/HostIsolation/Lib/TcLoader.c +++ b/non-GPL/HostIsolation/Lib/TcLoader.c @@ -695,6 +695,7 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma }; unsigned int seq = 0; int done = 0; + int dump_intr = 0; if (!ifname || !marker_name) { ebpf_log("netlink_filter_exists_on_parent error: NULL parameter\n"); @@ -752,8 +753,13 @@ netlink_filter_exists_on_parent(const char *ifname, __u32 parent, const char *ma continue; } + /* the kernel may set NLM_F_DUMP_INTR on any message in the dump, + * not necessarily on the final NLMSG_DONE */ + if (h->nlmsg_flags & NLM_F_DUMP_INTR) + dump_intr = 1; + if (h->nlmsg_type == NLMSG_DONE) { - if (h->nlmsg_flags & NLM_F_DUMP_INTR) { + if (dump_intr) { ebpf_log("netlink dump was interrupted\n"); free(buf); rv = -1; @@ -871,6 +877,7 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const }; unsigned int seq = 0; int done = 0; + int dump_intr = 0; unsigned int ifindex = 0; if (!ifname || !marker_name) { @@ -904,7 +911,8 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const /* Do not filter by TCA_KIND in the dump; enumerate all and match by name. */ restart_dump: - done = 0; + done = 0; + dump_intr = 0; if (rtnetlink_open(&filter_rth) < 0) { ebpf_log("failed to open netlink for listing\n"); @@ -944,8 +952,13 @@ static int netlink_filter_del_on_parent(const char *ifname, __u32 parent, const continue; } + /* the kernel may set NLM_F_DUMP_INTR on any message in the dump, + * not necessarily on the final NLMSG_DONE */ + if (h->nlmsg_flags & NLM_F_DUMP_INTR) + dump_intr = 1; + if (h->nlmsg_type == NLMSG_DONE) { - if (h->nlmsg_flags & NLM_F_DUMP_INTR) { + if (dump_intr) { ebpf_log("netlink dump was interrupted\n"); free(buf); rv = -1;