All of lore.kernel.org
 help / color / mirror / Atom feed
From: Kuniyuki Iwashima <kuniyu@google.com>
To: Alexei Starovoitov <ast@kernel.org>,
	Daniel Borkmann <daniel@iogearbox.net>,
	 Martin KaFai Lau <martin.lau@linux.dev>,
	Stanislav Fomichev <sdf@fomichev.me>,
	 Andrii Nakryiko <andrii@kernel.org>,
	John Fastabend <john.fastabend@gmail.com>,
	 Kumar Kartikeya Dwivedi <memxor@gmail.com>,
	Eduard Zingerman <eddyz87@gmail.com>
Cc: Song Liu <song@kernel.org>,
	Yonghong Song <yonghong.song@linux.dev>,
	 Jiri Olsa <jolsa@kernel.org>, Andrew Lunn <andrew@lunn.ch>,
	 "David S . Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@google.com>,
	 Jakub Kicinski <kuba@kernel.org>,
	Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
	 Willem de Bruijn <willemb@google.com>,
	Kuniyuki Iwashima <kuniyu@google.com>,
	 Kuniyuki Iwashima <kuni1840@gmail.com>,
	bpf@vger.kernel.org, netdev@vger.kernel.org
Subject: [PATCH v1 bpf-next/net 5/5] selftest: bpf: Add test for hwtstamp proxy.
Date: Fri, 12 Jun 2026 00:17:36 +0000	[thread overview]
Message-ID: <20260612001803.23341-6-kuniyu@google.com> (raw)
In-Reply-To: <20260612001803.23341-1-kuniyu@google.com>

This selftest simulates the hardware timestamp proxy scenario mentioned
in the previous commits using two UDP sockets.

Here, app_fd represents a standard socket application, and proxy_fd
simulates a userspace proxy that receives and injects encapsulated
packets from/to app_fd via a GENEVE device (geneve0).

TX:
   1. app_fd sends data w/ SCM_TS_OPT_ID
   2. BPF prog hooks at tc/egress of geneve0
   3. BPF inserts the GENEVE option with Type 0x1 to save SCM_TS_OPT_ID
   4. proxy_fd receives the encapsulated packet
   5. proxy changes the option Type to 0x2 and sets TX hwtstamp
   6. proxy sends it back to geneve0
   7. BPF prog hooks at tc/ingress of geneve0
   8. BPF extracts TX hwtstamp into skb
   9. BPF looks up the app_fd socket
  10. BPF enqueues skb to app_fd's sk->sk_error_queue
  11. app_fd receives TX hwtstamp and verifies the value

RX:
  12. proxy_fd generates RX packet from TX packet
       by swapping src/dst in each header
  13. proxy changes the option Type to 0x3 and sets RX hwtstamp
  14. proxy sends the encapsulated packet to geneve0
  15. BPF prog hooks at tc/ingress of geneve0
  16. BPF extracts RX hwtstamp into skb
  17. app_fd receives RX hwtstamp and verifies the value

The GENEVE TLV option is structured as follows:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |          Option Class         |      Type     |0|0|0| Length  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   +                     HW Timestamp  (8 bytes)                   +
   |                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                     Timestamp key (4 bytes)                   |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

   Type:
   - 0x1: TX packet
   - 0x2: TX completion packet w/ TX hwtstamp
   - 0x3: RX packet w/ RX hwtstamp

Signed-off-by: Kuniyuki Iwashima <kuniyu@google.com>
---
 tools/testing/selftests/bpf/bpf_kfuncs.h      |  10 +
 .../selftests/bpf/prog_tests/proxy_hwtstamp.c | 580 ++++++++++++++++++
 .../selftests/bpf/progs/bpf_tracing_net.h     |   1 +
 .../selftests/bpf/progs/proxy_hwtstamp.c      | 234 +++++++
 4 files changed, 825 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/prog_tests/proxy_hwtstamp.c
 create mode 100644 tools/testing/selftests/bpf/progs/proxy_hwtstamp.c

diff --git a/tools/testing/selftests/bpf/bpf_kfuncs.h b/tools/testing/selftests/bpf/bpf_kfuncs.h
index 7dad01439391..8d119b10ed0d 100644
--- a/tools/testing/selftests/bpf/bpf_kfuncs.h
+++ b/tools/testing/selftests/bpf/bpf_kfuncs.h
@@ -92,4 +92,14 @@ extern int bpf_set_dentry_xattr(struct dentry *dentry, const char *name__str,
 				const struct bpf_dynptr *value_p, int flags) __ksym __weak;
 extern int bpf_remove_dentry_xattr(struct dentry *dentry, const char *name__str) __ksym __weak;
 
+extern int bpf_skb_scrub_tx_tstamp(struct __sk_buff *s) __ksym __weak;
+
+struct bpf_hwtstamp;
+extern int bpf_skb_set_hwtstamp(struct __sk_buff *s,
+				struct bpf_hwtstamp *attrs, int attrs__sz) __ksym __weak;
+
+struct bpf_tx_tstamp_cmpl;
+extern int bpf_skb_complete_tx_tstamp(struct __sk_buff *s,
+				      struct bpf_tx_tstamp_cmpl *attrs,
+				      int attrs__sz) __ksym __weak;
 #endif
diff --git a/tools/testing/selftests/bpf/prog_tests/proxy_hwtstamp.c b/tools/testing/selftests/bpf/prog_tests/proxy_hwtstamp.c
new file mode 100644
index 000000000000..d0f90f22bea2
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/proxy_hwtstamp.c
@@ -0,0 +1,580 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright 2026 Google LLC */
+
+#include <sys/epoll.h>
+#include <net/if.h>
+#include <linux/errqueue.h>
+#include <linux/net_tstamp.h>
+
+#include "test_progs.h"
+#include <network_helpers.h>
+#include "proxy_hwtstamp.skel.h"
+
+#define swap(a, b)				\
+	do {					\
+		typeof(a) __tmp = (a);		\
+		(a) = (b);			\
+		(b) = __tmp;			\
+	} while (0)
+
+#define swap_array(a, b)			\
+	do {					\
+		char __tmp[sizeof(a)];		\
+		memcpy(__tmp, a, sizeof(a));	\
+		memcpy(a, b, sizeof(a));	\
+		memcpy(b, __tmp, sizeof(a));	\
+	} while (0)
+
+struct genevehdr {
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	u8 opt_len:6;
+	u8 ver:2;
+	u8 rsvd1:6;
+	u8 critical:1;
+	u8 oam:1;
+#else
+	u8 ver:2;
+	u8 opt_len:6;
+	u8 oam:1;
+	u8 critical:1;
+	u8 rsvd1:6;
+#endif
+	__be16 proto_type;
+	u8 vni[3];
+	u8 rsvd2;
+};
+
+struct geneve_opt {
+	__be16	opt_class;
+	u8	type;
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+	u8	length:5;
+	u8	r3:1;
+	u8	r2:1;
+	u8	r1:1;
+#else
+	u8	r1:1;
+	u8	r2:1;
+	u8	r3:1;
+	u8	length:5;
+#endif
+};
+
+struct proxy_header {
+	struct genevehdr geneve;
+	struct geneve_opt geneve_opt;
+	s64 hwtstamp;
+	u32 tskey;
+	struct ethhdr eth;
+	union {
+		struct {
+			struct iphdr ip;
+			struct udphdr udp;
+		} v4;
+		struct {
+			struct ipv6hdr ip;
+			struct udphdr udp;
+		} v6;
+	};
+} __attribute__((packed));
+
+#define GENEVE_VNI		0x900913
+#define GENEVE_OPT_CLASS	0x9009
+#define GENEVE_OPT_LEN		((sizeof(struct proxy_hwtstamp_opt)	\
+				  - sizeof(struct geneve_opt)) / 4)
+enum {
+	GENEVE_OPT_TYPE_TX	= 1,
+	GENEVE_OPT_TYPE_TX_CMPL	= 2,
+	GENEVE_OPT_TYPE_RX	= 3,
+};
+
+#define APP_DST_IPV4		"192.168.0.1"
+#define APP_DST_IPV6		"2001:db7::92"
+
+#define GENEVE_PORT		6081
+#define APP_SRC_IPV4		"10.0.3.1"
+#define APP_SRC_IPV6		"2001:db8::1"
+
+#define HWTSTAMP		0x12345678
+#define TSKEY			0xaabbccdd
+
+static struct proxy_hwtstamp_test_case {
+	char name[8];
+	int family;
+	char geneve_remote_ip[16];
+	char geneve_local_ip[16];
+	char app_dst_ip[16];
+	int app_dst_port;
+	int encap_payload_len;
+
+	/* fields below are populated during test. */
+	struct proxy_hwtstamp *skel;
+	struct netns_obj *netns;
+	struct sockaddr_storage geneve_remote_addr;
+	struct sockaddr_storage geneve_local_addr;
+	socklen_t addrlen;
+	int proxy_fd;
+	int app_fd;
+#define APP_PAYLOAD_LEN		512
+	char app_payload[APP_PAYLOAD_LEN];
+	char encap_payload[APP_PAYLOAD_LEN + sizeof(struct proxy_header)];
+} test_cases[] = {
+	{
+		.name = "IPv4",
+		.family = AF_INET,
+		.geneve_remote_ip = "127.0.0.1",
+		.geneve_local_ip = APP_SRC_IPV4,
+		.app_dst_ip = APP_DST_IPV4,
+		.app_dst_port = 443,
+		.encap_payload_len = APP_PAYLOAD_LEN + offsetofend(struct proxy_header, v4),
+	},
+	{
+		.name = "IPv6",
+		.family = AF_INET6,
+		.geneve_remote_ip = "::1",
+		.geneve_local_ip = APP_SRC_IPV6,
+		.app_dst_ip = APP_DST_IPV6,
+		.app_dst_port = 443,
+		.encap_payload_len = APP_PAYLOAD_LEN + offsetofend(struct proxy_header, v6),
+	},
+};
+
+char *ipv4_commands[] = {
+	"ip link set dev lo up",
+	"ip link add geneve0 type geneve local " APP_SRC_IPV4 " external",
+	"ip addr add " APP_SRC_IPV4 "/24 dev geneve0",
+	"ip link set dev geneve0 address aa:bb:cc:dd:ee:ff",
+	"ip link set dev geneve0 up",
+	"ip route add " APP_DST_IPV4 "/32 dev geneve0",
+	/* We do not forward ARP to the wire in this test,
+	 * so a static neighbour entry is needed for APP_DST_IPV4.
+	 */
+	"ip neigh add " APP_DST_IPV4 " lladdr ab:bc:cd:de:ef:fa dev geneve0",
+};
+
+char *ipv6_commands[] = {
+	"ip link set dev lo up",
+	"ip link add geneve0 type geneve local " APP_SRC_IPV6 " external",
+	"ip -6 addr add " APP_SRC_IPV6 "/32 dev geneve0 nodad",
+	"ip link set dev geneve0 address aa:bb:cc:dd:ee:ff",
+	"ip link set dev geneve0 up",
+	"ip -6 route add " APP_DST_IPV6 "/128 dev geneve0",
+	/* Similarly, APP_DST_IPV6 needs a static neighbour entry */
+	"ip -6 neigh add " APP_DST_IPV6 " lladdr ab:bc:cd:de:ef:fa dev geneve0",
+};
+
+static int setup_netns(struct proxy_hwtstamp_test_case *test_case)
+{
+	int i, array_size, ret;
+	char **commands;
+
+	if (test_case->family == AF_INET) {
+		commands = ipv4_commands;
+		array_size = ARRAY_SIZE(ipv4_commands);
+	} else {
+		commands = ipv6_commands;
+		array_size = ARRAY_SIZE(ipv6_commands);
+	}
+
+	for (i = 0; i < array_size; i++) {
+		ret = system(commands[i]);
+		if (!ASSERT_OK(ret, commands[i]))
+			break;
+	}
+
+	return ret;
+}
+
+static int setup_tcx(struct proxy_hwtstamp_test_case *test_case)
+{
+	struct proxy_hwtstamp *skel = test_case->skel;
+	LIBBPF_OPTS(bpf_tcx_opts, tcx_opts_ingress);
+	LIBBPF_OPTS(bpf_tcx_opts, tcx_opts_egress);
+	struct bpf_link *link;
+	int ifindex;
+
+	ifindex = if_nametoindex("geneve0");
+
+	if (make_sockaddr(test_case->family, test_case->geneve_remote_ip, GENEVE_PORT,
+			  &test_case->geneve_remote_addr, &test_case->addrlen))
+		goto err;
+
+	if (make_sockaddr(test_case->family, test_case->geneve_local_ip, GENEVE_PORT,
+			  &test_case->geneve_local_addr, &test_case->addrlen))
+		goto err;
+
+	/* Set up struct bpf_tunnel_key for GENEVE.
+	 * Note that bpf_skb_set_tunnel_key() expects
+	 *   IPv4 address in host byte order
+	 *   IPv6 address in network byte order.
+	 */
+	skel->bss->key_dst.tunnel_id = GENEVE_VNI;
+	if (test_case->family == AF_INET) {
+		struct sockaddr_in *addr4;
+
+		addr4 = (struct sockaddr_in *)&test_case->geneve_remote_addr;
+		skel->bss->key_dst.remote_ipv4 = ntohl(addr4->sin_addr.s_addr);
+
+		addr4 = (struct sockaddr_in *)&test_case->geneve_local_addr;
+		skel->bss->key_dst.local_ipv4 = ntohl(addr4->sin_addr.s_addr);
+
+		skel->bss->tunnel_tx_flags = BPF_F_ZERO_CSUM_TX;
+		skel->bss->tunnel_rx_flags = 0;
+	} else {
+		struct sockaddr_in6 *addr6;
+
+		addr6 = (struct sockaddr_in6 *)&test_case->geneve_remote_addr;
+		memcpy(&skel->bss->key_dst.remote_ipv6,
+		       &addr6->sin6_addr, sizeof(addr6->sin6_addr));
+
+		addr6 = (struct sockaddr_in6 *)&test_case->geneve_local_addr;
+		memcpy(&skel->bss->key_dst.local_ipv6,
+		       &addr6->sin6_addr, sizeof(addr6->sin6_addr));
+
+		/* IPv6 requires BPF_F_TUNINFO_IPV6.
+		 * Since udpv6_rcv() drops 0 csum packets unlike udp_rcv()
+		 * by default, UDP_NO_CHECK6_RX must be set on the proxy socket.
+		 */
+		skel->bss->tunnel_tx_flags = BPF_F_ZERO_CSUM_TX | BPF_F_TUNINFO_IPV6;
+		skel->bss->tunnel_rx_flags = BPF_F_TUNINFO_IPV6;
+	}
+
+	/* Attach BPF progs to egress and ingress. */
+	link = bpf_program__attach_tcx(skel->progs.proxy_hwtstamp_ingress,
+				       ifindex, &tcx_opts_ingress);
+	if (!ASSERT_OK_PTR(link, "attach_tcx(ingress)"))
+		goto err;
+
+	skel->links.proxy_hwtstamp_ingress = link;
+
+	link = bpf_program__attach_tcx(skel->progs.proxy_hwtstamp_egress,
+				       ifindex, &tcx_opts_egress);
+	if (!ASSERT_OK_PTR(link, "attach_tcx(egress)"))
+		goto err;
+
+	skel->links.proxy_hwtstamp_egress = link;
+
+	return 0;
+err:
+	return -1;
+}
+
+static int setup_fd(struct proxy_hwtstamp_test_case *test_case)
+{
+	int proxy_fd, app_fd;
+	int val, ret;
+
+	proxy_fd = start_server_addr(SOCK_DGRAM, &test_case->geneve_remote_addr,
+				     test_case->addrlen, NULL);
+	if (!ASSERT_OK_FD(proxy_fd, "start_server"))
+		goto err;
+
+	if (test_case->family == AF_INET6) {
+		/* udpv6_rcv() drops 0 csum (BPF_F_ZERO_CSUM_TX) packets
+		 * unless UDP_NO_CHECK6_RX is set.
+		 */
+		val = 1;
+		ret = setsockopt(proxy_fd, SOL_UDP, UDP_NO_CHECK6_RX, &val, sizeof(val));
+		if (!ASSERT_OK(ret, "setsockopt(UDP_NO_CHECK6_RX)"))
+			goto close_proxy;
+	}
+
+	app_fd = connect_to_addr_str(test_case->family, SOCK_DGRAM,
+				     test_case->app_dst_ip,
+				     test_case->app_dst_port, NULL);
+	if (!ASSERT_OK_FD(app_fd, "connect_to_addr_str"))
+		goto close_proxy;
+
+	val = SOF_TIMESTAMPING_RX_HARDWARE |
+	      SOF_TIMESTAMPING_TX_HARDWARE |
+	      SOF_TIMESTAMPING_RAW_HARDWARE |
+	      SOF_TIMESTAMPING_OPT_ID;
+	ret = setsockopt(app_fd, SOL_SOCKET, SO_TIMESTAMPING_NEW, &val, sizeof(val));
+	if (!ASSERT_OK(ret, "setsockopt(SO_TIMESTAMPING_NEW)"))
+		goto close_app;
+
+	test_case->proxy_fd = proxy_fd;
+	test_case->app_fd = app_fd;
+
+	return 0;
+
+close_app:
+	close(app_fd);
+close_proxy:
+	close(proxy_fd);
+err:
+	return -1;
+}
+
+static void destroy_env(struct proxy_hwtstamp_test_case *test_case)
+{
+	close(test_case->app_fd);
+	close(test_case->proxy_fd);
+	proxy_hwtstamp__destroy(test_case->skel);
+	netns_free(test_case->netns);
+}
+
+static int setup_env(struct proxy_hwtstamp_test_case *test_case)
+{
+	test_case->netns = netns_new("proxy_hwtstamp", true);
+	if (!ASSERT_OK_PTR(test_case->netns, "netns_new"))
+		goto err;
+
+	if (setup_netns(test_case))
+		goto free_netns;
+
+	test_case->skel = proxy_hwtstamp__open_and_load();
+	if (!ASSERT_OK_PTR(test_case->skel, "open_and_load"))
+		goto free_netns;
+
+	if (setup_tcx(test_case))
+		goto destroy_skel;
+
+	if (setup_fd(test_case))
+		goto destroy_skel;
+
+	return 0;
+
+destroy_skel:
+	proxy_hwtstamp__destroy(test_case->skel);
+free_netns:
+	netns_free(test_case->netns);
+err:
+	return -1;
+}
+
+static int wait_data(struct proxy_hwtstamp_test_case *test_case, bool tx)
+{
+	struct epoll_event event = {
+		.events = tx ? EPOLLERR : EPOLLIN,
+		.data.fd = test_case->app_fd,
+	};
+	int epoll_fd;
+	int ret = -1;
+
+	epoll_fd = epoll_create1(0);
+	if (!ASSERT_GE(epoll_fd, 0, "epoll_create1"))
+		goto out;
+
+	ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, test_case->app_fd, &event);
+	if (!ASSERT_OK(ret, "epoll_ctl"))
+		goto close_epoll;
+
+	ret = epoll_wait(epoll_fd, &event, 1, 3000);
+	if (ASSERT_EQ(ret, 1, "epoll_wait"))
+		ret = 0;
+	else
+		ret = -1;
+
+close_epoll:
+	close(epoll_fd);
+out:
+	return ret;
+}
+
+static int check_tstamp(struct proxy_hwtstamp_test_case *test_case, bool tx)
+{
+	char buf_msg[APP_PAYLOAD_LEN * 2], buf_cmsg[1024];
+	bool saw_tstamp = false, saw_tskey = false;
+	struct msghdr msg = {};
+	struct iovec iov = {};
+	struct cmsghdr *cmsg;
+	int ret;
+
+	if (wait_data(test_case, tx))
+		return -1;
+
+	iov.iov_base = buf_msg;
+	iov.iov_len = sizeof(buf_msg);
+
+	msg.msg_iov = &iov;
+	msg.msg_iovlen = 1;
+	msg.msg_control = buf_cmsg;
+	msg.msg_controllen = sizeof(buf_cmsg);
+
+	ret = recvmsg(test_case->app_fd, &msg, tx ? MSG_ERRQUEUE : 0);
+
+	if (ret > 0)
+		hexdump(tx ? "tx tstamp  " : "rx tstamp  ", buf_msg, ret);
+
+	if (!ASSERT_EQ(ret, APP_PAYLOAD_LEN, "recvmsg"))
+		return -1;
+
+	ret = memcmp(buf_msg, test_case->app_payload, sizeof(test_case->app_payload));
+	ASSERT_OK(ret, "memcmp");
+
+	ret = -1;
+
+	for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
+		if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SO_TIMESTAMPING_NEW) {
+			struct scm_timestamping *ts;
+
+			ts = (struct scm_timestamping *)CMSG_DATA(cmsg);
+			ASSERT_EQ(ts->ts[2].tv_sec, 0, "tv_sec");
+			ASSERT_EQ(ts->ts[2].tv_nsec, HWTSTAMP, "tv_nsec");
+
+			saw_tstamp = true;
+		} else if ((cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) ||
+			   (cmsg->cmsg_level == SOL_IPV6 && cmsg->cmsg_type == IPV6_RECVERR)) {
+			struct sock_extended_err *ee;
+
+			ee = (struct sock_extended_err *)CMSG_DATA(cmsg);
+
+			if (ee->ee_origin == SO_EE_ORIGIN_TIMESTAMPING) {
+				ASSERT_EQ(ee->ee_data, TSKEY, "tskey");
+				saw_tskey = true;
+			}
+		}
+	}
+
+	ASSERT_TRUE(saw_tstamp && (!tx || saw_tstamp), "no timestamp");
+
+	return ret;
+}
+
+static int test_proxy_hwtstamp_tx(struct proxy_hwtstamp_test_case *test_case)
+{
+	char h_source_dummy[ETH_HLEN] = {0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA};
+	char buf_cmsg[CMSG_SPACE(sizeof(u32))];
+	struct proxy_header *phdr;
+	struct msghdr msg = {};
+	struct iovec iov = {};
+	struct cmsghdr *cmsg;
+	int ret;
+
+	memset(test_case->app_payload, 0xAB, sizeof(test_case->app_payload));
+	iov.iov_base = test_case->app_payload;
+	iov.iov_len = sizeof(test_case->app_payload);
+
+	msg.msg_iov = &iov;
+	msg.msg_iovlen = 1;
+	msg.msg_control = buf_cmsg;
+	msg.msg_controllen = sizeof(buf_cmsg);
+
+	cmsg = CMSG_FIRSTHDR(&msg);
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_TS_OPT_ID;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(u32));
+	*(u32 *)CMSG_DATA(cmsg) = TSKEY;
+
+	ret = sendmsg(test_case->app_fd, &msg, 0);
+	if (!ASSERT_EQ(ret, sizeof(test_case->app_payload), "send"))
+		return -1;
+
+	while (1) {
+		memset(test_case->encap_payload, 0, sizeof(test_case->encap_payload));
+
+		ret = recv(test_case->proxy_fd, test_case->encap_payload,
+			   sizeof(test_case->encap_payload), 0);
+		if (ret <= (int)sizeof(phdr->geneve)) {
+			ASSERT_GT(ret, (int)sizeof(phdr->geneve), "recv(tx ingress)");
+			return -1;
+		}
+
+		phdr = (struct proxy_header *)test_case->encap_payload;
+
+		/* In the real world, we forward all packets,
+		 * including ARP, NDP, etc, but now we ignore them.
+		 * In this test case, we only care about skb with
+		 * the GENEVE option, meaning it was sent by app_fd.
+		 */
+		if (phdr->geneve.opt_len)
+			break;
+	}
+
+	hexdump("tx payload ", test_case->encap_payload,
+		test_case->encap_payload_len);
+
+	if (!ASSERT_EQ(ret, test_case->encap_payload_len, "encap payload len"))
+		return -1;
+
+	if (!ASSERT_EQ(phdr->tskey, TSKEY, "tskey"))
+		return -1;
+
+	/* Assume we have got TX hwtstamp now.
+	 * Reuse the original payload to "regenerate" the
+	 * same skb to put into app_fd's sk_error_queue.
+	 */
+	phdr->geneve_opt.type = GENEVE_OPT_TYPE_TX_CMPL;
+	phdr->hwtstamp = HWTSTAMP;
+
+	/* GENEVE drops a packet if the outer/inner eth headers
+	 * have the same source address. (See geneve_rx())
+	 * Work around it by filling a fake address.
+	 */
+	swap_array(phdr->eth.h_source, h_source_dummy);
+
+	/* Send the TX completion packet to geneve0. */
+	ret = sendto(test_case->proxy_fd,
+		     test_case->encap_payload, test_case->encap_payload_len, 0,
+		     (struct sockaddr *)&test_case->geneve_local_addr, test_case->addrlen);
+	if (!ASSERT_EQ(ret, test_case->encap_payload_len, "sendto(tx cmpl)"))
+		return -1;
+
+	swap_array(phdr->eth.h_source, h_source_dummy);
+
+	return check_tstamp(test_case, true);
+}
+
+static int test_proxy_hwtstamp_rx(struct proxy_hwtstamp_test_case *test_case)
+{
+	struct proxy_header *phdr;
+	int ret;
+
+	/* Assume we have received a packet w/ RX hwtstamp.
+	 * Generate RX packet by swapping source/dest of the
+	 * original TX packet.
+	 */
+	phdr = (struct proxy_header *)test_case->encap_payload;
+
+	swap_array(phdr->eth.h_dest, phdr->eth.h_source);
+
+	if (test_case->family == AF_INET) {
+		swap(phdr->v4.ip.daddr, phdr->v4.ip.saddr);
+		swap(phdr->v4.udp.dest, phdr->v4.udp.source);
+	} else {
+		swap(phdr->v6.ip.daddr, phdr->v6.ip.saddr);
+		swap(phdr->v6.udp.dest, phdr->v6.udp.source);
+	}
+
+	/* Embed RX hwtstamp into the GENEVE option. */
+	phdr->geneve_opt.type = GENEVE_OPT_TYPE_RX;
+	phdr->hwtstamp = HWTSTAMP;
+	phdr->tskey = 0;
+
+	/* Send the packet to geneve0. */
+	ret = sendto(test_case->proxy_fd,
+		     test_case->encap_payload, test_case->encap_payload_len, 0,
+		     (struct sockaddr *)&test_case->geneve_local_addr, test_case->addrlen);
+	if (!ASSERT_EQ(ret, test_case->encap_payload_len, "sendto(rx)"))
+		return -1;
+
+	return check_tstamp(test_case, false);
+}
+
+static void run_test(struct proxy_hwtstamp_test_case *test_case)
+{
+	int ret;
+
+	ret = setup_env(test_case);
+	if (ret)
+		return;
+
+	ret = test_proxy_hwtstamp_tx(test_case);
+	if (!ret)
+		test_proxy_hwtstamp_rx(test_case);
+
+	destroy_env(test_case);
+}
+
+void test_proxy_hwtstamp(void)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(test_cases); i++) {
+		if (!test__start_subtest(test_cases[i].name))
+			continue;
+
+		run_test(&test_cases[i]);
+	}
+}
diff --git a/tools/testing/selftests/bpf/progs/bpf_tracing_net.h b/tools/testing/selftests/bpf/progs/bpf_tracing_net.h
index d8dacef37c16..77a88dc20a64 100644
--- a/tools/testing/selftests/bpf/progs/bpf_tracing_net.h
+++ b/tools/testing/selftests/bpf/progs/bpf_tracing_net.h
@@ -73,6 +73,7 @@
 #define ETH_P_IPV6		0x86DD
 
 #define NEXTHDR_TCP		6
+#define NEXTHDR_UDP		17
 
 #define TCPOPT_NOP		1
 #define TCPOPT_EOL		0
diff --git a/tools/testing/selftests/bpf/progs/proxy_hwtstamp.c b/tools/testing/selftests/bpf/progs/proxy_hwtstamp.c
new file mode 100644
index 000000000000..c15428e4c20f
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/proxy_hwtstamp.c
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright 2026 Google LLC */
+
+#include "vmlinux.h"
+#include <errno.h>
+
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_endian.h>
+#include "bpf_tracing_net.h"
+
+struct proxy_hwtstamp_opt {
+	struct geneve_opt header;
+	ktime_t hwtstamp;
+	u32 tskey;
+} __attribute__((packed));
+
+#define GENEVE_VNI		0x900913
+#define GENEVE_OPT_CLASS	0x9009
+#define GENEVE_OPT_LEN		((sizeof(struct proxy_hwtstamp_opt)	\
+				  - sizeof(struct geneve_opt)) / 4)
+enum {
+	GENEVE_OPT_TYPE_TX	= 1,
+	GENEVE_OPT_TYPE_TX_CMPL	= 2,
+	GENEVE_OPT_TYPE_RX	= 3,
+};
+
+struct bpf_tunnel_key key_dst;	/* Populated from userspace for TX encap. */
+int tunnel_tx_flags;
+int tunnel_rx_flags;
+
+SEC("tcx/egress")
+int proxy_hwtstamp_egress(struct __sk_buff *skb)
+{
+	struct skb_shared_info *shared_info;
+	struct proxy_hwtstamp_opt opt = {};
+	struct sk_buff *kskb;
+	int ret;
+
+	/* Outgoing packet will be |ETH|IP|UDP|GENEVE|ETH|IP|UDP|Payload| */
+	ret = bpf_skb_set_tunnel_key(skb, &key_dst, sizeof(key_dst), tunnel_tx_flags);
+	if (ret < 0)
+		goto drop;
+
+	kskb = bpf_cast_to_kern_ctx(skb);
+	shared_info = bpf_core_cast(kskb->head + kskb->end, struct skb_shared_info);
+	if (!shared_info->tx_flags) {
+		/* If TX tstamp is not needed, don't insert the GENEVE option.
+		 * The proxy socket will see genevehdr.opt_len == 0.
+		 */
+		goto pass;
+	}
+
+	opt.header.opt_class = bpf_htons(GENEVE_OPT_CLASS);
+	opt.header.type = GENEVE_OPT_TYPE_TX;
+	opt.header.length = GENEVE_OPT_LEN;
+	opt.tskey = shared_info->tskey;
+
+	/* Outgoing packet will be |ETH|IP|UDP|GENEVE|GENEVE_OPT|ETH|IP|UDP|Payload| */
+	ret = bpf_skb_set_tunnel_opt(skb, &opt, sizeof(opt));
+	if (ret < 0)
+		goto drop;
+
+	bpf_skb_scrub_tx_tstamp(skb);
+pass:
+	return TCX_PASS;
+drop:
+	return TCX_DROP;
+}
+
+static int proxy_hwtstamp_sk_assign(struct __sk_buff *skb,
+				    struct bpf_tx_tstamp_cmpl *attrs)
+{
+	struct bpf_sock_tuple tuple;
+	void *data_end, *data_l4;
+	__be16 *dport, *sport;
+	struct bpf_sock *skc;
+	struct ethhdr *eth;
+	int protocol_l4;
+	int tuple_size;
+	int ret;
+
+	data_end = (void *)(long)skb->data_end;
+	eth = (struct ethhdr *)(long)skb->data;
+
+	if (eth + 1 > data_end)
+		goto drop;
+
+	attrs->protocol = eth->h_proto;
+
+	switch (bpf_ntohs(eth->h_proto)) {
+	case ETH_P_IP: {
+		struct iphdr *ipv4 = (struct iphdr *)(eth + 1);
+
+		if (ipv4 + 1 > data_end)
+			goto drop;
+
+		attrs->payload_offset += sizeof(struct iphdr);
+
+		protocol_l4 = ipv4->protocol;
+		data_l4 = ipv4 + 1;
+
+		/* Swap daddr/saddr since this skb has the original TX headers. */
+		tuple.ipv4.daddr = ipv4->saddr;
+		tuple.ipv4.saddr = ipv4->daddr;
+
+		tuple_size = sizeof(tuple.ipv4);
+		dport = &tuple.ipv4.dport;
+		sport = &tuple.ipv4.sport;
+		break;
+	}
+	case ETH_P_IPV6: {
+		struct ipv6hdr *ipv6 = (struct ipv6hdr *)(eth + 1);
+
+		if (ipv6 + 1 > data_end)
+			goto drop;
+
+		attrs->payload_offset += sizeof(struct ipv6hdr);
+
+		protocol_l4 = ipv6->nexthdr;
+		data_l4 = ipv6 + 1;
+
+		/* Swap daddr/saddr since this skb has the original TX headers. */
+		__builtin_memcpy(tuple.ipv6.daddr, &ipv6->saddr, sizeof(tuple.ipv6.daddr));
+		__builtin_memcpy(tuple.ipv6.saddr, &ipv6->daddr, sizeof(tuple.ipv6.saddr));
+
+		tuple_size = sizeof(tuple.ipv6);
+		dport = &tuple.ipv6.dport;
+		sport = &tuple.ipv6.sport;
+		break;
+	}
+	default:
+		goto drop;
+	}
+
+	switch (protocol_l4) {
+	case IPPROTO_UDP: {
+		struct udphdr *udp = data_l4;
+
+		if (udp + 1 > data_end)
+			goto drop;
+
+		attrs->payload_offset += sizeof(struct udphdr);
+
+		/* Swap sport/dport since this skb has the original TX headers. */
+		*dport = udp->source;
+		*sport = udp->dest;
+
+		skc = bpf_sk_lookup_udp(skb, &tuple, tuple_size, -1, 0);
+		break;
+	}
+	default:
+		goto drop;
+	}
+	if (!skc)
+		goto drop;
+
+	ret = bpf_sk_assign(skb, skc, 0);
+	bpf_sk_release(skc);
+
+	if (ret)
+		goto drop;
+
+	return 0;
+drop:
+	return TCX_DROP;
+}
+
+static int proxy_hwtstamp_tx_completion(struct __sk_buff *skb, u32 tskey)
+{
+	struct bpf_tx_tstamp_cmpl attrs = {
+		.network_offset = sizeof(struct ethhdr),
+		.payload_offset = sizeof(struct ethhdr),
+		.tskey = tskey,
+	};
+	int ret;
+
+	/* Set skb->sk to the socket of the original sender. */
+	ret = proxy_hwtstamp_sk_assign(skb, &attrs);
+	if (ret)
+		return ret;
+
+	ret = bpf_skb_complete_tx_tstamp(skb, &attrs, sizeof(attrs));
+	if (ret)
+		return TCX_DROP;
+
+	return TCX_ERRQUEUE;
+}
+
+SEC("tcx/ingress")
+int proxy_hwtstamp_ingress(struct __sk_buff *skb)
+{
+	struct proxy_hwtstamp_opt opt;
+	struct bpf_tunnel_key key;
+	int ret;
+
+	/* Get the GENEVE header. */
+	ret = bpf_skb_get_tunnel_key(skb, &key, sizeof(key), tunnel_rx_flags);
+	if (ret < 0)
+		goto drop;
+
+	if (key.tunnel_id != GENEVE_VNI)
+		goto drop;
+
+	/* Get the GENEVE option. */
+	ret = bpf_skb_get_tunnel_opt(skb, &opt, sizeof(opt));
+	if (ret < sizeof(opt)) {
+		 /* If TX/RX tstamp is not needed, the proxy socket
+		  * does not insert the GENEVE option.
+		  */
+		goto pass;
+	}
+
+	if (opt.header.opt_class != bpf_htons(GENEVE_OPT_CLASS) ||
+	    opt.header.length != GENEVE_OPT_LEN)
+		goto drop;
+
+	if (opt.header.type == GENEVE_OPT_TYPE_TX_CMPL ||
+	    opt.header.type == GENEVE_OPT_TYPE_RX) {
+		struct bpf_hwtstamp attrs = {
+			.hwtstamp = opt.hwtstamp,
+		};
+
+		bpf_skb_set_hwtstamp(skb, &attrs, sizeof(attrs));
+
+		if (opt.header.type == GENEVE_OPT_TYPE_TX_CMPL)
+			return proxy_hwtstamp_tx_completion(skb, opt.tskey);
+	}
+pass:
+	return TCX_PASS;
+drop:
+	return TCX_DROP;
+}
+
+char _license[] SEC("license") = "GPL";
-- 
2.54.0.1136.gdb2ca164c4-goog


  parent reply	other threads:[~2026-06-12  0:18 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-12  0:17 [PATCH v1 bpf-next/net 0/5] bpf: Support RX/TX HW timestamp proxy Kuniyuki Iwashima
2026-06-12  0:17 ` [PATCH v1 bpf-next/net 1/5] ethtool: Introduce ETHTOOL_MSG_TSINFO_SET for virtual interfaces Kuniyuki Iwashima
2026-06-12  0:17 ` [PATCH v1 bpf-next/net 2/5] bpf: Rename bpf_kfunc_set_tcp_reqsk to bpf_kfunc_set_sched_cls Kuniyuki Iwashima
2026-06-12  0:17 ` [PATCH v1 bpf-next/net 3/5] bpf: Add bpf_skb_set_hwtstamp() Kuniyuki Iwashima
2026-06-12  0:33   ` sashiko-bot
2026-06-12  5:41     ` Kuniyuki Iwashima
2026-06-12  0:17 ` [PATCH v1 bpf-next/net 4/5] bpf: Add kfunc to proxy TX HW Timestamp Kuniyuki Iwashima
2026-06-12  0:33   ` sashiko-bot
2026-06-12  5:59     ` Kuniyuki Iwashima
2026-06-12  3:37   ` Alexei Starovoitov
2026-06-12  4:16     ` Kuniyuki Iwashima
2026-06-12  0:17 ` Kuniyuki Iwashima [this message]
2026-06-12  0:31   ` [PATCH v1 bpf-next/net 5/5] selftest: bpf: Add test for hwtstamp proxy sashiko-bot
2026-06-12  6:03     ` Kuniyuki Iwashima
2026-06-12 16:04       ` Alexei Starovoitov
2026-06-12 18:55         ` Kuniyuki Iwashima

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=20260612001803.23341-6-kuniyu@google.com \
    --to=kuniyu@google.com \
    --cc=andrew@lunn.ch \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=eddyz87@gmail.com \
    --cc=edumazet@google.com \
    --cc=horms@kernel.org \
    --cc=john.fastabend@gmail.com \
    --cc=jolsa@kernel.org \
    --cc=kuba@kernel.org \
    --cc=kuni1840@gmail.com \
    --cc=martin.lau@linux.dev \
    --cc=memxor@gmail.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=sdf@fomichev.me \
    --cc=song@kernel.org \
    --cc=willemb@google.com \
    --cc=yonghong.song@linux.dev \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.