Netdev List
 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: 7+ 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:17 ` [PATCH v1 bpf-next/net 4/5] bpf: Add kfunc to proxy TX HW Timestamp Kuniyuki Iwashima
2026-06-12  3:37   ` Alexei Starovoitov
2026-06-12  0:17 ` Kuniyuki Iwashima [this message]

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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox