* [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-11 8:53 [PATCH v1 0/4] bpf: Introduce bpf_netpoll Mahe Tardy
@ 2026-05-11 8:53 ` Mahe Tardy
2026-05-11 9:40 ` bot+bpf-ci
` (2 more replies)
2026-05-11 8:53 ` [PATCH v1 2/4] selftests/bpf: Add netpoll kfunc sanity test Mahe Tardy
` (2 subsequent siblings)
3 siblings, 3 replies; 13+ messages in thread
From: Mahe Tardy @ 2026-05-11 8:53 UTC (permalink / raw)
To: bpf
Cc: andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart,
Mahe Tardy
From: Song Liu <song@kernel.org>
Add BPF kfuncs that allow BPF programs to send UDP packets via the
netpoll infrastructure. This provides a mechanism for BPF programs
(e.g., LSM hooks) to emit telemetry over UDP without depending on
the regular networking stack.
The API consists of four kfuncs:
bpf_netpoll_create() - Allocate and set up a netpoll context
(sleepable, SYSCALL prog type only)
bpf_netpoll_acquire() - Acquire a reference to a netpoll context
bpf_netpoll_release() - Release a reference (cleanup via
queue_rcu_work since netpoll_cleanup sleeps)
bpf_netpoll_send_udp() - Send a UDP packet (any context, LSM prog
type only for now)
The implementation follows the established kfunc lifecycle pattern
(create/acquire/release with refcounting, kptr map storage, dtor
registration). The netpoll context is wrapped in a refcounted
bpf_netpoll struct. Cleanup is deferred via queue_rcu_work() because
netpoll_cleanup() takes rtnl_lock.
AI was used to generate the code and each line was manually reviewed.
Reviewed-by: Mahe Tardy <mahe.tardy@gmail.com>
Signed-off-by: Song Liu <song@kernel.org>
---
drivers/net/Kconfig | 11 ++
include/linux/bpf_netpoll.h | 38 +++++++
kernel/bpf/verifier.c | 3 +
net/core/Makefile | 1 +
net/core/bpf_netpoll.c | 209 ++++++++++++++++++++++++++++++++++++
5 files changed, 262 insertions(+)
create mode 100644 include/linux/bpf_netpoll.h
create mode 100644 net/core/bpf_netpoll.c
diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
index edaab759dc50..8d94c9d58f32 100644
--- a/drivers/net/Kconfig
+++ b/drivers/net/Kconfig
@@ -364,6 +364,17 @@ config NETCONSOLE_PREPEND_RELEASE
message. See <file:Documentation/networking/netconsole.rst> for
details.
+config BPF_NETPOLL
+ bool "BPF netpoll UDP support"
+ depends on BPF_SYSCALL && NET
+ select NETPOLL
+ help
+ Enable BPF kfuncs for sending UDP packets via netpoll.
+ This allows BPF programs to send UDP packets from any
+ context using the netpoll infrastructure.
+
+ If unsure, say N.
+
config NETPOLL
def_bool NETCONSOLE
diff --git a/include/linux/bpf_netpoll.h b/include/linux/bpf_netpoll.h
new file mode 100644
index 000000000000..b738032548c7
--- /dev/null
+++ b/include/linux/bpf_netpoll.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#ifndef _BPF_NETPOLL_H
+#define _BPF_NETPOLL_H
+
+#include <linux/types.h>
+
+#define BPF_NETPOLL_DEV_NAME_LEN 16 /* IFNAMSIZ */
+
+/**
+ * struct bpf_netpoll_opts - BPF netpoll initialization parameters
+ * @dev_name: Network device name (e.g. "eth0"), null-terminated.
+ * @local_ip: Local IPv4 address in network byte order. 0 = auto-detect.
+ * @remote_ip: Remote IPv4 address in network byte order.
+ * @local_port: Local UDP port in host byte order.
+ * @remote_port: Remote UDP port in host byte order.
+ * @remote_mac: Remote MAC address (6 bytes).
+ * @ipv6: Set to 1 for IPv6, 0 for IPv4.
+ * @reserved: Must be zero. Reserved for future use.
+ * @local_ip6: Local IPv6 address (16 bytes). Used when ipv6=1.
+ * Zero = auto-detect.
+ * @remote_ip6: Remote IPv6 address (16 bytes). Used when ipv6=1.
+ */
+struct bpf_netpoll_opts {
+ char dev_name[BPF_NETPOLL_DEV_NAME_LEN];
+ __be32 local_ip;
+ __be32 remote_ip;
+ __u16 local_port;
+ __u16 remote_port;
+ __u8 remote_mac[6];
+ __u8 ipv6;
+ __u8 reserved;
+ __u8 local_ip6[16];
+ __u8 remote_ip6[16];
+};
+
+#endif /* _BPF_NETPOLL_H */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 185210b73385..2dc8d2fb4a01 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4641,6 +4641,9 @@ BTF_ID(struct, task_struct)
#ifdef CONFIG_CRYPTO
BTF_ID(struct, bpf_crypto_ctx)
#endif
+#ifdef CONFIG_BPF_NETPOLL
+BTF_ID(struct, bpf_netpoll)
+#endif
BTF_SET_END(rcu_protected_types)
static bool rcu_protected_object(const struct btf *btf, u32 btf_id)
diff --git a/net/core/Makefile b/net/core/Makefile
index dc17c5a61e9a..6484eb595698 100644
--- a/net/core/Makefile
+++ b/net/core/Makefile
@@ -26,6 +26,7 @@ obj-$(CONFIG_PAGE_POOL) += page_pool.o page_pool_user.o
obj-$(CONFIG_PROC_FS) += net-procfs.o
obj-$(CONFIG_NET_PKTGEN) += pktgen.o
obj-$(CONFIG_NETPOLL) += netpoll.o
+obj-$(CONFIG_BPF_NETPOLL) += bpf_netpoll.o
obj-$(CONFIG_FIB_RULES) += fib_rules.o
obj-$(CONFIG_TRACEPOINTS) += net-traces.o
obj-$(CONFIG_NET_DROP_MONITOR) += drop_monitor.o
diff --git a/net/core/bpf_netpoll.c b/net/core/bpf_netpoll.c
new file mode 100644
index 000000000000..05f4dcbd95f3
--- /dev/null
+++ b/net/core/bpf_netpoll.c
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <linux/bpf.h>
+#include <linux/bpf_netpoll.h>
+#include <linux/btf.h>
+#include <linux/btf_ids.h>
+#include <linux/netpoll.h>
+#include <linux/refcount.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+
+#define BPF_NETPOLL_MAX_UDP_CHUNK 1460
+
+/**
+ * struct bpf_netpoll - refcounted BPF netpoll context
+ * @np: The underlying netpoll structure.
+ * @usage: Reference counter.
+ * @rwork: RCU work for deferred cleanup (netpoll_cleanup sleeps).
+ */
+struct bpf_netpoll {
+ struct netpoll np;
+ refcount_t usage;
+ struct rcu_work rwork;
+};
+
+static void netpoll_release_work_fn(struct work_struct *work)
+{
+ struct bpf_netpoll *bnp = container_of(to_rcu_work(work),
+ struct bpf_netpoll, rwork);
+
+ netpoll_cleanup(&bnp->np);
+ kfree(bnp);
+}
+
+__bpf_kfunc_start_defs();
+
+/**
+ * bpf_netpoll_create() - Create a BPF netpoll context for sending UDP.
+ *
+ * Allocates and sets up a netpoll context that can be used to send UDP
+ * packets from BPF programs. The returned context must either be stored
+ * in a map as a kptr, or freed with bpf_netpoll_release().
+ *
+ * This function calls netpoll_setup() which takes rtnl_lock and may
+ * sleep, so it can only be used in sleepable BPF programs (SYSCALL).
+ *
+ * @opts: Pointer to struct bpf_netpoll_opts with connection parameters.
+ * @opts__sz: Size of the opts struct.
+ * @err: Integer to store error code when NULL is returned.
+ */
+__bpf_kfunc struct bpf_netpoll *
+bpf_netpoll_create(const struct bpf_netpoll_opts *opts, u32 opts__sz, int *err)
+{
+ struct bpf_netpoll *bnp;
+
+ if (!opts || opts__sz != sizeof(struct bpf_netpoll_opts)) {
+ *err = -EINVAL;
+ return NULL;
+ }
+
+ if (opts->reserved) {
+ *err = -EINVAL;
+ return NULL;
+ }
+
+ bnp = kzalloc_obj(*bnp);
+ if (!bnp) {
+ *err = -ENOMEM;
+ return NULL;
+ }
+
+ bnp->np.name = "bpf_netpoll";
+ strscpy(bnp->np.dev_name, opts->dev_name, IFNAMSIZ);
+ bnp->np.local_port = opts->local_port;
+ bnp->np.remote_port = opts->remote_port;
+ memcpy(bnp->np.remote_mac, opts->remote_mac, ETH_ALEN);
+
+ bnp->np.ipv6 = !!opts->ipv6;
+ if (opts->ipv6) {
+ memcpy(&bnp->np.local_ip.in6, opts->local_ip6,
+ sizeof(struct in6_addr));
+ memcpy(&bnp->np.remote_ip.in6, opts->remote_ip6,
+ sizeof(struct in6_addr));
+ } else {
+ bnp->np.local_ip.ip = opts->local_ip;
+ bnp->np.remote_ip.ip = opts->remote_ip;
+ }
+
+ *err = netpoll_setup(&bnp->np);
+ if (*err) {
+ kfree(bnp);
+ return NULL;
+ }
+
+ refcount_set(&bnp->usage, 1);
+ return bnp;
+}
+
+/**
+ * bpf_netpoll_acquire() - Acquire a reference to a BPF netpoll context.
+ * @bnp: The BPF netpoll context to acquire. Must be a trusted pointer.
+ *
+ * The acquired context must either be stored in a map as a kptr, or
+ * freed with bpf_netpoll_release().
+ */
+__bpf_kfunc struct bpf_netpoll *
+bpf_netpoll_acquire(struct bpf_netpoll *bnp)
+{
+ if (!refcount_inc_not_zero(&bnp->usage))
+ return NULL;
+ return bnp;
+}
+
+/**
+ * bpf_netpoll_release() - Release a BPF netpoll context.
+ * @bnp: The BPF netpoll context to release.
+ *
+ * When the final reference is released, the netpoll context is cleaned
+ * up via queue_rcu_work() (since netpoll_cleanup takes rtnl_lock and
+ * must run in process context).
+ */
+__bpf_kfunc void bpf_netpoll_release(struct bpf_netpoll *bnp)
+{
+ if (refcount_dec_and_test(&bnp->usage)) {
+ INIT_RCU_WORK(&bnp->rwork, netpoll_release_work_fn);
+ queue_rcu_work(system_wq, &bnp->rwork);
+ }
+}
+
+__bpf_kfunc void bpf_netpoll_release_dtor(void *bnp)
+{
+ bpf_netpoll_release(bnp);
+}
+CFI_NOSEAL(bpf_netpoll_release_dtor);
+
+/**
+ * bpf_netpoll_send_udp() - Send a UDP packet via netpoll.
+ * @bnp: The BPF netpoll context. Must be an RCU-protected pointer.
+ * @data: Pointer to the data to send.
+ * @data__sz: Size of the data to send (max 1460 bytes).
+ *
+ * Sends a UDP packet using the netpoll infrastructure. Can be called
+ * from any context (process, softirq, hardirq).
+ *
+ * Return: 0 on success, 1 (NET_XMIT_DROP) on drop, negative errno on error.
+ */
+__bpf_kfunc int bpf_netpoll_send_udp(struct bpf_netpoll *bnp,
+ const void *data, u32 data__sz)
+{
+ unsigned long flags;
+ int ret;
+
+ if (data__sz > BPF_NETPOLL_MAX_UDP_CHUNK)
+ return -E2BIG;
+
+ local_irq_save(flags);
+ ret = netpoll_send_udp(&bnp->np, data, data__sz);
+ local_irq_restore(flags);
+
+ return ret;
+}
+
+__bpf_kfunc_end_defs();
+
+BTF_KFUNCS_START(netpoll_init_kfunc_btf_ids)
+BTF_ID_FLAGS(func, bpf_netpoll_create, KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)
+BTF_ID_FLAGS(func, bpf_netpoll_release, KF_RELEASE)
+BTF_ID_FLAGS(func, bpf_netpoll_acquire, KF_ACQUIRE | KF_RCU | KF_RET_NULL)
+BTF_KFUNCS_END(netpoll_init_kfunc_btf_ids)
+
+static const struct btf_kfunc_id_set netpoll_init_kfunc_set = {
+ .owner = THIS_MODULE,
+ .set = &netpoll_init_kfunc_btf_ids,
+};
+
+BTF_KFUNCS_START(netpoll_kfunc_btf_ids)
+BTF_ID_FLAGS(func, bpf_netpoll_send_udp, KF_RCU)
+BTF_KFUNCS_END(netpoll_kfunc_btf_ids)
+
+static const struct btf_kfunc_id_set netpoll_kfunc_set = {
+ .owner = THIS_MODULE,
+ .set = &netpoll_kfunc_btf_ids,
+};
+
+BTF_ID_LIST(bpf_netpoll_dtor_ids)
+BTF_ID(struct, bpf_netpoll)
+BTF_ID(func, bpf_netpoll_release_dtor)
+
+static int __init bpf_netpoll_kfunc_init(void)
+{
+ int ret;
+ const struct btf_id_dtor_kfunc bpf_netpoll_dtors[] = {
+ {
+ .btf_id = bpf_netpoll_dtor_ids[0],
+ .kfunc_btf_id = bpf_netpoll_dtor_ids[1],
+ },
+ };
+
+ ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,
+ &netpoll_init_kfunc_set);
+ ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
+ &netpoll_kfunc_set);
+ return ret ?: register_btf_id_dtor_kfuncs(bpf_netpoll_dtors,
+ ARRAY_SIZE(bpf_netpoll_dtors),
+ THIS_MODULE);
+}
+
+late_initcall(bpf_netpoll_kfunc_init);
--
2.34.1
^ permalink raw reply related [flat|nested] 13+ messages in thread* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-11 8:53 ` [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets Mahe Tardy
@ 2026-05-11 9:40 ` bot+bpf-ci
2026-05-11 9:51 ` Mahe Tardy
2026-05-11 12:05 ` Daniel Borkmann
2026-05-12 1:20 ` Jakub Kicinski
2 siblings, 1 reply; 13+ messages in thread
From: bot+bpf-ci @ 2026-05-11 9:40 UTC (permalink / raw)
To: mahe.tardy, bpf
Cc: andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart,
mahe.tardy, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, clm, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3335 bytes --]
> The API consists of four kfuncs:
>
> bpf_netpoll_create() - Allocate and set up a netpoll context
> (sleepable, SYSCALL prog type only)
> bpf_netpoll_acquire() - Acquire a reference to a netpoll context
> bpf_netpoll_release() - Release a reference (cleanup via
> queue_rcu_work since netpoll_cleanup sleeps)
> bpf_netpoll_send_udp() - Send a UDP packet (any context, LSM prog
> type only for now)
The commit message states bpf_netpoll_send_udp() is "LSM prog type only
for now," but looking at the implementation in bpf_netpoll_kfunc_init(),
the function registers netpoll_kfunc_set using BPF_PROG_TYPE_UNSPEC:
net/core/bpf_netpoll.c:bpf_netpoll_kfunc_init() {
...
ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
&netpoll_kfunc_set);
...
}
Per bpf_prog_type_to_kfunc_hook() in kernel/bpf/btf.c, BPF_PROG_TYPE_UNSPEC
maps to BTF_KFUNC_HOOK_COMMON, which makes the kfunc available to ALL BPF
program types, not just LSM.
The check in btf_kfunc_is_allowed() tests BTF_KFUNC_HOOK_COMMON first and
returns true if the kfunc is present there, so any program type (tracing,
kprobe, XDP, sched-cls, etc.) can call bpf_netpoll_send_udp().
Is the commit message stale, or should the registration be
BPF_PROG_TYPE_LSM instead of BPF_PROG_TYPE_UNSPEC to match the stated
design?
> diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
> --- a/drivers/net/Kconfig
> +++ b/drivers/net/Kconfig
[ ... ]
> diff --git a/include/linux/bpf_netpoll.h b/include/linux/bpf_netpoll.h
> --- /dev/null
> +++ b/include/linux/bpf_netpoll.h
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> diff --git a/net/core/Makefile b/net/core/Makefile
> --- a/net/core/Makefile
> +++ b/net/core/Makefile
[ ... ]
> diff --git a/net/core/bpf_netpoll.c b/net/core/bpf_netpoll.c
> --- /dev/null
> +++ b/net/core/bpf_netpoll.c
> @@ -0,0 +1,209 @@
[ ... ]
> +BTF_KFUNCS_START(netpoll_kfunc_btf_ids)
> +BTF_ID_FLAGS(func, bpf_netpoll_send_udp, KF_RCU)
> +BTF_KFUNCS_END(netpoll_kfunc_btf_ids)
> +
> +static const struct btf_kfunc_id_set netpoll_kfunc_set = {
> + .owner = THIS_MODULE,
> + .set = &netpoll_kfunc_btf_ids,
> +};
> +
> +BTF_ID_LIST(bpf_netpoll_dtor_ids)
> +BTF_ID(struct, bpf_netpoll)
> +BTF_ID(func, bpf_netpoll_release_dtor)
> +
> +static int __init bpf_netpoll_kfunc_init(void)
> +{
> + int ret;
> + const struct btf_id_dtor_kfunc bpf_netpoll_dtors[] = {
> + {
> + .btf_id = bpf_netpoll_dtor_ids[0],
> + .kfunc_btf_id = bpf_netpoll_dtor_ids[1],
> + },
> + };
> +
> + ret = register_btf_kfunc_id_set(BPF_PROG_TYPE_SYSCALL,
> + &netpoll_init_kfunc_set);
> + ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
> + &netpoll_kfunc_set);
> + return ret ?: register_btf_id_dtor_kfuncs(bpf_netpoll_dtors,
> + ARRAY_SIZE(bpf_netpoll_dtors),
> + THIS_MODULE);
> +}
> +
> +late_initcall(bpf_netpoll_kfunc_init);
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/25660780250
^ permalink raw reply [flat|nested] 13+ messages in thread* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-11 9:40 ` bot+bpf-ci
@ 2026-05-11 9:51 ` Mahe Tardy
0 siblings, 0 replies; 13+ messages in thread
From: Mahe Tardy @ 2026-05-11 9:51 UTC (permalink / raw)
To: bot+bpf-ci
Cc: bpf, andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart,
martin.lau, yonghong.song, clm, ihor.solodrai
On Mon, May 11, 2026 at 09:40:32AM +0000, bot+bpf-ci@kernel.org wrote:
> > The API consists of four kfuncs:
> >
> > bpf_netpoll_create() - Allocate and set up a netpoll context
> > (sleepable, SYSCALL prog type only)
> > bpf_netpoll_acquire() - Acquire a reference to a netpoll context
> > bpf_netpoll_release() - Release a reference (cleanup via
> > queue_rcu_work since netpoll_cleanup sleeps)
> > bpf_netpoll_send_udp() - Send a UDP packet (any context, LSM prog
> > type only for now)
>
> The commit message states bpf_netpoll_send_udp() is "LSM prog type only
> for now," but looking at the implementation in bpf_netpoll_kfunc_init(),
> the function registers netpoll_kfunc_set using BPF_PROG_TYPE_UNSPEC:
>
> net/core/bpf_netpoll.c:bpf_netpoll_kfunc_init() {
> ...
> ret = ret ?: register_btf_kfunc_id_set(BPF_PROG_TYPE_UNSPEC,
> &netpoll_kfunc_set);
> ...
> }
>
> Per bpf_prog_type_to_kfunc_hook() in kernel/bpf/btf.c, BPF_PROG_TYPE_UNSPEC
> maps to BTF_KFUNC_HOOK_COMMON, which makes the kfunc available to ALL BPF
> program types, not just LSM.
>
> The check in btf_kfunc_is_allowed() tests BTF_KFUNC_HOOK_COMMON first and
> returns true if the kfunc is present there, so any program type (tracing,
> kprobe, XDP, sched-cls, etc.) can call bpf_netpoll_send_udp().
>
> Is the commit message stale, or should the registration be
> BPF_PROG_TYPE_LSM instead of BPF_PROG_TYPE_UNSPEC to match the stated
> design?
Ah indeed, this a stale commit message, will fix in v2. The real
intention is to make it BPF_PROG_TYPE_UNSPEC. Will wait on some more
reviews before submitting.
> ---
> AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
> See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
>
> CI run summary: https://github.com/kernel-patches/bpf/actions/runs/25660780250
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-11 8:53 ` [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets Mahe Tardy
2026-05-11 9:40 ` bot+bpf-ci
@ 2026-05-11 12:05 ` Daniel Borkmann
2026-05-12 8:51 ` Mahe Tardy
2026-05-12 1:20 ` Jakub Kicinski
2 siblings, 1 reply; 13+ messages in thread
From: Daniel Borkmann @ 2026-05-11 12:05 UTC (permalink / raw)
To: Mahe Tardy, bpf
Cc: andrew+netdev, andrii, ast, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart
On 5/11/26 10:53 AM, Mahe Tardy wrote:
> From: Song Liu <song@kernel.org>
>
> Add BPF kfuncs that allow BPF programs to send UDP packets via the
> netpoll infrastructure. This provides a mechanism for BPF programs
> (e.g., LSM hooks) to emit telemetry over UDP without depending on
> the regular networking stack.
>
> The API consists of four kfuncs:
>
> bpf_netpoll_create() - Allocate and set up a netpoll context
> (sleepable, SYSCALL prog type only)
> bpf_netpoll_acquire() - Acquire a reference to a netpoll context
> bpf_netpoll_release() - Release a reference (cleanup via
> queue_rcu_work since netpoll_cleanup sleeps)
> bpf_netpoll_send_udp() - Send a UDP packet (any context, LSM prog
> type only for now)
>
> The implementation follows the established kfunc lifecycle pattern
> (create/acquire/release with refcounting, kptr map storage, dtor
> registration). The netpoll context is wrapped in a refcounted
> bpf_netpoll struct. Cleanup is deferred via queue_rcu_work() because
> netpoll_cleanup() takes rtnl_lock.
>
> AI was used to generate the code and each line was manually reviewed.
>
> Reviewed-by: Mahe Tardy <mahe.tardy@gmail.com>
> Signed-off-by: Song Liu <song@kernel.org>
> ---
> drivers/net/Kconfig | 11 ++
> include/linux/bpf_netpoll.h | 38 +++++++
> kernel/bpf/verifier.c | 3 +
> net/core/Makefile | 1 +
> net/core/bpf_netpoll.c | 209 ++++++++++++++++++++++++++++++++++++
> 5 files changed, 262 insertions(+)
> create mode 100644 include/linux/bpf_netpoll.h
> create mode 100644 net/core/bpf_netpoll.c
>
> diff --git a/drivers/net/Kconfig b/drivers/net/Kconfig
> index edaab759dc50..8d94c9d58f32 100644
> --- a/drivers/net/Kconfig
> +++ b/drivers/net/Kconfig
> @@ -364,6 +364,17 @@ config NETCONSOLE_PREPEND_RELEASE
> message. See <file:Documentation/networking/netconsole.rst> for
> details.
>
> +config BPF_NETPOLL
> + bool "BPF netpoll UDP support"
> + depends on BPF_SYSCALL && NET
> + select NETPOLL
> + help
> + Enable BPF kfuncs for sending UDP packets via netpoll.
> + This allows BPF programs to send UDP packets from any
> + context using the netpoll infrastructure.
> +
> + If unsure, say N.
Do we need the extra Kconfig knob? For most other BPF functionality we don't
have such convention. BPF_NETPOLL could be a hidden config enabled when both
BPF_SYSCALL && NETPOLL is enabled?
> config NETPOLL
> def_bool NETCONSOLE
>
> diff --git a/include/linux/bpf_netpoll.h b/include/linux/bpf_netpoll.h
> new file mode 100644
> index 000000000000..b738032548c7
> --- /dev/null
> +++ b/include/linux/bpf_netpoll.h
> @@ -0,0 +1,38 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> +
> +#ifndef _BPF_NETPOLL_H
> +#define _BPF_NETPOLL_H
> +
> +#include <linux/types.h>
> +
> +#define BPF_NETPOLL_DEV_NAME_LEN 16 /* IFNAMSIZ */
Do we need this, can't we just reuse IFNAMSIZ?
> +/**
> + * struct bpf_netpoll_opts - BPF netpoll initialization parameters
> + * @dev_name: Network device name (e.g. "eth0"), null-terminated.
> + * @local_ip: Local IPv4 address in network byte order. 0 = auto-detect.
> + * @remote_ip: Remote IPv4 address in network byte order.
> + * @local_port: Local UDP port in host byte order.
> + * @remote_port: Remote UDP port in host byte order.
> + * @remote_mac: Remote MAC address (6 bytes).
> + * @ipv6: Set to 1 for IPv6, 0 for IPv4.
> + * @reserved: Must be zero. Reserved for future use.
> + * @local_ip6: Local IPv6 address (16 bytes). Used when ipv6=1.
> + * Zero = auto-detect.
> + * @remote_ip6: Remote IPv6 address (16 bytes). Used when ipv6=1.
> + */
> +struct bpf_netpoll_opts {
> + char dev_name[BPF_NETPOLL_DEV_NAME_LEN];
> + __be32 local_ip;
> + __be32 remote_ip;
> + __u16 local_port;
> + __u16 remote_port;
> + __u8 remote_mac[6];
> + __u8 ipv6;
> + __u8 reserved;
> + __u8 local_ip6[16];
> + __u8 remote_ip6[16];
> +};
Could we detangle this a bit and structure the design similar to what was
done for the bpf fib lookup helpers? __u8 family and then union.
Is the remote_mac strictly needed? Feels a bit like a burden, maybe for the
selftest, we should first do a bpf_fib_lookup before the bpf_netpoll_create
so that the use case where the user only cares about giving remote_ip/remote_port
and maybe local_port (e.g. to as indicator for different event types and/or
RSS hashing) works, but everything else is derived via kernel fib lookup.
Thanks,
Daniel
^ permalink raw reply [flat|nested] 13+ messages in thread* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-11 12:05 ` Daniel Borkmann
@ 2026-05-12 8:51 ` Mahe Tardy
0 siblings, 0 replies; 13+ messages in thread
From: Mahe Tardy @ 2026-05-12 8:51 UTC (permalink / raw)
To: Daniel Borkmann
Cc: bpf, andrew+netdev, andrii, ast, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart
On Mon, May 11, 2026 at 02:05:26PM +0200, Daniel Borkmann wrote:
> On 5/11/26 10:53 AM, Mahe Tardy wrote:
> > From: Song Liu <song@kernel.org>
> >
> > Add BPF kfuncs that allow BPF programs to send UDP packets via the
> > netpoll infrastructure. This provides a mechanism for BPF programs
> > (e.g., LSM hooks) to emit telemetry over UDP without depending on
> > the regular networking stack.
> >
> > [...]
> >
> > +config BPF_NETPOLL
> > + bool "BPF netpoll UDP support"
> > + depends on BPF_SYSCALL && NET
> > + select NETPOLL
> > + help
> > + Enable BPF kfuncs for sending UDP packets via netpoll.
> > + This allows BPF programs to send UDP packets from any
> > + context using the netpoll infrastructure.
> > +
> > + If unsure, say N.
>
> Do we need the extra Kconfig knob? For most other BPF functionality we don't
> have such convention. BPF_NETPOLL could be a hidden config enabled when both
> BPF_SYSCALL && NETPOLL is enabled?
Okay I changed it to:
+config BPF_NETPOLL
+ def_bool BPF_SYSCALL && NETPOLL
So now we need to explicitely enable NETCONSOLE since NETPOL is also a
hidden config defined by:
config NETPOLL
def_bool NETCONSOLE
I somehow initially wanted to define NETPOL by having def_bool
NETCONSOLE || BPF_NETPOLL but then we have a circular dependency...
> > config NETPOLL
> > def_bool NETCONSOLE
> >
> > diff --git a/include/linux/bpf_netpoll.h b/include/linux/bpf_netpoll.h
> > new file mode 100644
> > index 000000000000..b738032548c7
> > --- /dev/null
> > +++ b/include/linux/bpf_netpoll.h
> > @@ -0,0 +1,38 @@
> > +/* SPDX-License-Identifier: GPL-2.0-only */
> > +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
> > +
> > +#ifndef _BPF_NETPOLL_H
> > +#define _BPF_NETPOLL_H
> > +
> > +#include <linux/types.h>
> > +
> > +#define BPF_NETPOLL_DEV_NAME_LEN 16 /* IFNAMSIZ */
>
> Do we need this, can't we just reuse IFNAMSIZ?
We don't really need it, done!
> > +/**
> > + * struct bpf_netpoll_opts - BPF netpoll initialization parameters
> > + * @dev_name: Network device name (e.g. "eth0"), null-terminated.
> > + * @local_ip: Local IPv4 address in network byte order. 0 = auto-detect.
> > + * @remote_ip: Remote IPv4 address in network byte order.
> > + * @local_port: Local UDP port in host byte order.
> > + * @remote_port: Remote UDP port in host byte order.
> > + * @remote_mac: Remote MAC address (6 bytes).
> > + * @ipv6: Set to 1 for IPv6, 0 for IPv4.
> > + * @reserved: Must be zero. Reserved for future use.
> > + * @local_ip6: Local IPv6 address (16 bytes). Used when ipv6=1.
> > + * Zero = auto-detect.
> > + * @remote_ip6: Remote IPv6 address (16 bytes). Used when ipv6=1.
> > + */
> > +struct bpf_netpoll_opts {
> > + char dev_name[BPF_NETPOLL_DEV_NAME_LEN];
> > + __be32 local_ip;
> > + __be32 remote_ip;
> > + __u16 local_port;
> > + __u16 remote_port;
> > + __u8 remote_mac[6];
> > + __u8 ipv6;
> > + __u8 reserved;
> > + __u8 local_ip6[16];
> > + __u8 remote_ip6[16];
> > +};
>
> Could we detangle this a bit and structure the design similar to what was
> done for the bpf fib lookup helpers? __u8 family and then union.
Yeah I think initially it was heavily inspired by the struct netpoll for
IPv6 boolean but they have the union, let's do it, I wrote this one that
is similar to the fib lookup params:
struct bpf_netpoll_opts {
char dev_name[IFNAMSIZ];
__u8 family;
__u8 reserved;
__u16 local_port;
__u16 remote_port;
__u8 remote_mac[ETH_ALEN];
union {
__be32 ipv4_local;
__u32 ipv6_local[4]; /* in6_addr; network order */
};
union {
__be32 ipv4_remote;
__u32 ipv6_remote[4]; /* in6_addr; network order */
};
};
Also used family instead of the bool with this in the kfunc:
switch (opts->family) {
case AF_INET:
bnp->np.local_ip.ip = opts->ipv4_local;
bnp->np.remote_ip.ip = opts->ipv4_remote;
break;
case AF_INET6:
memcpy(&bnp->np.local_ip.in6, opts->ipv6_local,
sizeof(struct in6_addr));
memcpy(&bnp->np.remote_ip.in6, opts->ipv6_remote,
sizeof(struct in6_addr));
bnp->np.ipv6 = true;
break;
default:
*err = -EAFNOSUPPORT;
return NULL;
}
> Is the remote_mac strictly needed?
I think it's indeed needed since netpoll is pushing the MAC address in
the skb at the send step. It's not reusing the network stack for lookup.
I think netconsole is using the broadcast address by default, maybe the
kfunc could default to it as well.
> Feels a bit like a burden, maybe for the
> selftest, we should first do a bpf_fib_lookup before the bpf_netpoll_create
> so that the use case where the user only cares about giving remote_ip/remote_port
> and maybe local_port (e.g. to as indicator for different event types and/or
> RSS hashing) works, but everything else is derived via kernel fib lookup.
Yeah that would be nice but then it would mean we need to call fib
lookup from the SYSCALL programs without any network context so I'm not
sure how it would look like.
> Thanks,
> Daniel
Thanks for the review!
Mahe
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-11 8:53 ` [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets Mahe Tardy
2026-05-11 9:40 ` bot+bpf-ci
2026-05-11 12:05 ` Daniel Borkmann
@ 2026-05-12 1:20 ` Jakub Kicinski
2026-05-12 1:59 ` Alexei Starovoitov
2 siblings, 1 reply; 13+ messages in thread
From: Jakub Kicinski @ 2026-05-12 1:20 UTC (permalink / raw)
To: Mahe Tardy
Cc: bpf, andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, martin.lau, pabeni, song, liamwisehart
On Mon, 11 May 2026 08:53:41 +0000 Mahe Tardy wrote:
> Add BPF kfuncs that allow BPF programs to send UDP packets via the
> netpoll infrastructure. This provides a mechanism for BPF programs
> (e.g., LSM hooks) to emit telemetry over UDP without depending on
> the regular networking stack.
>
> The API consists of four kfuncs:
>
> bpf_netpoll_create() - Allocate and set up a netpoll context
> (sleepable, SYSCALL prog type only)
> bpf_netpoll_acquire() - Acquire a reference to a netpoll context
> bpf_netpoll_release() - Release a reference (cleanup via
> queue_rcu_work since netpoll_cleanup sleeps)
> bpf_netpoll_send_udp() - Send a UDP packet (any context, LSM prog
> type only for now)
>
> The implementation follows the established kfunc lifecycle pattern
> (create/acquire/release with refcounting, kptr map storage, dtor
> registration). The netpoll context is wrapped in a refcounted
> bpf_netpoll struct. Cleanup is deferred via queue_rcu_work() because
> netpoll_cleanup() takes rtnl_lock.
We have enough bug reports as is, let's not merge unusable toys.
For any use of netpoll by BPF:
Nacked-by: Jakub Kicinski <kuba@kernel.org>
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-12 1:20 ` Jakub Kicinski
@ 2026-05-12 1:59 ` Alexei Starovoitov
2026-05-12 2:36 ` Jakub Kicinski
0 siblings, 1 reply; 13+ messages in thread
From: Alexei Starovoitov @ 2026-05-12 1:59 UTC (permalink / raw)
To: Jakub Kicinski, Mahe Tardy
Cc: bpf, andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, martin.lau, pabeni, song, liamwisehart
On Mon May 11, 2026 at 6:20 PM PDT, Jakub Kicinski wrote:
> On Mon, 11 May 2026 08:53:41 +0000 Mahe Tardy wrote:
>> Add BPF kfuncs that allow BPF programs to send UDP packets via the
>> netpoll infrastructure. This provides a mechanism for BPF programs
>> (e.g., LSM hooks) to emit telemetry over UDP without depending on
>> the regular networking stack.
>>
>> The API consists of four kfuncs:
>>
>> bpf_netpoll_create() - Allocate and set up a netpoll context
>> (sleepable, SYSCALL prog type only)
>> bpf_netpoll_acquire() - Acquire a reference to a netpoll context
>> bpf_netpoll_release() - Release a reference (cleanup via
>> queue_rcu_work since netpoll_cleanup sleeps)
>> bpf_netpoll_send_udp() - Send a UDP packet (any context, LSM prog
>> type only for now)
>>
>> The implementation follows the established kfunc lifecycle pattern
>> (create/acquire/release with refcounting, kptr map storage, dtor
>> registration). The netpoll context is wrapped in a refcounted
>> bpf_netpoll struct. Cleanup is deferred via queue_rcu_work() because
>> netpoll_cleanup() takes rtnl_lock.
>
> We have enough bug reports as is, let's not merge unusable toys.
> For any use of netpoll by BPF:
>
> Nacked-by: Jakub Kicinski <kuba@kernel.org>
unusable toys? What are you talking about?
netpoll is already called from everywhere.
bpf_netpoll_send_udp() won't add any more bugs.
I like this netpoll approach way more then creating and keeping proper socket
within bpf and all head aches of keeping it around and doing send from
good context. With netpoll all of these issues are gone.
Just bpf_netpoll_send_udp() from anywhere and it works just like it does
for dmesg.
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-12 1:59 ` Alexei Starovoitov
@ 2026-05-12 2:36 ` Jakub Kicinski
2026-05-12 2:59 ` Alexei Starovoitov
0 siblings, 1 reply; 13+ messages in thread
From: Jakub Kicinski @ 2026-05-12 2:36 UTC (permalink / raw)
To: Alexei Starovoitov
Cc: Mahe Tardy, bpf, andrew+netdev, andrii, ast, daniel, davem,
eddyz87, edumazet, john.fastabend, martin.lau, pabeni, song,
liamwisehart
On Mon, 11 May 2026 18:59:56 -0700 Alexei Starovoitov wrote:
> unusable toys? What are you talking about?
Please refer to the comments on the RFC. netpoll bypasses most of
the networking stack. And there either has to be some user space
to negotiate crypto or this is unusable for anyone serious.
> netpoll is already called from everywhere.
You mean all contexts because it's used by console?
> bpf_netpoll_send_udp() won't add any more bugs.
Same as TLS + sockmap didn't? I spend enough time mopping up after
people.
> I like this netpoll approach way more then creating and keeping proper socket
> within bpf and all head aches of keeping it around and doing send from
> good context. With netpoll all of these issues are gone.
> Just bpf_netpoll_send_udp() from anywhere and it works just like it does
> for dmesg.
No, netpoll is high risk / brittle and tightly integrated with NAPI.
"I don't want to run a tiny user space program" is nowhere near strong
enough justification to expose this API. SMH.
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets
2026-05-12 2:36 ` Jakub Kicinski
@ 2026-05-12 2:59 ` Alexei Starovoitov
0 siblings, 0 replies; 13+ messages in thread
From: Alexei Starovoitov @ 2026-05-12 2:59 UTC (permalink / raw)
To: Jakub Kicinski
Cc: Mahe Tardy, bpf, andrew+netdev, andrii, ast, daniel, davem,
eddyz87, edumazet, john.fastabend, martin.lau, pabeni, song,
liamwisehart
On Mon May 11, 2026 at 7:36 PM PDT, Jakub Kicinski wrote:
> On Mon, 11 May 2026 18:59:56 -0700 Alexei Starovoitov wrote:
>> unusable toys? What are you talking about?
>
> Please refer to the comments on the RFC. netpoll bypasses most of
> the networking stack. And there either has to be some user space
> to negotiate crypto or this is unusable for anyone serious.
encryption inside UDP packet is orthogonal.
netpoll as-is is usable and useful to send plain text
best effort messages.
>> netpoll is already called from everywhere.
>
> You mean all contexts because it's used by console?
yes
>> bpf_netpoll_send_udp() won't add any more bugs.
>
> Same as TLS + sockmap didn't? I spend enough time mopping up after
> people.
huh? comparing this to sockmap is apples and oranges.
>> I like this netpoll approach way more then creating and keeping proper socket
>> within bpf and all head aches of keeping it around and doing send from
>> good context. With netpoll all of these issues are gone.
>> Just bpf_netpoll_send_udp() from anywhere and it works just like it does
>> for dmesg.
>
> No, netpoll is high risk / brittle and tightly integrated with NAPI.
>
> "I don't want to run a tiny user space program" is nowhere near strong
> enough justification to expose this API. SMH.
No. Nothing to do with 'user space prog'.
What's brittle in netpoll? It works today in any context and
that's the main point. All the gotchas were resolved over the years
by heavy netconsole usage. Sure, it only works in mainstream
drivers like mlx, brcm and likely broken in niche drivers.
That's not obstacle at all.
netpoll_send_udp() is a perfect building block for best effort
try_to_sendmsg. I don't see the reason to reinvent the whole thing.
Because alternatives are much worse.
bpf progs would need to create UDP socket, split udp_sendmsg
into who knows what. Lots of design questions, code reviews, etc.
Way more headaches for everyone involved.
Doing full networking with dst,fib,nh from kernel context is complicated.
Works for NFS, but it's an uncharted territory for bpf.
While netpoll_send_udp() is available, easy to use and proven to work.
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH v1 2/4] selftests/bpf: Add netpoll kfunc sanity test
2026-05-11 8:53 [PATCH v1 0/4] bpf: Introduce bpf_netpoll Mahe Tardy
2026-05-11 8:53 ` [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets Mahe Tardy
@ 2026-05-11 8:53 ` Mahe Tardy
2026-05-11 8:53 ` [PATCH v1 3/4] selftests/bpf: Add netpoll kfunc IPv6 variant test Mahe Tardy
2026-05-11 8:53 ` [PATCH v1 4/4] selftests/bpf: Add netpoll setup basic tests Mahe Tardy
3 siblings, 0 replies; 13+ messages in thread
From: Mahe Tardy @ 2026-05-11 8:53 UTC (permalink / raw)
To: bpf
Cc: andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart,
Mahe Tardy
From: Song Liu <song@kernel.org>
Add a selftest that exercises the bpf_netpoll kfuncs end-to-end:
1. A SYSCALL BPF program creates a netpoll context on a dummy
network device and stores it in a kptr map.
2. A fentry program is attached to dummy_xmit, the driver function
that will receive the netpoll sent packet.
3. An LSM program attached to security_file_open retrieves the
context from the map and calls bpf_netpoll_send_udp().
The userspace harness creates a network namespace with a dummy device,
runs the setup program, attach the fentry hook then triggers the LSM
hook by opening /dev/null. The dummy device accepts all packets, so
send_status == 0 confirms success and the packet content is verified by
reading the skb from the dummy driver xmit function.
The code was partially generated by AI and each line was manually
reviewed.
Signed-off-by: Song Liu <song@kernel.org>
Co-developed-by: Mahe Tardy <mahe.tardy@gmail.com>
Signed-off-by: Mahe Tardy <mahe.tardy@gmail.com>
---
tools/testing/selftests/bpf/config | 1 +
.../selftests/bpf/prog_tests/netpoll.c | 92 ++++++++++++++
.../selftests/bpf/progs/netpoll_common.h | 64 ++++++++++
.../selftests/bpf/progs/netpoll_sanity.c | 118 ++++++++++++++++++
4 files changed, 275 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/netpoll.c
create mode 100644 tools/testing/selftests/bpf/progs/netpoll_common.h
create mode 100644 tools/testing/selftests/bpf/progs/netpoll_sanity.c
diff --git a/tools/testing/selftests/bpf/config b/tools/testing/selftests/bpf/config
index 24855381290d..bd6d0a4e5239 100644
--- a/tools/testing/selftests/bpf/config
+++ b/tools/testing/selftests/bpf/config
@@ -7,6 +7,7 @@ CONFIG_BPF_JIT=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_BPF_LIRC_MODE2=y
CONFIG_BPF_LSM=y
+CONFIG_BPF_NETPOLL=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_BPF_SYSCALL=y
# CONFIG_BPF_UNPRIV_DEFAULT_OFF is not set
diff --git a/tools/testing/selftests/bpf/prog_tests/netpoll.c b/tools/testing/selftests/bpf/prog_tests/netpoll.c
new file mode 100644
index 000000000000..1cfac4b13e7d
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/netpoll.c
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <arpa/inet.h>
+#include <net/if.h>
+
+#include "test_progs.h"
+#include "network_helpers.h"
+#include "netpoll_sanity.skel.h"
+
+#define NS_TEST "netpoll_sanity_ns"
+#define DUMMY_DEV "dummy0"
+#define DUMMY_IP "10.0.0.1"
+#define REMOTE_IP "10.0.0.2"
+
+void test_netpoll_sanity(void)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct nstoken *nstoken = NULL;
+ struct netpoll_sanity *skel;
+ int err, pfd, fd;
+
+ skel = netpoll_sanity__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+ return;
+
+ /* Create a network namespace with a dummy device */
+ SYS(fail, "ip netns add %s", NS_TEST);
+ SYS(fail, "ip -net %s link add %s type dummy", NS_TEST, DUMMY_DEV);
+ SYS(fail, "ip -net %s addr add %s/24 dev %s", NS_TEST, DUMMY_IP, DUMMY_DEV);
+ SYS(fail, "ip -net %s link set %s up", NS_TEST, DUMMY_DEV);
+
+ nstoken = open_netns(NS_TEST);
+ if (!ASSERT_OK_PTR(nstoken, "open_netns"))
+ goto fail;
+
+ /* Configure the BPF program globals */
+ snprintf(skel->bss->dev_name, sizeof(skel->bss->dev_name), "%s", DUMMY_DEV);
+ skel->bss->remote_ip = inet_addr(REMOTE_IP);
+ skel->bss->local_port = 5555;
+ skel->bss->remote_port = 6666;
+ skel->bss->remote_mac[0] = 0xaa;
+ skel->bss->remote_mac[1] = 0xbb;
+ skel->bss->remote_mac[2] = 0xcc;
+ skel->bss->remote_mac[3] = 0xdd;
+ skel->bss->remote_mac[4] = 0xee;
+ skel->bss->remote_mac[5] = 0xff;
+
+ /* Step 1: Run the setup SYSCALL prog */
+ pfd = bpf_program__fd(skel->progs.netpoll_setup_test);
+ if (!ASSERT_GT(pfd, 0, "netpoll_setup_test fd"))
+ goto fail;
+
+ err = bpf_prog_test_run_opts(pfd, &opts);
+ if (!ASSERT_OK(err, "netpoll_setup_test run"))
+ goto fail;
+
+ if (!ASSERT_OK(skel->bss->status, "netpoll_setup_test status"))
+ goto fail;
+
+ /* Step 2: Attach the dummy xmit hook */
+ skel->links.netpoll_dummy_xmit = bpf_program__attach(skel->progs.netpoll_dummy_xmit);
+ if (!ASSERT_OK_PTR(skel->links.netpoll_dummy_xmit, "attach netpoll_dummy_xmit"))
+ goto fail;
+
+ /* Step 3: Attach the LSM prog and trigger via file_open */
+ skel->links.netpoll_send_test = bpf_program__attach(skel->progs.netpoll_send_test);
+ if (!ASSERT_OK_PTR(skel->links.netpoll_send_test, "attach netpoll_send_test"))
+ goto fail;
+
+ skel->bss->trigger_send = 1;
+
+ fd = open("/dev/null", O_RDONLY);
+ if (!ASSERT_GE(fd, 0, "open /dev/null"))
+ goto fail;
+ close(fd);
+
+ /* send_status should be 0 (NETDEV_TX_OK) -- dummy device accepts
+ * all packets.
+ */
+ ASSERT_OK(skel->bss->send_status, "netpoll_send_udp status");
+ /* dummy_xmit hooks the dummy driver ndo_start_xmit method called by
+ * netpoll and fetches the UDP payload.
+ */
+ ASSERT_STREQ(skel->bss->driver_xmit, skel->data->send_data, "dummy_xmit received");
+
+fail:
+ if (nstoken)
+ close_netns(nstoken);
+ SYS_NOFAIL("ip netns del " NS_TEST " &> /dev/null");
+ netpoll_sanity__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/netpoll_common.h b/tools/testing/selftests/bpf/progs/netpoll_common.h
new file mode 100644
index 000000000000..d1e4b2a58adb
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/netpoll_common.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#ifndef _NETPOLL_COMMON_H
+#define _NETPOLL_COMMON_H
+
+#include "errno.h"
+#include <stdbool.h>
+
+struct bpf_netpoll *bpf_netpoll_create(const struct bpf_netpoll_opts *opts,
+ u32 opts__sz, int *err) __ksym;
+struct bpf_netpoll *bpf_netpoll_acquire(struct bpf_netpoll *bnp) __ksym;
+void bpf_netpoll_release(struct bpf_netpoll *bnp) __ksym;
+int bpf_netpoll_send_udp(struct bpf_netpoll *bnp,
+ const void *data, u32 data__sz) __ksym;
+
+struct __netpoll_ctx_value {
+ struct bpf_netpoll __kptr * ctx;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __type(key, int);
+ __type(value, struct __netpoll_ctx_value);
+ __uint(max_entries, 1);
+} __netpoll_ctx_map SEC(".maps");
+
+static inline struct __netpoll_ctx_value *netpoll_ctx_value_lookup(void)
+{
+ u32 key = 0;
+
+ return bpf_map_lookup_elem(&__netpoll_ctx_map, &key);
+}
+
+static inline int netpoll_ctx_insert(struct bpf_netpoll *ctx)
+{
+ struct __netpoll_ctx_value local, *v;
+ struct bpf_netpoll *old;
+ u32 key = 0;
+ int err;
+
+ local.ctx = NULL;
+ err = bpf_map_update_elem(&__netpoll_ctx_map, &key, &local, 0);
+ if (err) {
+ bpf_netpoll_release(ctx);
+ return err;
+ }
+
+ v = bpf_map_lookup_elem(&__netpoll_ctx_map, &key);
+ if (!v) {
+ bpf_netpoll_release(ctx);
+ return -ENOENT;
+ }
+
+ old = bpf_kptr_xchg(&v->ctx, ctx);
+ if (old) {
+ bpf_netpoll_release(old);
+ return -EEXIST;
+ }
+
+ return 0;
+}
+
+#endif /* _NETPOLL_COMMON_H */
diff --git a/tools/testing/selftests/bpf/progs/netpoll_sanity.c b/tools/testing/selftests/bpf/progs/netpoll_sanity.c
new file mode 100644
index 000000000000..9e1e595eff2c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/netpoll_sanity.c
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+/* Copyright (c) 2026 Isovalent. */
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_endian.h>
+#include "bpf_tracing_net.h"
+#include "netpoll_common.h"
+
+/* Globals for passing config from userspace */
+char dev_name[16] = {};
+__be32 remote_ip;
+__u16 local_port;
+__u16 remote_port;
+__u8 remote_mac[6] = {};
+
+/* Results */
+int status;
+int send_status;
+int trigger_send;
+char driver_xmit[64];
+
+char send_data[64] = "hello from bpf netpoll";
+
+/* SYSCALL prog: set up the netpoll context */
+SEC("syscall")
+int netpoll_setup_test(void *ctx)
+{
+ struct bpf_netpoll_opts opts = {};
+ struct bpf_netpoll *bnp;
+ int err = 0;
+
+ status = 0;
+
+ __builtin_memcpy(opts.dev_name, dev_name, 16);
+ opts.remote_ip = remote_ip;
+ opts.local_port = local_port;
+ opts.remote_port = remote_port;
+ __builtin_memcpy(opts.remote_mac, remote_mac, 6);
+
+ bnp = bpf_netpoll_create(&opts, sizeof(opts), &err);
+ if (!bnp) {
+ status = err;
+ return 0;
+ }
+
+ err = netpoll_ctx_insert(bnp);
+ if (err && err != -EEXIST)
+ status = err;
+ return 0;
+}
+
+/* LSM prog: send UDP via the stored netpoll context */
+SEC("lsm/file_open")
+int BPF_PROG(netpoll_send_test, struct file *file)
+{
+ struct __netpoll_ctx_value *v;
+ struct bpf_netpoll *bnp;
+
+ if (!trigger_send)
+ return 0;
+
+ trigger_send = 0;
+ send_status = -ENOENT;
+
+ v = netpoll_ctx_value_lookup();
+ if (!v)
+ return 0;
+
+ bpf_rcu_read_lock();
+ bnp = v->ctx;
+ if (!bnp) {
+ bpf_rcu_read_unlock();
+ return 0;
+ }
+
+ send_status = bpf_netpoll_send_udp(bnp, send_data, sizeof(send_data));
+ bpf_rcu_read_unlock();
+ return 0;
+}
+
+/* Fentry prog: hook the dummy driver xmit */
+SEC("fentry/dummy_xmit")
+int BPF_PROG(netpoll_dummy_xmit, struct sk_buff *skb, struct net_device *dev)
+{
+ unsigned char *data;
+ struct ethhdr eth;
+ struct iphdr ip;
+ struct udphdr udp;
+
+ if (bpf_probe_read_kernel(&data, sizeof(data), &skb->data) < 0)
+ return 0;
+ if (!data)
+ return 0;
+
+ if (bpf_probe_read_kernel(ð, sizeof(eth), data) < 0)
+ return 0;
+ if (eth.h_proto != bpf_htons(ETH_P_IP))
+ return 0;
+
+ if (bpf_probe_read_kernel(&ip, sizeof(ip), data + sizeof(struct ethhdr)) < 0)
+ return 0;
+ if (ip.protocol != IPPROTO_UDP)
+ return 0;
+
+ if (bpf_probe_read_kernel(&udp, sizeof(udp), data + sizeof(struct ethhdr) + (ip.ihl * 4)) < 0)
+ return 0;
+ if (udp.dest != bpf_htons(remote_port))
+ return 0;
+ if (bpf_probe_read_kernel(&driver_xmit, sizeof(driver_xmit), data + sizeof(struct ethhdr) + (ip.ihl * 4) + sizeof(struct udphdr)) < 0)
+ return 0;
+
+ return 0;
+}
+
+char __license[] SEC("license") = "GPL";
--
2.34.1
^ permalink raw reply related [flat|nested] 13+ messages in thread* [PATCH v1 3/4] selftests/bpf: Add netpoll kfunc IPv6 variant test
2026-05-11 8:53 [PATCH v1 0/4] bpf: Introduce bpf_netpoll Mahe Tardy
2026-05-11 8:53 ` [PATCH v1 1/4] bpf: Add netpoll kfuncs for sending UDP packets Mahe Tardy
2026-05-11 8:53 ` [PATCH v1 2/4] selftests/bpf: Add netpoll kfunc sanity test Mahe Tardy
@ 2026-05-11 8:53 ` Mahe Tardy
2026-05-11 8:53 ` [PATCH v1 4/4] selftests/bpf: Add netpoll setup basic tests Mahe Tardy
3 siblings, 0 replies; 13+ messages in thread
From: Mahe Tardy @ 2026-05-11 8:53 UTC (permalink / raw)
To: bpf
Cc: andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart,
Mahe Tardy
Add a selftest that exercises the bpf_netpoll kfuncs end-to-end with
IPv6, reusing the code that was added for IPv4.
Signed-off-by: Mahe Tardy <mahe.tardy@gmail.com>
---
.../selftests/bpf/prog_tests/netpoll.c | 42 +++++++++++++++----
.../selftests/bpf/progs/netpoll_sanity.c | 33 +++++++++++----
2 files changed, 58 insertions(+), 17 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/netpoll.c b/tools/testing/selftests/bpf/prog_tests/netpoll.c
index 1cfac4b13e7d..eac0378c426a 100644
--- a/tools/testing/selftests/bpf/prog_tests/netpoll.c
+++ b/tools/testing/selftests/bpf/prog_tests/netpoll.c
@@ -9,15 +9,20 @@
#include "netpoll_sanity.skel.h"
#define NS_TEST "netpoll_sanity_ns"
+#define NS_TEST_V6 "netpoll_sanity_ns_v6"
#define DUMMY_DEV "dummy0"
#define DUMMY_IP "10.0.0.1"
#define REMOTE_IP "10.0.0.2"
+#define DUMMY_IP6 "fd00::1"
+#define REMOTE_IP6 "fd00::2"
-void test_netpoll_sanity(void)
+static void run_netpoll_test(const char *ns_name, const char *local_ip,
+ const char *remote_ip, bool ipv6)
{
LIBBPF_OPTS(bpf_test_run_opts, opts);
struct nstoken *nstoken = NULL;
struct netpoll_sanity *skel;
+ struct in6_addr addr6;
int err, pfd, fd;
skel = netpoll_sanity__open_and_load();
@@ -25,18 +30,28 @@ void test_netpoll_sanity(void)
return;
/* Create a network namespace with a dummy device */
- SYS(fail, "ip netns add %s", NS_TEST);
- SYS(fail, "ip -net %s link add %s type dummy", NS_TEST, DUMMY_DEV);
- SYS(fail, "ip -net %s addr add %s/24 dev %s", NS_TEST, DUMMY_IP, DUMMY_DEV);
- SYS(fail, "ip -net %s link set %s up", NS_TEST, DUMMY_DEV);
-
- nstoken = open_netns(NS_TEST);
+ SYS(fail, "ip netns add %s", ns_name);
+ SYS(fail, "ip -net %s link add %s type dummy", ns_name, DUMMY_DEV);
+ if (ipv6)
+ SYS(fail, "ip -net %s addr add %s/64 dev %s", ns_name, local_ip, DUMMY_DEV);
+ else
+ SYS(fail, "ip -net %s addr add %s/24 dev %s", ns_name, local_ip, DUMMY_DEV);
+ SYS(fail, "ip -net %s link set %s up", ns_name, DUMMY_DEV);
+
+ nstoken = open_netns(ns_name);
if (!ASSERT_OK_PTR(nstoken, "open_netns"))
goto fail;
/* Configure the BPF program globals */
snprintf(skel->bss->dev_name, sizeof(skel->bss->dev_name), "%s", DUMMY_DEV);
- skel->bss->remote_ip = inet_addr(REMOTE_IP);
+ if (ipv6) {
+ if (inet_pton(AF_INET6, remote_ip, &addr6) != 1)
+ goto fail;
+ __builtin_memcpy(&skel->bss->remote_ip6, &addr6, sizeof(addr6));
+ skel->bss->ipv6 = 1;
+ } else {
+ skel->bss->remote_ip = inet_addr(remote_ip);
+ }
skel->bss->local_port = 5555;
skel->bss->remote_port = 6666;
skel->bss->remote_mac[0] = 0xaa;
@@ -87,6 +102,15 @@ void test_netpoll_sanity(void)
fail:
if (nstoken)
close_netns(nstoken);
- SYS_NOFAIL("ip netns del " NS_TEST " &> /dev/null");
+ SYS_NOFAIL("ip netns del %s &> /dev/null", ns_name);
netpoll_sanity__destroy(skel);
}
+
+void test_netpoll_sanity(void)
+{
+ if (test__start_subtest("ipv4"))
+ run_netpoll_test(NS_TEST, DUMMY_IP, REMOTE_IP, false);
+
+ if (test__start_subtest("ipv6"))
+ run_netpoll_test(NS_TEST_V6, DUMMY_IP6, REMOTE_IP6, true);
+}
diff --git a/tools/testing/selftests/bpf/progs/netpoll_sanity.c b/tools/testing/selftests/bpf/progs/netpoll_sanity.c
index 9e1e595eff2c..26303632ebde 100644
--- a/tools/testing/selftests/bpf/progs/netpoll_sanity.c
+++ b/tools/testing/selftests/bpf/progs/netpoll_sanity.c
@@ -12,9 +12,11 @@
/* Globals for passing config from userspace */
char dev_name[16] = {};
__be32 remote_ip;
+struct in6_addr remote_ip6;
__u16 local_port;
__u16 remote_port;
__u8 remote_mac[6] = {};
+int ipv6;
/* Results */
int status;
@@ -35,10 +37,14 @@ int netpoll_setup_test(void *ctx)
status = 0;
__builtin_memcpy(opts.dev_name, dev_name, 16);
- opts.remote_ip = remote_ip;
+ if (ipv6)
+ __builtin_memcpy(&opts.remote_ip6, &remote_ip6, sizeof(remote_ip6));
+ else
+ opts.remote_ip = remote_ip;
opts.local_port = local_port;
opts.remote_port = remote_port;
__builtin_memcpy(opts.remote_mac, remote_mac, 6);
+ opts.ipv6 = ipv6;
bnp = bpf_netpoll_create(&opts, sizeof(opts), &err);
if (!bnp) {
@@ -88,7 +94,9 @@ int BPF_PROG(netpoll_dummy_xmit, struct sk_buff *skb, struct net_device *dev)
unsigned char *data;
struct ethhdr eth;
struct iphdr ip;
+ struct ipv6hdr ip6;
struct udphdr udp;
+ unsigned int offset;
if (bpf_probe_read_kernel(&data, sizeof(data), &skb->data) < 0)
return 0;
@@ -97,19 +105,28 @@ int BPF_PROG(netpoll_dummy_xmit, struct sk_buff *skb, struct net_device *dev)
if (bpf_probe_read_kernel(ð, sizeof(eth), data) < 0)
return 0;
- if (eth.h_proto != bpf_htons(ETH_P_IP))
- return 0;
- if (bpf_probe_read_kernel(&ip, sizeof(ip), data + sizeof(struct ethhdr)) < 0)
- return 0;
- if (ip.protocol != IPPROTO_UDP)
+ if (eth.h_proto == bpf_htons(ETH_P_IP)) {
+ if (bpf_probe_read_kernel(&ip, sizeof(ip), data + sizeof(struct ethhdr)) < 0)
+ return 0;
+ if (ip.protocol != IPPROTO_UDP)
+ return 0;
+ offset = sizeof(struct ethhdr) + (ip.ihl * 4);
+ } else if (eth.h_proto == bpf_htons(ETH_P_IPV6)) {
+ if (bpf_probe_read_kernel(&ip6, sizeof(ip6), data + sizeof(struct ethhdr)) < 0)
+ return 0;
+ if (ip6.nexthdr != IPPROTO_UDP)
+ return 0;
+ offset = sizeof(struct ethhdr) + sizeof(struct ipv6hdr);
+ } else {
return 0;
+ }
- if (bpf_probe_read_kernel(&udp, sizeof(udp), data + sizeof(struct ethhdr) + (ip.ihl * 4)) < 0)
+ if (bpf_probe_read_kernel(&udp, sizeof(udp), data + offset) < 0)
return 0;
if (udp.dest != bpf_htons(remote_port))
return 0;
- if (bpf_probe_read_kernel(&driver_xmit, sizeof(driver_xmit), data + sizeof(struct ethhdr) + (ip.ihl * 4) + sizeof(struct udphdr)) < 0)
+ if (bpf_probe_read_kernel(&driver_xmit, sizeof(driver_xmit), data + offset + sizeof(struct udphdr)) < 0)
return 0;
return 0;
--
2.34.1
^ permalink raw reply related [flat|nested] 13+ messages in thread* [PATCH v1 4/4] selftests/bpf: Add netpoll setup basic tests
2026-05-11 8:53 [PATCH v1 0/4] bpf: Introduce bpf_netpoll Mahe Tardy
` (2 preceding siblings ...)
2026-05-11 8:53 ` [PATCH v1 3/4] selftests/bpf: Add netpoll kfunc IPv6 variant test Mahe Tardy
@ 2026-05-11 8:53 ` Mahe Tardy
3 siblings, 0 replies; 13+ messages in thread
From: Mahe Tardy @ 2026-05-11 8:53 UTC (permalink / raw)
To: bpf
Cc: andrew+netdev, andrii, ast, daniel, davem, eddyz87, edumazet,
john.fastabend, kuba, martin.lau, pabeni, song, liamwisehart,
Mahe Tardy
Add a pair of BPF progs, netpoll_release and netpoll_acquire to test
that the respective bpf_netpoll_release and bpf_netpoll_acquire behave
as expected and that the verifier has been configured to catch
unreleased references.
Signed-off-by: Mahe Tardy <mahe.tardy@gmail.com>
---
.../selftests/bpf/prog_tests/netpoll.c | 23 +++++++++
.../selftests/bpf/progs/netpoll_basic.c | 47 +++++++++++++++++++
2 files changed, 70 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/netpoll_basic.c
diff --git a/tools/testing/selftests/bpf/prog_tests/netpoll.c b/tools/testing/selftests/bpf/prog_tests/netpoll.c
index eac0378c426a..daea1068a739 100644
--- a/tools/testing/selftests/bpf/prog_tests/netpoll.c
+++ b/tools/testing/selftests/bpf/prog_tests/netpoll.c
@@ -7,15 +7,38 @@
#include "test_progs.h"
#include "network_helpers.h"
#include "netpoll_sanity.skel.h"
+#include "netpoll_basic.skel.h"
#define NS_TEST "netpoll_sanity_ns"
#define NS_TEST_V6 "netpoll_sanity_ns_v6"
+#define NS_BASIC_TEST "netpoll_basic_ns"
#define DUMMY_DEV "dummy0"
#define DUMMY_IP "10.0.0.1"
#define REMOTE_IP "10.0.0.2"
#define DUMMY_IP6 "fd00::1"
#define REMOTE_IP6 "fd00::2"
+void test_netpoll_basic(void)
+{
+ struct nstoken *nstoken = NULL;
+
+ SYS(fail, "ip netns add %s", NS_BASIC_TEST);
+ SYS(fail, "ip -net %s link add %s type dummy", NS_BASIC_TEST, DUMMY_DEV);
+ SYS(fail, "ip -net %s addr add %s/24 dev %s", NS_BASIC_TEST, DUMMY_IP, DUMMY_DEV);
+ SYS(fail, "ip -net %s link set %s up", NS_BASIC_TEST, DUMMY_DEV);
+
+ nstoken = open_netns(NS_BASIC_TEST);
+ if (!ASSERT_OK_PTR(nstoken, "open_netns"))
+ goto fail;
+
+ RUN_TESTS(netpoll_basic);
+
+fail:
+ if (nstoken)
+ close_netns(nstoken);
+ SYS_NOFAIL("ip netns del %s &> /dev/null", NS_BASIC_TEST);
+}
+
static void run_netpoll_test(const char *ns_name, const char *local_ip,
const char *remote_ip, bool ipv6)
{
diff --git a/tools/testing/selftests/bpf/progs/netpoll_basic.c b/tools/testing/selftests/bpf/progs/netpoll_basic.c
new file mode 100644
index 000000000000..2650e1e5410a
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/netpoll_basic.c
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Isovalent. */
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+#include "errno.h"
+
+SEC("syscall")
+__success __retval(0)
+int netpoll_release(void *ctx)
+{
+ struct bpf_netpoll_opts opts = { .dev_name = "dummy0" };
+ struct bpf_netpoll *bnp;
+ int err = 0;
+
+ bnp = bpf_netpoll_create(&opts, sizeof(opts), &err);
+ if (!bnp)
+ return err;
+
+ bpf_netpoll_release(bnp);
+
+ return 0;
+}
+
+SEC("syscall")
+__failure __msg("Unreleased reference")
+int netpoll_acquire(void *ctx)
+{
+ struct bpf_netpoll_opts opts = { .dev_name = "dummy0" };
+ struct bpf_netpoll *bnp;
+ int err = 0;
+
+ bnp = bpf_netpoll_create(&opts, sizeof(opts), &err);
+ if (!bnp)
+ return err;
+
+ bnp = bpf_netpoll_acquire(bnp);
+ if (!bnp)
+ return -EINVAL;
+
+ bpf_netpoll_release(bnp);
+
+ return 0;
+}
+
+char __license[] SEC("license") = "GPL";
--
2.34.1
^ permalink raw reply related [flat|nested] 13+ messages in thread