From: ThisSeanZhang <thisseanzhang@gmail.com>
To: bpf@vger.kernel.org
Cc: ThisSeanZhang <thisseanzhang@gmail.com>,
Alexei Starovoitov <ast@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Andrii Nakryiko <andrii@kernel.org>,
Eduard Zingerman <eddyz87@gmail.com>,
netdev@vger.kernel.org, Nick Hudson <nhudson@akamai.com>,
Felix Fietkau <nbd@openwrt.org>, Qingfang Deng <dqfext@gmail.com>
Subject: [RFC bpf-next 3/3] selftests/bpf: Add a test for the PPPoE encap/decap adjust_room flags
Date: Sat, 26 Sep 2026 17:07:57 -0400 [thread overview]
Message-ID: <20260926210757.2152159-4-thisseanzhang@gmail.com> (raw)
In-Reply-To: <20260926210757.2152159-1-thisseanzhang@gmail.com>
Add a tc test that exercises the new BPF_F_ADJ_ROOM_ENCAP_PPPOE and
BPF_F_ADJ_ROOM_DECAP_PPPOE flags of bpf_skb_adjust_room().
The encap program is run over plain Ethernet/IPv4 and Ethernet/IPv6
packets, and the test verifies for both that the PPPoE session header
shows up between the Ethernet and the network header with the right
PPP protocol, that the payload is passed through unchanged, and that
the skb protocol is switched to ETH_P_PPP_SES. The decap program is
then run over the encapsulated packets, and the test verifies that
the payload and the Ethernet header are restored after the BPF
program completes the decapsulation, and that the skb protocol is
switched back according to the PPP protocol field.
A rejection program exercises calls to bpf_skb_adjust_room() that
the helper must reject: an encap room size other than
PPPOE_SES_HLEN, encap in BPF_ADJ_ROOM_NET mode, a shrinking encap,
the encap flag combined with a decap flag, a shrinking of a PPPoE
packet without the PPPoE flag, and a decap of a packet whose
skb->protocol is not ETH_P_PPP_SES. Decapsulation of a PPPoE packet
with an unsupported PPP protocol and of a packet too short to still
hold an IP header is rejected as well.
Signed-off-by: ThisSeanZhang <thisseanzhang@gmail.com>
---
.../selftests/bpf/prog_tests/tc_pppoe.c | 249 ++++++++++++++++++
tools/testing/selftests/bpf/progs/tc_pppoe.c | 159 +++++++++++
2 files changed, 408 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/tc_pppoe.c
create mode 100644 tools/testing/selftests/bpf/progs/tc_pppoe.c
diff --git a/tools/testing/selftests/bpf/prog_tests/tc_pppoe.c b/tools/testing/selftests/bpf/prog_tests/tc_pppoe.c
new file mode 100644
index 000000000..ee914e828
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/tc_pppoe.c
@@ -0,0 +1,249 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 ThisSeanZhang */
+
+#include <arpa/inet.h>
+#include <test_progs.h>
+#include "tc_pppoe.skel.h"
+
+/* Ethernet + IPv4 + TCP, 54 bytes in total. */
+static const __u8 ip4_pkt[] = {
+ /* ethernet header */
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
+ 0x08, 0x00,
+ /* IPv4 header */
+ 0x45, 0x00, 0x00, 0x28,
+ 0x12, 0x34, 0x40, 0x00,
+ 0x40, 0x06, 0x00, 0x00,
+ 0xc0, 0xa8, 0x01, 0x01,
+ 0xc0, 0xa8, 0x01, 0x02,
+ /* TCP header */
+ 0x00, 0x50, 0x1f, 0x90,
+ 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x50, 0x02, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+/* Ethernet + IPv6 + TCP, 74 bytes in total. */
+static const __u8 ip6_pkt[] = {
+ /* ethernet header */
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
+ 0x86, 0xdd,
+ /* IPv6 header */
+ 0x60, 0x00, 0x00, 0x00,
+ 0x00, 0x14, 0x06, 0x40,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+ /* TCP header */
+ 0x00, 0x50, 0x1f, 0x90,
+ 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x50, 0x02, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+};
+
+#define PPP_SES_HLEN 8
+#define TC_ACT_SHOT 2
+
+static int run_prog(int prog_fd, const void *data_in, __u32 size_in,
+ void *data_out, __u32 size_out, __u32 *retval,
+ __u32 *size_out_actual)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts,
+ .data_in = (void *)data_in,
+ .data_size_in = size_in,
+ .data_out = data_out,
+ .data_size_out = size_out,
+ );
+ int ret;
+
+ ret = bpf_prog_test_run_opts(prog_fd, &opts);
+ if (!ret && retval)
+ *retval = opts.retval;
+ if (!ret && size_out_actual)
+ *size_out_actual = opts.data_size_out;
+
+ return ret;
+}
+
+static void test_encap_decap(struct tc_pppoe *skel, const char *subtest,
+ const __u8 *pkt, __u32 pkt_len, int ethertype,
+ __u8 ppp_proto)
+{
+ __u8 encap_pkt[128];
+ __u8 decap_pkt[128];
+ __u32 retval, out_len;
+ int ret;
+
+ if (!test__start_subtest(subtest))
+ return;
+
+ skel->bss->encap_proto = 0;
+ ret = run_prog(bpf_program__fd(skel->progs.tc_pppoe_encap),
+ pkt, pkt_len, encap_pkt, sizeof(encap_pkt),
+ &retval, &out_len);
+ ASSERT_OK(ret, "encap test_run");
+ ASSERT_OK(retval, "encap retval");
+ ASSERT_EQ(out_len, pkt_len + PPP_SES_HLEN, "encap pkt len");
+ ASSERT_EQ(skel->bss->encap_proto, htons(0x8864),
+ "encap skb protocol");
+
+ /* Ethernet header, with the ethertype changed to PPPoE session. */
+ ASSERT_EQ(encap_pkt[12], 0x88, "encap eth h_proto");
+ ASSERT_EQ(encap_pkt[13], 0x64, "encap eth h_proto");
+ /* PPPoE session header: ver/type/code, session id, length and
+ * PPP protocol.
+ */
+ ASSERT_EQ(encap_pkt[14], 0x11, "encap pppoe ver/type");
+ ASSERT_EQ(encap_pkt[15], 0x00, "encap pppoe code");
+ ASSERT_EQ(encap_pkt[16], 0xde, "encap pppoe sid");
+ ASSERT_EQ(encap_pkt[17], 0xad, "encap pppoe sid");
+ ASSERT_EQ(encap_pkt[18], 0x00, "encap pppoe length");
+ ASSERT_EQ(encap_pkt[19], pkt_len - 14 + 2, "encap pppoe length");
+ ASSERT_EQ(encap_pkt[20], 0x00, "encap ppp proto");
+ ASSERT_EQ(encap_pkt[21], ppp_proto, "encap ppp proto");
+ /* The original packet must be shifted unchanged behind the
+ * new header.
+ */
+ ASSERT_MEMEQ(encap_pkt + 14 + PPP_SES_HLEN, pkt + 14,
+ pkt_len - 14, "encap payload");
+
+ skel->bss->decap_proto = 0;
+ ret = run_prog(bpf_program__fd(skel->progs.tc_pppoe_decap),
+ encap_pkt, pkt_len + PPP_SES_HLEN, decap_pkt,
+ sizeof(decap_pkt), &retval, &out_len);
+ ASSERT_OK(ret, "decap test_run");
+ ASSERT_OK(retval, "decap retval");
+ ASSERT_EQ(out_len, pkt_len, "decap pkt len");
+ ASSERT_EQ(skel->bss->decap_proto, ethertype, "decap skb protocol");
+ ASSERT_MEMEQ(decap_pkt, pkt, pkt_len, "decap packet");
+}
+
+static void test_reject(struct tc_pppoe *skel, int case_id,
+ const void *data_in, __u32 size_in, const char *name)
+{
+ __u8 out[128];
+ __u32 retval = 0;
+ int ret;
+
+ if (!test__start_subtest(name))
+ return;
+
+ skel->bss->reject_case = case_id;
+ skel->bss->reject_unexpected = 0;
+ ret = run_prog(bpf_program__fd(skel->progs.tc_pppoe_reject),
+ data_in, size_in, out, sizeof(out), &retval, NULL);
+ ASSERT_OK(ret, "reject test_run");
+ ASSERT_EQ(retval, TC_ACT_SHOT, "helper rejected the call");
+ ASSERT_EQ(skel->bss->reject_unexpected, 0, "no unexpected success");
+}
+
+static void test_decap_reject_input(struct tc_pppoe *skel,
+ const __u8 *pkt, __u32 pkt_len,
+ const char *subtest)
+{
+ __u8 out[128];
+ __u32 retval = 0;
+ int ret;
+
+ if (!test__start_subtest(subtest))
+ return;
+
+ skel->bss->decap_proto = 0;
+ ret = run_prog(bpf_program__fd(skel->progs.tc_pppoe_decap),
+ pkt, pkt_len, out, sizeof(out), &retval, NULL);
+ ASSERT_OK(ret, "decap reject test_run");
+ ASSERT_EQ(retval, TC_ACT_SHOT, "helper rejected the call");
+ ASSERT_EQ(skel->bss->decap_proto, 0, "skb protocol unchanged");
+}
+
+void test_tc_pppoe(void)
+{
+ /* A PPPoE packet whose PPP protocol is neither IPv4 nor IPv6:
+ * IP control protocol (0x8021) in this case.
+ */
+ static const __u8 bad_ppp_pkt[] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
+ 0x88, 0x64,
+ 0x11, 0x00, 0x00, 0x00, 0x00, 0x28, 0x80, 0x21,
+ 0x45, 0x00, 0x00, 0x28,
+ 0x12, 0x34, 0x40, 0x00,
+ 0x40, 0x06, 0x00, 0x00,
+ 0xc0, 0xa8, 0x01, 0x01,
+ 0xc0, 0xa8, 0x01, 0x02,
+ 0x00, 0x50, 0x1f, 0x90,
+ 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x50, 0x02, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ };
+ /* A PPPoE packet whose payload is too short to still contain a
+ * full IP header after decapsulation.
+ */
+ static const __u8 truncated_pkt[] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
+ 0x88, 0x64,
+ 0x11, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x21,
+ 0x45, 0x00,
+ };
+ /* Non-PPPoE IPv4 fragment whose bytes 20/21 read as PPP_IP. */
+ static const __u8 fake_ppp_pkt[] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66,
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
+ 0x08, 0x00,
+ 0x45, 0x00, 0x00, 0x28,
+ 0x12, 0x34, 0x00, 0x21,
+ 0x40, 0x06, 0x00, 0x00,
+ 0xc0, 0xa8, 0x01, 0x01,
+ 0xc0, 0xa8, 0x01, 0x02,
+ 0x00, 0x50, 0x1f, 0x90,
+ 0x00, 0x00, 0x00, 0x01,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x50, 0x02, 0x10, 0x00,
+ 0x00, 0x00, 0x00, 0x00,
+ };
+ __u8 encap_pkt[128];
+ struct tc_pppoe *skel;
+ __u32 retval, out_len;
+
+ skel = tc_pppoe__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel open_and_load"))
+ return;
+
+ test_encap_decap(skel, "encap-decap-v4", ip4_pkt, sizeof(ip4_pkt),
+ htons(0x0800), 0x21);
+ test_encap_decap(skel, "encap-decap-v6", ip6_pkt, sizeof(ip6_pkt),
+ htons(0x86dd), 0x57);
+
+ test_decap_reject_input(skel, bad_ppp_pkt, sizeof(bad_ppp_pkt),
+ "decap-bad-ppp-proto");
+ test_decap_reject_input(skel, truncated_pkt, sizeof(truncated_pkt),
+ "decap-truncated");
+ test_reject(skel, 6, fake_ppp_pkt, sizeof(fake_ppp_pkt),
+ "reject-decap-fake-ppp-proto");
+
+ /* Encapsulate a v4 packet once more to get a PPPoE packet as
+ * input for the "decap without the flag" rejection case.
+ */
+ ASSERT_OK(run_prog(bpf_program__fd(skel->progs.tc_pppoe_encap),
+ ip4_pkt, sizeof(ip4_pkt), encap_pkt,
+ sizeof(encap_pkt), &retval, &out_len),
+ "encap v4 for reject input");
+
+ test_reject(skel, 1, ip4_pkt, sizeof(ip4_pkt), "reject-encap-len");
+ test_reject(skel, 2, ip4_pkt, sizeof(ip4_pkt), "reject-encap-mode");
+ test_reject(skel, 3, ip4_pkt, sizeof(ip4_pkt), "reject-encap-shrink");
+ test_reject(skel, 4, ip4_pkt, sizeof(ip4_pkt), "reject-flag-mix");
+ test_reject(skel, 5, encap_pkt, sizeof(ip4_pkt) + PPP_SES_HLEN,
+ "reject-decap-no-flag");
+ test_reject(skel, 6, ip4_pkt, sizeof(ip4_pkt),
+ "reject-decap-non-pppoe");
+
+ tc_pppoe__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/tc_pppoe.c b/tools/testing/selftests/bpf/progs/tc_pppoe.c
new file mode 100644
index 000000000..357e1b570
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/tc_pppoe.c
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 ThisSeanZhang */
+
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_endian.h>
+
+#define ETH_P_IP_TEST 0x0800
+#define ETH_P_IPV6_TEST 0x86dd
+#define ETH_P_PPP_SES_TEST 0x8864
+#define PPP_IP_TEST 0x21
+#define PPP_IPV6_TEST 0x57
+
+#define ETH_HLEN_TEST 14
+#define PPPOE_SES_HLEN_TEST 8
+
+#define TC_ACT_OK_TEST 0
+#define TC_ACT_SHOT_TEST 2
+
+/* Selects the bpf_skb_adjust_room() call made by tc_pppoe_reject. */
+int reject_case;
+
+/* skb->protocol as observed after the helper call. */
+int encap_proto;
+int decap_proto;
+
+/* Set when tc_pppoe_reject observes a call that should have been
+ * rejected by the helper.
+ */
+int reject_unexpected;
+
+SEC("tc")
+int tc_pppoe_encap(struct __sk_buff *skb)
+{
+ __u8 hdr[PPPOE_SES_HLEN_TEST] = {
+ 0x11, 0x00, /* ver, type, code */
+ 0xde, 0xad, /* session id */
+ 0x00, 0x00, /* length, set below */
+ 0x00, 0x00, /* PPP protocol, set below */
+ };
+ struct ethhdr eth;
+ /* The PPPoE length field covers everything after the 6 byte
+ * session header: the PPP protocol field plus the payload.
+ */
+ __u16 plen = skb->len - ETH_HLEN_TEST + 2;
+
+ hdr[4] = (plen >> 8) & 0xff;
+ hdr[5] = plen & 0xff;
+
+ switch (skb->protocol) {
+ case bpf_htons(ETH_P_IP_TEST):
+ hdr[7] = PPP_IP_TEST;
+ break;
+ case bpf_htons(ETH_P_IPV6_TEST):
+ hdr[7] = PPP_IPV6_TEST;
+ break;
+ default:
+ return TC_ACT_SHOT_TEST;
+ }
+
+ if (bpf_skb_adjust_room(skb, PPPOE_SES_HLEN_TEST, BPF_ADJ_ROOM_MAC,
+ BPF_F_ADJ_ROOM_ENCAP_PPPOE))
+ return TC_ACT_SHOT_TEST;
+
+ encap_proto = skb->protocol;
+
+ if (bpf_skb_store_bytes(skb, ETH_HLEN_TEST, hdr, sizeof(hdr), 0))
+ return TC_ACT_SHOT_TEST;
+
+ if (bpf_skb_load_bytes(skb, 0, ð, sizeof(eth)))
+ return TC_ACT_SHOT_TEST;
+ eth.h_proto = bpf_htons(ETH_P_PPP_SES_TEST);
+ if (bpf_skb_store_bytes(skb, 0, ð, sizeof(eth), 0))
+ return TC_ACT_SHOT_TEST;
+
+ return TC_ACT_OK_TEST;
+}
+
+SEC("tc")
+int tc_pppoe_decap(struct __sk_buff *skb)
+{
+ struct ethhdr eth;
+
+ if (bpf_skb_load_bytes(skb, 0, ð, sizeof(eth)))
+ return TC_ACT_SHOT_TEST;
+ if (eth.h_proto != bpf_htons(ETH_P_PPP_SES_TEST))
+ return TC_ACT_SHOT_TEST;
+
+ if (bpf_skb_adjust_room(skb, -PPPOE_SES_HLEN_TEST, BPF_ADJ_ROOM_MAC,
+ BPF_F_ADJ_ROOM_DECAP_PPPOE))
+ return TC_ACT_SHOT_TEST;
+
+ decap_proto = skb->protocol;
+
+ /* Restore the ethertype to the protocol of the decapsulated
+ * payload, as picked by the kernel from the PPP protocol field.
+ */
+ eth.h_proto = (__be16)skb->protocol;
+ if (bpf_skb_store_bytes(skb, 0, ð, sizeof(eth), 0))
+ return TC_ACT_SHOT_TEST;
+
+ return TC_ACT_OK_TEST;
+}
+
+/* Every bpf_skb_adjust_room() call below must be rejected by the
+ * helper; tc_pppoe_reject reports (and fails the test) if one of
+ * them unexpectedly succeeds.
+ */
+SEC("tc")
+int tc_pppoe_reject(struct __sk_buff *skb)
+{
+ int ret = 0;
+
+ switch (reject_case) {
+ case 1:
+ /* encap with a wrong room size */
+ ret = bpf_skb_adjust_room(skb, PPPOE_SES_HLEN_TEST - 4,
+ BPF_ADJ_ROOM_MAC,
+ BPF_F_ADJ_ROOM_ENCAP_PPPOE);
+ break;
+ case 2:
+ /* encap at the wrong position */
+ ret = bpf_skb_adjust_room(skb, PPPOE_SES_HLEN_TEST,
+ BPF_ADJ_ROOM_NET,
+ BPF_F_ADJ_ROOM_ENCAP_PPPOE);
+ break;
+ case 3:
+ /* encap with a negative room size */
+ ret = bpf_skb_adjust_room(skb, -PPPOE_SES_HLEN_TEST,
+ BPF_ADJ_ROOM_MAC,
+ BPF_F_ADJ_ROOM_ENCAP_PPPOE);
+ break;
+ case 4:
+ /* encap flag combined with a decap flag */
+ ret = bpf_skb_adjust_room(skb, PPPOE_SES_HLEN_TEST,
+ BPF_ADJ_ROOM_MAC,
+ BPF_F_ADJ_ROOM_ENCAP_PPPOE |
+ BPF_F_ADJ_ROOM_DECAP_PPPOE);
+ break;
+ case 5:
+ /* shrink of a PPPoE packet without the PPPoE flag */
+ ret = bpf_skb_adjust_room(skb, -PPPOE_SES_HLEN_TEST,
+ BPF_ADJ_ROOM_MAC, 0);
+ break;
+ case 6:
+ /* decap of a non-PPPoE packet (plain IP or fake PPP proto) */
+ ret = bpf_skb_adjust_room(skb, -PPPOE_SES_HLEN_TEST,
+ BPF_ADJ_ROOM_MAC,
+ BPF_F_ADJ_ROOM_DECAP_PPPOE);
+ break;
+ }
+
+ if (!ret)
+ reject_unexpected = 1;
+
+ return TC_ACT_SHOT_TEST;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.47.3
next prev parent reply other threads:[~2026-09-26 21:08 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-26 21:07 [RFC bpf-next 0/3] bpf: Add PPPoE encap/decap support to bpf_skb_adjust_room ThisSeanZhang
2026-09-26 21:07 ` [RFC bpf-next 1/3] bpf: Add PPPoE encap " ThisSeanZhang
2026-09-26 21:07 ` [RFC bpf-next 2/3] bpf: Add PPPoE decap " ThisSeanZhang
2026-09-26 21:07 ` ThisSeanZhang [this message]
2026-09-26 21:33 ` [RFC bpf-next 3/3] selftests/bpf: Add a test for the PPPoE encap/decap adjust_room flags sashiko-bot
2026-09-27 4:41 ` [RFC bpf-next 0/3] bpf: Add PPPoE encap/decap support to bpf_skb_adjust_room Alexei Starovoitov
2026-09-27 6:13 ` Sean zhang
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260926210757.2152159-4-thisseanzhang@gmail.com \
--to=thisseanzhang@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=dqfext@gmail.com \
--cc=eddyz87@gmail.com \
--cc=nbd@openwrt.org \
--cc=netdev@vger.kernel.org \
--cc=nhudson@akamai.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox