diff options
Diffstat (limited to 'tools/testing/selftests/net')
197 files changed, 20762 insertions, 2091 deletions
diff --git a/tools/testing/selftests/net/.gitignore b/tools/testing/selftests/net/.gitignore index 97ad4d551d44..c9f46031ac73 100644 --- a/tools/testing/selftests/net/.gitignore +++ b/tools/testing/selftests/net/.gitignore @@ -6,6 +6,7 @@ busy_poller cmsg_sender epoll_busy_poll fin_ack_lat +getsockopt_iter hwtstamp_config icmp_rfc4884 io_uring_zerocopy_tx @@ -40,7 +41,6 @@ skf_net_off socket so_incoming_cpu so_netns_cookie -so_txtime so_rcv_listener stress_reuseport_listen tap diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile index afdea6d95bde..3ee3378f8b26 100644 --- a/tools/testing/selftests/net/Makefile +++ b/tools/testing/selftests/net/Makefile @@ -13,8 +13,11 @@ TEST_PROGS := \ arp_ndisc_untracked_subnets.sh \ bareudp.sh \ big_tcp.sh \ + big_tcp_tunnels.sh \ bind_bhash.sh \ bpf_offload.py \ + bridge_stp_mode.sh \ + bridge_vlan_dump.sh \ broadcast_ether_dst.sh \ broadcast_pmtu.sh \ busy_poll_test.sh \ @@ -22,8 +25,11 @@ TEST_PROGS := \ cmsg_so_mark.sh \ cmsg_so_priority.sh \ cmsg_time.sh \ + cork_fragsize.py \ double_udp_encap.sh \ drop_monitor_tests.sh \ + ecmp_rehash.sh \ + exception_cache.sh \ fcnal-ipv4.sh \ fcnal-ipv6.sh \ fcnal-other.sh \ @@ -36,6 +42,7 @@ TEST_PROGS := \ fib_rule_tests.sh \ fib_tests.sh \ fin_ack_lat.sh \ + fou_mcast_encap.sh \ fq_band_pktlimit.sh \ gre_gso.sh \ gre_ipv6_lladdr.sh \ @@ -44,6 +51,7 @@ TEST_PROGS := \ io_uring_zerocopy_tx.sh \ ioam6.sh \ ip6_gre_headroom.sh \ + ip6_tunnel.sh \ ip_defrag.sh \ ip_local_port_range.sh \ ipv6_flowlabel.sh \ @@ -54,6 +62,7 @@ TEST_PROGS := \ l2tp.sh \ link_netns.py \ lwt_dst_cache_ref_loop.sh \ + macvlan_mcast_shared_mac.sh \ msg_zerocopy.sh \ nat6to4.sh \ ndisc_unsolicited_na_test.sh \ @@ -61,8 +70,11 @@ TEST_PROGS := \ netdevice.sh \ netns-name.sh \ netns-sysctl.sh \ + nk_qlease.py \ nl_netdev.py \ + nl_nlctrl.py \ pmtu.sh \ + protodown.sh \ psock_snd.sh \ reuseaddr_ports_exhausted.sh \ reuseport_addr_any.sh \ @@ -77,7 +89,7 @@ TEST_PROGS := \ rxtimestamp.sh \ sctp_vrf.sh \ skf_net_off.sh \ - so_txtime.sh \ + srv6_encap_lookup_l3vpn_test.sh \ srv6_end_dt46_l3vpn_test.sh \ srv6_end_dt4_l3vpn_test.sh \ srv6_end_dt6_l3vpn_test.sh \ @@ -88,7 +100,9 @@ TEST_PROGS := \ srv6_end_x_next_csid_l3vpn_test.sh \ srv6_hencap_red_l3vpn_test.sh \ srv6_hl2encap_red_l2vpn_test.sh \ + srv6_iptunnel_cache.sh \ stress_reuseport_listen.sh \ + tcp_ecmp_failover.sh \ tcp_fastopen_backup_key.sh \ test_bpf.sh \ test_bridge_backup_port.sh \ @@ -101,6 +115,7 @@ TEST_PROGS := \ test_vxlan_nh.sh \ test_vxlan_nolocalbypass.sh \ test_vxlan_under_vrf.sh \ + test_vxlan_vnifilter_notify.sh \ test_vxlan_vnifiltering.sh \ tfo_passive.sh \ traceroute.sh \ @@ -119,6 +134,7 @@ TEST_PROGS := \ vrf_route_leaking.sh \ vrf_strict_mode_test.sh \ xfrm_policy.sh \ + xfrm_state.sh \ # end of TEST_PROGS TEST_PROGS_EXTENDED := \ @@ -136,7 +152,6 @@ TEST_GEN_FILES := \ ip_local_port_range \ ipsec \ ipv6_flowlabel \ - ipv6_flowlabel_mgr \ msg_zerocopy \ nettest \ psock_fanout \ @@ -149,7 +164,6 @@ TEST_GEN_FILES := \ skf_net_off \ so_netns_cookie \ so_rcv_listener \ - so_txtime \ socket \ stress_reuseport_listen \ tcp_fastopen_backup_key \ @@ -168,7 +182,9 @@ TEST_GEN_PROGS := \ bind_timewait \ bind_wildcard \ epoll_busy_poll \ + getsockopt_iter \ icmp_rfc4884 \ + ipv6_flowlabel_mgr \ ipv6_fragmentation \ proc_net_pktgen \ reuseaddr_conflict \ diff --git a/tools/testing/selftests/net/af_unix/.gitignore b/tools/testing/selftests/net/af_unix/.gitignore index 240b26740c9e..954f0958dd03 100644 --- a/tools/testing/selftests/net/af_unix/.gitignore +++ b/tools/testing/selftests/net/af_unix/.gitignore @@ -3,6 +3,9 @@ msg_oob scm_inq scm_pidfd scm_rights +scm_rights_denial_lsm +scm_rights_denial_lsm.bpf.o so_peek_off unix_connect unix_connreset +unix_listen diff --git a/tools/testing/selftests/net/af_unix/Makefile b/tools/testing/selftests/net/af_unix/Makefile index 4c0375e28bbe..a66f10fb0c23 100644 --- a/tools/testing/selftests/net/af_unix/Makefile +++ b/tools/testing/selftests/net/af_unix/Makefile @@ -11,9 +11,18 @@ TEST_GEN_PROGS := \ scm_inq \ scm_pidfd \ scm_rights \ + scm_rights_denial_lsm \ so_peek_off \ unix_connect \ unix_connreset \ + unix_listen \ # end of TEST_GEN_PROGS +TEST_GEN_FILES := scm_rights_denial_lsm.bpf.o + include ../../lib.mk +include ../bpf.mk + +$(OUTPUT)/scm_rights_denial_lsm: $(BPFOBJ) +$(OUTPUT)/scm_rights_denial_lsm: CFLAGS += -I$(SCRATCH_DIR)/include +$(OUTPUT)/scm_rights_denial_lsm: LDLIBS += -lelf -lz diff --git a/tools/testing/selftests/net/af_unix/config b/tools/testing/selftests/net/af_unix/config index b5429c15a53c..46450fea8407 100644 --- a/tools/testing/selftests/net/af_unix/config +++ b/tools/testing/selftests/net/af_unix/config @@ -1,3 +1,11 @@ CONFIG_AF_UNIX_OOB=y +CONFIG_BPF=y +CONFIG_BPF_EVENTS=y +CONFIG_BPF_JIT=y +CONFIG_BPF_LSM=y +CONFIG_BPF_SYSCALL=y +CONFIG_DEBUG_INFO_BTF=y +CONFIG_SECURITY=y CONFIG_UNIX=y CONFIG_UNIX_DIAG=m +CONFIG_USER_NS=y diff --git a/tools/testing/selftests/net/af_unix/msg_oob.c b/tools/testing/selftests/net/af_unix/msg_oob.c index 1b499d56656c..f051d79f7a8e 100644 --- a/tools/testing/selftests/net/af_unix/msg_oob.c +++ b/tools/testing/selftests/net/af_unix/msg_oob.c @@ -290,6 +290,25 @@ static void __setinlinepair(struct __test_metadata *_metadata, } } +static void __setblockingpair(struct __test_metadata *_metadata, + FIXTURE_DATA(msg_oob) *self) +{ + int i; + + for (i = 0; i < 2; i++) { + int ret, old_flags, flags; + + old_flags = fcntl(self->fd[i * 2 + 1], F_GETFL, 0); + ASSERT_NE(-1, old_flags); + + ret = fcntl(self->fd[i * 2 + 1], F_SETFL, old_flags & ~O_NONBLOCK); + ASSERT_EQ(0, ret); + + flags = fcntl(self->fd[i * 2 + 1], F_GETFL, 0); + ASSERT_EQ(old_flags & ~O_NONBLOCK, flags); + } +} + static void __siocatmarkpair(struct __test_metadata *_metadata, FIXTURE_DATA(msg_oob) *self, bool oob_head) @@ -347,6 +366,9 @@ static void __resetpair(struct __test_metadata *_metadata, #define setinlinepair() \ __setinlinepair(_metadata, self) +#define setblockingpair() \ + __setblockingpair(_metadata, self) + #define resetpair(reset) \ __resetpair(_metadata, self, variant, reset) @@ -888,4 +910,49 @@ TEST_F(msg_oob, inline_ex_oob_siocatmark) resetpair(true); } +TEST_F(msg_oob, zero_buf_oob) +{ + sendpair("a", 1, MSG_OOB); + recvpair("", 0, 0, 0); +} + +TEST_F(msg_oob, zero_buf_oob_blocking) +{ + sendpair("a", 1, MSG_OOB); + setblockingpair(); + recvpair("", 0, 0, 0); +} + +TEST_F(msg_oob, zero_buf_non_oob_oob) +{ + sendpair("ab", 2, MSG_OOB); + recvpair("", 0, 0, 0); +} + +TEST_F(msg_oob, zero_buf_non_oob_oob_blocking) +{ + sendpair("ab", 2, MSG_OOB); + setblockingpair(); + recvpair("", 0, 0, 0); +} + +TEST_F(msg_oob, zero_buf_ex_oob_oob) +{ + sendpair("a", 1, MSG_OOB); + recvpair("a", 1, 1, MSG_OOB); + + sendpair("b", 1, MSG_OOB); + recvpair("", 0, 0, 0); +} + +TEST_F(msg_oob, zero_buf_ex_oob_oob_blocking) +{ + sendpair("a", 1, MSG_OOB); + recvpair("a", 1, 1, MSG_OOB); + + sendpair("b", 1, MSG_OOB); + setblockingpair(); + recvpair("", 0, 0, 0); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/af_unix/scm_inq.c b/tools/testing/selftests/net/af_unix/scm_inq.c index 3a86be9bda17..6268b5bf50be 100644 --- a/tools/testing/selftests/net/af_unix/scm_inq.c +++ b/tools/testing/selftests/net/af_unix/scm_inq.c @@ -8,8 +8,9 @@ #include "kselftest_harness.h" -#define NR_CHUNKS 100 -#define MSG_LEN 256 +#define NR_CHUNKS 100 +#define MSG_LEN 256 +#define NR_PARTIAL_READS 3 FIXTURE(scm_inq) { @@ -120,4 +121,53 @@ TEST_F(scm_inq, basic) recv_chunks(_metadata, self); } +TEST_F(scm_inq, partial_read) +{ + char buf[MSG_LEN * NR_PARTIAL_READS] = {}; + char cmsg_buf[CMSG_SPACE(sizeof(int))]; + struct msghdr msg = {}; + struct iovec iov = {}; + struct cmsghdr *cmsg; + int err, inq, ret, i; + int remain; + + err = setsockopt(self->fd[1], SOL_SOCKET, SO_INQ, &(int){1}, sizeof(int)); + if (variant->type != SOCK_STREAM) { + ASSERT_EQ(-ENOPROTOOPT, -errno); + return; + } + ASSERT_EQ(0, err); + + ret = send(self->fd[0], buf, sizeof(buf), 0); + ASSERT_EQ(sizeof(buf), ret); + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buf; + msg.msg_controllen = sizeof(cmsg_buf); + + iov.iov_base = buf; + iov.iov_len = MSG_LEN; + + for (i = 0; i < NR_PARTIAL_READS; i++) { + remain = MSG_LEN * (NR_PARTIAL_READS - 1 - i); + + memset(buf, 0, MSG_LEN); + memset(cmsg_buf, 0, sizeof(cmsg_buf)); + ret = recvmsg(self->fd[1], &msg, 0); + ASSERT_EQ(MSG_LEN, ret); + + cmsg = CMSG_FIRSTHDR(&msg); + ASSERT_NE(NULL, cmsg); + ASSERT_EQ(CMSG_LEN(sizeof(int)), cmsg->cmsg_len); + ASSERT_EQ(SOL_SOCKET, cmsg->cmsg_level); + ASSERT_EQ(SCM_INQ, cmsg->cmsg_type); + ASSERT_EQ(remain, *(int *)CMSG_DATA(cmsg)); + + ret = ioctl(self->fd[1], SIOCINQ, &inq); + ASSERT_EQ(0, ret); + ASSERT_EQ(remain, inq); + } +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/af_unix/scm_rights.c b/tools/testing/selftests/net/af_unix/scm_rights.c index d82a79c21c17..c165f250220a 100644 --- a/tools/testing/selftests/net/af_unix/scm_rights.c +++ b/tools/testing/selftests/net/af_unix/scm_rights.c @@ -378,4 +378,21 @@ TEST_F(scm_rights, backtrack_from_scc) close_sockets(10); } +TEST_F(scm_rights, mixed_lowpoint) +{ + create_sockets(6); + + send_fd(0, 1); + send_fd(1, 2); + send_fd(2, 1); + send_fd(1, 0); + + send_fd(3, 4); + send_fd(4, 5); + send_fd(5, 4); + send_fd(4, 3); + + close_sockets(6); +} + TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c new file mode 100644 index 000000000000..4f2414465bfd --- /dev/null +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.bpf.c @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0 +#include <linux/bpf.h> +#include <linux/errno.h> +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> + +char _license[] SEC("license") = "GPL"; + +struct inode { + unsigned long i_ino; +} __attribute__((preserve_access_index)); + +struct file { + struct inode *f_inode; +} __attribute__((preserve_access_index)); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 16); + __type(key, __u64); /* inode number */ + __type(value, __u32); /* tgid of the receiver being tested */ +} denied_inodes SEC(".maps"); + +SEC("lsm/file_receive") +int BPF_PROG(scm_rights_deny, struct file *file) +{ + __u32 tgid = bpf_get_current_pid_tgid() >> 32; + __u64 ino = file->f_inode->i_ino; + __u32 *owner; + + owner = bpf_map_lookup_elem(&denied_inodes, &ino); + if (owner && *owner == tgid) + return -EPERM; + + return 0; +} diff --git a/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c new file mode 100644 index 000000000000..55c7ecdbb5fe --- /dev/null +++ b/tools/testing/selftests/net/af_unix/scm_rights_denial_lsm.c @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: GPL-2.0 +#define _GNU_SOURCE +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include <bpf/bpf.h> +#include <bpf/libbpf.h> + +#include "kselftest_harness.h" + +#ifndef SO_RIGHTS_NOTRUNC +#define SO_RIGHTS_NOTRUNC 85 +#endif + +#define NR_FILES 2 + +/* Per-file content, so a received fd can be matched to the file sent */ +#define SECRET(n) "secret %d", (n) + +/* Indices into the socketpair */ +#define SK_SENDER 0 +#define SK_RECEIVER 1 + +FIXTURE(scm_rights_denial_bpf) +{ + struct bpf_object *obj; + struct bpf_link *link; + int map_fd; + int sk[2]; + int files[NR_FILES]; + __u64 inos[NR_FILES]; + char paths[NR_FILES][64]; +}; + +FIXTURE_VARIANT(scm_rights_denial_bpf) +{ + int sock_type; +}; + +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, stream) +{ + .sock_type = SOCK_STREAM, +}; + +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, dgram) +{ + .sock_type = SOCK_DGRAM, +}; + +FIXTURE_VARIANT_ADD(scm_rights_denial_bpf, seqpacket) +{ + .sock_type = SOCK_SEQPACKET, +}; + +FIXTURE_SETUP(scm_rights_denial_bpf) +{ + struct bpf_program *prog; + char lsms[256] = {}; + int i, fd; + + if (geteuid() != 0) + SKIP(return, "requires root"); + + fd = open("/sys/kernel/security/lsm", O_RDONLY); + ASSERT_GE(fd, 0); + ASSERT_LT(0, read(fd, lsms, sizeof(lsms) - 1)); + close(fd); + + if (!strstr(lsms, "bpf")) + SKIP(return, "BPF LSM not active (boot with lsm=...,bpf)"); + + self->obj = bpf_object__open_file("scm_rights_denial_lsm.bpf.o", NULL); + ASSERT_NE(NULL, self->obj); + ASSERT_EQ(0, bpf_object__load(self->obj)); + + prog = bpf_object__find_program_by_name(self->obj, "scm_rights_deny"); + ASSERT_NE(NULL, prog); + + self->link = bpf_program__attach_lsm(prog); + ASSERT_NE(NULL, self->link); + + self->map_fd = bpf_object__find_map_fd_by_name(self->obj, + "denied_inodes"); + ASSERT_GE(self->map_fd, 0); + + ASSERT_EQ(0, socketpair(AF_UNIX, variant->sock_type, 0, self->sk)); + + for (i = 0; i < NR_FILES; i++) { + struct stat st; + + snprintf(self->paths[i], sizeof(self->paths[i]), + "/tmp/scm_rights_denial_bpf.%d.XXXXXX", i); + self->files[i] = mkstemp(self->paths[i]); + ASSERT_GE(self->files[i], 0); + + ASSERT_LT(0, dprintf(self->files[i], SECRET(i))); + + ASSERT_EQ(0, fstat(self->files[i], &st)); + self->inos[i] = st.st_ino; + } +} + +FIXTURE_TEARDOWN(scm_rights_denial_bpf) +{ + bpf_link__destroy(self->link); + bpf_object__close(self->obj); + + for (int i = 0; i < NR_FILES; i++) { + if (self->files[i] >= 0) { + close(self->files[i]); + unlink(self->paths[i]); + } + } + + close(self->sk[SK_SENDER]); + close(self->sk[SK_RECEIVER]); +} + +static int deny_inode(int map_fd, __u64 ino) +{ + __u32 tgid = getpid(); + + return bpf_map_update_elem(map_fd, &ino, &tgid, BPF_ANY); +} + +static int set_notrunc(int sk) +{ + int one = 1; + + return setsockopt(sk, SOL_SOCKET, SO_RIGHTS_NOTRUNC, + &one, sizeof(one)); +} + +static int send_fds(int sk, int *fds, int n) +{ + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))] = {}; + char data = 'x'; + struct iovec iov = { + .iov_base = &data, + .iov_len = sizeof(data), + }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = ctrl, + .msg_controllen = CMSG_SPACE(n * sizeof(int)), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + int ret; + + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(n * sizeof(int)); + memcpy(CMSG_DATA(cmsg), fds, n * sizeof(int)); + + ret = sendmsg(sk, &msg, 0); + if (ret != 1) + return -1; + + return 0; +} + +static int recv_fd_slots(int sk, int *slots, int *msg_flags) +{ + int nr_slots; + char ctrl[CMSG_SPACE(NR_FILES * sizeof(int))]; + char data; + struct iovec iov = { + .iov_base = &data, + .iov_len = sizeof(data), + }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = ctrl, + .msg_controllen = sizeof(ctrl), + }; + struct cmsghdr *cmsg; + + if (recvmsg(sk, &msg, 0) < 0) + return -1; + + *msg_flags = msg.msg_flags; + + cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) + return 0; + + nr_slots = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int); + memcpy(slots, CMSG_DATA(cmsg), nr_slots * sizeof(int)); + + return nr_slots; +} + +/* Prove a received fd works by reading back the file's content. */ +static int check_secret(int fd, int idx) +{ + char want[32], got[32] = {}; + + snprintf(want, sizeof(want), SECRET(idx)); + if (pread(fd, got, sizeof(got) - 1, 0) < 0) + return -1; + + return strcmp(want, got); +} + +TEST_F(scm_rights_denial_bpf, all_allowed) +{ + int slots[NR_FILES], nr_slots, flags; + + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER])); + ASSERT_EQ(0, send_fds(self->sk[SK_SENDER], self->files, NR_FILES)); + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags); + + ASSERT_EQ(NR_FILES, nr_slots); + EXPECT_EQ(0, flags & MSG_CTRUNC); + + for (int i = 0; i < nr_slots; i++) { + ASSERT_GE(slots[i], 0); + EXPECT_EQ(0, check_secret(slots[i], i)); + close(slots[i]); + } +} + +TEST_F(scm_rights_denial_bpf, first_denied) +{ + int slots[NR_FILES], nr_slots, flags; + + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[0])); + + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER])); + ASSERT_EQ(0, send_fds(self->sk[SK_SENDER], self->files, NR_FILES)); + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags); + + ASSERT_EQ(NR_FILES, nr_slots); + EXPECT_EQ(0, flags & MSG_CTRUNC); + + EXPECT_EQ(-EPERM, slots[0]); + for (int i = 1; i < nr_slots; i++) { + ASSERT_GE(slots[i], 0); + EXPECT_EQ(0, check_secret(slots[i], i)); + close(slots[i]); + } +} + +TEST_F(scm_rights_denial_bpf, all_denied) +{ + int slots[NR_FILES], nr_slots, flags, i; + + for (i = 0; i < NR_FILES; i++) + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[i])); + + ASSERT_EQ(0, set_notrunc(self->sk[SK_RECEIVER])); + ASSERT_EQ(0, send_fds(self->sk[SK_SENDER], self->files, NR_FILES)); + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags); + + ASSERT_EQ(NR_FILES, nr_slots); + EXPECT_EQ(0, flags & MSG_CTRUNC); + + for (i = 0; i < nr_slots; i++) + EXPECT_EQ(-EPERM, slots[i]); +} + +TEST_F(scm_rights_denial_bpf, denied_without_notrunc) +{ + int slots[NR_FILES], nr_slots, flags; + + /* + * Baseline behaviour without SO_RIGHTS_NOTRUNC: the fd array is + * truncated at the first denied fd and MSG_CTRUNC is set. + */ + ASSERT_EQ(0, deny_inode(self->map_fd, self->inos[1])); + + ASSERT_EQ(0, send_fds(self->sk[SK_SENDER], self->files, NR_FILES)); + nr_slots = recv_fd_slots(self->sk[SK_RECEIVER], slots, &flags); + + ASSERT_EQ(1, nr_slots); + EXPECT_NE(0, flags & MSG_CTRUNC); + + ASSERT_GE(slots[0], 0); + EXPECT_EQ(0, check_secret(slots[0], 0)); + close(slots[0]); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/af_unix/so_peek_off.c b/tools/testing/selftests/net/af_unix/so_peek_off.c index 86e7b0fb522d..f6466a717f49 100644 --- a/tools/testing/selftests/net/af_unix/so_peek_off.c +++ b/tools/testing/selftests/net/af_unix/so_peek_off.c @@ -76,6 +76,19 @@ FIXTURE_TEARDOWN(so_peek_off) ASSERT_STREQ(str, buf); \ } while (0) +#define peekoffeq(fd, expected) \ + do { \ + socklen_t optlen = sizeof(int); \ + int off = -1; \ + int ret; \ + \ + ret = getsockopt(fd, SOL_SOCKET, SO_PEEK_OFF, \ + &off, &optlen); \ + ASSERT_EQ(0, ret); \ + ASSERT_EQ((socklen_t)sizeof(off), optlen); \ + ASSERT_EQ(expected, off); \ + } while (0) + #define async \ for (pid_t pid = (pid = fork(), \ pid < 0 ? \ @@ -91,7 +104,12 @@ TEST_F(so_peek_off, single_chunk) sendeq(self->fd[0], "aaaabbbb", 0); recveq(self->fd[1], "aaaa", 4, MSG_PEEK); + peekoffeq(self->fd[1], 4); recveq(self->fd[1], "bbbb", 100, MSG_PEEK); + peekoffeq(self->fd[1], 8); + + recveq(self->fd[1], "aaaabbbb", 8, 0); + peekoffeq(self->fd[1], 0); } TEST_F(so_peek_off, two_chunks) @@ -100,7 +118,13 @@ TEST_F(so_peek_off, two_chunks) sendeq(self->fd[0], "bbbb", 0); recveq(self->fd[1], "aaaa", 4, MSG_PEEK); + peekoffeq(self->fd[1], 4); recveq(self->fd[1], "bbbb", 100, MSG_PEEK); + peekoffeq(self->fd[1], 8); + + recveq(self->fd[1], "aaaa", 4, 0); + recveq(self->fd[1], "bbbb", 4, 0); + peekoffeq(self->fd[1], 0); } TEST_F(so_peek_off, two_chunks_blocking) @@ -111,6 +135,7 @@ TEST_F(so_peek_off, two_chunks_blocking) } recveq(self->fd[1], "aaaa", 4, MSG_PEEK); + peekoffeq(self->fd[1], 4); async { usleep(1000); @@ -119,24 +144,38 @@ TEST_F(so_peek_off, two_chunks_blocking) /* goto again; -> goto redo; in unix_stream_read_generic(). */ recveq(self->fd[1], "bbbb", 100, MSG_PEEK); + peekoffeq(self->fd[1], 8); + + recveq(self->fd[1], "aaaa", 4, 0); + recveq(self->fd[1], "bbbb", 4, 0); + peekoffeq(self->fd[1], 0); } TEST_F(so_peek_off, two_chunks_overlap) { sendeq(self->fd[0], "aaaa", 0); recveq(self->fd[1], "aa", 2, MSG_PEEK); + peekoffeq(self->fd[1], 2); sendeq(self->fd[0], "bbbb", 0); if (variant->type == SOCK_STREAM) { /* SOCK_STREAM tries to fill the buffer. */ recveq(self->fd[1], "aabb", 4, MSG_PEEK); + peekoffeq(self->fd[1], 6); recveq(self->fd[1], "bb", 100, MSG_PEEK); + peekoffeq(self->fd[1], 8); } else { /* SOCK_DGRAM and SOCK_SEQPACKET returns at the skb boundary. */ recveq(self->fd[1], "aa", 100, MSG_PEEK); + peekoffeq(self->fd[1], 4); recveq(self->fd[1], "bbbb", 100, MSG_PEEK); + peekoffeq(self->fd[1], 8); } + + recveq(self->fd[1], "aaaa", 4, 0); + recveq(self->fd[1], "bbbb", 4, 0); + peekoffeq(self->fd[1], 0); } TEST_F(so_peek_off, two_chunks_overlap_blocking) @@ -147,6 +186,7 @@ TEST_F(so_peek_off, two_chunks_overlap_blocking) } recveq(self->fd[1], "aa", 2, MSG_PEEK); + peekoffeq(self->fd[1], 2); async { usleep(1000); @@ -155,8 +195,14 @@ TEST_F(so_peek_off, two_chunks_overlap_blocking) /* Even SOCK_STREAM does not wait if at least one byte is read. */ recveq(self->fd[1], "aa", 100, MSG_PEEK); + peekoffeq(self->fd[1], 4); recveq(self->fd[1], "bbbb", 100, MSG_PEEK); + peekoffeq(self->fd[1], 8); + + recveq(self->fd[1], "aaaa", 4, 0); + recveq(self->fd[1], "bbbb", 4, 0); + peekoffeq(self->fd[1], 0); } TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/af_unix/unix_listen.c b/tools/testing/selftests/net/af_unix/unix_listen.c new file mode 100644 index 000000000000..416fa3e5bfe9 --- /dev/null +++ b/tools/testing/selftests/net/af_unix/unix_listen.c @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Tests for the state checks in AF_UNIX listen(). + * + * The central case is a regression test: listen() on a bound socket that + * is already connected (i.e. not in TCP_CLOSE or TCP_LISTEN state) must + * fail with EINVAL. A prior change accidentally let it return success + * without doing anything, because a helper called in between reset the + * error code to 0. The neighbouring checks (unbound, already listening) + * are tested too so they cannot silently regress the same way. + * + * Every case runs for both listenable socket types (SOCK_STREAM and + * SOCK_SEQPACKET) and both pathname and abstract addresses. + */ +#define _GNU_SOURCE + +#include <errno.h> +#include <stddef.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <sys/socket.h> +#include <sys/un.h> + +#include "kselftest_harness.h" + +#define SK_NAME "unix_listen_sk" +#define SRV_NAME "unix_listen_srv" + +FIXTURE(unix_listen) +{ + int sk; /* socket under test */ + int server; /* a listening peer, when a test needs one */ + struct sockaddr_un addr, srv_addr; + socklen_t addrlen, srv_addrlen; +}; + +FIXTURE_VARIANT(unix_listen) +{ + int type; + int abstract; +}; + +FIXTURE_VARIANT_ADD(unix_listen, stream_pathname) +{ + .type = SOCK_STREAM, + .abstract = 0, +}; + +FIXTURE_VARIANT_ADD(unix_listen, stream_abstract) +{ + .type = SOCK_STREAM, + .abstract = 1, +}; + +FIXTURE_VARIANT_ADD(unix_listen, seqpacket_pathname) +{ + .type = SOCK_SEQPACKET, + .abstract = 0, +}; + +FIXTURE_VARIANT_ADD(unix_listen, seqpacket_abstract) +{ + .type = SOCK_SEQPACKET, + .abstract = 1, +}; + +/* Fill @addr with a pathname or abstract address named @name. */ +static socklen_t unix_set_addr(struct sockaddr_un *addr, const char *name, + int abstract) +{ + size_t len = strlen(name); + + memset(addr, 0, sizeof(*addr)); + addr->sun_family = AF_UNIX; + /* An abstract address leads with a NUL and has no filesystem entry. */ + memcpy(addr->sun_path + (abstract ? 1 : 0), name, len); + + return offsetof(struct sockaddr_un, sun_path) + len + 1; +} + +FIXTURE_SETUP(unix_listen) +{ + self->sk = -1; + self->server = -1; + self->addrlen = unix_set_addr(&self->addr, SK_NAME, variant->abstract); + self->srv_addrlen = unix_set_addr(&self->srv_addr, SRV_NAME, + variant->abstract); +} + +FIXTURE_TEARDOWN(unix_listen) +{ + if (self->sk >= 0) + close(self->sk); + if (self->server >= 0) + close(self->server); + + /* Pathname sockets leave a filesystem entry behind; abstract ones do not. */ + if (!variant->abstract) { + remove(SK_NAME); + remove(SRV_NAME); + } +} + +/* A bound socket in TCP_CLOSE is the normal, allowed case. */ +TEST_F(unix_listen, bound_is_ok) +{ + int err; + + self->sk = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, self->sk); + + err = bind(self->sk, (struct sockaddr *)&self->addr, self->addrlen); + ASSERT_EQ(0, err); + + err = listen(self->sk, 8); + EXPECT_EQ(0, err); +} + +/* Listening again on an already-listening socket (TCP_LISTEN) is allowed. */ +TEST_F(unix_listen, relisten_is_ok) +{ + int err; + + self->sk = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, self->sk); + + err = bind(self->sk, (struct sockaddr *)&self->addr, self->addrlen); + ASSERT_EQ(0, err); + + err = listen(self->sk, 8); + ASSERT_EQ(0, err); + + err = listen(self->sk, 16); + EXPECT_EQ(0, err); +} + +/* listen() on an unbound socket fails: there is nothing to listen on. */ +TEST_F(unix_listen, unbound_is_einval) +{ + int err; + + self->sk = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, self->sk); + + err = listen(self->sk, 8); + EXPECT_EQ(-1, err); + EXPECT_EQ(EINVAL, errno); +} + +/* + * The regression: a bound socket that has already been connected is not in + * TCP_CLOSE or TCP_LISTEN, so listen() must reject it with EINVAL rather + * than quietly succeeding. + */ +TEST_F(unix_listen, connected_is_einval) +{ + int err; + + self->server = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, self->server); + + err = bind(self->server, (struct sockaddr *)&self->srv_addr, + self->srv_addrlen); + ASSERT_EQ(0, err); + + err = listen(self->server, 8); + ASSERT_EQ(0, err); + + self->sk = socket(AF_UNIX, variant->type, 0); + ASSERT_LE(0, self->sk); + + /* Bind first so the unbound check does not mask the state check. */ + err = bind(self->sk, (struct sockaddr *)&self->addr, self->addrlen); + ASSERT_EQ(0, err); + + err = connect(self->sk, (struct sockaddr *)&self->srv_addr, + self->srv_addrlen); + ASSERT_EQ(0, err); + + err = listen(self->sk, 8); + EXPECT_EQ(-1, err); + EXPECT_EQ(EINVAL, errno); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/big_tcp_tunnels.sh b/tools/testing/selftests/net/big_tcp_tunnels.sh new file mode 100755 index 000000000000..cc0875e52fb9 --- /dev/null +++ b/tools/testing/selftests/net/big_tcp_tunnels.sh @@ -0,0 +1,218 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-2.0 +# +# Testing for IPv4 and IPv6 BIG TCP over VXLAN and GENEVE tunnels. + +source "$(dirname "$0")/lib.sh" + +SERVER_NS=$(mktemp -u server-XXXXXXXX) +SERVER_IP4="192.168.1.1" +SERVER_IP6="2001:db8::1:1" +SERVER_IP4_TUN="192.168.2.1" +SERVER_IP6_TUN="2001:db8::2:1" + +CLIENT_NS=$(mktemp -u client-XXXXXXXX) +CLIENT_IP4="192.168.1.2" +CLIENT_IP6="2001:db8::1:2" +CLIENT_IP4_TUN="192.168.2.2" +CLIENT_IP6_TUN="2001:db8::2:2" + +# Kselftest framework requirement - SKIP code is 4. +ksft_skip=4 + +if [ -z "$PACKETS_THRESHOLD" ]; then + if [ "$KSFT_MACHINE_SLOW" = yes ]; then + echo 'Debug kernel detected, lowering the default threshold' + PACKETS_THRESHOLD=100 + else + PACKETS_THRESHOLD=1000 + fi +fi + +setup() { + ip netns add "$SERVER_NS" + ip netns add "$CLIENT_NS" + ip -netns "$SERVER_NS" link add link1 type veth peer name link0 netns "$CLIENT_NS" + + ip -netns "$CLIENT_NS" link set link0 up + ip -netns "$CLIENT_NS" addr replace "$CLIENT_IP4/24" dev link0 + ip -netns "$CLIENT_NS" addr replace "$CLIENT_IP6/112" dev link0 nodad + ip -netns "$CLIENT_NS" link set link0 \ + gso_max_size 196608 gso_ipv4_max_size 196608 \ + gro_max_size 196608 gro_ipv4_max_size 196608 + ip -netns "$SERVER_NS" link set link1 up + ip -netns "$SERVER_NS" addr replace "$SERVER_IP4/24" dev link1 + ip -netns "$SERVER_NS" addr replace "$SERVER_IP6/112" dev link1 nodad + ip -netns "$SERVER_NS" link set link1 \ + gso_max_size 196608 gso_ipv4_max_size 196608 \ + gro_max_size 196608 gro_ipv4_max_size 196608 + + ip netns exec "$SERVER_NS" netserver >/dev/null + wait_local_port_listen "$SERVER_NS" 12865 tcp + + DEFAULT_TCP_MIN_TSO_SEGS=$(ip netns exec "$CLIENT_NS" sysctl -n net.ipv4.tcp_min_tso_segs) +} + +setup_tunnel() { + if [ "$2" = 4 ]; then + SERVER_IP="$SERVER_IP4" + CLIENT_IP="$CLIENT_IP4" + echo "Setting up ${1^^} over IPv4, veth tx csum offload $3" + else + SERVER_IP="$SERVER_IP6" + CLIENT_IP="$CLIENT_IP6" + echo "Setting up ${1^^} over IPv6, veth tx csum offload $3" + fi + + if [ "$1" = vxlan ]; then + ip -netns "$CLIENT_NS" link add tun0 type vxlan \ + id 5001 remote "$SERVER_IP" local "$CLIENT_IP" dev link0 dstport 4789 + else + ip -netns "$CLIENT_NS" link add tun0 type geneve \ + id 5001 remote "$SERVER_IP" + fi + ip -netns "$CLIENT_NS" link set tun0 up + ip -netns "$CLIENT_NS" addr replace "$CLIENT_IP4_TUN/24" dev tun0 + ip -netns "$CLIENT_NS" addr replace "$CLIENT_IP6_TUN/112" dev tun0 nodad + ip -netns "$CLIENT_NS" link set tun0 \ + gso_max_size 196608 gso_ipv4_max_size 196608 \ + gro_max_size 196608 gro_ipv4_max_size 196608 + if [ "$1" = vxlan ]; then + ip -netns "$SERVER_NS" link add tun1 type vxlan \ + id 5001 remote "$CLIENT_IP" local "$SERVER_IP" dev link1 dstport 4789 + else + ip -netns "$SERVER_NS" link add tun1 type geneve \ + id 5001 remote "$CLIENT_IP" + fi + ip -netns "$SERVER_NS" link set tun1 up + ip -netns "$SERVER_NS" addr replace "$SERVER_IP4_TUN/24" dev tun1 + ip -netns "$SERVER_NS" addr replace "$SERVER_IP6_TUN/112" dev tun1 nodad + ip -netns "$SERVER_NS" link set tun1 \ + gso_max_size 196608 gso_ipv4_max_size 196608 \ + gro_max_size 196608 gro_ipv4_max_size 196608 + + ip netns exec "$CLIENT_NS" ethtool -K link0 tx-checksumming "$3" > /dev/null + ip netns exec "$SERVER_NS" ethtool -K link1 tx-checksumming "$3" > /dev/null +} + +cleanup_tunnel() { + ip -netns "$CLIENT_NS" link del tun0 + ip -netns "$SERVER_NS" link del tun1 +} + +cleanup() { + ip netns pids "$SERVER_NS" | xargs -r kill + ip netns pids "$CLIENT_NS" | xargs -r kill + ip netns del "$SERVER_NS" + ip netns del "$CLIENT_NS" + rm -rf "$WORKDIR" +} + +do_test() { + local packets_threshold="$PACKETS_THRESHOLD" + + # When tx csum offload is off, software GSO is performed before passing the + # packet to veth. Check BIG TCP packets inside the VXLAN tunnel to verify + # the software checksum path: if the checksum code is broken, these packets + # will be dropped. + if [ "$3" = on ]; then + CAPTURE_IFACE='link' + if [ "$1" = 4 ]; then + IPTABLES=iptables + else + IPTABLES=ip6tables + fi + else + CAPTURE_IFACE='tun' + if [ "$2" = 4 ]; then + IPTABLES=iptables + else + IPTABLES=ip6tables + fi + packets_threshold=$(( PACKETS_THRESHOLD / 10 )) + fi + if [ "$2" = 4 ]; then + IPTABLES_SACK=iptables + else + IPTABLES_SACK=ip6tables + fi + + if [ "$3" != 'on' ] && [ "$KSFT_MACHINE_SLOW" = yes ]; then + echo 'Slow configuration; increasing net.ipv4.tcp_min_tso_segs and initcwnd' + ip netns exec "$CLIENT_NS" sysctl -w net.ipv4.tcp_min_tso_segs=52 + if [ "$2" = 4 ]; then + ip -netns "$CLIENT_NS" \ + route change 192.168.2.0/24 dev tun0 initcwnd 100 + else + ip -netns "$CLIENT_NS" -6 \ + route change 2001:db8::2:0/112 dev tun0 initcwnd 100 + fi + else + ip netns exec "$CLIENT_NS" \ + sysctl -w net.ipv4.tcp_min_tso_segs="$DEFAULT_TCP_MIN_TSO_SEGS" + fi + + ip netns exec "$SERVER_NS" "$IPTABLES" -w -t raw -I PREROUTING -i "${CAPTURE_IFACE}1" -m length ! --length 0:65535 -m comment --comment "bigtcp" + ip netns exec "$CLIENT_NS" "$IPTABLES" -w -t raw -I OUTPUT -o "${CAPTURE_IFACE}0" -m length ! --length 0:65535 -m comment --comment "bigtcp" + ip netns exec "$SERVER_NS" "$IPTABLES_SACK" -w -t raw -I OUTPUT -o "tun1" -p tcp -m tcp --tcp-flags ACK ACK --tcp-option 5 -m comment --comment "sack" + + if [ "$2" = 4 ]; then + SERVER_IP="$SERVER_IP4_TUN" + echo "Running IPv4 traffic in the tunnel" + else + SERVER_IP="$SERVER_IP6_TUN" + echo "Running IPv6 traffic in the tunnel" + fi + + ip netns exec "$CLIENT_NS" netperf -t TCP_STREAM -l 5 -H "$SERVER_IP" -- \ + -m 80000 > /dev/null + + PACKETS_SERVER=$(ip netns exec "$SERVER_NS" "$IPTABLES-save" -c -t raw | sed -rn '/ --comment bigtcp/{s/^\[([0-9]+):.*/\1/p;q}') + PACKETS_CLIENT=$(ip netns exec "$CLIENT_NS" "$IPTABLES-save" -c -t raw | sed -rn '/ --comment bigtcp/{s/^\[([0-9]+):.*/\1/p;q}') + PACKETS_SACK=$(ip netns exec "$SERVER_NS" "$IPTABLES_SACK-save" -c -t raw | sed -rn '/ --comment sack/{s/^\[([0-9]+):.*/\1/p;q}') + ip netns exec "$SERVER_NS" "$IPTABLES" -w -t raw -D PREROUTING -i "${CAPTURE_IFACE}1" -m length ! --length 0:65535 -m comment --comment "bigtcp" + ip netns exec "$CLIENT_NS" "$IPTABLES" -w -t raw -D OUTPUT -o "${CAPTURE_IFACE}0" -m length ! --length 0:65535 -m comment --comment "bigtcp" + ip netns exec "$SERVER_NS" "$IPTABLES_SACK" -w -t raw -D OUTPUT -o "tun1" -p tcp -m tcp --tcp-flags ACK ACK --tcp-option 5 -m comment --comment "sack" + + echo "Captured BIG TCP RX packets: $PACKETS_SERVER" + echo "Captured BIG TCP TX packets: $PACKETS_CLIENT" + echo "Captured TCP SACK packets: $PACKETS_SACK" + [ "$PACKETS_SERVER" -gt "$packets_threshold" ] || return 1 + [ "$PACKETS_CLIENT" -gt "$packets_threshold" ] || return 1 + [ "$PACKETS_SACK" -lt "$(( PACKETS_CLIENT / 2 ))" ] || return 1 +} + +if ! netperf -V &> /dev/null; then + echo "SKIP: Could not run test without netperf tool" + exit "$ksft_skip" +fi + +if ! iptables --version &> /dev/null; then + echo "SKIP: Could not run test without iptables tool" + exit "$ksft_skip" +fi + +if ! ethtool --version &> /dev/null; then + echo "SKIP: Could not run test without ethtool tool" + exit "$ksft_skip" +fi + +if ! ip link help 2>&1 | grep gso_ipv4_max_size &> /dev/null; then + echo "SKIP: Could not run test without gso/gro_ipv4_max_size supported in ip-link" + exit "$ksft_skip" +fi + +WORKDIR=$(mktemp -d) +trap cleanup EXIT +setup +for tunnel in vxlan geneve; do + for tun_family in 4 6; do + for traffic_family in 4 6; do + for csum_offload in on off; do + setup_tunnel "$tunnel" "$tun_family" "$csum_offload" || exit "$?" + do_test "$tun_family" "$traffic_family" "$csum_offload" || exit "$?" + cleanup_tunnel + done + done + done +done diff --git a/tools/testing/selftests/net/bind_bhash.c b/tools/testing/selftests/net/bind_bhash.c index da04b0b19b73..2bd100777448 100644 --- a/tools/testing/selftests/net/bind_bhash.c +++ b/tools/testing/selftests/net/bind_bhash.c @@ -52,18 +52,19 @@ static int bind_socket(int opt, const char *addr) err = setsockopt(sock_fd, SOL_SOCKET, opt, &reuse, sizeof(reuse)); if (err) { perror("setsockopt failed"); - goto cleanup; + goto err_free_info; } } err = bind(sock_fd, res->ai_addr, res->ai_addrlen); if (err) { perror("failed to bind to port"); - goto cleanup; + goto err_free_info; } - + freeaddrinfo(res); return sock_fd; - +err_free_info: + freeaddrinfo(res); cleanup: close(sock_fd); return err; diff --git a/tools/testing/selftests/net/bridge_stp_mode.sh b/tools/testing/selftests/net/bridge_stp_mode.sh new file mode 100755 index 000000000000..0c81fd029d79 --- /dev/null +++ b/tools/testing/selftests/net/bridge_stp_mode.sh @@ -0,0 +1,288 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2034,SC2154,SC2317,SC2329 +# +# Test for bridge STP mode selection (IFLA_BR_STP_MODE). +# +# Verifies that: +# - stp_mode defaults to auto on new bridges +# - stp_mode can be toggled between user, kernel, and auto +# - stp_mode change is rejected while STP is active (-EBUSY) +# - stp_mode user in a netns yields userspace STP (stp_state=2) +# - stp_mode kernel forces kernel STP (stp_state=1) +# - stp_mode auto preserves traditional fallback to kernel STP +# - stp_mode and stp_state can be set atomically in one message +# - stp_mode persists across STP disable/enable cycles + +source lib.sh + +require_command jq + +ALL_TESTS=" + test_default_auto + test_set_modes + test_reject_change_while_stp_active + test_idempotent_mode_while_stp_active + test_user_mode_in_netns + test_kernel_mode + test_auto_mode + test_atomic_mode_and_state + test_mode_persistence +" + +bridge_info_get() +{ + ip -n "$NS1" -d -j link show "$1" | \ + jq -r ".[0].linkinfo.info_data.$2" +} + +check_stp_mode() +{ + local br=$1; shift + local expected=$1; shift + local msg=$1; shift + local val + + val=$(bridge_info_get "$br" stp_mode) + [ "$val" = "$expected" ] + check_err $? "$msg: expected $expected, got $val" +} + +check_stp_state() +{ + local br=$1; shift + local expected=$1; shift + local msg=$1; shift + local val + + val=$(bridge_info_get "$br" stp_state) + [ "$val" = "$expected" ] + check_err $? "$msg: expected $expected, got $val" +} + +# Create a bridge in NS1, bring it up, and defer its deletion. +bridge_create() +{ + ip -n "$NS1" link add "$1" type bridge + ip -n "$NS1" link set "$1" up + defer ip -n "$NS1" link del "$1" +} + +setup_prepare() +{ + setup_ns NS1 +} + +cleanup() +{ + defer_scopes_cleanup + cleanup_all_ns +} + +# Check that stp_mode defaults to auto when creating a bridge. +test_default_auto() +{ + RET=0 + + ip -n "$NS1" link add br-test type bridge + defer ip -n "$NS1" link del br-test + + check_stp_mode br-test auto "stp_mode default" + + log_test "stp_mode defaults to auto" +} + +# Test setting stp_mode to user, kernel, and back to auto. +test_set_modes() +{ + RET=0 + + ip -n "$NS1" link add br-test type bridge + defer ip -n "$NS1" link del br-test + + ip -n "$NS1" link set dev br-test type bridge stp_mode user + check_err $? "Failed to set stp_mode to user" + check_stp_mode br-test user "after set user" + + ip -n "$NS1" link set dev br-test type bridge stp_mode kernel + check_err $? "Failed to set stp_mode to kernel" + check_stp_mode br-test kernel "after set kernel" + + ip -n "$NS1" link set dev br-test type bridge stp_mode auto + check_err $? "Failed to set stp_mode to auto" + check_stp_mode br-test auto "after set auto" + + log_test "stp_mode set user/kernel/auto" +} + +# Verify that stp_mode cannot be changed while STP is active. +test_reject_change_while_stp_active() +{ + RET=0 + + bridge_create br-test + + ip -n "$NS1" link set dev br-test type bridge stp_mode kernel + check_err $? "Failed to set stp_mode to kernel" + + ip -n "$NS1" link set dev br-test type bridge stp_state 1 + check_err $? "Failed to enable STP" + + # Changing stp_mode while STP is active should fail. + ip -n "$NS1" link set dev br-test type bridge stp_mode auto 2>/dev/null + check_fail $? "Changing stp_mode should fail while STP is active" + + check_stp_mode br-test kernel "mode unchanged after rejected change" + + # Disable STP, then change should succeed. + ip -n "$NS1" link set dev br-test type bridge stp_state 0 + check_err $? "Failed to disable STP" + + ip -n "$NS1" link set dev br-test type bridge stp_mode auto + check_err $? "Changing stp_mode should succeed after STP is disabled" + + log_test "reject stp_mode change while STP is active" +} + +# Verify that re-setting the same stp_mode while STP is active succeeds. +test_idempotent_mode_while_stp_active() +{ + RET=0 + + bridge_create br-test + + ip -n "$NS1" link set dev br-test type bridge stp_mode user stp_state 1 + check_err $? "Failed to enable STP with user mode" + + # Re-setting the same mode while STP is active should succeed. + ip -n "$NS1" link set dev br-test type bridge stp_mode user + check_err $? "Idempotent stp_mode set should succeed while STP is active" + + check_stp_state br-test 2 "stp_state after idempotent set" + + # Changing mode while disabling STP in the same message should succeed. + ip -n "$NS1" link set dev br-test type bridge stp_mode auto stp_state 0 + check_err $? "Mode change with simultaneous STP disable should succeed" + + check_stp_mode br-test auto "mode changed after disable+change" + check_stp_state br-test 0 "stp_state after disable+change" + + log_test "idempotent and simultaneous mode change while STP active" +} + +# Test that stp_mode user in a non-init netns yields userspace STP +# (stp_state == 2). This is the key use case: userspace STP without +# needing /sbin/bridge-stp or being in init_net. +test_user_mode_in_netns() +{ + RET=0 + + bridge_create br-test + + ip -n "$NS1" link set dev br-test type bridge stp_mode user + check_err $? "Failed to set stp_mode to user" + + ip -n "$NS1" link set dev br-test type bridge stp_state 1 + check_err $? "Failed to enable STP" + + check_stp_state br-test 2 "stp_state with user mode" + + log_test "stp_mode user in netns yields userspace STP" +} + +# Test that stp_mode kernel forces kernel STP (stp_state == 1) +# regardless of whether /sbin/bridge-stp exists. +test_kernel_mode() +{ + RET=0 + + bridge_create br-test + + ip -n "$NS1" link set dev br-test type bridge stp_mode kernel + check_err $? "Failed to set stp_mode to kernel" + + ip -n "$NS1" link set dev br-test type bridge stp_state 1 + check_err $? "Failed to enable STP" + + check_stp_state br-test 1 "stp_state with kernel mode" + + log_test "stp_mode kernel forces kernel STP" +} + +# Test that stp_mode auto preserves traditional behavior: in a netns +# (non-init_net), bridge-stp is not called and STP falls back to +# kernel mode (stp_state == 1). +test_auto_mode() +{ + RET=0 + + bridge_create br-test + + # Auto mode is the default; enable STP in a netns. + ip -n "$NS1" link set dev br-test type bridge stp_state 1 + check_err $? "Failed to enable STP" + + # In a netns with auto mode, bridge-stp is skipped (init_net only), + # so STP should fall back to kernel mode (stp_state == 1). + check_stp_state br-test 1 "stp_state with auto mode in netns" + + log_test "stp_mode auto preserves traditional behavior" +} + +# Test that stp_mode and stp_state can be set in a single netlink +# message. This is the intended atomic usage pattern. +test_atomic_mode_and_state() +{ + RET=0 + + bridge_create br-test + + # Set both stp_mode and stp_state in one command. + ip -n "$NS1" link set dev br-test type bridge stp_mode user stp_state 1 + check_err $? "Failed to set stp_mode user and stp_state 1 atomically" + + check_stp_state br-test 2 "stp_state after atomic set" + + log_test "atomic stp_mode user + stp_state 1 in single message" +} + +# Test that stp_mode persists across STP disable/enable cycles. +test_mode_persistence() +{ + RET=0 + + bridge_create br-test + + # Set user mode and enable STP. + ip -n "$NS1" link set dev br-test type bridge stp_mode user + ip -n "$NS1" link set dev br-test type bridge stp_state 1 + check_err $? "Failed to enable STP with user mode" + + # Disable STP. + ip -n "$NS1" link set dev br-test type bridge stp_state 0 + check_err $? "Failed to disable STP" + + # Verify mode is still user. + check_stp_mode br-test user "stp_mode after STP disable" + + # Re-enable STP -- should use user mode again. + ip -n "$NS1" link set dev br-test type bridge stp_state 1 + check_err $? "Failed to re-enable STP" + + check_stp_state br-test 2 "stp_state after re-enable" + + log_test "stp_mode persists across STP disable/enable cycles" +} + +# Check iproute2 support before setting up resources. +if ! ip link add type bridge help 2>&1 | grep -q "stp_mode"; then + echo "SKIP: iproute2 too old, missing stp_mode support" + exit "$ksft_skip" +fi + +trap cleanup EXIT + +setup_prepare +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/bridge_vlan_dump.sh b/tools/testing/selftests/net/bridge_vlan_dump.sh new file mode 100755 index 000000000000..90e18e2104e3 --- /dev/null +++ b/tools/testing/selftests/net/bridge_vlan_dump.sh @@ -0,0 +1,227 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test bridge VLAN range grouping. VLANs are collapsed into a range entry in +# the dump if they have the same per-VLAN options. These tests verify that +# VLANs with different per-VLAN option values are not grouped together. + +# shellcheck disable=SC1091,SC2034,SC2154,SC2317 +source lib.sh + +ALL_TESTS=" + vlan_range_neigh_suppress + vlan_range_mcast_max_groups + vlan_range_mcast_n_groups + vlan_range_mcast_enabled + vlan_range_pvid +" + +setup_prepare() +{ + setup_ns NS + defer cleanup_all_ns + + ip -n "$NS" link add name br0 type bridge vlan_filtering 1 \ + vlan_default_pvid 0 mcast_snooping 1 mcast_vlan_snooping 1 + ip -n "$NS" link set dev br0 up + + ip -n "$NS" link add name dummy0 type dummy + ip -n "$NS" link set dev dummy0 master br0 + ip -n "$NS" link set dev dummy0 up +} + +vlan_range_neigh_suppress() +{ + RET=0 + + # Add two new consecutive VLANs for range grouping test + bridge -n "$NS" vlan add vid 10 dev dummy0 + defer bridge -n "$NS" vlan del vid 10 dev dummy0 + + bridge -n "$NS" vlan add vid 11 dev dummy0 + defer bridge -n "$NS" vlan del vid 11 dev dummy0 + + # Configure different neigh_suppress values and verify no range grouping + bridge -n "$NS" vlan set vid 10 dev dummy0 neigh_suppress on + check_err $? "Failed to set neigh_suppress for VLAN 10" + + bridge -n "$NS" vlan set vid 11 dev dummy0 neigh_suppress off + check_err $? "Failed to set neigh_suppress for VLAN 11" + + # Verify VLANs are not shown as a range, but individual entries exist + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_fail $? "VLANs with different neigh_suppress incorrectly grouped" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+10$|^\s+10$" + check_err $? "VLAN 10 individual entry not found" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+11$|^\s+11$" + check_err $? "VLAN 11 individual entry not found" + + # Configure same neigh_suppress value and verify range grouping + bridge -n "$NS" vlan set vid 11 dev dummy0 neigh_suppress on + check_err $? "Failed to set neigh_suppress for VLAN 11" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_err $? "VLANs with same neigh_suppress not grouped" + + log_test "VLAN range grouping with neigh_suppress" +} + +vlan_range_mcast_max_groups() +{ + RET=0 + + # Add two new consecutive VLANs for range grouping test + bridge -n "$NS" vlan add vid 10 dev dummy0 + defer bridge -n "$NS" vlan del vid 10 dev dummy0 + + bridge -n "$NS" vlan add vid 11 dev dummy0 + defer bridge -n "$NS" vlan del vid 11 dev dummy0 + + # Configure different mcast_max_groups values and verify no range grouping + bridge -n "$NS" vlan set vid 10 dev dummy0 mcast_max_groups 100 + check_err $? "Failed to set mcast_max_groups for VLAN 10" + + bridge -n "$NS" vlan set vid 11 dev dummy0 mcast_max_groups 200 + check_err $? "Failed to set mcast_max_groups for VLAN 11" + + # Verify VLANs are not shown as a range, but individual entries exist + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_fail $? "VLANs with different mcast_max_groups incorrectly grouped" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+10$|^\s+10$" + check_err $? "VLAN 10 individual entry not found" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+11$|^\s+11$" + check_err $? "VLAN 11 individual entry not found" + + # Configure same mcast_max_groups value and verify range grouping + bridge -n "$NS" vlan set vid 11 dev dummy0 mcast_max_groups 100 + check_err $? "Failed to set mcast_max_groups for VLAN 11" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_err $? "VLANs with same mcast_max_groups not grouped" + + log_test "VLAN range grouping with mcast_max_groups" +} + +vlan_range_mcast_n_groups() +{ + RET=0 + + # Add two new consecutive VLANs for range grouping test + bridge -n "$NS" vlan add vid 10 dev dummy0 + defer bridge -n "$NS" vlan del vid 10 dev dummy0 + + bridge -n "$NS" vlan add vid 11 dev dummy0 + defer bridge -n "$NS" vlan del vid 11 dev dummy0 + + # Add different numbers of multicast groups to each VLAN + bridge -n "$NS" mdb add dev br0 port dummy0 grp 239.1.1.1 vid 10 + check_err $? "Failed to add mdb entry to VLAN 10" + defer bridge -n "$NS" mdb del dev br0 port dummy0 grp 239.1.1.1 vid 10 + + bridge -n "$NS" mdb add dev br0 port dummy0 grp 239.1.1.2 vid 10 + check_err $? "Failed to add second mdb entry to VLAN 10" + defer bridge -n "$NS" mdb del dev br0 port dummy0 grp 239.1.1.2 vid 10 + + bridge -n "$NS" mdb add dev br0 port dummy0 grp 239.1.1.1 vid 11 + check_err $? "Failed to add mdb entry to VLAN 11" + defer bridge -n "$NS" mdb del dev br0 port dummy0 grp 239.1.1.1 vid 11 + + # Verify VLANs are not shown as a range due to different mcast_n_groups + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_fail $? "VLANs with different mcast_n_groups incorrectly grouped" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+10$|^\s+10$" + check_err $? "VLAN 10 individual entry not found" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+11$|^\s+11$" + check_err $? "VLAN 11 individual entry not found" + + # Add another group to VLAN 11 to match VLAN 10's count + bridge -n "$NS" mdb add dev br0 port dummy0 grp 239.1.1.2 vid 11 + check_err $? "Failed to add second mdb entry to VLAN 11" + defer bridge -n "$NS" mdb del dev br0 port dummy0 grp 239.1.1.2 vid 11 + + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_err $? "VLANs with same mcast_n_groups not grouped" + + log_test "VLAN range grouping with mcast_n_groups" +} + +vlan_range_mcast_enabled() +{ + RET=0 + + # Add two new consecutive VLANs for range grouping test + bridge -n "$NS" vlan add vid 10 dev br0 self + defer bridge -n "$NS" vlan del vid 10 dev br0 self + + bridge -n "$NS" vlan add vid 11 dev br0 self + defer bridge -n "$NS" vlan del vid 11 dev br0 self + + bridge -n "$NS" vlan add vid 10 dev dummy0 + defer bridge -n "$NS" vlan del vid 10 dev dummy0 + + bridge -n "$NS" vlan add vid 11 dev dummy0 + defer bridge -n "$NS" vlan del vid 11 dev dummy0 + + # Configure different mcast_snooping for bridge VLANs + # Port VLANs inherit BR_VLFLAG_MCAST_ENABLED from bridge VLANs + bridge -n "$NS" vlan global set dev br0 vid 10 mcast_snooping 1 + bridge -n "$NS" vlan global set dev br0 vid 11 mcast_snooping 0 + + # Verify port VLANs are not grouped due to different mcast_enabled + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_fail $? "VLANs with different mcast_enabled incorrectly grouped" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+10$|^\s+10$" + check_err $? "VLAN 10 individual entry not found" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -Eq "^\S+\s+11$|^\s+11$" + check_err $? "VLAN 11 individual entry not found" + + # Configure same mcast_snooping and verify range grouping + bridge -n "$NS" vlan global set dev br0 vid 11 mcast_snooping 1 + + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "10-11" + check_err $? "VLANs with same mcast_enabled not grouped" + + log_test "VLAN range grouping with mcast_enabled" +} + +vlan_range_pvid() +{ + RET=0 + + ip -n "$NS" link set dev br0 type bridge vlan_default_pvid 1 + check_err $? "Failed to configure default PVID" + defer ip -n "$NS" link set dev br0 type bridge vlan_default_pvid 0 + + bridge -n "$NS" vlan add vid 2 dev dummy0 untagged + check_err $? "Failed to add VLAN 2" + defer bridge -n "$NS" vlan del vid 2 dev dummy0 + + bridge -n "$NS" -d vlan show dev dummy0 | + grep -Eq '(^|[[:space:]])2([[:space:]]|$)' + check_err $? "VLAN following PVID is missing from detailed dump" + + bridge -n "$NS" -d vlan show dev dummy0 | grep -q "1-2" + check_fail $? "PVID was incorrectly included in a VLAN range" + + log_test "PVID is isolated from VLAN dump ranges" +} + +# Verify the newest tested option is supported +if ! bridge vlan help 2>&1 | grep -q "neigh_suppress"; then + echo "SKIP: iproute2 too old, missing per-VLAN neighbor suppression support" + exit "$ksft_skip" +fi + +trap defer_scopes_cleanup EXIT +setup_prepare +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/broadcast_ether_dst.sh b/tools/testing/selftests/net/broadcast_ether_dst.sh index 334a7eca8a80..cc571f607429 100755 --- a/tools/testing/selftests/net/broadcast_ether_dst.sh +++ b/tools/testing/selftests/net/broadcast_ether_dst.sh @@ -44,7 +44,7 @@ test_broadcast_ether_dst() { # tcpdump will exit after receiving a single packet # timeout will kill tcpdump if it is still running after 2s timeout 2s ip netns exec "${CLIENT_NS}" \ - tcpdump -i link0 -c 1 -w "${CAPFILE}" icmp &> "${OUTPUT}" & + tcpdump -i link0 -c 1 -w "${CAPFILE}" -Z root icmp &> "${OUTPUT}" & pid=$! slowwait 1 grep -qs "listening" "${OUTPUT}" diff --git a/tools/testing/selftests/net/config b/tools/testing/selftests/net/config index cd49b7dfe216..737e7e6327b3 100644 --- a/tools/testing/selftests/net/config +++ b/tools/testing/selftests/net/config @@ -38,11 +38,15 @@ CONFIG_IP_NF_TARGET_REJECT=m CONFIG_IP_NF_TARGET_TTL=m CONFIG_IP_SCTP=m CONFIG_IPV6=y +CONFIG_IPV6_FOU=m +CONFIG_IPV6_FOU_TUNNEL=m CONFIG_IPV6_GRE=m CONFIG_IPV6_ILA=m CONFIG_IPV6_IOAM6_LWTUNNEL=y CONFIG_IPV6_MROUTE=y CONFIG_IPV6_MULTIPLE_TABLES=y +CONFIG_IPV6_ROUTE_INFO=y +CONFIG_IPV6_ROUTER_PREF=y CONFIG_IPV6_RPL_LWTUNNEL=y CONFIG_IPV6_SEG6_LWTUNNEL=y CONFIG_IPV6_SIT=y @@ -78,6 +82,7 @@ CONFIG_NETFILTER=y CONFIG_NETFILTER_ADVANCED=y CONFIG_NETFILTER_XTABLES_LEGACY=y CONFIG_NETFILTER_XT_MATCH_BPF=m +CONFIG_NETFILTER_XT_MATCH_COMMENT=y CONFIG_NETFILTER_XT_MATCH_LENGTH=m CONFIG_NETFILTER_XT_MATCH_POLICY=m CONFIG_NETFILTER_XT_NAT=m @@ -99,6 +104,7 @@ CONFIG_NET_SCH_HTB=m CONFIG_NET_SCH_INGRESS=m CONFIG_NET_SCH_NETEM=y CONFIG_NET_SCH_PRIO=m +CONFIG_NET_TEAM=y CONFIG_NET_VRF=y CONFIG_NF_CONNTRACK=m CONFIG_NF_CONNTRACK_OVS=y @@ -111,14 +117,14 @@ CONFIG_NFT_COMPAT=m CONFIG_NFT_NAT=m CONFIG_NUMA=y CONFIG_OPENVSWITCH=m -CONFIG_OPENVSWITCH_GENEVE=m -CONFIG_OPENVSWITCH_GRE=m -CONFIG_OPENVSWITCH_VXLAN=m -CONFIG_PROC_SYSCTL=y +CONFIG_PAGE_POOL_STATS=y CONFIG_PSAMPLE=m CONFIG_RPS=y +CONFIG_SYN_COOKIES=y +CONFIG_SYSCTL=y CONFIG_SYSFS=y CONFIG_TAP=m +CONFIG_TCP_CONG_DCTCP=y CONFIG_TCP_MD5SIG=y CONFIG_TEST_BLACKHOLE_DEV=m CONFIG_TEST_BPF=m diff --git a/tools/testing/selftests/net/cork_fragsize.py b/tools/testing/selftests/net/cork_fragsize.py new file mode 100755 index 000000000000..7afd643d07ec --- /dev/null +++ b/tools/testing/selftests/net/cork_fragsize.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +'''Test possible UDP length overflow in udp_send_skb/udp_v6_send_skb.''' + +import errno +import gzip +import os +import socket +import struct +import subprocess +from contextlib import contextmanager + +from lib.py import ( + KsftNamedVariant, + KsftSkipEx, + NetNS, + NetNSEnter, + defer, + ip, + ksft_eq, + ksft_exit, + ksft_pr, + ksft_raises, + ksft_run, + ksft_true, + ksft_variants, +) + +IP_MTU_DISCOVER = 10 +IP_PMTUDISC_PROBE = 3 +IPV6_MTU_DISCOVER = 23 +IPV6_PMTUDISC_DO = 2 +IPV6_PMTUDISC_PROBE = 3 +IPV6_TLV_JUMBO = 194 + + +def check_kernel_config(option: str) -> bool | None: + ''' + Check whether the option is enabled in the config of the running kernel. + Returns None if the config is not found; otherwise returns True/False + depending on the option value in the config. + ''' + + for filename, method in [ + ('/proc/config.gz', gzip.open), + (f'/boot/config-{os.uname().release}', open), + ]: + try: + with method(filename, 'rt') as config: + for line in config: + if line.rstrip() == f'{option}=y': + return True + return False + except OSError: + continue + return None + + +def assert_debug_kernel() -> None: + ''' + Skip the test if CONFIG_DEBUG_NET is not set in the kernel config. + ''' + + res = check_kernel_config('CONFIG_DEBUG_NET') + if res is None: + ksft_pr("WARN: Can't read kernel config; assuming debug kernel, and running the test") + elif not res: + raise KsftSkipEx('CONFIG_DEBUG_NET is not set') + + +def check_dmesg_clean(func: str) -> bool: + ''' + Check if the given function produced a WARN in dmesg. + ''' + + with subprocess.Popen(['dmesg'], stdout=subprocess.PIPE) as dmesg: + res = subprocess.run(['grep', '-q', f'WARNING:.*{func}'], stdin=dmesg.stdout, check=False) + return res.returncode != 0 and dmesg.returncode == 0 + + +@contextmanager +def dummy_netdev(ns: NetNS, mtu: int, ipv6: bool) -> None: + ''' + Create a dummy netdev inside the given namespace, and tune it for the test. + ''' + + ip('link add dummy type dummy', ns=ns) + with defer(ip, 'link del dummy', ns=ns): + ip(f'link set dummy mtu {mtu}', ns=ns) + ip('link set dummy up', ns=ns) + flag = '-6' if ipv6 else '' + nodad = 'nodad' if ipv6 else '' + local = 'fd00::1/64' if ipv6 else '10.0.0.1/24' + remote = 'fd00::2' if ipv6 else '10.0.0.2' + ip(f'{flag} addr add {local} dev dummy {nodad}', ns=ns) + ip(f'{flag} neigh add {remote} lladdr 02:00:00:00:00:02 dev dummy nud permanent', ns=ns) + yield + + +@ksft_variants([ + KsftNamedVariant( + 'ipv6', + True, + socket.AF_INET6, + (socket.IPPROTO_IPV6, IPV6_MTU_DISCOVER, IPV6_PMTUDISC_DO), + 'fd00::2', + 'udp_v6_send_skb', + ), + KsftNamedVariant( + 'ipv4', + False, + socket.AF_INET, + (socket.IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_PROBE), + '10.0.0.2', + 'udp_send_skb', + ), +]) +def test_udp( + ipv6: bool, + af: socket.AddressFamily, + sockopts: tuple[int, int, int], + destip: str, + func: str +) -> None: + ''' + Test that sending an oversized UDP packet over a UDP socket doesn't overflow + the 16-bit length field in the UDP header, which could happen on older + kernels in udp_send_skb/udp_v6_send_skb. + + IPv4: The packet will be dropped with EMSGSIZE, but the overflow could + happen before it happens. The only way to test this is to check dmesg on + CONFIG_DEBUG_NET=y kernels that have udp_set_len_short with the warning. + + IPv6: The packet will be dropped with EMSGSIZE on fixed kernels, and will be + sent corrupted on older kernels. Test both: sendto must return EMSGSIZE, and + dmesg must be clean of warnings on CONFIG_DEBUG_NET=y kernels. + ''' + + if not ipv6: + assert_debug_kernel() + + with ( + NetNS() as ns, + dummy_netdev(ns, 65556 + 20 * ipv6, ipv6), + NetNSEnter(ns), + socket.socket(af, socket.SOCK_DGRAM) as fd, + ): + fd.setsockopt(*sockopts) + with ksft_raises(OSError) as e: + fd.sendto(b' ' * 65528, (destip, 1234)) + # IPv6: EMSGSIZE happens on kernels with the fix. + # IPv4: EMSGSIZE happens on both fixed and unfixed kernels, after the + # WARN is printed - ignore it and rely on the dmesg check. + if e.exception is not None: + ksft_eq(e.exception.errno, errno.EMSGSIZE) + + ksft_true(check_dmesg_clean(func), 'WARNING detected in dmesg') + + +def test_ipv6_jumbo() -> None: + ''' + Test that sending UDP jumbograms over a raw IPv6 socket works, despite + having the fix for oversized UDP packets. sendto must not raise an OSError + exception (when raised, the test fails automatically). + ''' + + with ( + NetNS() as ns, + dummy_netdev(ns, 65584, True), + NetNSEnter(ns), + socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_UDP) as fd, + ): + hopopts = struct.pack('!BBBBI', 0, 0, IPV6_TLV_JUMBO, 4, 65544) + fd.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_HOPOPTS, hopopts) + fd.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_CHECKSUM, 6) + fd.setsockopt(socket.IPPROTO_IPV6, IPV6_MTU_DISCOVER, IPV6_PMTUDISC_PROBE) + udp = struct.pack('!HHHH', 1234, 1234, 0, 0) + b' ' * 65528 + fd.sendto(udp, ('fd00::2', 0)) + + +if __name__ == "__main__": + ksft_run([ + test_udp, + test_ipv6_jumbo, + ]) + ksft_exit() diff --git a/tools/testing/selftests/net/ecmp_rehash.sh b/tools/testing/selftests/net/ecmp_rehash.sh new file mode 100755 index 000000000000..f05a6c8edd2a --- /dev/null +++ b/tools/testing/selftests/net/ecmp_rehash.sh @@ -0,0 +1,1109 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test local ECMP path re-selection on TCP retransmission timeout and PLB. +# +# Two namespaces connected by two parallel veth pairs with a 2-way ECMP +# route. When a TCP path is blocked (via tc drop) or congested (via +# netem ECN marking), the kernel rehashes the connection via +# sk_rethink_txhash() + __sk_dst_reset(), causing the next route lookup +# to select the other ECMP path. +# +# Expected runtime: ~60 seconds. Most time is spent waiting for TCP +# retransmission timeouts (1-7s per test) and running multi-round +# consistency checks (10 rounds each). The large slowwait/connect-timeout +# values (30-120s) are worst-case bounds for CI; a correctly functioning +# kernel reaches each check well before the timeout expires. + +source lib.sh + +SUBNETS=(a b) +PORT=9900 +: "${ECMP_REBUILD_ROUNDS:=10}" + +# alloc_ports NAME [COUNT]: set NAME to the next free port and reserve +# COUNT ports (default 1) from a shared counter. Each test allocates its +# own port(s) where it runs, so a retry or a newly added test never +# collides; the per-round tests reserve ECMP_REBUILD_ROUNDS each. +NEXT_PORT=$PORT +alloc_ports() +{ + printf -v "$1" '%d' "$NEXT_PORT" + NEXT_PORT=$((NEXT_PORT + ${2:-1})) +} + +ALL_TESTS=" + test_ecmp_syn_rehash + test_ecmp_synack_rehash + test_ecmp_midstream_rehash + test_ecmp_midstream_ack_rehash + test_ecmp_plb_rehash + test_ecmp_hash_policy1_no_rehash + test_ecmp_no_flowlabel_leak + test_ecmp_dst_rebuild_consistency + test_ecmp_syncookie_path_consistency +" + +link_tx_packets_get() +{ + local ns=$1; shift + local dev=$1; shift + + ip netns exec "$ns" cat "/sys/class/net/$dev/statistics/tx_packets" +} + +# Return the number of packets matched by the tc filter action on a device. +# When tc drops packets via "action drop", the device's tx_packets is not +# incremented (packet never reaches veth_xmit), but the tc action maintains +# its own counter. +tc_filter_pkt_count() +{ + local ns=$1; shift + local dev=$1; shift + + ip netns exec "$ns" tc -s filter show dev "$dev" parent 1: 2>/dev/null | + awk '/Sent .* pkt/ { + for (i=1; i<=NF; i++) + if ($i == "pkt") { print $(i-1); exit } + }' +} + +# Read a TcpExt counter from /proc/net/netstat in a namespace. +# Returns 0 if the counter is not found. +get_netstat_counter() +{ + local ns=$1; shift + local field=$1; shift + local val + + # shellcheck disable=SC2016 + val=$(ip netns exec "$ns" awk -v key="$field" ' + /^TcpExt:/ { + if (!h) { split($0, n); h=1 } + else { + split($0, v) + for (i in n) + if (n[i] == key) print v[i] + } + } + ' /proc/net/netstat) + echo "${val:-0}" +} + +# Apply netem ECN marking: CE-mark all ECT packets instead of dropping them. +mark_ecn() +{ + local ns=$1; shift + local dev=$1; shift + + ip netns exec "$ns" tc qdisc add dev "$dev" root netem loss 100% ecn +} + +# Block TCP (IPv6 next-header = 6) egress, allowing ICMPv6 through. +block_tcp() +{ + local ns=$1; shift + local dev=$1; shift + + ip netns exec "$ns" tc qdisc add dev "$dev" root handle 1: prio + ip netns exec "$ns" tc filter add dev "$dev" parent 1: \ + protocol ipv6 prio 1 u32 match u8 0x06 0xff at 6 action drop +} + +unblock_tcp() +{ + local ns=$1; shift + local dev=$1; shift + + ip netns exec "$ns" tc qdisc del dev "$dev" root 2>/dev/null +} + +# Return success when a device's TX counter exceeds a baseline value. +dev_tx_packets_above() +{ + local ns=$1; shift + local dev=$1; shift + local baseline=$1; shift + + local cur + cur=$(link_tx_packets_get "$ns" "$dev") + [ "$cur" -gt "$baseline" ] +} + +# Return success when both devices have dropped at least one TCP packet. +both_devs_attempted() +{ + local ns=$1; shift + local dev0=$1; shift + local dev1=$1; shift + + local c0 c1 + c0=$(tc_filter_pkt_count "$ns" "$dev0") + c1=$(tc_filter_pkt_count "$ns" "$dev1") + [ "${c0:-0}" -ge 1 ] && [ "${c1:-0}" -ge 1 ] +} + +link_tx_packets_total() +{ + local ns=$1; shift + local dev0=${1:-veth0a}; shift 2>/dev/null + local dev1=${1:-veth1a} + + echo $(( $(link_tx_packets_get "$ns" "$dev0") + + $(link_tx_packets_get "$ns" "$dev1") )) +} + +# (Re)install the ECMP multipath routes between NS1 and NS2. $1 is the +# ip route operation ("add" to create, "change" to replace). If $2 is +# given it names a congestion control to pin on both routes via "congctl"; +# because dctcp carries TCP_CONG_NEEDS_ECN, this also tags the route with +# DST_FEATURE_ECN_CA, which makes the server negotiate ECN without the +# listener itself having to run dctcp. The nexthop topology lives here +# only, so a test can re-pin the routes and restore them with one call. +install_ecmp_routes() +{ + local op=$1 cc=$2 + local -a cc_attr=() + + [ -n "$cc" ] && cc_attr=(congctl "$cc") + + ip -n "$NS1" -6 route "$op" fd00:ff::2/128 "${cc_attr[@]}" \ + nexthop via fd00:a::2 dev veth0a \ + nexthop via fd00:b::2 dev veth1a + + ip -n "$NS2" -6 route "$op" fd00:ff::1/128 "${cc_attr[@]}" \ + nexthop via fd00:a::1 dev veth0b \ + nexthop via fd00:b::1 dev veth1b +} + +setup() +{ + setup_ns NS1 NS2 + + local ns + for ns in "$NS1" "$NS2"; do + ip netns exec "$ns" sysctl -qw net.ipv6.conf.all.accept_dad=0 + ip netns exec "$ns" sysctl -qw net.ipv6.conf.default.accept_dad=0 + ip netns exec "$ns" sysctl -qw net.ipv6.conf.all.forwarding=1 + ip netns exec "$ns" sysctl -qw net.core.txrehash=1 + done + + local i sub + for i in 0 1; do + sub=${SUBNETS[$i]} + ip link add "veth${i}a" type veth peer name "veth${i}b" + ip link set "veth${i}a" netns "$NS1" + ip link set "veth${i}b" netns "$NS2" + ip -n "$NS1" addr add "fd00:${sub}::1/64" dev "veth${i}a" + ip -n "$NS2" addr add "fd00:${sub}::2/64" dev "veth${i}b" + ip -n "$NS1" link set "veth${i}a" up + ip -n "$NS2" link set "veth${i}b" up + done + + ip -n "$NS1" addr add fd00:ff::1/128 dev lo + ip -n "$NS2" addr add fd00:ff::2/128 dev lo + + # Allow many SYN retries at 1-second intervals (linear, no + # exponential backoff) so the rehash test has enough attempts + # to exercise both ECMP paths. + if ! ip netns exec "$NS1" sysctl -qw \ + net.ipv4.tcp_syn_linear_timeouts=25; then + echo "SKIP: tcp_syn_linear_timeouts not supported" + return "$ksft_skip" + fi + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_syn_retries=25 + + # Keep the server's request socket alive during the blocking + # period so SYN/ACK retransmits continue. + ip netns exec "$NS2" sysctl -qw net.ipv4.tcp_synack_retries=25 + + install_ecmp_routes add + + for i in 0 1; do + sub=${SUBNETS[$i]} + ip netns exec "$NS1" \ + ping -6 -c1 -W5 "fd00:${sub}::2" &>/dev/null + ip netns exec "$NS2" \ + ping -6 -c1 -W5 "fd00:${sub}::1" &>/dev/null + done + + if ! ip netns exec "$NS1" ping -6 -c1 -W5 fd00:ff::2 &>/dev/null; then + echo "Basic connectivity check failed" + return "$ksft_skip" + fi +} + +# Block ALL paths, start a connection, wait until SYNs have been dropped +# on both interfaces (proving rehash steered the SYN to a new path), then +# unblock so the connection completes. +test_ecmp_syn_rehash() +{ + RET=0 + local port + alloc_ports port + + block_tcp "$NS1" veth0a + defer unblock_tcp "$NS1" veth0a + block_tcp "$NS1" veth1a + defer unblock_tcp "$NS1" veth1a + + ip netns exec "$NS2" socat \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr,fork" \ + EXEC:"echo ESTABLISH_OK" & + defer kill_process $! + + wait_local_port_listen "$NS2" "$port" tcp + + local rehash_before + rehash_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + + # Start the connection in the background; it will retry SYNs at + # 1-second intervals until an unblocked path is found. + # Use -u (unidirectional) to only receive from the server; + # sending data back would risk SIGPIPE if the server's EXEC + # child has already exited. + local tmpfile + tmpfile=$(mktemp) + defer rm -f "$tmpfile" + + ip netns exec "$NS1" socat -u \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=60" \ + STDOUT >"$tmpfile" 2>&1 & + local client_pid=$! + defer kill_process "$client_pid" + + # Wait until both paths have seen at least one dropped SYN. + # This proves sk_rethink_txhash() rehashed the connection from + # one ECMP path to the other. + slowwait 30 both_devs_attempted "$NS1" veth0a veth1a > /dev/null + check_err $? "SYNs did not appear on both paths (rehash not working)" + if [ "$RET" -ne 0 ]; then + log_test "Local ECMP SYN rehash: establish with blocked paths" + return + fi + + # Unblock both paths and let the next SYN retransmit succeed. + unblock_tcp "$NS1" veth0a + unblock_tcp "$NS1" veth1a + + local rc=0 + wait "$client_pid" || rc=$? + + local result + result=$(cat "$tmpfile" 2>/dev/null) + + if [[ "$result" != *"ESTABLISH_OK"* ]]; then + check_err 1 "connection failed after unblocking (rc=$rc): $result" + fi + + local rehash_after + rehash_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + if [ "$rehash_after" -le "$rehash_before" ]; then + check_err 1 "TcpTimeoutRehash counter did not increment" + fi + + log_test "Local ECMP SYN rehash: establish with blocked paths" +} + +# Block the server's return paths so SYN/ACKs are dropped. The client +# retransmits SYNs at 1-second intervals; each duplicate SYN arriving at +# the server triggers tcp_rtx_synack() which re-rolls txhash, so the +# retransmitted SYN/ACK selects a different ECMP return path. +test_ecmp_synack_rehash() +{ + RET=0 + local port + alloc_ports port + + block_tcp "$NS2" veth0b + defer unblock_tcp "$NS2" veth0b + block_tcp "$NS2" veth1b + defer unblock_tcp "$NS2" veth1b + + ip netns exec "$NS2" socat \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr,fork" \ + EXEC:"echo SYNACK_OK" & + defer kill_process $! + + wait_local_port_listen "$NS2" "$port" tcp + + # Start the connection; SYNs reach the server (client egress is + # open) but SYN/ACKs are dropped on the server's return path. + local tmpfile + tmpfile=$(mktemp) + defer rm -f "$tmpfile" + + ip netns exec "$NS1" socat -u \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=60" \ + STDOUT >"$tmpfile" 2>&1 & + local client_pid=$! + defer kill_process "$client_pid" + + # Wait until both server-side interfaces have dropped at least + # one SYN/ACK, proving the server rehashed its return path. + slowwait 30 both_devs_attempted "$NS2" veth0b veth1b > /dev/null + check_err $? "SYN/ACKs did not appear on both return paths" + if [ "$RET" -ne 0 ]; then + log_test "Local ECMP SYN/ACK rehash: blocked return path" + return + fi + + # Unblock and let the connection complete. + unblock_tcp "$NS2" veth0b + unblock_tcp "$NS2" veth1b + + local rc=0 + wait "$client_pid" || rc=$? + + local result + result=$(cat "$tmpfile" 2>/dev/null) + + if [[ "$result" != *"SYNACK_OK"* ]]; then + check_err 1 "connection failed after unblocking (rc=$rc): $result" + fi + + log_test "Local ECMP SYN/ACK rehash: blocked return path" +} + +# Establish a data transfer with both paths open, then block the +# active path. Verify that data appears on the previously inactive +# path (proving RTO triggered a rehash) and that TcpTimeoutRehash +# incremented. +# +# With 2-way ECMP each rehash may pick the same path, so a single +# attempt can occasionally fail. Retry once for robustness. + +# Single attempt at the midstream rehash check. Returns 0 on success. +ecmp_midstream_rehash_attempt() +{ + local port=$1; shift + local reason="" + + ip netns exec "$NS2" socat -u \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" - >/dev/null & + local server_pid=$! + + wait_local_port_listen "$NS2" "$port" tcp + + local base_tx0 base_tx1 + base_tx0=$(link_tx_packets_get "$NS1" veth0a) + base_tx1=$(link_tx_packets_get "$NS1" veth1a) + + # Continuous data source; timeout caps overall test duration and + # must exceed the slowwait below so data keeps flowing. + ip netns exec "$NS1" timeout 90 socat -u \ + OPEN:/dev/zero \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" &>/dev/null & + local client_pid=$! + + # Wait for enough packets to identify the active path. + if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \ + ">= $((base_tx0 + base_tx1 + 10))" \ + link_tx_packets_total "$NS1" > /dev/null; then + kill "$client_pid" "$server_pid" 2>/dev/null + wait "$client_pid" "$server_pid" 2>/dev/null + echo "no TX activity" + return 1 + fi + + # Find the active path and block it. + local current_tx0 current_tx1 active_idx inactive_idx + current_tx0=$(link_tx_packets_get "$NS1" veth0a) + current_tx1=$(link_tx_packets_get "$NS1" veth1a) + if [ $((current_tx0 - base_tx0)) -ge $((current_tx1 - base_tx1)) ]; then + active_idx=0; inactive_idx=1 + else + active_idx=1; inactive_idx=0 + fi + + local rehash_before + rehash_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + # Suppress __dst_negative_advice() in tcp_write_timeout() so + # that __sk_dst_reset() is the only dst-invalidation mechanism + # on the RTO path. + local saved_retries1 + saved_retries1=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_retries1) + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_retries1=255 + + block_tcp "$NS1" "veth${active_idx}a" + + # Capture baseline after block_tcp returns. block_tcp adds a + # prio qdisc then a tc filter; between those two steps the + # qdisc's CAN_BYPASS fast-path lets packets through unfiltered. + local inactive_before + inactive_before=$(link_tx_packets_get "$NS1" "veth${inactive_idx}a") + + # Wait for meaningful data on the previously inactive path, + # proving RTO triggered a rehash and data actually moved. + if ! slowwait 60 dev_tx_packets_above \ + "$NS1" "veth${inactive_idx}a" "$((inactive_before + 100))" \ + > /dev/null; then + reason="no data on alternate path" + fi + + local rehash_after + rehash_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + if [ "$rehash_after" -le "$rehash_before" ]; then + reason="${reason:+$reason; }TcpTimeoutRehash did not increment" + fi + + unblock_tcp "$NS1" "veth${active_idx}a" + ip netns exec "$NS1" sysctl -qw \ + net.ipv4.tcp_retries1="$saved_retries1" + kill "$client_pid" "$server_pid" 2>/dev/null + wait "$client_pid" "$server_pid" 2>/dev/null + if [ -n "$reason" ]; then + echo "$reason" + return 1 + fi + return 0 +} + +test_ecmp_midstream_rehash() +{ + RET=0 + local port retry_port + alloc_ports port + alloc_ports retry_port + + local fail_reason + if ! ecmp_midstream_rehash_attempt "$port" >/dev/null; then + fail_reason=$(ecmp_midstream_rehash_attempt "$retry_port") + check_err $? "$fail_reason" + fi + + log_test "Local ECMP midstream rehash: block active path" +} + +# Single attempt at the ACK rehash check. Returns 0 on success. +ecmp_ack_rehash_attempt() +{ + local port=$1; shift + local reason="" + + ip netns exec "$NS2" socat -u \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" - >/dev/null & + local server_pid=$! + + wait_local_port_listen "$NS2" "$port" tcp + + local base_tx0 base_tx1 + base_tx0=$(link_tx_packets_get "$NS2" veth0b) + base_tx1=$(link_tx_packets_get "$NS2" veth1b) + + # Continuous data source from NS1 to NS2. Cap the send buffer + # so in-flight data stays below the receiver's advertised window. + # Without this, the sender can exhaust the receiver's window and + # enter persist mode (zero-window probing) instead of RTO when + # ACKs are blocked, and persist probes do not trigger flowlabel + # rehash. + ip netns exec "$NS1" timeout 120 socat -u \ + OPEN:/dev/zero \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],sndbuf=16384" \ + &>/dev/null & + local client_pid=$! + + # Wait for enough server TX (ACKs) to identify the active return path. + if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \ + ">= $((base_tx0 + base_tx1 + 10))" \ + link_tx_packets_total "$NS2" veth0b veth1b > /dev/null; then + kill "$client_pid" "$server_pid" 2>/dev/null + wait "$client_pid" "$server_pid" 2>/dev/null + echo "no server TX activity" + return 1 + fi + + local cur_tx0 cur_tx1 active_dev inactive_dev + cur_tx0=$(link_tx_packets_get "$NS2" veth0b) + cur_tx1=$(link_tx_packets_get "$NS2" veth1b) + if [ $((cur_tx0 - base_tx0)) -ge $((cur_tx1 - base_tx1)) ]; then + active_dev=veth0b; inactive_dev=veth1b + else + active_dev=veth1b; inactive_dev=veth0b + fi + + local rehash_before + rehash_before=$(get_netstat_counter "$NS2" TcpDuplicateDataRehash) + + # Block the inactive return path first (no effect on current + # ACK flow), then block the active path. This avoids counting + # normal ACK drops as rehash evidence. + block_tcp "$NS2" "$inactive_dev" + local inactive_before + inactive_before=$(tc_filter_pkt_count "$NS2" "$inactive_dev") + block_tcp "$NS2" "$active_dev" + + # NS1 will RTO (no ACKs), retransmit with new flowlabel. + # NS2 detects the flowlabel change via tcp_rcv_spurious_retrans(), + # rehashes, and NS2's ACKs try the previously inactive return + # path. One successful rehash is sufficient. + if ! slowwait 60 until_counter_is \ + ">= $((${inactive_before:-0} + 1))" \ + tc_filter_pkt_count "$NS2" "$inactive_dev" > /dev/null; then + reason="no ACKs on alternate return path after blocking" + fi + + local rehash_after + rehash_after=$(get_netstat_counter "$NS2" TcpDuplicateDataRehash) + if [ "$rehash_after" -le "$rehash_before" ]; then + reason="${reason:+$reason; }TcpDuplicateDataRehash did not increment" + fi + + unblock_tcp "$NS2" "$active_dev" + unblock_tcp "$NS2" "$inactive_dev" + kill "$client_pid" "$server_pid" 2>/dev/null + wait "$client_pid" "$server_pid" 2>/dev/null + if [ -n "$reason" ]; then + echo "$reason" + return 1 + fi + return 0 +} + +# Block the receiver's (NS2) ACK return paths while data flows from +# NS1 to NS2. The sender (NS1) times out and retransmits with a new +# flowlabel; the receiver detects the changed flowlabel via +# tcp_rcv_spurious_retrans() and rehashes its own txhash so that its +# ACKs try a different ECMP return path. +# +# With 2-way ECMP each rehash may pick the same path, so a single +# attempt can occasionally fail. Retry once for robustness. +test_ecmp_midstream_ack_rehash() +{ + RET=0 + local port retry_port + alloc_ports port + alloc_ports retry_port + + local fail_reason + if ! ecmp_ack_rehash_attempt "$port" >/dev/null; then + fail_reason=$(ecmp_ack_rehash_attempt "$retry_port") + check_err $? "$fail_reason" + fi + + log_test "Local ECMP midstream ACK rehash: blocked return path" +} + +# Establish a DCTCP data transfer with PLB enabled, then ECN-mark both +# paths. Sustained CE marking triggers PLB to call sk_rethink_txhash() +# + __sk_dst_reset(), bouncing the connection between ECMP paths. +# Verify data appears on both paths and that TCPPLBRehash incremented. +test_ecmp_plb_rehash() +{ + RET=0 + local port + alloc_ports port + + # PLB needs DCTCP, a restricted congestion control. Adding it to + # the host-global tcp_allowed_congestion_control would relax the + # restricted-CC policy for the whole host (there is no per-netns + # allowed set). Instead pin dctcp on the test routes with + # "congctl": the route's RTAX_CC_ALGO is honoured on both the + # connect and accept paths without the restricted-CC check, and a + # dctcp route also carries DST_FEATURE_ECN_CA so the server + # negotiates ECN -- all confined to the test namespaces. + local available + available=$(ip netns exec "$NS1" sysctl -n \ + net.ipv4.tcp_available_congestion_control) + if ! echo "$available" | grep -qw dctcp; then + log_test_skip "Local ECMP PLB rehash: DCTCP not available" + return "$ksft_skip" + fi + install_ecmp_routes change dctcp + defer install_ecmp_routes change + + # Save NS1 sysctls before modifying them. + local saved_ecn1 saved_plb_enabled saved_plb_rounds + local saved_plb_thresh saved_plb_suspend + saved_ecn1=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_ecn) + saved_plb_enabled=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_enabled) + saved_plb_rounds=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_rehash_rounds) + saved_plb_thresh=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_cong_thresh) + saved_plb_suspend=$(ip netns exec "$NS1" sysctl -n net.ipv4.tcp_plb_suspend_rto_sec) + + # Enable ECN and PLB on the sender; dctcp comes from the route. + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_ecn=1 + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_enabled=1 + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_rehash_rounds=3 + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_cong_thresh=1 + ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_suspend_rto_sec=0 + defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_ecn="$saved_ecn1" + defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_enabled="$saved_plb_enabled" + defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_rehash_rounds="$saved_plb_rounds" + defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_cong_thresh="$saved_plb_thresh" + defer ip netns exec "$NS1" sysctl -qw net.ipv4.tcp_plb_suspend_rto_sec="$saved_plb_suspend" + + ip netns exec "$NS2" socat -u \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" - >/dev/null & + defer kill_process $! + + wait_local_port_listen "$NS2" "$port" tcp + + local base_tx0 base_tx1 + base_tx0=$(link_tx_packets_get "$NS1" veth0a) + base_tx1=$(link_tx_packets_get "$NS1" veth1a) + + ip netns exec "$NS1" timeout 90 socat -u \ + OPEN:/dev/zero \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" &>/dev/null & + local client_pid=$! + defer kill_process "$client_pid" + + # Wait for data to start flowing before applying ECN marking. + busywait "$BUSYWAIT_TIMEOUT" until_counter_is \ + ">= $((base_tx0 + base_tx1 + 10))" \ + link_tx_packets_total "$NS1" > /dev/null + check_err $? "no TX activity detected" + if [ "$RET" -ne 0 ]; then + log_test "Local ECMP PLB rehash: ECN-marked path" + return + fi + + # Snapshot TX counters and rehash stats before ECN marking. + local pre_ecn_tx0 pre_ecn_tx1 + pre_ecn_tx0=$(link_tx_packets_get "$NS1" veth0a) + pre_ecn_tx1=$(link_tx_packets_get "$NS1" veth1a) + + local plb_before rto_before + plb_before=$(get_netstat_counter "$NS1" TCPPLBRehash) + rto_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + + # CE-mark all data on both paths. PLB detects sustained + # congestion and rehashes, bouncing traffic between paths. + mark_ecn "$NS1" veth0a + defer unblock_tcp "$NS1" veth0a # removes the marking rule + mark_ecn "$NS1" veth1a + defer unblock_tcp "$NS1" veth1a # removes the marking rule + + # Wait for meaningful data on both paths, proving PLB rehashed + # the connection and traffic actually moved. Require at least + # 100 packets beyond the baseline to rule out stray control + # packets (ND, etc.) satisfying the check. + slowwait 60 dev_tx_packets_above \ + "$NS1" veth0a "$((pre_ecn_tx0 + 100))" > /dev/null + check_err $? "no data on veth0a after ECN marking" + + slowwait 60 dev_tx_packets_above \ + "$NS1" veth1a "$((pre_ecn_tx1 + 100))" > /dev/null + check_err $? "no data on veth1a after ECN marking" + + local plb_after rto_after + plb_after=$(get_netstat_counter "$NS1" TCPPLBRehash) + rto_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + if [ "$plb_after" -le "$plb_before" ]; then + check_err 1 "TCPPLBRehash counter did not increment" + fi + if [ "$rto_after" -gt "$rto_before" ]; then + check_err 1 "TcpTimeoutRehash incremented; rehash was RTO-driven, not PLB" + fi + + log_test "Local ECMP PLB rehash: ECN-marked path" +} + +# Verify that hash policy 1 (L3+L4 symmetric) preserves the ECMP path +# across rehash. Policy 1 computes a deterministic hash from the +# 5-tuple, so mp_hash stays 0 and rt6_multipath_hash() always selects +# the same path regardless of txhash changes. +test_ecmp_hash_policy1_no_rehash() +{ + RET=0 + local port + alloc_ports port + + local saved_policy + saved_policy=$(ip netns exec "$NS1" sysctl -n \ + net.ipv6.fib_multipath_hash_policy) + ip netns exec "$NS1" sysctl -qw net.ipv6.fib_multipath_hash_policy=1 + defer ip netns exec "$NS1" sysctl -qw \ + net.ipv6.fib_multipath_hash_policy="$saved_policy" + + block_tcp "$NS1" veth0a + defer unblock_tcp "$NS1" veth0a + block_tcp "$NS1" veth1a + defer unblock_tcp "$NS1" veth1a + + ip netns exec "$NS2" socat \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr,fork" \ + EXEC:"echo POLICY1_OK" & + defer kill_process $! + + wait_local_port_listen "$NS2" "$port" tcp + + local rehash_before + rehash_before=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + + ip netns exec "$NS1" timeout 10 socat -u \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=8" \ + STDOUT >/dev/null 2>&1 & + local client_pid=$! + defer kill_process "$client_pid" + + # With policy 1, the deterministic 5-tuple hash always selects + # the same path. Wait for multiple SYN retransmits (proving + # rehash was attempted), then verify all SYNs landed on the + # same interface. + local rehash_after + slowwait 8 until_counter_is ">= $((rehash_before + 3))" \ + get_netstat_counter "$NS1" TcpTimeoutRehash > /dev/null + rehash_after=$(get_netstat_counter "$NS1" TcpTimeoutRehash) + if [ "$rehash_after" -le "$rehash_before" ]; then + check_err 1 "TcpTimeoutRehash counter did not increment" + fi + + local c0 c1 + c0=$(tc_filter_pkt_count "$NS1" veth0a) + c1=$(tc_filter_pkt_count "$NS1" veth1a) + if [ "${c0:-0}" -ge 1 ] && [ "${c1:-0}" -ge 1 ]; then + check_err 1 "SYNs appeared on both paths despite policy 1" + fi + if [ "${c0:-0}" -eq 0 ] && [ "${c1:-0}" -eq 0 ]; then + check_err 1 "no SYNs observed on either path" + fi + + log_test "Local ECMP policy 1: no path change on rehash" +} + +# Verify that mp_hash does not leak into the on-wire flowlabel. +# With auto_flowlabels=0, the wire flowlabel must be 0. Install tc +# filters that pass TCP with flowlabel=0 but drop TCP with nonzero +# flowlabel, then establish a connection and transfer data. If +# mp_hash leaked into fl6->flowlabel, the SYN or data packets would +# be dropped and the connection would fail. +test_ecmp_no_flowlabel_leak() +{ + RET=0 + local port + alloc_ports port + + local saved_afl + saved_afl=$(ip netns exec "$NS1" sysctl -n \ + net.ipv6.auto_flowlabels) + ip netns exec "$NS1" sysctl -qw net.ipv6.auto_flowlabels=0 + defer ip netns exec "$NS1" sysctl -qw \ + net.ipv6.auto_flowlabels="$saved_afl" + + # On both egress interfaces: pass TCP with flowlabel=0 (prio 1), + # drop any remaining TCP (nonzero flowlabel, prio 2). ICMPv6 + # matches neither filter and passes through normally. + local dev + for dev in veth0a veth1a; do + ip netns exec "$NS1" tc qdisc add dev "$dev" \ + root handle 1: prio + ip netns exec "$NS1" tc filter add dev "$dev" parent 1: \ + protocol ipv6 prio 1 u32 \ + match u32 0x00000000 0x000FFFFF at 0 \ + match u8 0x06 0xff at 6 \ + action ok + ip netns exec "$NS1" tc filter add dev "$dev" parent 1: \ + protocol ipv6 prio 2 u32 \ + match u8 0x06 0xff at 6 \ + action drop + defer unblock_tcp "$NS1" "$dev" + done + + ip netns exec "$NS2" socat \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" \ + EXEC:"echo FLOWLABEL_OK" & + defer kill_process $! + + wait_local_port_listen "$NS2" "$port" tcp + + local tmpfile + tmpfile=$(mktemp) + defer rm -f "$tmpfile" + + ip netns exec "$NS1" socat -u \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1],connect-timeout=10" \ + STDOUT >"$tmpfile" 2>&1 + + local result + result=$(cat "$tmpfile" 2>/dev/null) + if [[ "$result" != *"FLOWLABEL_OK"* ]]; then + check_err 1 "connection failed: mp_hash may have leaked into wire flowlabel" + fi + + log_test "No flowlabel leak with auto_flowlabels=0" +} + +# Helper: stream data, invalidate the cached dst by adding and +# removing a dummy route (bumps fib6_node sernum), then check that +# traffic stays on the same ECMP path. Used by both the normal +# tcp_v6_connect and syncookie variants. +ecmp_dst_rebuild_check() +{ + local ns_client=$1; shift + local port=$1; shift + local rc=0 + + # Suppress __dst_negative_advice() during the test so that a + # real TCP timeout cannot trigger an additional dst + # invalidation via a different code path. + local saved_retries1 + saved_retries1=$(ip netns exec "$ns_client" sysctl -n \ + net.ipv4.tcp_retries1) + ip netns exec "$ns_client" sysctl -qw net.ipv4.tcp_retries1=255 + + local base0 base1 + base0=$(link_tx_packets_get "$ns_client" veth0a) + base1=$(link_tx_packets_get "$ns_client" veth1a) + + ip netns exec "$ns_client" timeout 15 socat -u \ + OPEN:/dev/zero \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" \ + &>/dev/null & + local client_pid=$! + + # Wait for enough packets to identify the active path. + # Return 2 for setup failure (distinct from 1 = path changed). + if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \ + ">= $((base0 + base1 + 50))" \ + link_tx_packets_total "$ns_client" > /dev/null; then + ip netns exec "$ns_client" sysctl -qw \ + net.ipv4.tcp_retries1="$saved_retries1" + kill "$client_pid" 2>/dev/null + wait "$client_pid" 2>/dev/null + return 2 + fi + + local mid0 mid1 active_dev inactive_dev + mid0=$(link_tx_packets_get "$ns_client" veth0a) + mid1=$(link_tx_packets_get "$ns_client" veth1a) + if [ $((mid0 - base0)) -ge $((mid1 - base1)) ]; then + active_dev=veth0a; inactive_dev=veth1a + else + active_dev=veth1a; inactive_dev=veth0a + fi + + local active_before inactive_before + active_before=$(link_tx_packets_get "$ns_client" "$active_dev") + inactive_before=$(link_tx_packets_get "$ns_client" "$inactive_dev") + + # Invalidate the cached dst by bumping the fib6_node sernum. + # Adding and removing a high-metric dummy route achieves this + # without touching the ECMP nexthops, avoiding a transient + # single-nexthop state during multipath route replace. + ip -n "$ns_client" -6 route add fd00:ff::2/128 dev lo metric 9999 + ip -n "$ns_client" -6 route del fd00:ff::2/128 dev lo metric 9999 + + # Wait for enough post-rebuild traffic to detect a path change. + if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \ + ">= $((active_before + inactive_before + 50))" \ + link_tx_packets_total "$ns_client" > /dev/null; then + ip netns exec "$ns_client" sysctl -qw \ + net.ipv4.tcp_retries1="$saved_retries1" + kill "$client_pid" 2>/dev/null + wait "$client_pid" 2>/dev/null + return 2 + fi + + local active_after inactive_after + active_after=$(link_tx_packets_get "$ns_client" "$active_dev") + inactive_after=$(link_tx_packets_get "$ns_client" "$inactive_dev") + + local active_delta=$((active_after - active_before)) + local inactive_delta=$((inactive_after - inactive_before)) + + if [ "$inactive_delta" -gt "$active_delta" ]; then + rc=1 + fi + + ip netns exec "$ns_client" sysctl -qw \ + net.ipv4.tcp_retries1="$saved_retries1" + kill "$client_pid" 2>/dev/null + wait "$client_pid" 2>/dev/null + return "$rc" +} + +# Run ecmp_dst_rebuild_check for ECMP_REBUILD_ROUNDS rounds, each with +# a fresh server and connection. With a correct kernel the path is +# deterministic (same txhash always selects the same ECMP nexthop), +# so any path change is a bug. Multiple rounds catch a buggy kernel +# that picks a random path: each round has 50% chance of accidentally +# matching, so 10 rounds gives < 0.1% false-pass probability. +ecmp_dst_rebuild_loop() +{ + local base_port=$1; shift + local label=$1; shift + local path_changes=0 + local r + + for r in $(seq 1 "$ECMP_REBUILD_ROUNDS"); do + local port=$((base_port + r - 1)) + + ip netns exec "$NS2" socat -u \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" \ + - >/dev/null & + local server_pid=$! + + wait_local_port_listen "$NS2" "$port" tcp + + local check_rc=0 + ecmp_dst_rebuild_check "$NS1" "$port" || check_rc=$? + + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + + busywait "$BUSYWAIT_TIMEOUT" \ + port_has_no_active_tcp "$NS1" "$port" > /dev/null + busywait "$BUSYWAIT_TIMEOUT" \ + port_has_no_active_tcp "$NS2" "$port" > /dev/null + + if [ "$check_rc" -eq 2 ]; then + check_err 1 "no TX activity in round $r" + break + elif [ "$check_rc" -eq 1 ]; then + path_changes=$((path_changes + 1)) + fi + done + + if [ "$path_changes" -gt 0 ]; then + check_err 1 "$path_changes/$ECMP_REBUILD_ROUNDS changed path" + fi + + log_test "$label" +} + +# Verify that a dst invalidation does not cause the connection to +# switch ECMP paths. With the fix, both the initial route lookup +# (tcp_v6_connect) and subsequent rebuilds (inet6_csk_route_socket) +# use sk_txhash >> 1, so the path is stable. +test_ecmp_dst_rebuild_consistency() +{ + RET=0 + local base_port + alloc_ports base_port "$ECMP_REBUILD_ROUNDS" + + ecmp_dst_rebuild_loop "$base_port" \ + "ECMP path stable after dst invalidation" +} + +# Return 0 (true) when no active TCP sockets remain on a port. +# TIME_WAIT is excluded because it does not generate outgoing traffic. +port_has_no_active_tcp() +{ + local ns=$1; shift + local port=$1; shift + + ! ip netns exec "$ns" ss -tnH \ + state established \ + state fin-wait-1 \ + state fin-wait-2 \ + state close-wait \ + state last-ack \ + state closing \ + state syn-sent \ + state syn-recv \ + "sport = :$port or dport = :$port" | grep -q . +} + +# Count TCP packets on server egress without blocking them. +# Uses tc filters with "action ok" so packets are counted and passed. +count_tcp() +{ + local ns=$1; shift + local dev=$1; shift + + ip netns exec "$ns" tc qdisc add dev "$dev" root handle 1: prio + ip netns exec "$ns" tc filter add dev "$dev" parent 1: \ + protocol ipv6 prio 1 u32 match u8 0x06 0xff at 6 action ok +} + +# Verify that the server's SYN-ACK (sent from the request socket) and +# subsequent ACKs (sent from the full socket created in cookie_v6_check) +# use the same ECMP path. With syncookies the request socket is freed +# after the SYN-ACK and a new one is created during cookie validation; +# this test catches the case where the two request sockets pick +# different ECMP paths due to independent txhash values. +test_ecmp_syncookie_path_consistency() +{ + RET=0 + + local saved_syncookies + saved_syncookies=$(ip netns exec "$NS2" sysctl -n \ + net.ipv4.tcp_syncookies 2>/dev/null) + if [ -z "$saved_syncookies" ]; then + log_test_skip "Syncookie server ECMP path consistent" + return "$ksft_skip" + fi + ip netns exec "$NS2" sysctl -qw net.ipv4.tcp_syncookies=2 + defer ip netns exec "$NS2" sysctl -qw \ + net.ipv4.tcp_syncookies="$saved_syncookies" + + count_tcp "$NS2" veth0b + defer unblock_tcp "$NS2" veth0b + count_tcp "$NS2" veth1b + defer unblock_tcp "$NS2" veth1b + + local path_splits=0 + local r base_port + alloc_ports base_port "$ECMP_REBUILD_ROUNDS" + + for r in $(seq 1 "$ECMP_REBUILD_ROUNDS"); do + local port=$((base_port + r - 1)) + + ip netns exec "$NS2" socat -u \ + "TCP6-LISTEN:$port,bind=[fd00:ff::2],reuseaddr" \ + - >/dev/null & + local server_pid=$! + + wait_local_port_listen "$NS2" "$port" tcp + + local srv_base0 srv_base1 + srv_base0=$(tc_filter_pkt_count "$NS2" veth0b) + srv_base1=$(tc_filter_pkt_count "$NS2" veth1b) + + ip netns exec "$NS1" timeout 5 socat -u \ + OPEN:/dev/zero \ + "TCP6:[fd00:ff::2]:$port,bind=[fd00:ff::1]" \ + &>/dev/null & + local client_pid=$! + + local cli_base + cli_base=$(link_tx_packets_total "$NS1") + if ! busywait "$BUSYWAIT_TIMEOUT" until_counter_is \ + ">= $((cli_base + 200))" \ + link_tx_packets_total "$NS1" > /dev/null; then + check_err 1 "no TX activity in round $r" + kill "$client_pid" 2>/dev/null + wait "$client_pid" 2>/dev/null + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + break + fi + + local srv_tcp0 srv_tcp1 + srv_tcp0=$(tc_filter_pkt_count "$NS2" veth0b) + srv_tcp1=$(tc_filter_pkt_count "$NS2" veth1b) + local srv_delta0=$(( ${srv_tcp0:-0} - ${srv_base0:-0} )) + local srv_delta1=$(( ${srv_tcp1:-0} - ${srv_base1:-0} )) + + if [ "$srv_delta0" -gt 0 ] && [ "$srv_delta1" -gt 0 ]; then + path_splits=$((path_splits + 1)) + fi + + kill "$client_pid" 2>/dev/null + wait "$client_pid" 2>/dev/null + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + + # Wait for TCP teardown packets (FIN/RST) to finish so + # they do not pollute the next round's tc filter counters. + busywait "$BUSYWAIT_TIMEOUT" \ + port_has_no_active_tcp "$NS1" "$port" > /dev/null + busywait "$BUSYWAIT_TIMEOUT" \ + port_has_no_active_tcp "$NS2" "$port" > /dev/null + done + + if [ "$path_splits" -gt 0 ]; then + check_err 1 "$path_splits/$ECMP_REBUILD_ROUNDS had split server path" + fi + + log_test "Syncookie server ECMP path consistent" +} + +require_command socat + +trap 'defer_scopes_cleanup; cleanup_all_ns' EXIT +setup || exit $? +tests_run +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/exception_cache.sh b/tools/testing/selftests/net/exception_cache.sh new file mode 100755 index 000000000000..8d3eed5c532a --- /dev/null +++ b/tools/testing/selftests/net/exception_cache.sh @@ -0,0 +1,521 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test that the state of the route exception cache after an ICMP error is +# processed does not depend on whether the quoted packet was matched to a +# socket. Otherwise, an off-path attacker can probe the cache to discover the +# ephemeral port used by a connected UDP socket. +# +# When the quoted packet is not matched to a socket, the same exception is +# created as when it is matched, so that neither its presence nor its contents +# reveal the result of socket matching. +# +# +----+ +# +---------| r1 | +# | +----+ +# +----+ +--------+ | .1 +# | h1 |---| bridge | | 198.51.100.0/30 +# +----+ +--------+ | 2001:db8:2::/64 +# .1 | | .2 +# | +----+ +----+ +# +---------| r2 |-----------| h2 | +# .2 .3 +----+ .1 .2 +----+ +# 203.0.113.0/24 +# 2001:db8:3::/64 +# 192.0.2.0/24 +# 2001:db8:1::/64 +# +# Traffic from h1 to h2 is routed via r1, which reaches h2's network via r2 +# over the point-to-point link. The MTU of the r2 - h2 link is lowered so that +# r2 emits ICMP errors towards h1. +# +# For the redirect tests r1's route to h2's network is replaced with one via r2 +# on the shared segment, so that r1 forwards the packet back to the segment it +# arrived from and emits a redirect towards h1. +# +# The packets that provoke the ICMP errors are injected with a packet socket so +# that no socket is ever associated with them. A socket is created separately, +# with socat, when a test needs the ICMP error to be matched. + +# shellcheck disable=SC1091,SC2034,SC2154,SC2329 +source lib.sh + +require_command jq +require_command mausezahn +require_command nstat +require_command socat + +ALL_TESTS=" + pmtu_no_socket_ipv4 + pmtu_no_socket_ipv6 + pmtu_socket_ipv4 + pmtu_socket_ipv6 + pmtu_omit_ipv4 + pmtu_omit_ipv6 + redirect_no_socket_ipv4 + redirect_no_socket_ipv6 + redirect_socket_ipv4 + redirect_socket_ipv6 +" + +# Shared segment. +H1_ADDR4=192.0.2.1 +R1_ADDR4=192.0.2.2 +R2_ADDR4=192.0.2.3 +H1_ADDR6=2001:db8:1::1 +R1_ADDR6=2001:db8:1::2 +R2_ADDR6=2001:db8:1::3 + +# r1 - r2 link. +R2_R1_ADDR4=198.51.100.2 +R2_R1_ADDR6=2001:db8:2::2 + +# r2 - h2 link. +H2_ADDR4=203.0.113.2 +H2_NET4=203.0.113.0/24 +H2_ADDR6=2001:db8:3::2 +H2_NET6=2001:db8:3::/64 + +SPORT=12345 +DPORT=54321 + +# The MTU of the shared segment and of the r1 - r2 link. Large enough for the +# injected packets to reach r2 intact. +SEGMENT_MTU=2000 +# Size of the injected packets. The PMTU tests need a size that exceeds every +# MTU used for the r2 - h2 link, so that r2 responds with an ICMP error. The +# redirect tests need a size that does not, otherwise r2 would respond with an +# ICMP error in addition to the redirect emitted by r1. +PMTU_PACKET_SIZE=1800 +REDIRECT_PACKET_SIZE=100 + +# The MTUs used for the r2 - h2 link. All of them must be at least +# IPV6_MIN_MTU, otherwise IPv6 silently ignores the error instead of creating +# an exception. +MTU_MID=1400 +MTU_LOW=1300 + +# Values for the IP{,V6}_MTU_DISCOVER socket option. +PMTUDISC_DONT=0 +PMTUDISC_OMIT=5 + +SOCAT_PID= + +linklocal_get() +{ + local ns=$1; shift + local dev=$1; shift + + ip -n "$ns" -j -6 addr show dev "$dev" | \ + jq -r '.[]["addr_info"][] | select(.scope == "link") | .local' +} + +linklocal_exists() +{ + local ns=$1; shift + local dev=$1; shift + + [ -n "$(linklocal_get "$ns" "$dev")" ] +} + +family_vars_set() +{ + local family=$1; shift + + FAMILY=$family + + if [ "$family" -eq 4 ]; then + H1_ADDR=$H1_ADDR4 + H2_ADDR=$H2_ADDR4 + MZ_FAMILY_OPT=() + # Without the Don't Fragment bit set r2 fragments the packet + # instead of reporting the MTU of the next hop. + MZ_IP_OPTS="df," + SOCAT_DST="UDP4-CONNECT:$H2_ADDR4:$DPORT" + SOCAT_BIND="bind=$H1_ADDR4:$SPORT" + SOCAT_PMTUDISC="ip-mtu-discover" + else + H1_ADDR=$H1_ADDR6 + H2_ADDR=$H2_ADDR6 + MZ_FAMILY_OPT=(-6) + MZ_IP_OPTS= + SOCAT_DST="UDP6-CONNECT:[$H2_ADDR6]:$DPORT" + SOCAT_BIND="bind=[$H1_ADDR6]:$SPORT" + SOCAT_PMTUDISC="ipv6-mtu-discover" + fi +} + +topology_setup() +{ + local ns + + setup_ns h1 r1 r2 h2 sw + defer cleanup_all_ns + + # Link-local addresses are generated from the MAC address and read + # back during setup, so request that generation mode explicitly and + # make the addresses available as soon as the devices are brought up. + for ns in "$h1" "$r1" "$r2" "$h2" "$sw"; do + ip netns exec "$ns" sysctl -qw \ + net.ipv6.conf.default.addr_gen_mode=0 \ + net.ipv6.conf.default.accept_dad=0 \ + net.ipv6.conf.all.accept_dad=0 + done + + ip -n "$sw" link add name br0 type bridge + ip -n "$sw" link set dev br0 mtu "$SEGMENT_MTU" up + + ip -n "$h1" link add name eth0 mtu "$SEGMENT_MTU" type veth \ + peer name swp1 mtu "$SEGMENT_MTU" netns "$sw" + ip -n "$r1" link add name eth0 mtu "$SEGMENT_MTU" type veth \ + peer name swp2 mtu "$SEGMENT_MTU" netns "$sw" + ip -n "$r2" link add name eth0 mtu "$SEGMENT_MTU" type veth \ + peer name swp3 mtu "$SEGMENT_MTU" netns "$sw" + ip -n "$r1" link add name eth1 mtu "$SEGMENT_MTU" type veth \ + peer name eth1 mtu "$SEGMENT_MTU" netns "$r2" + ip -n "$r2" link add name eth2 type veth peer name eth0 netns "$h2" + + ip -n "$sw" link set dev swp1 master br0 up + ip -n "$sw" link set dev swp2 master br0 up + ip -n "$sw" link set dev swp3 master br0 up + + ip -n "$h1" link set dev eth0 up + ip -n "$r1" link set dev eth0 up + ip -n "$r1" link set dev eth1 up + ip -n "$r2" link set dev eth0 up + ip -n "$r2" link set dev eth1 up + ip -n "$r2" link set dev eth2 up + ip -n "$h2" link set dev eth0 up + + ip -n "$h1" address add "$H1_ADDR4/24" dev eth0 + ip -n "$r1" address add "$R1_ADDR4/24" dev eth0 + ip -n "$r2" address add "$R2_ADDR4/24" dev eth0 + ip -n "$r1" address add 198.51.100.1/30 dev eth1 + ip -n "$r2" address add "$R2_R1_ADDR4/30" dev eth1 + ip -n "$r2" address add 203.0.113.1/24 dev eth2 + ip -n "$h2" address add "$H2_ADDR4/24" dev eth0 + + ip -n "$h1" -6 address add "$H1_ADDR6/64" dev eth0 nodad + ip -n "$r1" -6 address add "$R1_ADDR6/64" dev eth0 nodad + ip -n "$r2" -6 address add "$R2_ADDR6/64" dev eth0 nodad + ip -n "$r1" -6 address add 2001:db8:2::1/64 dev eth1 nodad + ip -n "$r2" -6 address add "$R2_R1_ADDR6/64" dev eth1 nodad + ip -n "$r2" -6 address add 2001:db8:3::1/64 dev eth2 nodad + ip -n "$h2" -6 address add "$H2_ADDR6/64" dev eth0 nodad + + ip netns exec "$r1" sysctl -qw net.ipv4.ip_forward=1 + ip netns exec "$r1" sysctl -qw net.ipv4.conf.all.send_redirects=1 + ip netns exec "$r1" sysctl -qw net.ipv6.conf.all.forwarding=1 + ip netns exec "$r2" sysctl -qw net.ipv4.ip_forward=1 + ip netns exec "$r2" sysctl -qw net.ipv6.conf.all.forwarding=1 + + ip netns exec "$h1" sysctl -qw net.ipv4.conf.all.accept_redirects=1 + ip netns exec "$h1" sysctl -qw net.ipv4.conf.eth0.accept_redirects=1 + ip netns exec "$h1" sysctl -qw net.ipv6.conf.all.accept_redirects=1 + ip netns exec "$h1" sysctl -qw net.ipv6.conf.eth0.accept_redirects=1 + + slowwait 5 linklocal_exists "$r1" eth0 + check_err $? "r1: link-local address was not generated" + slowwait 5 linklocal_exists "$r2" eth0 + check_err $? "r2: link-local address was not generated" + + R1_LLADDR=$(linklocal_get "$r1" eth0) + R2_LLADDR=$(linklocal_get "$r2" eth0) + R1_MAC=$(ip -n "$r1" -j link show dev eth0 | jq -r '.[]["address"]') + R2_MAC=$(ip -n "$r2" -j link show dev eth0 | jq -r '.[]["address"]') + + ip -n "$h1" route add "$H2_NET4" via "$R1_ADDR4" dev eth0 + ip -n "$h1" -6 route add "$H2_NET6" via "$R1_LLADDR" dev eth0 + ip -n "$r1" route add "$H2_NET4" via "$R2_R1_ADDR4" dev eth1 + ip -n "$r1" -6 route add "$H2_NET6" via "$R2_R1_ADDR6" dev eth1 + ip -n "$h2" route add default via 203.0.113.1 dev eth0 + ip -n "$h2" -6 route add default via 2001:db8:3::1 dev eth0 + + far_mtu_set "$MTU_MID" +} + +# Make r1 forward towards h2's network over the segment it receives the packet +# from, so that it emits a redirect towards h1. +redirect_route_set() +{ + ip -n "$r1" route replace "$H2_NET4" via "$R2_ADDR4" dev eth0 + ip -n "$r1" -6 route replace "$H2_NET6" via "$R2_LLADDR" dev eth0 + + # __ip_do_redirect() only creates an exception if the new gateway is + # already a valid neighbour. Otherwise it merely triggers address + # resolution. IPv6 resolves the target itself, in rt6_do_redirect(). + ip -n "$h1" neigh replace "$R2_ADDR4" lladdr "$R2_MAC" dev eth0 \ + nud permanent +} + +far_mtu_set() +{ + local mtu=$1; shift + + ip -n "$r2" link set dev eth2 mtu "$mtu" + ip -n "$h2" link set dev eth0 mtu "$mtu" +} + +socket_is_open() +{ + ip netns exec "$h1" ss -uHn "sport = :$SPORT" | grep -q . +} + +socket_start() +{ + # Disable PMTU discovery by default so that ICMP errors are not + # reported to the socket. Otherwise socat would exit when the first one + # arrives and later packets in the same test would not be matched to a + # socket. The exception is still created, as ip{,6}_sk_accept_pmtu() + # only rejects IP{,V6}_PMTUDISC_{INTERFACE,OMIT}. + local pmtudisc=${1:-$PMTUDISC_DONT} + + # Send socat's diagnostics to /dev/null. It reports the ICMP errors + # that reach the socket, which is exactly what the tests provoke. + ip netns exec "$h1" socat -u -lf/dev/null \ + "$SOCAT_DST,$SOCAT_BIND,$SOCAT_PMTUDISC=$pmtudisc" \ + OPEN:/dev/null,wronly=1 & + SOCAT_PID=$! + defer socket_stop + + slowwait 5 socket_is_open + check_err $? "socket did not open" +} + +socket_stop() +{ + [ -z "$SOCAT_PID" ] && return 0 + + kill "$SOCAT_PID" &> /dev/null + wait "$SOCAT_PID" 2> /dev/null + SOCAT_PID= +} + +# Inject a packet towards h2 with a packet socket. No socket is associated with +# it, so an ICMP error quoting it is matched to a socket only if one was +# created separately with the same source port. +packet_send() +{ + local size=$1; shift + + ip netns exec "$h1" mausezahn "${MZ_FAMILY_OPT[@]}" eth0 \ + -a own -b "$R1_MAC" -A "$H1_ADDR" -B "$H2_ADDR" \ + -t udp "${MZ_IP_OPTS}sp=$SPORT,dp=$DPORT" \ + -p "$size" -c 1 -q +} + +exception_show() +{ + if [ "$FAMILY" -eq 4 ]; then + # IPv4 exceptions without a bound route are not dumped, but + # "route get" reports the exception and binds a route to it. + ip -n "$h1" route get "$H2_ADDR" + else + # IPv6 does not report a cache indication in "route get" + # output, so dump the exceptions instead. + ip -n "$h1" -6 route show cache | grep -F "$H2_ADDR" || true + fi +} + +exception_mtu_get() +{ + exception_show | grep -o "mtu [0-9]*" | cut -d ' ' -f 2 +} + +exception_gw_get() +{ + exception_show | grep -o "via [0-9a-f.:]*" | cut -d ' ' -f 2 +} + +exception_mtu_check() +{ + local expected=$1; shift + + [ "$(exception_mtu_get)" = "$expected" ] +} + +icmp_errors_get() +{ + local ctr=IcmpInDestUnreachs + + [ "$FAMILY" -eq 6 ] && ctr=Icmp6InPktTooBigs + + ip netns exec "$h1" nstat -asz "$ctr" | \ + awk -v ctr="$ctr" '$1 == ctr { print $2 }' +} + +exception_pmtu_check() +{ + local mtu=$1; shift + local desc=$1; shift + + busywait "$BUSYWAIT_TIMEOUT" exception_mtu_check "$mtu" + check_err $? "$desc: exception does not carry an MTU of $mtu" +} + +pmtu_no_socket() +{ + local family=$1; shift + + RET=0 + family_vars_set "$family" + topology_setup + + packet_send "$PMTU_PACKET_SIZE" + exception_pmtu_check "$MTU_MID" "No socket" + + log_test "IPv$family: PMTU: exception without a matching socket" +} + +pmtu_no_socket_ipv4() +{ + pmtu_no_socket 4 +} + +pmtu_no_socket_ipv6() +{ + pmtu_no_socket 6 +} + +pmtu_socket() +{ + local family=$1; shift + local t0 + + RET=0 + family_vars_set "$family" + topology_setup + socket_start + + packet_send "$PMTU_PACKET_SIZE" + exception_pmtu_check "$MTU_MID" "Matching socket" + + # A lower PMTU replaces the one currently stored in the exception. + far_mtu_set "$MTU_LOW" + packet_send "$PMTU_PACKET_SIZE" + exception_pmtu_check "$MTU_LOW" "Lower PMTU" + + # A higher PMTU is ignored, so the exception is left as it is. Wait + # for the error to be received, as otherwise the check below would + # pass even if it never was. + far_mtu_set "$MTU_MID" + t0=$(icmp_errors_get) + packet_send "$PMTU_PACKET_SIZE" + busywait "$BUSYWAIT_TIMEOUT" until_counter_is ">= $((t0 + 1))" \ + icmp_errors_get > /dev/null + check_err $? "Higher PMTU: ICMP error was not received" + + exception_mtu_check "$MTU_LOW" + check_err $? "Higher PMTU: exception does not carry an MTU of $MTU_LOW" + + log_test "IPv$family: PMTU: exception with a matching socket" +} + +pmtu_socket_ipv4() +{ + pmtu_socket 4 +} + +pmtu_socket_ipv6() +{ + pmtu_socket 6 +} + +pmtu_omit() +{ + local family=$1; shift + + RET=0 + family_vars_set "$family" + topology_setup + socket_start "$PMTUDISC_OMIT" + + packet_send "$PMTU_PACKET_SIZE" + exception_pmtu_check "$MTU_MID" "PMTU discovery disabled" + + log_test "IPv$family: PMTU: exception with a socket ignoring it" +} + +pmtu_omit_ipv4() +{ + pmtu_omit 4 +} + +pmtu_omit_ipv6() +{ + pmtu_omit 6 +} + +exception_gw_check() +{ + local expected=$1; shift + + [ -n "$expected" ] && [ "$(exception_gw_get)" = "$expected" ] +} + +redirect_gw_new() +{ + if [ "$FAMILY" -eq 4 ]; then + echo "$R2_ADDR4" + else + echo "$R2_LLADDR" + fi +} + +redirect_no_socket() +{ + local family=$1; shift + + RET=0 + family_vars_set "$family" + topology_setup + redirect_route_set + + packet_send "$REDIRECT_PACKET_SIZE" + busywait "$BUSYWAIT_TIMEOUT" exception_gw_check "$(redirect_gw_new)" + check_err $? "No socket: exception does not carry the new gateway" + + log_test "IPv$family: Redirect: exception without a matching socket" +} + +redirect_no_socket_ipv4() +{ + redirect_no_socket 4 +} + +redirect_no_socket_ipv6() +{ + redirect_no_socket 6 +} + +redirect_socket() +{ + local family=$1; shift + + RET=0 + family_vars_set "$family" + topology_setup + redirect_route_set + socket_start + + packet_send "$REDIRECT_PACKET_SIZE" + busywait "$BUSYWAIT_TIMEOUT" exception_gw_check "$(redirect_gw_new)" + check_err $? "Matching socket: exception does not carry the new gateway" + + log_test "IPv$family: Redirect: exception with a matching socket" +} + +redirect_socket_ipv4() +{ + redirect_socket 4 +} + +redirect_socket_ipv6() +{ + redirect_socket 6 +} + +trap defer_scopes_cleanup EXIT +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/fib_nexthops.sh b/tools/testing/selftests/net/fib_nexthops.sh index 21026b667667..431d7bed7622 100755 --- a/tools/testing/selftests/net/fib_nexthops.sh +++ b/tools/testing/selftests/net/fib_nexthops.sh @@ -30,6 +30,7 @@ IPV4_TESTS=" ipv4_large_res_grp ipv4_compat_mode ipv4_fdb_grp_fcnal + ipv4_fdb_port_fcnal ipv4_mpath_select ipv4_torture ipv4_res_torture @@ -44,6 +45,7 @@ IPV6_TESTS=" ipv6_large_res_grp ipv6_compat_mode ipv6_fdb_grp_fcnal + ipv6_fdb_port_fcnal ipv6_mpath_select ipv6_torture ipv6_res_torture @@ -432,6 +434,15 @@ check_nexthop_fdb_support() fi } +check_nexthop_fdb_port_support() +{ + $IP nexthop help 2>&1 | grep -q "dst_port" + if [ $? -ne 0 ]; then + echo "SKIP: iproute2 too old, missing nexthop dst_port support" + return $ksft_skip + fi +} + check_nexthop_res_support() { $IP nexthop help 2>&1 | grep -q resilient @@ -522,6 +533,20 @@ ipv6_fdb_grp_fcnal() run_cmd "$BRIDGE fdb add 02:02:00:00:00:14 dev vx10 nhid 61 self" log_test $? 255 "Fdb mac add with nexthop" + # fdb entries with a nexthop group cannot be aged out + run_cmd "$BRIDGE fdb add 02:02:00:00:00:15 dev vx10 nhid 102 self static" + log_test $? 0 "Fdb mac add with nexthop group and static state" + + run_cmd "$BRIDGE fdb add 02:02:00:00:00:16 dev vx10 nhid 102 self dynamic" + log_test $? 255 "Fdb mac add with nexthop group and dynamic state" + + run_cmd "$BRIDGE fdb add 02:02:00:00:00:17 dev vx10 nhid 102 self" + run_cmd "$BRIDGE fdb replace 02:02:00:00:00:17 dev vx10 dst 2001:db8:91::11 self dynamic" + log_test $? 255 "Fdb mac replace with nexthop group and dynamic state" + + run_cmd "$BRIDGE fdb append 02:02:00:00:00:17 dev vx10 dst 2001:db8:91::11 self dynamic" + log_test $? 255 "Fdb mac append with nexthop group and dynamic state" + run_cmd "$IP -6 ro add 2001:db8:101::1/128 nhid 66" log_test $? 2 "Route add with fdb nexthop" @@ -541,6 +566,42 @@ ipv6_fdb_grp_fcnal() $IP link del dev vx10 } +ipv6_fdb_port_fcnal() +{ + echo + echo "IPv6 fdb nexthop dst_port functional" + echo "------------------------------------" + + check_nexthop_fdb_port_support + if [ $? -eq $ksft_skip ]; then + return $ksft_skip + fi + + # NHA_DST_PORT: optional per-nexthop VXLAN destination UDP port, + # letting an fdb nexthop group balance a flow across legs that share + # an underlay IP but listen on different UDP ports. + run_cmd "$IP nexthop add id 80 via 2001:db8:91::2 fdb dst_port 4790" + check_nexthop "id 80" \ + "id 80 via 2001:db8:91::2 scope link fdb dst_port 4790" + log_test $? 0 "Fdb nexthop with dst_port" + + run_cmd "$IP nexthop add id 81 fdb dst_port 4790" + log_test $? 2 "Fdb nexthop with dst_port but no gateway" + + run_cmd "$IP nexthop add id 81 via 2001:db8:91::2 fdb dst_port 0" + log_test $? 2 "Fdb nexthop with dst_port 0" + + run_cmd "$IP nexthop add id 82 via 2001:db8:91::2 fdb dst_port 4789" + run_cmd "$IP nexthop add id 83 via 2001:db8:91::3 fdb dst_port 5789" + run_cmd "$IP nexthop add id 106 group 82/83 fdb" + check_nexthop "id 106" "id 106 group 82/83 fdb" + log_test $? 0 "Fdb nexthop group with legs differing in dst_port" + + run_cmd "$IP nexthop add id 84 via 2001:db8:91::2 fdb" + check_nexthop "id 84" "id 84 via 2001:db8:91::2 scope link fdb" + log_test $? 0 "Fdb nexthop without dst_port omits dst_port" +} + ipv4_fdb_grp_fcnal() { local rc @@ -622,6 +683,20 @@ ipv4_fdb_grp_fcnal() run_cmd "$BRIDGE fdb add 02:02:00:00:00:14 dev vx10 nhid 12 self" log_test $? 255 "Fdb mac add with nexthop" + # fdb entries with a nexthop group cannot be aged out + run_cmd "$BRIDGE fdb add 02:02:00:00:00:15 dev vx10 nhid 102 self static" + log_test $? 0 "Fdb mac add with nexthop group and static state" + + run_cmd "$BRIDGE fdb add 02:02:00:00:00:16 dev vx10 nhid 102 self dynamic" + log_test $? 255 "Fdb mac add with nexthop group and dynamic state" + + run_cmd "$BRIDGE fdb add 02:02:00:00:00:17 dev vx10 nhid 102 self" + run_cmd "$BRIDGE fdb replace 02:02:00:00:00:17 dev vx10 dst 10.0.0.3 self dynamic" + log_test $? 255 "Fdb mac replace with nexthop group and dynamic state" + + run_cmd "$BRIDGE fdb append 02:02:00:00:00:17 dev vx10 dst 10.0.0.3 self dynamic" + log_test $? 255 "Fdb mac append with nexthop group and dynamic state" + run_cmd "$IP ro add 172.16.0.0/22 nhid 16" log_test $? 2 "Route add with fdb nexthop" @@ -641,6 +716,42 @@ ipv4_fdb_grp_fcnal() $IP link del dev vx10 } +ipv4_fdb_port_fcnal() +{ + echo + echo "IPv4 fdb nexthop dst_port functional" + echo "------------------------------------" + + check_nexthop_fdb_port_support + if [ $? -eq $ksft_skip ]; then + return $ksft_skip + fi + + # NHA_DST_PORT: optional per-nexthop VXLAN destination UDP port, + # letting an fdb nexthop group balance a flow across legs that share + # an underlay IP but listen on different UDP ports. + run_cmd "$IP nexthop add id 30 via 172.16.1.2 fdb dst_port 4790" + check_nexthop "id 30" \ + "id 30 via 172.16.1.2 scope link fdb dst_port 4790" + log_test $? 0 "Fdb nexthop with dst_port" + + run_cmd "$IP nexthop add id 31 fdb dst_port 4790" + log_test $? 2 "Fdb nexthop with dst_port but no gateway" + + run_cmd "$IP nexthop add id 31 via 172.16.1.2 fdb dst_port 0" + log_test $? 2 "Fdb nexthop with dst_port 0" + + run_cmd "$IP nexthop add id 32 via 172.16.1.2 fdb dst_port 4789" + run_cmd "$IP nexthop add id 33 via 172.16.1.3 fdb dst_port 5789" + run_cmd "$IP nexthop add id 105 group 32/33 fdb" + check_nexthop "id 105" "id 105 group 32/33 fdb" + log_test $? 0 "Fdb nexthop group with legs differing in dst_port" + + run_cmd "$IP nexthop add id 34 via 172.16.1.2 fdb" + check_nexthop "id 34" "id 34 via 172.16.1.2 scope link fdb" + log_test $? 0 "Fdb nexthop without dst_port omits dst_port" +} + ipv4_mpath_select() { local rc dev match h addr @@ -1209,6 +1320,28 @@ ipv6_fcnal_runtime() run_cmd "$IP ro replace 2001:db8:101::1/128 nhid 124" log_test $? 0 "IPv6 route using a group after replacing v4 gateways" + # Replacing an IPv6 nexthop with an IPv4 nexthop should update has_v4 + # for all groups using it, preventing IPv6 routes from referencing the + # group after the replace. + run_cmd "$IP nexthop add id 89 via 2001:db8:91::2 dev veth1" + run_cmd "$IP nexthop add id 125 group 89" + run_cmd "$IP nexthop replace id 89 via 172.16.1.1 dev veth1" + run_cmd "$IP ro replace 2001:db8:101::1/128 nhid 125" + log_test $? 2 "IPv6 route can not use group after v6 nexthop replaced by v4" + + # Same scenario but with a blackhole nexthop: the group has no IPv6 + # routes yet when the replace happens, so fib6_check_nh_list returns + # early without checking. has_v4 must still be updated to block + # subsequent IPv6 route additions. + run_cmd "$IP nexthop flush >/dev/null 2>&1" + run_cmd "$IP -6 nexthop add id 90 blackhole" + run_cmd "$IP nexthop add id 125 group 90" + run_cmd "$IP nexthop replace id 90 blackhole" + run_cmd "$IP -6 ro add 2001:db8:101::1/128 nhid 125" + log_test $? 2 "IPv6 route reject v6 blackhole replaced by v4 blackhole" + run_cmd "ip netns exec $me ping -6 2001:db8:101::1 -c1 -w$PING_TIMEOUT" + log_test $? 2 "Ping unreachable after rejected route" + $IP nexthop flush >/dev/null 2>&1 # @@ -1672,6 +1805,17 @@ ipv4_withv6_fcnal() run_cmd "$IP ro replace 172.16.101.1/32 via inet6 2001:db8:50::1 dev veth1" log_test $? 2 "IPv4 route with invalid IPv6 gateway" + + # Test IPv4 route with loopback IPv6 nexthop + # Regression test: loopback IPv6 nexthop was misclassified as reject + # route, skipping nhc_pcpu_rth_output allocation, causing panic when + # an IPv4 route references it and triggers __mkroute_output(). + run_cmd "$IP -6 nexthop add id 20 dev lo" + run_cmd "$IP ro add 172.20.20.0/24 nhid 20" + run_cmd "ip netns exec $me ping -c1 -W1 172.20.20.1" + log_test $? 1 "IPv4 route with loopback IPv6 nexthop (no crash)" + run_cmd "$IP ro del 172.20.20.0/24" + run_cmd "$IP nexthop del id 20" } ipv4_fcnal_runtime() diff --git a/tools/testing/selftests/net/fib_tests.sh b/tools/testing/selftests/net/fib_tests.sh index c5694cc4ddd2..b338bfb196a2 100755 --- a/tools/testing/selftests/net/fib_tests.sh +++ b/tools/testing/selftests/net/fib_tests.sh @@ -12,7 +12,9 @@ TESTS="unregister down carrier nexthop suppress ipv6_notify ipv4_notify \ ipv4_route_metrics ipv4_route_v6_gw rp_filter ipv4_del_addr \ ipv6_del_addr ipv4_mangle ipv6_mangle ipv4_bcast_neigh fib6_gc_test \ ipv4_mpath_list ipv6_mpath_list ipv4_mpath_balance ipv6_mpath_balance \ - ipv4_mpath_balance_preferred fib6_ra_to_static" + ipv4_mpath_balance_preferred ipv4_mpath_oif ipv4_mpath_oif_nh \ + ipv4_mpath_oif_vrf ipv6_mpath_oif ipv6_mpath_oif_nh ipv6_mpath_oif_vrf \ + fib6_ra_to_static fib6_temp_addr_renewal" VERBOSE=0 PAUSE_ON_FAIL=no @@ -545,7 +547,7 @@ fib4_nexthop() fib6_nexthop() { local lldummy=$(get_linklocal dummy0) - local llv1=$(get_linklocal dummy0) + local llv1=$(get_linklocal veth1) if [ -z "$lldummy" ]; then echo "Failed to get linklocal address for dummy0" @@ -868,6 +870,64 @@ fib6_gc_test() check_rt_num 5 $($IP -6 route list |grep -v expires|grep 2001:20::|wc -l) log_test $ret 0 "ipv6 route garbage collection (replace with permanent)" + # Delete dummy_10 and remove all routes + $IP link del dev dummy_10 + + # rd6 is required for the next test. (ipv6toolkit) + if [ ! -x "$(command -v rd6)" ]; then + echo "SKIP: rd6 not found." + set +e + cleanup &> /dev/null + return + fi + + setup_ns ns2 + $IP link add veth1 type veth peer veth2 netns $ns2 + $IP link set veth1 up + ip -netns $ns2 link set veth2 up + $IP addr add fe80:dead::1/64 dev veth1 + ip -netns $ns2 addr add fe80:dead::2/64 dev veth2 + + # Add NTF_ROUTER neighbour to prevent rt6_age_examine_exception() + # from removing not-yet-expired exceptions. + ip -netns $ns2 link set veth2 address 00:11:22:33:44:55 + $IP neigh add fe80:dead::3 lladdr 00:11:22:33:44:55 dev veth1 router + + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.accept_redirects=1 + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.forwarding=0 + + # Temporary routes + for i in $(seq 1 5); do + # Expire route after $EXPIRE seconds + $IP -6 route add 2001:10::$i \ + via fe80:dead::2 dev veth1 expires $EXPIRE + + ip netns exec $ns2 rd6 -i veth2 \ + -s fe80:dead::2 -d fe80:dead::1 \ + -r 2001:10::$i -t fe80:dead::3 -p ICMP6 + done + + check_rt_num 5 $($IP -6 route list | grep expires | grep 2001:10:: | wc -l) + + # Promote to permanent routes by "prepend" (w/o NLM_F_EXCL and NLM_F_REPLACE) + for i in $(seq 1 5); do + # -EEXIST, but the temporary route becomes the permanent route. + $IP -6 route append 2001:10::$i \ + via fe80:dead::2 dev veth1 2>/dev/null || true + done + + check_rt_num 5 $($IP -6 route list | grep -v expires | grep 2001:10:: | wc -l) + check_rt_num 5 $($IP -6 route list cache | grep 2001:10:: | wc -l) + + # Trigger GC instead of waiting $GC_WAIT_TIME. + # rt6_nh_dump_exceptions() just skips expired exceptions. + $NS_EXEC sysctl -wq net.ipv6.route.flush=1 + check_rt_num 0 $($IP -6 route list cache | grep 2001:10:: | wc -l) + log_test $ret 0 "ipv6 route garbage collection (promote to permanent routes)" + + $IP neigh del fe80:dead::3 lladdr 00:11:22:33:44:55 dev veth1 router + $IP link del veth1 + # ra6 is required for the next test. (ipv6toolkit) if [ ! -x "$(command -v ra6)" ]; then echo "SKIP: ra6 not found." @@ -876,9 +936,6 @@ fib6_gc_test() return fi - # Delete dummy_10 and remove all routes - $IP link del dev dummy_10 - # Create a pair of veth devices to send a RA message from one # device to another. $IP link add veth1 type veth peer name veth2 @@ -1534,6 +1591,79 @@ fib6_ra_to_static() log_test $ret 0 "ipv6 promote RA route to static" + # Prepare for RA route with gateway + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.accept_ra_rt_info_max_plen=64 + + # Add initial route to cause ECMP merging + $IP -6 route add 2001:12::/64 via fe80::dead:beef dev veth1 + + $NS_EXEC ra6 -i veth2 -d 2001:10::1 -R 2001:12::/64#1#120 + + # Routes are not merged as RA routes are not elegible for ECMP + check_rt_num 2 "$($IP -6 route list | grep -c "2001:12::/64 via")" + + $IP -6 route append 2001:12::/64 via fe80::dead:feeb dev veth1 + + check_rt_num 2 "$($IP -6 route list | grep -c "nexthop via")" + + log_test "$ret" 0 "ipv6 RA route with nexthop do not merge into ECMP with static" + + set +e + + cleanup &> /dev/null +} + +fib6_temp_addr_renewal() { + setup + + echo + echo "Fib6 temporary address renewal test" + set -e + + # ra6 is required for the test. (ipv6toolkit) + if [ ! -x "$(command -v ra6)" ]; then + echo "SKIP: ra6 not found." + set +e + cleanup &> /dev/null + return + fi + + # Create a pair of veth devices to send a RA message from one + # device to another. + $IP link add veth1 type veth peer name veth2 + $IP link set dev veth1 up + $IP link set dev veth2 up + + # Make veth1 ready to receive RA messages. + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.accept_ra=2 + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.use_tempaddr=2 + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.temp_prefered_lft=15 + $NS_EXEC sysctl -wq net.ipv6.conf.veth1.max_desync_factor=0 + + # Send a RA message with a prefix from veth2. + $NS_EXEC ra6 -i veth2 -s fe80::1 -d ff02::1 -P 2001:12::/64\#LA\#3600\#3600 -e + sleep 3 + + # Deprecate it + $NS_EXEC ra6 -i veth2 -s fe80::1 -d ff02::1 -P 2001:12::/64\#LA\#3600\#0 -e + sleep 3 + + # Restore it + $NS_EXEC ra6 -i veth2 -s fe80::1 -d ff02::1 -P 2001:12::/64\#LA\#3600\#3600 -e + + ret=1 + for i in $(seq 1 25); do + sleep 1 + num_dep="$($IP -6 addr | grep -c "temporary deprecated" || true)" + num_tot="$($IP -6 addr | grep -c "temporary" || true)" + + if [ "$num_dep" -eq 1 ] && [ "$num_tot" -ge 2 ]; then + ret=0 + break + fi + done + log_test "$ret" 0 "IPv6 temporary address cleanly deprecated and regenerated" + set +e cleanup &> /dev/null @@ -2843,6 +2973,247 @@ ipv6_mpath_balance_test() forwarding_cleanup } +ipv4_mpath_oif_test_common() +{ + local get_param=$1; shift + local expected_oif=$1; shift + local test_name=$1; shift + local tmp_file + + tmp_file=$(mktemp) + + for i in {1..100}; do + $IP route get 203.0.113.${i} $get_param >> "$tmp_file" + done + + [[ $(grep "$expected_oif" "$tmp_file" | wc -l) -eq 100 ]] + log_test $? 0 "$test_name" + + rm "$tmp_file" +} + +ipv4_mpath_oif_test() +{ + echo + echo "IPv4 multipath oif test" + + setup + + set -e + $IP link add dummy1 up type dummy + $IP address add 192.0.2.1/28 dev dummy1 + $IP address add 192.0.2.17/32 dev lo + + $IP route add 203.0.113.0/24 \ + nexthop via 198.51.100.2 dev dummy0 \ + nexthop via 192.0.2.2 dev dummy1 + set +e + + ipv4_mpath_oif_test_common "oif dummy0" "dummy0" \ + "IPv4 multipath via first nexthop" + + ipv4_mpath_oif_test_common "oif dummy1" "dummy1" \ + "IPv4 multipath via second nexthop" + + ipv4_mpath_oif_test_common "oif dummy0 from 192.0.2.17" "dummy0" \ + "IPv4 multipath via first nexthop with source address" + + ipv4_mpath_oif_test_common "oif dummy1 from 192.0.2.17" "dummy1" \ + "IPv4 multipath via second nexthop with source address" + + cleanup +} + +ipv4_mpath_oif_nh_test() +{ + echo + echo "IPv4 multipath oif with nexthop object test" + + setup + + set -e + $IP link add dummy1 up type dummy + $IP address add 192.0.2.1/28 dev dummy1 + $IP address add 192.0.2.17/32 dev lo + + $IP nexthop add id 1 via 198.51.100.2 dev dummy0 + $IP nexthop add id 2 via 192.0.2.2 dev dummy1 + $IP nexthop add id 3 group 1/2 + $IP route add 203.0.113.0/24 nhid 3 + set +e + + ipv4_mpath_oif_test_common "oif dummy0" "dummy0" \ + "IPv4 multipath via first nexthop" + + ipv4_mpath_oif_test_common "oif dummy1" "dummy1" \ + "IPv4 multipath via second nexthop" + + ipv4_mpath_oif_test_common "oif dummy0 from 192.0.2.17" "dummy0" \ + "IPv4 multipath via first nexthop with source address" + + ipv4_mpath_oif_test_common "oif dummy1 from 192.0.2.17" "dummy1" \ + "IPv4 multipath via second nexthop with source address" + + cleanup +} + +ipv4_mpath_oif_vrf_test() +{ + echo + echo "IPv4 multipath oif with VRF test" + + setup + + set -e + $IP -4 rule add pref 32765 table local + $IP -4 rule del pref 0 + $IP link add name vrf-123 up type vrf table 123 + $IP link set dev dummy0 master vrf-123 + $IP link add dummy1 up master vrf-123 type dummy + $IP address add 192.0.2.1/28 dev dummy1 + $IP address add 192.0.2.17/32 dev vrf-123 + + $IP route add 203.0.113.0/24 vrf vrf-123 \ + nexthop via 198.51.100.2 dev dummy0 \ + nexthop via 192.0.2.2 dev dummy1 + set +e + + ipv4_mpath_oif_test_common "oif dummy0" "dummy0" \ + "IPv4 multipath via first nexthop" + + ipv4_mpath_oif_test_common "oif dummy1" "dummy1" \ + "IPv4 multipath via second nexthop" + + ipv4_mpath_oif_test_common "oif dummy0 from 192.0.2.17" "dummy0" \ + "IPv4 multipath via first nexthop with source address" + + ipv4_mpath_oif_test_common "oif dummy1 from 192.0.2.17" "dummy1" \ + "IPv4 multipath via second nexthop with source address" + + cleanup +} + +ipv6_mpath_oif_test_common() +{ + local get_param=$1; shift + local expected_oif=$1; shift + local test_name=$1; shift + local tmp_file + + tmp_file=$(mktemp) + + for i in {1..100}; do + $IP route get 2001:db8:10::${i} $get_param >> "$tmp_file" + done + + [[ $(grep "$expected_oif" "$tmp_file" | wc -l) -eq 100 ]] + log_test $? 0 "$test_name" + + rm "$tmp_file" +} + +ipv6_mpath_oif_test() +{ + echo + echo "IPv6 multipath oif test" + + setup + + set -e + $IP link add dummy1 up type dummy + $IP address add 2001:db8:2::1/64 dev dummy1 + $IP address add 2001:db8:100::1/128 dev lo + + $IP route add 2001:db8:10::/64 \ + nexthop via 2001:db8:1::2 dev dummy0 \ + nexthop via 2001:db8:2::2 dev dummy1 + set +e + + ipv6_mpath_oif_test_common "oif dummy0" "dummy0" \ + "IPv6 multipath via first nexthop" + + ipv6_mpath_oif_test_common "oif dummy1" "dummy1" \ + "IPv6 multipath via second nexthop" + + ipv6_mpath_oif_test_common "oif dummy0 from 2001:db8:100::1" "dummy0" \ + "IPv6 multipath via first nexthop with source address" + + ipv6_mpath_oif_test_common "oif dummy1 from 2001:db8:100::1" "dummy1" \ + "IPv6 multipath via second nexthop with source address" + + cleanup +} + +ipv6_mpath_oif_nh_test() +{ + echo + echo "IPv6 multipath oif with nexthop object test" + + setup + + set -e + $IP link add dummy1 up type dummy + $IP address add 2001:db8:2::1/64 dev dummy1 + $IP address add 2001:db8:100::1/128 dev lo + + $IP nexthop add id 1 via 2001:db8:1::2 dev dummy0 + $IP nexthop add id 2 via 2001:db8:2::2 dev dummy1 + $IP nexthop add id 3 group 1/2 + $IP route add 2001:db8:10::/64 nhid 3 + set +e + + ipv6_mpath_oif_test_common "oif dummy0" "dummy0" \ + "IPv6 multipath via first nexthop" + + ipv6_mpath_oif_test_common "oif dummy1" "dummy1" \ + "IPv6 multipath via second nexthop" + + ipv6_mpath_oif_test_common "oif dummy0 from 2001:db8:100::1" "dummy0" \ + "IPv6 multipath via first nexthop with source address" + + ipv6_mpath_oif_test_common "oif dummy1 from 2001:db8:100::1" "dummy1" \ + "IPv6 multipath via second nexthop with source address" + + cleanup +} + +ipv6_mpath_oif_vrf_test() +{ + echo + echo "IPv6 multipath oif with VRF test" + + setup + + set -e + $NS_EXEC sysctl -qw net.ipv6.conf.all.keep_addr_on_down=1 + $IP -6 rule add pref 32765 table local + $IP -6 rule del pref 0 + $IP link add name vrf-123 up type vrf table 123 + $IP link set dev dummy0 master vrf-123 + $IP link add dummy1 up master vrf-123 type dummy + $IP address add 2001:db8:2::1/64 dev dummy1 + $IP address add 2001:db8:100::1/128 dev vrf-123 + + $IP route add 2001:db8:10::/64 vrf vrf-123 \ + nexthop via 2001:db8:1::2 dev dummy0 \ + nexthop via 2001:db8:2::2 dev dummy1 + set +e + + ipv6_mpath_oif_test_common "oif dummy0" "dummy0" \ + "IPv6 multipath via first nexthop" + + ipv6_mpath_oif_test_common "oif dummy1" "dummy1" \ + "IPv6 multipath via second nexthop" + + ipv6_mpath_oif_test_common "oif dummy0 from 2001:db8:100::1" "dummy0" \ + "IPv6 multipath via first nexthop with source address" + + ipv6_mpath_oif_test_common "oif dummy1 from 2001:db8:100::1" "dummy1" \ + "IPv6 multipath via second nexthop with source address" + + cleanup +} + ################################################################################ # usage @@ -2929,7 +3300,14 @@ do ipv4_mpath_balance) ipv4_mpath_balance_test;; ipv6_mpath_balance) ipv6_mpath_balance_test;; ipv4_mpath_balance_preferred) ipv4_mpath_balance_preferred_test;; + ipv4_mpath_oif) ipv4_mpath_oif_test;; + ipv4_mpath_oif_nh) ipv4_mpath_oif_nh_test;; + ipv4_mpath_oif_vrf) ipv4_mpath_oif_vrf_test;; + ipv6_mpath_oif) ipv6_mpath_oif_test;; + ipv6_mpath_oif_nh) ipv6_mpath_oif_nh_test;; + ipv6_mpath_oif_vrf) ipv6_mpath_oif_vrf_test;; fib6_ra_to_static) fib6_ra_to_static;; + fib6_temp_addr_renewal) fib6_temp_addr_renewal;; help) echo "Test names: $TESTS"; exit 0;; esac diff --git a/tools/testing/selftests/net/fin_ack_lat.c b/tools/testing/selftests/net/fin_ack_lat.c index 70187494b57a..4068f8e227cf 100644 --- a/tools/testing/selftests/net/fin_ack_lat.c +++ b/tools/testing/selftests/net/fin_ack_lat.c @@ -69,7 +69,7 @@ static void client(int port) lat = timediff(start, end); sum_lat += lat; nr_lat++; - if (lat < 100000) + if (lat < 1000000) goto close; if (getsockname(sock, (struct sockaddr *)&laddr, &len) == -1) @@ -103,7 +103,8 @@ static void server(int sock, struct sockaddr_in address) static void sig_handler(int signum) { - kill(SIGTERM, child_pid); + if (child_pid > 0) + kill(child_pid, SIGTERM); exit(0); } @@ -142,6 +143,8 @@ int main(int argc, char const *argv[]) fprintf(stderr, "server port: %d\n", ntohs(laddr.sin_port)); child_pid = fork(); + if (child_pid < 0) + error(-1, errno, "fork"); if (!child_pid) client(ntohs(laddr.sin_port)); else diff --git a/tools/testing/selftests/net/fin_ack_lat.sh b/tools/testing/selftests/net/fin_ack_lat.sh index a3ff6e0b2c7a..a8aa2238ab5c 100755 --- a/tools/testing/selftests/net/fin_ack_lat.sh +++ b/tools/testing/selftests/net/fin_ack_lat.sh @@ -9,7 +9,7 @@ set -e tmpfile=$(mktemp /tmp/fin_ack_latency.XXXX.log) cleanup() { - kill $(pidof fin_ack_lat) + kill $(pidof fin_ack_lat) 2>/dev/null || true rm -f $tmpfile } diff --git a/tools/testing/selftests/net/forwarding/.gitignore b/tools/testing/selftests/net/forwarding/.gitignore index 2dea317f12e7..418ff96c52ef 100644 --- a/tools/testing/selftests/net/forwarding/.gitignore +++ b/tools/testing/selftests/net/forwarding/.gitignore @@ -1,2 +1,3 @@ # SPDX-License-Identifier: GPL-2.0-only forwarding.config +ipmr diff --git a/tools/testing/selftests/net/forwarding/Makefile b/tools/testing/selftests/net/forwarding/Makefile index ff4a00d91a26..bbaf4d937dd8 100644 --- a/tools/testing/selftests/net/forwarding/Makefile +++ b/tools/testing/selftests/net/forwarding/Makefile @@ -133,6 +133,10 @@ TEST_FILES := \ tc_common.sh \ # end of TEST_FILES +TEST_GEN_PROGS := \ + ipmr +# end of TEST_GEN_PROGS + TEST_INCLUDES := \ $(wildcard ../lib/sh/*.sh) \ ../lib.sh \ diff --git a/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh b/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh index 72dfbeaf56b9..c4cd2078a8db 100755 --- a/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh +++ b/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh @@ -4,7 +4,7 @@ ALL_TESTS="vlmc_control_test vlmc_querier_test vlmc_igmp_mld_version_test \ vlmc_last_member_test vlmc_startup_query_test vlmc_membership_test \ vlmc_querier_intvl_test vlmc_query_intvl_test vlmc_query_response_intvl_test \ - vlmc_router_port_test vlmc_filtering_test" + vlmc_router_port_test vlmc_filtering_test vlmc_mcast_toggle_test" NUM_NETIFS=4 CHECK_TC="yes" TEST_GROUP="239.10.10.10" @@ -162,14 +162,27 @@ vlmc_query_cnt_setup() { local type=$1 local dev=$2 + local match=($3) if [[ $type == "igmp" ]]; then - tc filter add dev $dev egress pref 10 prot 802.1Q \ + # This matches: IP Protocol 2 (IGMP) + tc filter add dev "$dev" egress pref 10 prot 802.1Q \ flower vlan_id 10 vlan_ethtype ipv4 dst_ip 224.0.0.1 ip_proto 2 \ + action goto chain 1 + # AND Type 0x11 (Query) at offset 0 of IGMP header + # 20 bytes IPv4 header + 4 bytes Router Alert option + IGMP[offset 0] + tc filter add dev "$dev" egress pref 20 chain 1 prot 802.1Q u32 \ + match u8 0x11 0xff at 24 "${match[@]}" \ action pass else - tc filter add dev $dev egress pref 10 prot 802.1Q \ + # This matches: ICMPv6 + tc filter add dev "$dev" egress pref 10 prot 802.1Q \ flower vlan_id 10 vlan_ethtype ipv6 dst_ip ff02::1 ip_proto icmpv6 \ + action goto chain 1 + # AND Type 0x82 (Query) at offset 0 of MLD header + # 40 bytes IPv6 header + 8 bytes Hop-by-hop option + MLD[offset 0] + tc filter add dev "$dev" egress pref 20 chain 1 prot 802.1Q u32 \ + match u8 0x82 0xff at 48 "${match[@]}" \ action pass fi @@ -181,7 +194,39 @@ vlmc_query_cnt_cleanup() local dev=$1 ip link set dev br0 type bridge mcast_stats_enabled 0 - tc filter del dev $dev egress pref 10 + tc filter del dev "$dev" egress pref 20 chain 1 + tc filter del dev "$dev" egress pref 10 +} + +vlmc_query_get_intvl_match() +{ + local type=$1 + local version=$2 + local test=$3 + local enc_val=$4 + + if [ "$test" = "qqic" ]; then + # QQIC is 8-bit floating point encoding for IGMPv3 and MLDv2 + if [ "${type}v${version}" = "igmpv3" ]; then + # QQIC is at offset 9 of IGMP header + # 20 bytes IPv4 header + 4 bytes Router Alert option + IGMP[offset 9] + echo "match u8 $enc_val 0xff at 33" + elif [ "${type}v${version}" = "mldv2" ]; then + # QQIC is at offset 25 of MLD header + # 40 bytes IPv6 header + 8 bytes Hop-by-hop option + MLD[offset 25] + echo "match u8 $enc_val 0xff at 73" + fi + elif [ "$test" = "mrc" ]; then + if [ "${type}v${version}" = "igmpv3" ]; then + # MRC is 8-bit floating point encoding at offset 1 of IGMP header + # 20 bytes IPv4 header + 4 bytes Router Alert option + IGMP[offset 1] + echo "match u8 $enc_val 0xff at 25" + elif [ "${type}v${version}" = "mldv2" ]; then + # MRC is 16-bit floating point encoding at offset 4 of MLD header + # 40 bytes IPv6 header + 8 bytes Hop-by-hop option + MLD[offset 4] + echo "match u16 $enc_val 0xffff at 52" + fi + fi } vlmc_check_query() @@ -191,9 +236,13 @@ vlmc_check_query() local dev=$3 local expect=$4 local time=$5 + local test=$6 + local enc_val=$7 + local intvl_match="" local ret=0 - vlmc_query_cnt_setup $type $dev + intvl_match="$(vlmc_query_get_intvl_match "$type" "$version" "$test" "$enc_val")" + vlmc_query_cnt_setup "$type" "$dev" "$intvl_match" local pre_tx_xstats=$(vlmc_query_cnt_xstats $type $version $dev) bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_querier 1 @@ -201,7 +250,7 @@ vlmc_check_query() if [[ $ret -eq 0 ]]; then sleep $time - local tcstats=$(tc_rule_stats_get $dev 10 egress) + local tcstats=$(tc_rule_stats_get "$dev" 20 egress) local post_tx_xstats=$(vlmc_query_cnt_xstats $type $version $dev) if [[ $tcstats != $expect || \ @@ -414,6 +463,7 @@ vlmc_querier_intvl_test() bridge vlan add vid 10 dev br1 self pvid untagged ip link set dev $h1 master br1 ip link set dev br1 up + setup_wait_dev $h1 0 bridge vlan add vid 10 dev $h1 master bridge vlan global set vid 10 dev br1 mcast_snooping 1 mcast_querier 1 sleep 2 @@ -447,8 +497,46 @@ vlmc_query_intvl_test() # 1 is sent immediately, then 2 more in the next 5 seconds vlmc_check_query igmp 2 $swp1 3 5 check_err $? "Wrong number of tagged IGMPv2 general queries sent" - log_test "Vlan 10 mcast_query_interval option changed to 200" + log_test "Number of tagged IGMPv2 general query" + RET=0 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 3 + check_err $? "Could not set mcast_igmp_version in vlan 10" + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 2 + check_err $? "Could not set mcast_mld_version in vlan 10" + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 6000 + check_err $? "Could not set mcast_query_interval in vlan 10" + # 1 is sent immediately, IGMPv3 QQIC should match with linear value 60 (0x3c) + # which is 8-bit encoded value of 60 [units of seconds] + vlmc_check_query igmp 3 $swp1 1 1 qqic 0x3c + check_err $? "Wrong QQIC in generated IGMPv3 general queries" + log_test "IGMPv3 QQIC linear value 60(s)" + + RET=0 + # 1 is sent immediately, MLDv2 QQIC should match with linear value 60 (0x3c) + # which is 8-bit encoded value of 60 [units of seconds] + vlmc_check_query mld 2 $swp1 1 1 qqic 0x3c + check_err $? "Wrong QQIC in generated MLDv2 general queries" + log_test "MLDv2 QQIC linear value 60(s)" + + RET=0 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 16000 + check_err $? "Could not set mcast_query_interval in vlan 10" + # 1 is sent immediately, IGMPv3 QQIC should match with non linear value 132 (0x84) + # which is 8-bit encoded value of 160 [units of seconds] + vlmc_check_query igmp 3 $swp1 1 1 qqic 0x84 + check_err $? "Wrong QQIC in generated IGMPv3 general queries" + log_test "IGMPv3 QQIC non linear value 160(s)" + + RET=0 + # 1 is sent immediately, MLDv2 QQIC should match with non linear value 132 (0x84) + # which is 8-bit encoded value of 160 [units of seconds] + vlmc_check_query mld 2 $swp1 1 1 qqic 0x84 + check_err $? "Wrong QQIC in generated MLDv2 general queries" + log_test "MLDv2 QQIC non linear value 160(s)" + + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 2 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 1 bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 2 bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 12500 } @@ -468,10 +556,47 @@ vlmc_query_response_intvl_test() log_test "Vlan mcast_query_response_interval global option default value" RET=0 - bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 200 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 0 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 3 + check_err $? "Could not set mcast_igmp_version in vlan 10" + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 2 + check_err $? "Could not set mcast_mld_version in vlan 10" + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 600 + check_err $? "Could not set mcast_query_response_interval in vlan 10" + # 1 is sent immediately, IGMPv3 MRC should match with linear value 60 (0x3c) + # which is 8-bit encoded value of 60 [units of 0.1s = 6 seconds] + vlmc_check_query igmp 3 $swp1 1 1 mrc 0x3c + check_err $? "Wrong MRC in generated IGMPv3 general queries" + log_test "IGMPv3 MRC linear value of 60(x0.1s)" + + RET=0 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 2400 check_err $? "Could not set mcast_query_response_interval in vlan 10" - log_test "Vlan 10 mcast_query_response_interval option changed to 200" + # 1 is sent immediately, MLDv2 MRC should match with linear value 0x5dc0 (24000) + # which is 16-bit encoded value of 24000 [units of ms / 24 seconds] + vlmc_check_query mld 2 $swp1 1 1 mrc 0x5dc0 + check_err $? "Wrong MRC in generated MLDv2 general queries" + log_test "MLDv2 MRC linear value of 24000(ms)" + RET=0 + # 1 is sent immediately, IGMPv3 MRC should match with non linear value 142 (0x8e) + # which is 8-bit encoded value of 240 [units of 0.1s = 24 seconds] + vlmc_check_query igmp 3 $swp1 1 1 mrc 0x8e + check_err $? "Wrong MRC in generated IGMPv3 general queries" + log_test "IGMPv3 MRC non linear value of 240(x0.1s)" + + RET=0 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 4800 + check_err $? "Could not set mcast_query_response_interval in vlan 10" + # 1 is sent immediately, MLDv2 MRC should match with non linear value 0x8770 (34672) + # which is 16-bit encoded value of 48000 [units of ms / 48 seconds] + vlmc_check_query mld 2 $swp1 1 1 mrc 0x8770 + check_err $? "Wrong MRC in generated MLDv2 general queries" + log_test "MLDv2 MRC non linear value of 48000(ms)" + + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 2 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 1 + bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 2 bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 1000 } @@ -536,6 +661,34 @@ vlmc_filtering_test() log_test "Disable multicast vlan snooping when vlan filtering is disabled" } +vlmc_mcast_toggle_test() +{ + RET=0 + + ip link add name br1-mcast up type bridge mcast_snooping 1 mcast_querier 1 vlan_filtering 1 + ip link add name dummy1-mcast up master br1-mcast type dummy + + # Enabling per-VLAN multicast snooping should disable the per-port + # multicast context on "dummy1-mcast". + ip link set dev br1-mcast type bridge mcast_vlan_snooping 1 + + # Toggling multicast snooping on the bridge should not affect the + # per-port multicast context on "dummy1-mcast" given that per-VLAN + # multicast snooping is enabled. + ip link set dev br1-mcast type bridge mcast_snooping 0 + ip link set dev br1-mcast type bridge mcast_snooping 1 + + # If both the per-port and per-{port, VLAN} multicast contexts are + # enabled on "dummy1-mcast", removing it from the bridge will result + # in a splat. + ip link set dev dummy1-mcast nomaster + + log_test "Toggling mcast snooping with per-VLAN mcast snooping enabled" + + ip link del dev dummy1-mcast + ip link del dev br1-mcast +} + trap cleanup EXIT setup_prepare diff --git a/tools/testing/selftests/net/forwarding/gre_multipath.sh b/tools/testing/selftests/net/forwarding/gre_multipath.sh index 57531c1d884d..ce4ae74843d9 100755 --- a/tools/testing/selftests/net/forwarding/gre_multipath.sh +++ b/tools/testing/selftests/net/forwarding/gre_multipath.sh @@ -65,7 +65,7 @@ source lib.sh h1_create() { - simple_if_init $h1 192.0.2.1/28 2001:db8:1::1/64 + simple_if_init $h1 192.0.2.1/28 ip route add vrf v$h1 192.0.2.16/28 via 192.0.2.2 } diff --git a/tools/testing/selftests/net/forwarding/gre_multipath_nh.sh b/tools/testing/selftests/net/forwarding/gre_multipath_nh.sh index 7d5b2b9cc133..c667b81da37f 100755 --- a/tools/testing/selftests/net/forwarding/gre_multipath_nh.sh +++ b/tools/testing/selftests/net/forwarding/gre_multipath_nh.sh @@ -80,7 +80,7 @@ h1_destroy() { ip route del vrf v$h1 2001:db8:2::/64 via 2001:db8:1::2 ip route del vrf v$h1 192.0.2.16/28 via 192.0.2.2 - simple_if_fini $h1 192.0.2.1/28 + simple_if_fini $h1 192.0.2.1/28 2001:db8:1::1/64 } sw1_create() diff --git a/tools/testing/selftests/net/forwarding/gre_multipath_nh_res.sh b/tools/testing/selftests/net/forwarding/gre_multipath_nh_res.sh index 370f9925302d..d04bad58a96a 100755 --- a/tools/testing/selftests/net/forwarding/gre_multipath_nh_res.sh +++ b/tools/testing/selftests/net/forwarding/gre_multipath_nh_res.sh @@ -80,7 +80,7 @@ h1_destroy() { ip route del vrf v$h1 2001:db8:2::/64 via 2001:db8:1::2 ip route del vrf v$h1 192.0.2.16/28 via 192.0.2.2 - simple_if_fini $h1 192.0.2.1/28 + simple_if_fini $h1 192.0.2.1/28 2001:db8:1::1/64 } sw1_create() diff --git a/tools/testing/selftests/net/forwarding/ipip_lib.sh b/tools/testing/selftests/net/forwarding/ipip_lib.sh index 01e62c4ac94d..b255646b737a 100644 --- a/tools/testing/selftests/net/forwarding/ipip_lib.sh +++ b/tools/testing/selftests/net/forwarding/ipip_lib.sh @@ -144,7 +144,7 @@ h1_create() { - simple_if_init $h1 192.0.2.1/28 2001:db8:1::1/64 + simple_if_init $h1 192.0.2.1/28 ip route add vrf v$h1 192.0.2.16/28 via 192.0.2.2 } diff --git a/tools/testing/selftests/net/forwarding/ipmr.c b/tools/testing/selftests/net/forwarding/ipmr.c new file mode 100644 index 000000000000..9cd9f70de132 --- /dev/null +++ b/tools/testing/selftests/net/forwarding/ipmr.c @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright 2026 Google LLC */ + +#include <linux/if.h> +#include <linux/in6.h> +#include <linux/mroute.h> +#include <linux/mroute6.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <linux/socket.h> +#include <sched.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +#include "kselftest_harness.h" + +FIXTURE(ipmr) +{ + int netlink_sk; + int raw_sk; + int veth_ifindex; + union { + struct vifctl vif; + struct mif6ctl vif6; + }; + union { + struct mfcctl mfc; + struct mf6cctl mfc6; + }; +}; + +FIXTURE_VARIANT(ipmr) +{ + int family; + int protocol; + int level; + int rtm_family; + int opts[MRT_MAX - MRT_BASE + 1]; + int flush_flags; + int vif_size; + char vif_check_cmd_pimreg[64]; + char vif_check_cmd_veth[64]; + int mfc_size; + char mfc_check_cmd[1024]; +}; + +FIXTURE_VARIANT_ADD(ipmr, ipv4) +{ + .family = AF_INET, + .protocol = IPPROTO_IGMP, + .level = IPPROTO_IP, + .rtm_family = RTNL_FAMILY_IPMR, + .opts = { + MRT_INIT, + MRT_DONE, + MRT_ADD_VIF, + MRT_DEL_VIF, + MRT_ADD_MFC, + MRT_DEL_MFC, + MRT_VERSION, + MRT_ASSERT, + MRT_PIM, + MRT_TABLE, + MRT_ADD_MFC_PROXY, + MRT_DEL_MFC_PROXY, + MRT_FLUSH, + }, + .flush_flags = MRT_FLUSH_MFC | MRT_FLUSH_MFC_STATIC | + MRT_FLUSH_VIFS | MRT_FLUSH_VIFS_STATIC, + .vif_size = sizeof(struct vifctl), + .vif_check_cmd_pimreg = "cat /proc/net/ip_mr_vif | grep -q pimreg", + .vif_check_cmd_veth = "cat /proc/net/ip_mr_vif | grep -q veth", + .mfc_size = sizeof(struct mfcctl), + .mfc_check_cmd = "cat /proc/net/ip_mr_cache | grep -q '00000000 00000000'", +}; + +FIXTURE_VARIANT_ADD(ipmr, ipv6) +{ + .family = AF_INET6, + .protocol = IPPROTO_ICMPV6, + .level = IPPROTO_IPV6, + .rtm_family = RTNL_FAMILY_IP6MR, + .opts = { + MRT6_INIT, + MRT6_DONE, + MRT6_ADD_MIF, + MRT6_DEL_MIF, + MRT6_ADD_MFC, + MRT6_DEL_MFC, + MRT6_VERSION, + MRT6_ASSERT, + MRT6_PIM, + MRT6_TABLE, + MRT6_ADD_MFC_PROXY, + MRT6_DEL_MFC_PROXY, + MRT6_FLUSH, + }, + .flush_flags = MRT6_FLUSH_MFC | MRT6_FLUSH_MFC_STATIC | + MRT6_FLUSH_MIFS | MRT6_FLUSH_MIFS_STATIC, + .vif_size = sizeof(struct mif6ctl), + .vif_check_cmd_pimreg = "cat /proc/net/ip6_mr_vif | grep -q pim6reg", + .vif_check_cmd_veth = "cat /proc/net/ip6_mr_vif | grep -q veth", + .mfc_size = sizeof(struct mf6cctl), + .mfc_check_cmd = "cat /proc/net/ip6_mr_cache | " + "grep -q '0000:0000:0000:0000:0000:0000:0000:0000 0000:0000:0000:0000:0000:0000:0000:0000'", +}; + +struct mfc_attr { + int table; + __u32 origin; + __u32 group; + int ifindex; + bool proxy; +}; + +static struct rtattr *nl_add_rtattr(struct nlmsghdr *nlmsg, struct rtattr *rta, + int type, const void *data, int len) +{ + int unused = 0; + + rta->rta_type = type; + rta->rta_len = RTA_LENGTH(len); + memcpy(RTA_DATA(rta), data, len); + + nlmsg->nlmsg_len += NLMSG_ALIGN(rta->rta_len); + + return RTA_NEXT(rta, unused); +} + +static int nl_sendmsg_mfc(struct __test_metadata *_metadata, + FIXTURE_DATA(ipmr) *self, + const FIXTURE_VARIANT(ipmr) *variant, + __u16 nlmsg_type, struct mfc_attr *mfc_attr) +{ + struct { + struct nlmsghdr nlmsg; + struct rtmsg rtm; + char buf[4096]; + } req = { + .nlmsg = { + .nlmsg_len = NLMSG_LENGTH(sizeof(req.rtm)), + /* ipmr does not care about NLM_F_CREATE and NLM_F_EXCL ... */ + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + .nlmsg_type = nlmsg_type, + }, + .rtm = { + /* hard requirements in rtm_to_ipmr_mfcc() */ + .rtm_family = variant->rtm_family, + .rtm_dst_len = 32, + .rtm_type = RTN_MULTICAST, + .rtm_scope = RT_SCOPE_UNIVERSE, + .rtm_protocol = RTPROT_MROUTED, + }, + }; + struct nlmsghdr *nlmsg = &req.nlmsg; + struct nlmsgerr *errmsg; + struct rtattr *rta; + int err; + + rta = (struct rtattr *)&req.buf; + rta = nl_add_rtattr(nlmsg, rta, RTA_TABLE, &mfc_attr->table, sizeof(mfc_attr->table)); + rta = nl_add_rtattr(nlmsg, rta, RTA_SRC, &mfc_attr->origin, sizeof(mfc_attr->origin)); + rta = nl_add_rtattr(nlmsg, rta, RTA_DST, &mfc_attr->group, sizeof(mfc_attr->group)); + if (mfc_attr->ifindex) + rta = nl_add_rtattr(nlmsg, rta, RTA_IIF, &mfc_attr->ifindex, sizeof(mfc_attr->ifindex)); + if (mfc_attr->proxy) + rta = nl_add_rtattr(nlmsg, rta, RTA_PREFSRC, NULL, 0); + + err = send(self->netlink_sk, &req, req.nlmsg.nlmsg_len, 0); + ASSERT_EQ(err, req.nlmsg.nlmsg_len); + + memset(&req, 0, sizeof(req)); + + err = recv(self->netlink_sk, &req, sizeof(req), 0); + ASSERT_TRUE(NLMSG_OK(nlmsg, err)); + ASSERT_EQ(NLMSG_ERROR, nlmsg->nlmsg_type); + + errmsg = (struct nlmsgerr *)NLMSG_DATA(nlmsg); + return errmsg->error; +} + +FIXTURE_SETUP(ipmr) +{ + struct ifreq ifr = { + .ifr_name = "veth0", + }; + int err; + + err = unshare(CLONE_NEWNET); + ASSERT_EQ(0, err); + + self->netlink_sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + ASSERT_LE(0, self->netlink_sk); + + self->raw_sk = socket(variant->family, SOCK_RAW, variant->protocol); + ASSERT_LT(0, self->raw_sk); + + err = system("ip link add veth0 type veth peer veth1"); + ASSERT_EQ(0, err); + + err = ioctl(self->raw_sk, SIOCGIFINDEX, &ifr); + ASSERT_EQ(0, err); + + self->veth_ifindex = ifr.ifr_ifindex; + + if (variant->family == AF_INET) { + self->vif = (struct vifctl){ + .vifc_flags = VIFF_USE_IFINDEX, + .vifc_lcl_ifindex = self->veth_ifindex, + }; + } else { + self->vif6 = (struct mif6ctl){ + .mif6c_flags = 0, + .mif6c_pifi = self->veth_ifindex, + }; + } +} + +FIXTURE_TEARDOWN(ipmr) +{ + close(self->raw_sk); + close(self->netlink_sk); +} + +TEST_F(ipmr, mrt_init) +{ + int err, val = 0; /* any value is ok, but size must be int for MRT_INIT. */ + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_INIT - MRT_BASE], + &val, sizeof(val)); + ASSERT_EQ(0, err); + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_DONE - MRT_BASE], + &val, sizeof(val)); + ASSERT_EQ(0, err); +} + +TEST_F(ipmr, mrt_add_vif_register) +{ + int err; + + memset(&self->vif, 0, variant->vif_size); + + if (variant->family == AF_INET) + self->vif.vifc_flags = VIFF_REGISTER; + else + self->vif6.mif6c_flags = MIFF_REGISTER; + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); + + err = system(variant->vif_check_cmd_pimreg); + ASSERT_EQ(0, err); + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_DEL_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); +} + +TEST_F(ipmr, mrt_del_vif_unreg) +{ + int err; + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); + + err = system(variant->vif_check_cmd_veth); + ASSERT_EQ(0, err); + + /* VIF is removed along with its device. */ + err = system("ip link del veth0"); + ASSERT_EQ(0, err); + + /* mrt->vif_table[veth_ifindex]->dev is NULL. */ + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_DEL_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(-1, err); + ASSERT_EQ(EADDRNOTAVAIL, errno); +} + +TEST_F(ipmr, mrt_del_vif_netns_dismantle) +{ + int err; + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); + + /* Let cleanup_net() remove veth0 and VIF. */ +} + +TEST_F(ipmr, mrt_add_mfc) +{ + int err; + + /* MRT_ADD_MFC / MRT_ADD_MFC_PROXY does not need vif to exist (unlike netlink). */ + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_MFC - MRT_BASE], + &self->mfc, variant->mfc_size); + ASSERT_EQ(0, err); + + /* (0.0.0.0 -> 0.0.0.0) */ + err = system(variant->mfc_check_cmd); + ASSERT_EQ(0, err); + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_DEL_MFC - MRT_BASE], + &self->mfc, variant->mfc_size); +} + +TEST_F(ipmr, mrt_add_mfc_proxy) +{ + int err; + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_MFC_PROXY - MRT_BASE], + &self->mfc, variant->mfc_size); + ASSERT_EQ(0, err); + + err = system(variant->mfc_check_cmd); + ASSERT_EQ(0, err); + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_DEL_MFC_PROXY - MRT_BASE], + &self->mfc, variant->mfc_size); +} + +TEST_F(ipmr, mrt_add_mfc_netlink) +{ + struct mfc_attr mfc_attr = { + .table = RT_TABLE_DEFAULT, + .origin = 0, + .group = 0, + .ifindex = self->veth_ifindex, + .proxy = false, + }; + int err; + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); + + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_NEWROUTE, &mfc_attr); + ASSERT_EQ(0, err); + + err = system(variant->mfc_check_cmd); + ASSERT_EQ(0, err); + + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_DELROUTE, &mfc_attr); + ASSERT_EQ(0, err); +} + +TEST_F(ipmr, mrt_add_mfc_netlink_proxy) +{ + struct mfc_attr mfc_attr = { + .table = RT_TABLE_DEFAULT, + .origin = 0, + .group = 0, + .ifindex = self->veth_ifindex, + .proxy = true, + }; + int err; + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); + + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_NEWROUTE, &mfc_attr); + ASSERT_EQ(0, err); + + err = system(variant->mfc_check_cmd); + ASSERT_EQ(0, err); + + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_DELROUTE, &mfc_attr); + ASSERT_EQ(0, err); +} + +TEST_F(ipmr, mrt_add_mfc_netlink_no_vif) +{ + struct mfc_attr mfc_attr = { + .table = RT_TABLE_DEFAULT, + .origin = 0, + .group = 0, + .proxy = false, + }; + int err; + + /* netlink always requires RTA_IIF of an existing vif. */ + mfc_attr.ifindex = 0; + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_NEWROUTE, &mfc_attr); + ASSERT_EQ(-ENFILE, err); + + /* netlink always requires RTA_IIF of an existing vif. */ + mfc_attr.ifindex = self->veth_ifindex; + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_NEWROUTE, &mfc_attr); + ASSERT_EQ(-ENFILE, err); +} + +TEST_F(ipmr, mrt_del_mfc_netlink_netns_dismantle) +{ + struct vifctl vifs[2] = { + { + .vifc_vifi = 0, + .vifc_flags = VIFF_USE_IFINDEX, + .vifc_lcl_ifindex = self->veth_ifindex, + }, + { + .vifc_vifi = 1, + .vifc_flags = VIFF_REGISTER, + } + }; + struct mfc_attr mfc_attr = { + .table = RT_TABLE_DEFAULT, + .origin = 0, + .group = 0, + .ifindex = self->veth_ifindex, + .proxy = false, + }; + int i, err; + + for (i = 0; i < 2; i++) { + /* Create 2 VIFs just to avoid -ENFILE later. */ + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &vifs[i], sizeof(vifs[i])); + ASSERT_EQ(0, err); + } + + /* Create a MFC for mrt->vif_table[0]. */ + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_NEWROUTE, &mfc_attr); + ASSERT_EQ(0, err); + + err = system(variant->mfc_check_cmd); + ASSERT_EQ(0, err); + + /* Remove mrt->vif_table[0]. */ + err = system("ip link del veth0"); + ASSERT_EQ(0, err); + + /* MFC entry is NOT removed even if the tied VIF is removed... */ + err = system(variant->mfc_check_cmd); + ASSERT_EQ(0, err); + + /* ... and netlink is not capable of removing such an entry + * because netlink always requires a valid RTA_IIF ... :/ + */ + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_DELROUTE, &mfc_attr); + ASSERT_EQ(-ENODEV, err); + + /* It can be removed by setsockopt(), but let cleanup_net() remove this time. */ +} + +TEST_F(ipmr, mrt_table_flush) +{ + struct mfc_attr mfc_attr = { + .origin = 0, + .group = 0, + .ifindex = self->veth_ifindex, + .proxy = false, + }; + int table_id = 92; + int err; + + /* Set a random table id rather than RT_TABLE_DEFAULT. + * Note that /proc/net/ip_mr_{vif,cache} only supports RT_TABLE_DEFAULT. + */ + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_TABLE - MRT_BASE], + &table_id, sizeof(table_id)); + ASSERT_EQ(0, err); + + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_VIF - MRT_BASE], + &self->vif, variant->vif_size); + ASSERT_EQ(0, err); + + if (variant->family == AF_INET) { + mfc_attr.table = table_id; + err = nl_sendmsg_mfc(_metadata, self, variant, RTM_NEWROUTE, &mfc_attr); + } else { + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_ADD_MFC - MRT_BASE], + &self->mfc, variant->mfc_size); + } + ASSERT_EQ(0, err); + + /* Flush mrt->vif_table[] and all caches. */ + err = setsockopt(self->raw_sk, + variant->level, variant->opts[MRT_FLUSH - MRT_BASE], + &variant->flush_flags, sizeof(variant->flush_flags)); + ASSERT_EQ(0, err); +} + +XFAIL_ADD(ipmr, ipv6, mrt_add_mfc_netlink); +XFAIL_ADD(ipmr, ipv6, mrt_add_mfc_netlink_proxy); +XFAIL_ADD(ipmr, ipv6, mrt_add_mfc_netlink_no_vif); +XFAIL_ADD(ipmr, ipv6, mrt_del_mfc_netlink_netns_dismantle); + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/forwarding/lib.sh b/tools/testing/selftests/net/forwarding/lib.sh index a9034f0bb58b..05acd4011456 100644 --- a/tools/testing/selftests/net/forwarding/lib.sh +++ b/tools/testing/selftests/net/forwarding/lib.sh @@ -1,5 +1,6 @@ #!/bin/bash # SPDX-License-Identifier: GPL-2.0 +#shellcheck disable=SC2034 # SC doesn't see our uses of global variables ############################################################################## # Topology description. p1 looped back to p2, p3 to p4 and so on. @@ -340,17 +341,160 @@ fi ############################################################################## # Command line options handling -count=0 +check_env() { + if [[ ! (( -n "$LOCAL_V4" && -n "$REMOTE_V4") || + ( -n "$LOCAL_V6" && -n "$REMOTE_V6" )) ]]; then + echo "SKIP: Invalid environment, missing or inconsistent LOCAL_V4/REMOTE_V4/LOCAL_V6/REMOTE_V6" + echo "Please see tools/testing/selftests/drivers/net/README.rst" + exit "$ksft_skip" + fi + + if [[ -z "$REMOTE_TYPE" ]]; then + echo "SKIP: Invalid environment, missing REMOTE_TYPE" + exit "$ksft_skip" + fi + + if [[ -z "$REMOTE_ARGS" ]]; then + echo "SKIP: Invalid environment, missing REMOTE_ARGS" + exit "$ksft_skip" + fi +} + +__run_on() +{ + local target=$1; shift + local type args + + IFS=':' read -r type args <<< "$target" + + case "$type" in + netns) + # Execute command in network namespace + # args contains the namespace name + ip netns exec "$args" "$@" + ;; + ssh) + # Execute command via SSH args contains user@host + ssh -n "$args" "$@" + ;; + local|*) + # Execute command locally. This is also the fallback + # case for when the interface's target is not found in + # the TARGETS array. + "$@" + ;; + esac +} + +run_on() +{ + local iface=$1; shift + local target="local:" + + if [ "${DRIVER_TEST_CONFORMANT}" = "yes" ]; then + target="${TARGETS[$iface]}" + fi + + __run_on "$target" "$@" +} + +get_ifname_by_ip() +{ + local target=$1; shift + local ip_addr=$1; shift + + __run_on "$target" ip -j addr show to "$ip_addr" | jq -r '.[].ifname' +} + +# Wait for the device to refresh its HW statistics. Devices latch the stats +# reported via ethtool only every stats-block-usecs, so sample after that. +hw_stats_settle() +{ + local iface=$1; shift + local usecs + + # Match only a non-zero integer; 0 or "n/a" use default (20msec) + usecs=$(run_on "$iface" ethtool -c "$iface" 2>/dev/null | \ + sed -n 's/^stats-block-usecs:[[:space:]]*\([1-9][0-9]*\)$/\1/p') + usecs=${usecs:-20000} + + sleep "$(echo "$usecs * 1.25 / 1000 / 1000" | bc -l)" +} + +# Whether the test is conforming to the requirements and usage described in +# drivers/net/README.rst. +: "${DRIVER_TEST_CONFORMANT:=no}" + +declare -A TARGETS + +# Based on DRIVER_TEST_CONFORMANT, decide if to source drivers/net/net.config +# or not. In the "yes" case, the test expects to pass the arguments through the +# variables specified in drivers/net/README.rst file. If not, fallback on +# parsing the script arguments for interface names. +if [ "${DRIVER_TEST_CONFORMANT}" = "yes" ]; then + if [[ -f $net_forwarding_dir/../../drivers/net/net.config ]]; then + source "$net_forwarding_dir/../../drivers/net/net.config" + fi -while [[ $# -gt 0 ]]; do - if [[ "$count" -eq "0" ]]; then + if (( NUM_NETIFS > 2)); then + echo "SKIP: DRIVER_TEST_CONFORMANT=yes and NUM_NETIFS is bigger than 2" + exit "$ksft_skip" + fi + + check_env + + # Populate the NETIFS and TARGETS arrays automatically based on the + # environment variables. The TARGETS array is indexed by the network + # interface name keeping track of the target on which the interface + # resides. Values will be strings of the following format - + # <type>:<args>. + # + # TARGETS[eth0]="local:" - meaning that the eth0 interface is + # accessible locally + # TARGETS[eth1]="netns:foo" - eth1 is in the foo netns + # TARGETS[eth2]="ssh:root@10.0.0.2" - eth2 is accessible through + # running the 'ssh root@10.0.0.2' command. + + unset NETIFS + declare -A NETIFS + + NETIFS[p1]="$NETIF" + TARGETS[$NETIF]="local:" + + # Locate the name of the remote interface + remote_target="$REMOTE_TYPE:$REMOTE_ARGS" + if [[ -v REMOTE_V4 ]]; then + remote_netif=$(get_ifname_by_ip "$remote_target" "$REMOTE_V4") + else + remote_netif=$(get_ifname_by_ip "$remote_target" "$REMOTE_V6") + fi + if [[ ! -n "$remote_netif" ]]; then + echo "SKIP: cannot find remote interface" + exit "$ksft_skip" + fi + + if [[ "$NETIF" == "$remote_netif" ]]; then + echo "SKIP: local and remote interfaces cannot have the same name" + exit "$ksft_skip" + fi + + NETIFS[p2]="$remote_netif" + TARGETS[$remote_netif]="$REMOTE_TYPE:$REMOTE_ARGS" +else + count=0 + # Prime NETIFS from the command line, but retain if none given. + if [[ $# -gt 0 ]]; then unset NETIFS declare -A NETIFS + + while [[ $# -gt 0 ]]; do + count=$((count + 1)) + NETIFS[p$count]="$1" + TARGETS[$1]="local:" + shift + done fi - count=$((count + 1)) - NETIFS[p$count]="$1" - shift -done +fi ############################################################################## # Network interfaces configuration @@ -418,10 +562,11 @@ mac_addr_prepare() dev=${NETIFS[p$i]} new_addr=$(printf "00:01:02:03:04:%02x" $i) - MAC_ADDR_ORIG["$dev"]=$(ip -j link show dev $dev | jq -e '.[].address') + MAC_ADDR_ORIG["$dev"]=$(run_on "$dev" \ + ip -j link show dev "$dev" | jq -e '.[].address') # Strip quotes MAC_ADDR_ORIG["$dev"]=${MAC_ADDR_ORIG["$dev"]//\"/} - ip link set dev $dev address $new_addr + run_on "$dev" ip link set dev "$dev" address $new_addr done } @@ -431,7 +576,8 @@ mac_addr_restore() for ((i = 1; i <= NUM_NETIFS; ++i)); do dev=${NETIFS[p$i]} - ip link set dev $dev address ${MAC_ADDR_ORIG["$dev"]} + run_on "$dev" \ + ip link set dev "$dev" address ${MAC_ADDR_ORIG["$dev"]} done } @@ -444,7 +590,9 @@ if [[ "$STABLE_MAC_ADDRS" = "yes" ]]; then fi for ((i = 1; i <= NUM_NETIFS; ++i)); do - ip link show dev ${NETIFS[p$i]} &> /dev/null + int="${NETIFS[p$i]}" + + run_on "$int" ip link show dev "$int" &> /dev/null if [[ $? -ne 0 ]]; then echo "SKIP: could not find all required interfaces" exit $ksft_skip @@ -527,7 +675,7 @@ setup_wait_dev_with_timeout() local i for ((i = 1; i <= $max_iterations; ++i)); do - ip link show dev $dev up \ + run_on "$dev" ip link show dev "$dev" up \ | grep 'state UP' &> /dev/null if [[ $? -ne 0 ]]; then sleep 1 @@ -831,8 +979,15 @@ ethtool_std_stats_get() local name=$1; shift local src=$1; shift - ethtool --json -S $dev --groups $grp -- --src $src | \ - jq '.[]."'"$grp"'"."'$name'"' + if [[ "$grp" == "pause" ]]; then + run_on "$dev" ethtool -I --json -a "$dev" --src "$src" | \ + jq --arg name "$name" '.[].statistics[$name]' + return + fi + + run_on "$dev" \ + ethtool --json -S "$dev" --groups "$grp" -- --src "$src" | \ + jq --arg grp "$grp" --arg name "$name" '.[][$grp][$name]' } qdisc_stats_get() @@ -1610,12 +1765,17 @@ tcpdump_start() sleep 1 } -tcpdump_stop() +tcpdump_stop_nosleep() { local if_name=$1 local pid=${cappid[$if_name]} $ns_cmd kill "$pid" && wait "$pid" +} + +tcpdump_stop() +{ + tcpdump_stop_nosleep "$1" sleep 1 } @@ -1630,7 +1790,7 @@ tcpdump_show() { local if_name=$1 - tcpdump -e -n -r ${capfile[$if_name]} 2>&1 + tcpdump -e -nn -r ${capfile[$if_name]} 2>&1 } # return 0 if the packet wasn't seen on host2_if or 1 if it was diff --git a/tools/testing/selftests/net/forwarding/local_termination.sh b/tools/testing/selftests/net/forwarding/local_termination.sh index 1f2bf6e81847..15b1a1255a41 100755 --- a/tools/testing/selftests/net/forwarding/local_termination.sh +++ b/tools/testing/selftests/net/forwarding/local_termination.sh @@ -57,21 +57,21 @@ PTP_1588_L2_PDELAY_REQ=" \ PTP_1588_IPV4_SYNC=" \ 01:00:5e:00:01:81 00:00:de:ad:be:ef 08:00 45 00 \ 00 48 0a 9a 40 00 01 11 cb 88 c0 00 02 01 e0 00 \ -01 81 01 3f 01 3f 00 34 a3 c8 00 02 00 2c 00 00 \ +01 81 01 3f 01 3f 00 34 9f 41 00 02 00 2c 00 00 \ 02 00 00 00 00 00 00 00 00 00 00 00 00 00 3e 37 \ 63 ff fe cf 17 0e 00 01 00 00 00 00 00 00 00 00 \ 00 00 00 00 00 00" PTP_1588_IPV4_FOLLOW_UP=" 01:00:5e:00:01:81 00:00:de:ad:be:ef 08:00 45 00 \ 00 48 0a 9b 40 00 01 11 cb 87 c0 00 02 01 e0 00 \ -01 81 01 40 01 40 00 34 a3 c8 08 02 00 2c 00 00 \ +01 81 01 40 01 40 00 34 eb 8a 08 02 00 2c 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3e 37 \ 63 ff fe cf 17 0e 00 01 00 00 02 00 00 00 66 83 \ c6 0f 1d 9a 61 87" PTP_1588_IPV4_PDELAY_REQ=" \ 01:00:5e:00:00:6b 00:00:de:ad:be:ef 08:00 45 00 \ 00 52 35 a9 40 00 01 11 a1 85 c0 00 02 01 e0 00 \ -00 6b 01 3f 01 3f 00 3e a2 bc 02 02 00 36 00 00 \ +00 6b 01 3f 01 3f 00 3e 9a b9 02 02 00 36 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3e 37 \ 63 ff fe cf 17 0e 00 01 00 01 05 7f 00 00 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" @@ -79,7 +79,7 @@ PTP_1588_IPV6_SYNC=" \ 33:33:00:00:01:81 00:00:de:ad:be:ef 86:dd 60 06 \ 7c 2f 00 36 11 01 20 01 0d b8 00 01 00 00 00 00 \ 00 00 00 00 00 01 ff 0e 00 00 00 00 00 00 00 00 \ -00 00 00 00 01 81 01 3f 01 3f 00 36 2e 92 00 02 \ +00 00 00 00 01 81 01 3f 01 3f 00 36 14 76 00 02 \ 00 2c 00 00 02 00 00 00 00 00 00 00 00 00 00 00 \ 00 00 3e 37 63 ff fe cf 17 0e 00 01 00 00 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00" @@ -87,7 +87,7 @@ PTP_1588_IPV6_FOLLOW_UP=" \ 33:33:00:00:01:81 00:00:de:ad:be:ef 86:dd 60 0a \ 00 bc 00 36 11 01 20 01 0d b8 00 01 00 00 00 00 \ 00 00 00 00 00 01 ff 0e 00 00 00 00 00 00 00 00 \ -00 00 00 00 01 81 01 40 01 40 00 36 2e 92 08 02 \ +00 00 00 00 01 81 01 40 01 40 00 36 f0 47 08 02 \ 00 2c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 00 00 3e 37 63 ff fe cf 17 0e 00 01 00 00 02 00 \ 00 00 66 83 c6 2a 32 09 bd 74 00 00" @@ -95,11 +95,20 @@ PTP_1588_IPV6_PDELAY_REQ=" \ 33:33:00:00:00:6b 00:00:de:ad:be:ef 86:dd 60 0c \ 5c fd 00 40 11 01 fe 80 00 00 00 00 00 00 3c 37 \ 63 ff fe cf 17 0e ff 02 00 00 00 00 00 00 00 00 \ -00 00 00 00 00 6b 01 3f 01 3f 00 40 b4 54 02 02 \ +00 00 00 00 00 6b 01 3f 01 3f 00 40 89 1f 02 02 \ 00 36 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 00 00 3e 37 63 ff fe cf 17 0e 00 01 00 01 05 7f \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 00 00 00 00 00 00" +LINK_LOCAL_STP_BPDU=" \ +01:80:c2:00:00:00 00:00:de:ad:be:ef 00 26 42 42 03 \ +00 00 00 00 00 80 00 aa bb cc dd ee ff 00 00 00 00 \ +80 00 aa bb cc dd ee ff 80 01 00 00 14 00 02 00 \ +0f 00" +LINK_LOCAL_LLDP=" \ +01:80:c2:00:00:0e 00:00:de:ad:be:ef 88:cc 02 07 04 \ +00 11 22 33 44 55 04 05 05 65 74 68 30 06 02 00 \ +78 00 00" # Disable promisc to ensure we don't receive unknown MAC DA packets export TCPDUMP_EXTRA_FLAGS="-pl" @@ -213,7 +222,15 @@ run_test() mc_route_destroy $rcv_if_name mc_route_destroy $send_if_name + ip maddress add 01:80:c2:00:00:00 dev $rcv_if_name + send_raw $send_if_name "$LINK_LOCAL_STP_BPDU" + ip maddress del 01:80:c2:00:00:00 dev $rcv_if_name + if [ $skip_ptp = false ]; then + ip maddress add 01:80:c2:00:00:0e dev $rcv_if_name + send_raw $send_if_name "$LINK_LOCAL_LLDP" + ip maddress del 01:80:c2:00:00:0e dev $rcv_if_name + ip maddress add 01:1b:19:00:00:00 dev $rcv_if_name send_raw $send_if_name "$PTP_1588_L2_SYNC" send_raw $send_if_name "$PTP_1588_L2_FOLLOW_UP" @@ -304,7 +321,15 @@ run_test() "$smac > $UNKNOWN_MACV6_MC_ADDR3, ethertype IPv6 (0x86dd)" \ true "$test_name" + check_rcv $rcv_if_name "Link-local STP BPDU" \ + "> 01:80:c2:00:00:00" \ + true "$test_name" + if [ $skip_ptp = false ]; then + check_rcv $rcv_if_name "Link-local LLDP" \ + "> 01:80:c2:00:00:0e" \ + true "$test_name" + check_rcv $rcv_if_name "1588v2 over L2 transport, Sync" \ "ethertype PTP (0x88f7).* PTPv2.* msg type *: sync msg" \ true "$test_name" diff --git a/tools/testing/selftests/net/fou_mcast_encap.sh b/tools/testing/selftests/net/fou_mcast_encap.sh new file mode 100755 index 000000000000..70210d39fba3 --- /dev/null +++ b/tools/testing/selftests/net/fou_mcast_encap.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test that UDP encapsulation (FOU) correctly handles packet resubmit +# when packets are delivered via the multicast UDP delivery path. +# +# When a FOU-encapsulated packet arrives with a multicast destination IP, +# __udp4_lib_mcast_deliver() / __udp6_lib_mcast_deliver() must resubmit +# it to the inner protocol handler (e.g., GRE) rather than consuming it. +# This test verifies both IPv4 and IPv6 paths by creating a FOU/GRETAP +# tunnel with a multicast remote address and sending ping through it. +# +# The early demux optimization can mask this issue by routing packets via +# the unicast path (udp[6]_unicast_rcv_skb), so we disable it to force +# packets through the multicast delivery function. + +source lib.sh + +NSENDER="" +NRECV="" + +FOU_PORT4=4797 +FOU_PORT6=4798 +MCAST4=239.0.0.1 +MCAST6=ff0e::1 + +TUN4_S=192.168.99.1 +TUN4_R=192.168.99.2 +TUN6_S=2001:db8:99::1 +TUN6_R=2001:db8:99::2 + +cleanup() { + cleanup_all_ns +} + +trap cleanup EXIT + +setup_common() { + setup_ns NSENDER NRECV + + # Create veth pair directly inside namespaces to avoid name + # collisions with devices in the root namespace. + ip link add veth_s netns "$NSENDER" type veth \ + peer name veth_r netns "$NRECV" + + ip -n "$NSENDER" link set veth_s up + ip -n "$NRECV" link set veth_r up + + # Same sysctl controls early demux for both IPv4 and IPv6. + ip netns exec "$NRECV" sysctl -wq net.ipv4.ip_early_demux=0 +} + +setup_ipv4() { + # IPv4 FOU (CONFIG_NET_FOU) is built in on kernels configured for + # these tests, so no module load is needed here. + ip -n "$NSENDER" addr add 10.0.0.1/24 dev veth_s + ip -n "$NRECV" addr add 10.0.0.2/24 dev veth_r + + # Join multicast group on receiver + ip -n "$NRECV" addr add "$MCAST4/32" dev veth_r autojoin + + ip -n "$NSENDER" route add 239.0.0.0/8 dev veth_s + ip -n "$NRECV" route add 239.0.0.0/8 dev veth_r + + # Sender: GRETAP with FOU encap (no FOU listener needed on TX side) + ip -n "$NSENDER" link add eoudp4 type gretap \ + remote "$MCAST4" local 10.0.0.1 \ + encap fou encap-sport "$FOU_PORT4" encap-dport "$FOU_PORT4" \ + key "$MCAST4" + ip -n "$NSENDER" link set eoudp4 up + ip -n "$NSENDER" addr add "$TUN4_S/24" dev eoudp4 + + # Receiver: FOU listener + GRETAP + ip netns exec "$NRECV" ip fou add port "$FOU_PORT4" ipproto 47 + ip -n "$NRECV" link add eoudp4 type gretap \ + remote "$MCAST4" local 10.0.0.2 \ + encap fou encap-sport "$FOU_PORT4" encap-dport "$FOU_PORT4" \ + key "$MCAST4" + ip -n "$NRECV" link set eoudp4 up + ip -n "$NRECV" addr add "$TUN4_R/24" dev eoudp4 + + # Static neigh on sender: ARP replies cannot traverse the + # unidirectional multicast tunnel. + local recv_mac + recv_mac=$(ip -n "$NRECV" link show eoudp4 | awk '/ether/{print $2}') + ip -n "$NSENDER" neigh add "$TUN4_R" lladdr "$recv_mac" dev eoudp4 +} + +setup_ipv6() { + # Skip cleanly if IPv6 or the fou6 module is not available. + [ -e /proc/sys/net/ipv6 ] || return "$ksft_skip" + modprobe -q fou6 || return "$ksft_skip" + + ip -n "$NSENDER" addr add 2001:db8::1/64 dev veth_s nodad + ip -n "$NRECV" addr add 2001:db8::2/64 dev veth_r nodad + + # Join multicast group on receiver + ip -n "$NRECV" addr add "$MCAST6/128" dev veth_r autojoin + + ip -n "$NSENDER" -6 route add ff00::/8 dev veth_s + ip -n "$NRECV" -6 route add ff00::/8 dev veth_r + + # Sender: ip6gretap with FOU encap + ip -n "$NSENDER" link add eoudp6 type ip6gretap \ + remote "$MCAST6" local 2001:db8::1 \ + encap fou encap-sport "$FOU_PORT6" encap-dport "$FOU_PORT6" \ + key 42 + ip -n "$NSENDER" link set eoudp6 up + ip -n "$NSENDER" addr add "$TUN6_S/64" dev eoudp6 nodad + + # Receiver: FOU listener (IPv6) + ip6gretap + ip netns exec "$NRECV" ip fou add port "$FOU_PORT6" ipproto 47 -6 + ip -n "$NRECV" link add eoudp6 type ip6gretap \ + remote "$MCAST6" local 2001:db8::2 \ + encap fou encap-sport "$FOU_PORT6" encap-dport "$FOU_PORT6" \ + key 42 + ip -n "$NRECV" link set eoudp6 up + ip -n "$NRECV" addr add "$TUN6_R/64" dev eoudp6 nodad + + # Static neigh on sender: neighbor discovery cannot traverse the + # unidirectional multicast tunnel. + local recv_mac + recv_mac=$(ip -n "$NRECV" link show eoudp6 | awk '/ether/{print $2}') + ip -n "$NSENDER" neigh add "$TUN6_R" lladdr "$recv_mac" dev eoudp6 +} + +get_rx_packets() { + local dev="$1" + + ip -n "$NRECV" -s link show "$dev" | awk '/RX:/{getline; print $2}' +} + +run_ping_test() { + local family="$1" + local dev="$2" + local dst="$3" + local name="$4" + local count=100 + local rx_before rx_after rx_delta + + # Warmup: let any initial broadcast/ND traffic settle + ip netns exec "$NSENDER" ping "$family" -c 1 -W 1 "$dst" \ + >/dev/null 2>&1 + sleep 1 + + rx_before=$(get_rx_packets "$dev") + ip netns exec "$NSENDER" ping "$family" -i 0.01 -c $count -W 1 "$dst" \ + >/dev/null 2>&1 + sleep 1 + rx_after=$(get_rx_packets "$dev") + + rx_delta=$((rx_after - rx_before)) + + if [ "$rx_delta" -ge "$count" ]; then + RET=$ksft_pass + else + RET=$ksft_fail + fi + log_test "$name (received $rx_delta/$count)" +} + +setup_common +setup_ipv4 +run_ping_test -4 eoudp4 "$TUN4_R" "FOU/GRETAP IPv4 multicast encap resubmit" + +if setup_ipv6; then + run_ping_test -6 eoudp6 "$TUN6_R" "FOU/ip6gretap IPv6 multicast encap resubmit" +else + log_test_skip "FOU/ip6gretap IPv6 multicast encap resubmit" +fi + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/fq_band_pktlimit.sh b/tools/testing/selftests/net/fq_band_pktlimit.sh index 977070ed42b3..223f9efe4090 100755 --- a/tools/testing/selftests/net/fq_band_pktlimit.sh +++ b/tools/testing/selftests/net/fq_band_pktlimit.sh @@ -32,19 +32,19 @@ tc qdisc replace dev dummy0 root handle 1: fq quantum 1514 initial_quantum 1514 DELAY=400000 ./cmsg_sender -6 -p u -d "${DELAY}" -n 20 fdaa::2 8000 -OUT1="$(tc -s qdisc show dev dummy0 | grep '^\ Sent')" +OUT1="$(tc -s qdisc show dev dummy0 | grep '^ Sent')" ./cmsg_sender -6 -p u -d "${DELAY}" -n 20 fdaa::2 8000 -OUT2="$(tc -s qdisc show dev dummy0 | grep '^\ Sent')" +OUT2="$(tc -s qdisc show dev dummy0 | grep '^ Sent')" ./cmsg_sender -6 -p u -d "${DELAY}" -n 20 -P 7 fdaa::2 8000 -OUT3="$(tc -s qdisc show dev dummy0 | grep '^\ Sent')" +OUT3="$(tc -s qdisc show dev dummy0 | grep '^ Sent')" # Initial stats will report zero sent, as all packets are still # queued in FQ. Sleep for at least the delay period and see that # twenty are now sent. sleep 0.6 -OUT4="$(tc -s qdisc show dev dummy0 | grep '^\ Sent')" +OUT4="$(tc -s qdisc show dev dummy0 | grep '^ Sent')" # Log the output after the test echo "${OUT1}" @@ -53,7 +53,7 @@ echo "${OUT3}" echo "${OUT4}" # Test the output for expected values -echo "${OUT1}" | grep -q '0\ pkt\ (dropped\ 10' || die "unexpected drop count at 1" -echo "${OUT2}" | grep -q '0\ pkt\ (dropped\ 30' || die "unexpected drop count at 2" -echo "${OUT3}" | grep -q '0\ pkt\ (dropped\ 40' || die "unexpected drop count at 3" -echo "${OUT4}" | grep -q '20\ pkt\ (dropped\ 40' || die "unexpected accept count at 4" +echo "${OUT1}" | grep -q '0 pkt (dropped 10' || die "unexpected drop count at 1" +echo "${OUT2}" | grep -q '0 pkt (dropped 30' || die "unexpected drop count at 2" +echo "${OUT3}" | grep -q '0 pkt (dropped 40' || die "unexpected drop count at 3" +echo "${OUT4}" | grep -q '20 pkt (dropped 40' || die "unexpected accept count at 4" diff --git a/tools/testing/selftests/net/getsockopt_iter.c b/tools/testing/selftests/net/getsockopt_iter.c new file mode 100644 index 000000000000..6c2408df4612 --- /dev/null +++ b/tools/testing/selftests/net/getsockopt_iter.c @@ -0,0 +1,636 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Quick test for getsockopt{_iter} tests. + * + * Each fixture targets one converted protocol and pins down the + * returned-length / errno semantics across buffer-size variations, + * an unknown optname and a bogus level. + * + * - netlink: NETLINK_PKTINFO covers the flag-style int path; the + * NETLINK_LIST_MEMBERSHIPS cases cover the size-discovery path + * that always reports the required buffer length back via optlen, + * even when the user buffer is too small to receive any group bits. + * - vsock: SO_VM_SOCKETS_BUFFER_SIZE covers the u64 path. + * - raw: ICMP_FILTER covers a fixed-size struct payload that clamps + * the length down on a short buffer instead of failing. + * + * Author: Breno Leitao <leitao@debian.org> + */ + +#include <errno.h> +#include <stdint.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <linux/netlink.h> +#include <linux/rtnetlink.h> +#include <linux/time_types.h> +#include <linux/vm_sockets.h> +#include <linux/icmp.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <arpa/inet.h> +#include <sys/socket.h> +#include <linux/tls.h> +#include "kselftest_harness.h" + +#ifndef AF_VSOCK +#define AF_VSOCK 40 +#endif +#ifndef SOL_RAW +#define SOL_RAW 255 +#endif +#ifndef ICMP_FILTER +#define ICMP_FILTER 1 +#endif +#ifndef IPV6_HDRINCL +#define IPV6_HDRINCL 36 +#endif +#ifndef IPV6_CHECKSUM +#define IPV6_CHECKSUM 7 +#endif +#ifndef SOL_TLS +#define SOL_TLS 282 +#endif +#ifndef TCP_ULP +#define TCP_ULP 31 +#endif + +/* ---------- netlink ---------- */ + +FIXTURE(netlink) +{ + int fd; +}; + +FIXTURE_SETUP(netlink) +{ + int group = RTNLGRP_LINK; + + self->fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (self->fd < 0) + SKIP(return, "AF_NETLINK socket: %s", strerror(errno)); + + /* Joining a multicast group grows nlk->ngroups so the + * NETLINK_LIST_MEMBERSHIPS path has a non-zero size to report. + */ + if (setsockopt(self->fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, + &group, sizeof(group)) < 0) + SKIP(return, "NETLINK_ADD_MEMBERSHIP: %s", strerror(errno)); +} + +FIXTURE_TEARDOWN(netlink) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(netlink, pktinfo_exact) +{ + socklen_t optlen; + int val = -1; + + optlen = sizeof(val); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_NETLINK, NETLINK_PKTINFO, + &val, &optlen)); + ASSERT_EQ(sizeof(int), optlen); + ASSERT_TRUE(val == 0 || val == 1); +} + +TEST_F(netlink, pktinfo_oversize_clamped) +{ + char buf[16] = {}; + socklen_t optlen; + + optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_NETLINK, NETLINK_PKTINFO, + buf, &optlen)); + ASSERT_EQ(sizeof(int), optlen); +} + +TEST_F(netlink, pktinfo_undersize) +{ + char buf[2] = {}; + socklen_t optlen; + + optlen = sizeof(buf); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_NETLINK, NETLINK_PKTINFO, + buf, &optlen)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(sizeof(buf), optlen); +} + +TEST_F(netlink, list_memberships_size_discovery) +{ + socklen_t optlen = 0; + char dummy; + + ASSERT_EQ(0, getsockopt(self->fd, SOL_NETLINK, + NETLINK_LIST_MEMBERSHIPS, + &dummy, &optlen)); + ASSERT_GT(optlen, 0); + ASSERT_EQ(0, optlen % sizeof(__u32)); +} + +TEST_F(netlink, list_memberships_full_read) +{ + __u32 buf[64] = {}; + socklen_t optlen; + + optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_NETLINK, + NETLINK_LIST_MEMBERSHIPS, + buf, &optlen)); + ASSERT_GT(optlen, 0); + ASSERT_LE(optlen, sizeof(buf)); + ASSERT_EQ(0, optlen % sizeof(__u32)); +} + +TEST_F(netlink, bad_level) +{ + socklen_t optlen; + int val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_SOCKET + 1, NETLINK_PKTINFO, + &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +TEST_F(netlink, bad_optname) +{ + socklen_t optlen; + int val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_NETLINK, 0x7fff, + &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +/* ---------- vsock ---------- */ + +FIXTURE(vsock) +{ + int fd; +}; + +FIXTURE_SETUP(vsock) +{ + self->fd = socket(AF_VSOCK, SOCK_STREAM, 0); + if (self->fd < 0) + SKIP(return, "AF_VSOCK socket: %s", strerror(errno)); +} + +FIXTURE_TEARDOWN(vsock) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(vsock, buffer_size_exact) +{ + socklen_t optlen; + uint64_t val = 0; + + optlen = sizeof(val); + + ASSERT_EQ(0, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_BUFFER_SIZE, + &val, &optlen)); + ASSERT_EQ(sizeof(uint64_t), optlen); + ASSERT_GT(val, 0); +} + +TEST_F(vsock, buffer_size_oversize_clamped) +{ + char buf[16] = {}; + socklen_t optlen; + + optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_BUFFER_SIZE, + buf, &optlen)); + ASSERT_EQ(sizeof(uint64_t), optlen); +} + +TEST_F(vsock, buffer_size_undersize) +{ + char buf[4] = {}; + socklen_t optlen; + + optlen = sizeof(buf); + + ASSERT_EQ(-1, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_BUFFER_SIZE, + buf, &optlen)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(sizeof(buf), optlen); +} + +TEST_F(vsock, bad_level) +{ + socklen_t optlen; + uint64_t val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_SOCKET + 1, + SO_VM_SOCKETS_BUFFER_SIZE, + &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +TEST_F(vsock, bad_optname) +{ + socklen_t optlen; + uint64_t val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, AF_VSOCK, 0x7fff, + &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +/* SO_VM_SOCKETS_CONNECT_TIMEOUT_{NEW,OLD} return a sock_timeval-shaped + * payload, which is wider than u64 on 64-bit. They exercise the path + * where the protocol's reported lv (16 bytes) is larger than the + * common 8-byte u64 case covered above. + */ +TEST_F(vsock, connect_timeout_new_exact) +{ + struct __kernel_sock_timeval tv = {}; + socklen_t optlen; + + optlen = sizeof(tv); + + ASSERT_EQ(0, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_CONNECT_TIMEOUT_NEW, + &tv, &optlen)); + ASSERT_EQ(sizeof(tv), optlen); +} + +TEST_F(vsock, connect_timeout_new_oversize_clamped) +{ + char buf[sizeof(struct __kernel_sock_timeval) * 2] = {}; + socklen_t optlen; + + optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_CONNECT_TIMEOUT_NEW, + buf, &optlen)); + ASSERT_EQ(sizeof(struct __kernel_sock_timeval), optlen); +} + +TEST_F(vsock, connect_timeout_new_undersize) +{ + socklen_t optlen; + uint64_t val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_CONNECT_TIMEOUT_NEW, + &val, &optlen)); + ASSERT_EQ(EINVAL, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +TEST_F(vsock, connect_timeout_old_exact) +{ + struct __kernel_old_timeval tv = {}; + socklen_t optlen; + + optlen = sizeof(tv); + + ASSERT_EQ(0, getsockopt(self->fd, AF_VSOCK, + SO_VM_SOCKETS_CONNECT_TIMEOUT_OLD, + &tv, &optlen)); + ASSERT_EQ(sizeof(tv), optlen); +} + +/* ---------- raw (ipv4) ---------- */ + +FIXTURE(raw) +{ + int fd; +}; + +FIXTURE_SETUP(raw) +{ + struct icmp_filter filt = { .data = 0xdeadbeef }; + + self->fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + if (self->fd < 0) + SKIP(return, "SOCK_RAW/ICMP socket: %s", strerror(errno)); + + if (setsockopt(self->fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt)) < 0) + SKIP(return, "set ICMP_FILTER: %s", strerror(errno)); +} + +FIXTURE_TEARDOWN(raw) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(raw, icmpfilter_exact) +{ + struct icmp_filter filt = {}; + socklen_t optlen = sizeof(filt); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RAW, ICMP_FILTER, + &filt, &optlen)); + ASSERT_EQ(sizeof(filt), optlen); + ASSERT_EQ(0xdeadbeef, filt.data); +} + +TEST_F(raw, icmpfilter_oversize_clamped) +{ + char buf[16] = {}; + socklen_t optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RAW, ICMP_FILTER, + buf, &optlen)); + ASSERT_EQ(sizeof(struct icmp_filter), optlen); +} + +/* Unlike the int/u64 options above, ICMP_FILTER clamps the length down + * to the user buffer instead of returning EINVAL: a short buffer + * succeeds and reports the truncated length back via optlen. + */ +TEST_F(raw, icmpfilter_undersize_clamped) +{ + char buf[2] = {}; + socklen_t optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RAW, ICMP_FILTER, + buf, &optlen)); + ASSERT_EQ(sizeof(buf), optlen); +} + +TEST_F(raw, icmpfilter_wrong_proto) +{ + struct icmp_filter filt; + socklen_t optlen = sizeof(filt); + int fd; + + fd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); + if (fd < 0) + SKIP(return, "SOCK_RAW/UDP socket: %s", strerror(errno)); + + ASSERT_EQ(-1, getsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, &optlen)); + ASSERT_EQ(EOPNOTSUPP, errno); + close(fd); +} + +TEST_F(raw, bad_optname) +{ + socklen_t optlen; + int val; + + optlen = sizeof(val); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_RAW, 0x7fff, &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +/* ---------- raw (ipv6) ---------- */ + +FIXTURE(rawv6) +{ + int fd; +}; + +FIXTURE_SETUP(rawv6) +{ + self->fd = socket(AF_INET6, SOCK_RAW, IPPROTO_UDP); + if (self->fd < 0) + SKIP(return, "SOCK_RAW/IPv6 socket: %s", strerror(errno)); +} + +FIXTURE_TEARDOWN(rawv6) +{ + if (self->fd >= 0) + close(self->fd); +} + +TEST_F(rawv6, hdrincl_exact) +{ + socklen_t optlen; + int val = -1; + + optlen = sizeof(val); + + ASSERT_EQ(0, getsockopt(self->fd, IPPROTO_IPV6, IPV6_HDRINCL, + &val, &optlen)); + ASSERT_EQ(sizeof(int), optlen); + ASSERT_TRUE(val == 0 || val == 1); +} + +TEST_F(rawv6, hdrincl_oversize_clamped) +{ + char buf[16] = {}; + socklen_t optlen = sizeof(buf); + + ASSERT_EQ(0, getsockopt(self->fd, IPPROTO_IPV6, IPV6_HDRINCL, + buf, &optlen)); + ASSERT_EQ(sizeof(int), optlen); +} + +/* Raw int options clamp the reported length down to the user buffer + * instead of returning EINVAL on a short buffer. + */ +TEST_F(rawv6, hdrincl_undersize_clamped) +{ + socklen_t optlen = 2; + int val = 0; + + ASSERT_EQ(0, getsockopt(self->fd, IPPROTO_IPV6, IPV6_HDRINCL, + &val, &optlen)); + ASSERT_EQ(2, optlen); +} + +TEST_F(rawv6, checksum_default) +{ + socklen_t optlen; + int val = 0; + + optlen = sizeof(val); + + /* A non-ICMPv6 raw socket has the checksum disabled, reported as -1. */ + ASSERT_EQ(0, getsockopt(self->fd, IPPROTO_IPV6, IPV6_CHECKSUM, + &val, &optlen)); + ASSERT_EQ(sizeof(int), optlen); + ASSERT_EQ(-1, val); +} + +TEST_F(rawv6, bad_optname) +{ + socklen_t optlen; + int val; + + optlen = sizeof(val); + + /* SOL_RAW reaches do_rawv6_getsockopt() directly. */ + ASSERT_EQ(-1, getsockopt(self->fd, SOL_RAW, 0x7fff, &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); + ASSERT_EQ(sizeof(val), optlen); +} + +/* ---------- tls ---------- */ + +FIXTURE(tls) +{ + int fd; + int sfd; +}; + +FIXTURE_SETUP(tls) +{ + struct sockaddr_in a = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + }; + socklen_t alen = sizeof(a); + int lfd; + + self->fd = -1; + self->sfd = -1; + + lfd = socket(AF_INET, SOCK_STREAM, 0); + if (lfd < 0) + SKIP(return, "TCP socket: %s", strerror(errno)); + if (bind(lfd, (struct sockaddr *)&a, sizeof(a)) || listen(lfd, 1) || + getsockname(lfd, (struct sockaddr *)&a, &alen)) { + close(lfd); + SKIP(return, "listener setup: %s", strerror(errno)); + } + self->fd = socket(AF_INET, SOCK_STREAM, 0); + if (self->fd < 0) { + close(lfd); + SKIP(return, "TCP socket: %s", strerror(errno)); + } + if (connect(self->fd, (struct sockaddr *)&a, sizeof(a))) { + close(lfd); + SKIP(return, "connect: %s", strerror(errno)); + } + self->sfd = accept(lfd, NULL, NULL); + close(lfd); + if (setsockopt(self->fd, IPPROTO_TCP, TCP_ULP, "tls", sizeof("tls"))) + SKIP(return, "TCP_ULP=tls: %s (built without TLS?)", + strerror(errno)); +} + +FIXTURE_TEARDOWN(tls) +{ + if (self->fd >= 0) + close(self->fd); + if (self->sfd >= 0) + close(self->sfd); +} + +/* do_tls_getsockopt_tx_zc(): fixed-size int, exact length required. */ +TEST_F(tls, tx_zerocopy_exact) +{ + socklen_t optlen = sizeof(int); + int val = -1; + + ASSERT_EQ(0, getsockopt(self->fd, SOL_TLS, TLS_TX_ZEROCOPY_RO, + &val, &optlen)); + ASSERT_EQ(sizeof(int), optlen); + ASSERT_TRUE(val == 0 || val == 1); +} + +TEST_F(tls, tx_zerocopy_wrong_len) +{ + socklen_t optlen = 2; + int val; + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_TLS, TLS_TX_ZEROCOPY_RO, + &val, &optlen)); + ASSERT_EQ(EINVAL, errno); +} + +/* do_tls_getsockopt_conf(): NULL optval still yields EINVAL -- the + * converted code tests opt->iter_out.ubuf in place of optval. + */ +TEST_F(tls, conf_null_optval) +{ + socklen_t optlen = 64; + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_TLS, TLS_TX, NULL, &optlen)); + ASSERT_EQ(EINVAL, errno); +} + +TEST_F(tls, conf_short) +{ + socklen_t optlen = 2; + char buf[2]; + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_TLS, TLS_TX, buf, &optlen)); + ASSERT_EQ(EINVAL, errno); +} + +/* TLS_TX before crypto is set reports not-ready. */ +TEST_F(tls, conf_not_ready) +{ + struct tls_crypto_info info; + socklen_t optlen = sizeof(info); + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_TLS, TLS_TX, &info, &optlen)); + ASSERT_EQ(EBUSY, errno); +} + +/* Set TX crypto, then read it back at the base and full sizes, exercising + * both copy_to_iter() branches. SKIP if AES-GCM is unavailable. + */ +TEST_F(tls, conf_crypto_roundtrip) +{ + struct tls12_crypto_info_aes_gcm_128 tx = { + .info.version = TLS_1_2_VERSION, + .info.cipher_type = TLS_CIPHER_AES_GCM_128, + }; + struct tls12_crypto_info_aes_gcm_128 full; + struct tls_crypto_info base; + socklen_t optlen; + + if (setsockopt(self->fd, SOL_TLS, TLS_TX, &tx, sizeof(tx))) + SKIP(return, "set TLS_TX aes_gcm_128: %s", strerror(errno)); + + optlen = sizeof(base); + ASSERT_EQ(0, getsockopt(self->fd, SOL_TLS, TLS_TX, &base, &optlen)); + ASSERT_EQ(sizeof(base), optlen); + ASSERT_EQ(TLS_1_2_VERSION, base.version); + ASSERT_EQ(TLS_CIPHER_AES_GCM_128, base.cipher_type); + + optlen = sizeof(full); + ASSERT_EQ(0, getsockopt(self->fd, SOL_TLS, TLS_TX, &full, &optlen)); + ASSERT_EQ(sizeof(full), optlen); + ASSERT_EQ(TLS_CIPHER_AES_GCM_128, full.info.cipher_type); +} + +TEST_F(tls, bad_optname) +{ + socklen_t optlen = sizeof(int); + int val; + + ASSERT_EQ(-1, getsockopt(self->fd, SOL_TLS, 0x7fff, &val, &optlen)); + ASSERT_EQ(ENOPROTOOPT, errno); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/hsr/Makefile b/tools/testing/selftests/net/hsr/Makefile index 31fb9326cf53..2150e487ac7d 100644 --- a/tools/testing/selftests/net/hsr/Makefile +++ b/tools/testing/selftests/net/hsr/Makefile @@ -4,6 +4,7 @@ top_srcdir = ../../../../.. TEST_PROGS := \ hsr_ping.sh \ + hsr_prp_redbox.sh \ hsr_redbox.sh \ link_faults.sh \ prp_ping.sh \ diff --git a/tools/testing/selftests/net/hsr/hsr_prp_redbox.sh b/tools/testing/selftests/net/hsr/hsr_prp_redbox.sh new file mode 100755 index 000000000000..479c892225b1 --- /dev/null +++ b/tools/testing/selftests/net/hsr/hsr_prp_redbox.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test a PRP RedBox (PRP-SAN): a SAN that sits behind the interlink port must +# reach, and be reached by, a peer DANP on the PRP network with its own MAC +# preserved on the wire, and the RedBox must announce the SAN with a RedBox-MAC +# TLV (terminated by an EOT marker) in its PRP supervision frames. +# +# RB PRP RedBox: prp0 over rb_a/rb_b (LAN A/B) + interlink rb_il +# PEER peer DANP : prp0 over pe_a/pe_b, 100.64.0.2 +# SAN SAN : san_il, own MAC, 100.64.0.51 (behind the interlink) + +ipv6=false + +source ./hsr_common.sh + +check_prerequisites + +if ! command -v tcpdump >/dev/null 2>&1; then + echo "SKIP: This test requires tcpdump" + exit $ksft_skip +fi + +if ! ip link help hsr 2>&1 | grep -q interlink; then + echo "SKIP: iproute2 too old (no hsr interlink support)" + exit $ksft_skip +fi + +setup_ns RB PEER SAN +trap 'cleanup_ns "$RB" "$PEER" "$SAN"' EXIT + +ip link add rb_a netns "$RB" type veth peer name pe_a netns "$PEER" +ip link add rb_b netns "$RB" type veth peer name pe_b netns "$PEER" +ip link add rb_il netns "$RB" type veth peer name san_il netns "$SAN" + +ip -n "$RB" link set rb_a up +ip -n "$RB" link set rb_b up +ip -n "$RB" link set rb_il up +ip -n "$PEER" link set pe_a up +ip -n "$PEER" link set pe_b up +ip -n "$SAN" link set san_il up +ip -n "$SAN" addr add 100.64.0.51/24 dev san_il + +# Feature gate: PRP interlink (RedBox) creation. A kernel without PRP RedBox +# support rejects this with -EINVAL, so SKIP rather than FAIL. +if ! ip -n "$RB" link add name prp0 type hsr slave1 rb_a slave2 rb_b \ + interlink rb_il proto 1 2>/dev/null; then + echo "SKIP: kernel without PRP RedBox (interlink) support" + exit $ksft_skip +fi +ip -n "$RB" link set prp0 up +ip -n "$PEER" link add name prp0 type hsr slave1 pe_a slave2 pe_b proto 1 +ip -n "$PEER" link set prp0 up +ip -n "$PEER" addr add 100.64.0.2/24 dev prp0 +sleep 1 + +san_mac=$(ip -n "$SAN" -br link show san_il | awk '{print $3}') +rb_mac=$(ip -n "$RB" -br link show rb_il | awk '{print $3}') + +# Bidirectional unicast across the interlink. +do_ping "$PEER" 100.64.0.51 +do_ping "$SAN" 100.64.0.2 +stop_if_error "PRP RedBox bidirectional unicast failed" + +# The SAN source MAC must be preserved on the PRP network, not laundered to the +# RedBox MAC: the peer resolves the SAN IP to the SAN's own MAC. +neigh=$(ip -n "$PEER" neigh show 100.64.0.51 | awk '{print $5}') +if [ "$neigh" != "$san_mac" ]; then + echo "SAN MAC preservation [ FAIL ]: peer resolved 100.64.0.51 to" \ + "'$neigh', expected $san_mac" 1>&2 + ret=1 +fi +stop_if_error "SAN MAC not preserved on the PRP network" + +# The proxy-announce supervision frame must carry, in order, the life-check TLV +# (type 0x14, len 6) + MacAddressA == SAN MAC + the RedBox-MAC TLV (type 0x1e, +# len 6) + MacAddressRedBox == RedBox MAC + the EOT marker (0x0000). +ip netns exec "$SAN" ping -i 0.2 -q 100.64.0.2 >/dev/null 2>&1 & +ping_pid=$! +cap=$(ip netns exec "$PEER" timeout 5 tcpdump -i pe_a -nn -x \ + "ether proto 0x88fb and ether src $rb_mac" 2>/dev/null || true) +kill "$ping_pid" 2>/dev/null || true +wait "$ping_pid" 2>/dev/null || true + +san_hex=$(echo "$san_mac" | tr -d ':') +rb_hex=$(echo "$rb_mac" | tr -d ':') +# Reassemble contiguous frame hex: drop the "0x0010:" offset labels and spaces. +frame_hex=$(echo "$cap" | awk '/^[[:space:]]*0x[0-9a-f]+:/ { + sub(/^[[:space:]]*0x[0-9a-f]+:[[:space:]]*/, ""); + gsub(/ /, ""); printf "%s", $0 }') +if ! echo "$frame_hex" | grep -q "1406${san_hex}1e06${rb_hex}0000"; then + echo "supervision RedBox-MAC TLV [ FAIL ]: missing SAN MAC, Type-30" \ + "payload, or EOT" 1>&2 + ret=1 +fi +stop_if_error "PRP RedBox supervision RedBox-MAC TLV/EOT check failed" + +echo "INFO: PRP RedBox (PRP-SAN) conformance checks passed" +exit $ret diff --git a/tools/testing/selftests/net/io_uring_zerocopy_tx.sh b/tools/testing/selftests/net/io_uring_zerocopy_tx.sh index 123439545013..8c3647de9b4c 100755 --- a/tools/testing/selftests/net/io_uring_zerocopy_tx.sh +++ b/tools/testing/selftests/net/io_uring_zerocopy_tx.sh @@ -77,9 +77,13 @@ esac # Start of state changes: install cleanup handler +old_io_uring_disabled=0 cleanup() { ip netns del "${NS2}" ip netns del "${NS1}" + if [ "$old_io_uring_disabled" -ne 0 ]; then + sysctl -w -q kernel.io_uring_disabled="$old_io_uring_disabled" 2>/dev/null || true + fi } trap cleanup EXIT @@ -122,5 +126,10 @@ do_test() { wait } +old_io_uring_disabled=$(sysctl -n kernel.io_uring_disabled 2>/dev/null || echo "0") +if [ "$old_io_uring_disabled" -ne 0 ]; then + sysctl -w -q kernel.io_uring_disabled=0 +fi + do_test "${EXTRA_ARGS}" echo ok diff --git a/tools/testing/selftests/net/ioam6.sh b/tools/testing/selftests/net/ioam6.sh index b2b99889942f..845c26dd01a9 100755 --- a/tools/testing/selftests/net/ioam6.sh +++ b/tools/testing/selftests/net/ioam6.sh @@ -273,8 +273,8 @@ setup() ip -netns $ioam_node_beta link set ioam-veth-betaR name veth1 &>/dev/null ip -netns $ioam_node_gamma link set ioam-veth-gamma name veth0 &>/dev/null - ip -netns $ioam_node_alpha addr add 2001:db8:1::2/64 dev veth0 &>/dev/null ip -netns $ioam_node_alpha addr add 2001:db8:1::50/64 dev veth0 &>/dev/null + ip -netns $ioam_node_alpha addr add 2001:db8:1::2/64 dev veth0 &>/dev/null ip -netns $ioam_node_alpha link set veth0 up &>/dev/null ip -netns $ioam_node_alpha link set lo up &>/dev/null ip -netns $ioam_node_alpha route add 2001:db8:2::/64 \ diff --git a/tools/testing/selftests/net/ip6_tunnel.sh b/tools/testing/selftests/net/ip6_tunnel.sh new file mode 100755 index 000000000000..fe081a521819 --- /dev/null +++ b/tools/testing/selftests/net/ip6_tunnel.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Test that IPv4-over-IPv6 tunneling works. + +source lib.sh +set -e + +setup_prepare() { + ip link add transport1 type veth peer name transport2 + + setup_ns ns1 + ip link set transport1 netns $ns1 + ip -n $ns1 address add 2001:db8::1/64 dev transport1 nodad + ip -n $ns1 address add 2001:db8::3/64 dev transport1 nodad + ip -n $ns1 link set transport1 up + ip -n $ns1 link add link transport1 name tunnel4 type ip6tnl mode ipip6 local 2001:db8::1 remote 2001:db8::2 + ip -n $ns1 address add 172.0.0.1/32 peer 172.0.0.2/32 dev tunnel4 + ip -n $ns1 link set tunnel4 up + ip -n $ns1 link add link transport1 name tunnel6 type ip6tnl mode ip6ip6 local 2001:db8::3 remote 2001:db8::4 + ip -n $ns1 address add 2001:db8:6::1/64 dev tunnel6 + ip -n $ns1 link set tunnel6 up + + setup_ns ns2 + ip link set transport2 netns $ns2 + ip -n $ns2 address add 2001:db8::2/64 dev transport2 nodad + ip -n $ns2 address add 2001:db8::4/64 dev transport2 nodad + ip -n $ns2 link set transport2 up + ip -n $ns2 link add link transport2 name tunnel4 type ip6tnl mode ipip6 local 2001:db8::2 remote 2001:db8::1 + ip -n $ns2 address add 172.0.0.2/32 peer 172.0.0.1/32 dev tunnel4 + ip -n $ns2 link set tunnel4 up + ip -n $ns2 link add link transport2 name tunnel6 type ip6tnl mode ip6ip6 local 2001:db8::4 remote 2001:db8::3 + ip -n $ns2 address add 2001:db8:6::2/64 dev tunnel6 + ip -n $ns2 link set tunnel6 up +} + +cleanup() { + cleanup_all_ns + # in case the namespaces haven't been set up yet + ip link delete transport1 &>/dev/null || true +} + +trap cleanup EXIT +setup_prepare +ip netns exec $ns1 ping -q -W1 -c1 172.0.0.2 >/dev/null +ip netns exec $ns1 ping -q -W1 -c1 2001:db8:6::2 >/dev/null diff --git a/tools/testing/selftests/net/ipsec.c b/tools/testing/selftests/net/ipsec.c index f4afef51b930..89c32c354c00 100644 --- a/tools/testing/selftests/net/ipsec.c +++ b/tools/testing/selftests/net/ipsec.c @@ -62,8 +62,6 @@ #define VETH_FMT "ktst-%d" #define VETH_LEN 12 -#define XFRM_ALGO_NR_KEYS 29 - static int nsfd_parent = -1; static int nsfd_childa = -1; static int nsfd_childb = -1; @@ -96,7 +94,6 @@ struct xfrm_key_entry xfrm_key_entries[] = { {"cbc(cast5)", 128}, {"cbc(serpent)", 128}, {"hmac(sha1)", 160}, - {"hmac(rmd160)", 160}, {"cbc(des3_ede)", 192}, {"hmac(sha256)", 256}, {"cbc(aes)", 256}, @@ -813,7 +810,7 @@ static int xfrm_fill_key(char *name, char *buf, { int i; - for (i = 0; i < XFRM_ALGO_NR_KEYS; i++) { + for (i = 0; i < ARRAY_SIZE(xfrm_key_entries); i++) { if (strncmp(name, xfrm_key_entries[i].algo_name, ALGO_LEN) == 0) *key_len = xfrm_key_entries[i].key_len; } @@ -2061,8 +2058,7 @@ static int write_desc(int proto, int test_desc_fd, int proto_list[] = { IPPROTO_AH, IPPROTO_COMP, IPPROTO_ESP }; char *ah_list[] = { "digest_null", "hmac(md5)", "hmac(sha1)", "hmac(sha256)", - "hmac(sha384)", "hmac(sha512)", "hmac(rmd160)", - "xcbc(aes)", "cmac(aes)" + "hmac(sha384)", "hmac(sha512)", "xcbc(aes)", "cmac(aes)" }; char *comp_list[] = { "deflate", diff --git a/tools/testing/selftests/net/ipv6_flowlabel.sh b/tools/testing/selftests/net/ipv6_flowlabel.sh index cee95e252bee..5d1b5464c54c 100755 --- a/tools/testing/selftests/net/ipv6_flowlabel.sh +++ b/tools/testing/selftests/net/ipv6_flowlabel.sh @@ -7,9 +7,6 @@ set -e -echo "TEST management" -./in_netns.sh ./ipv6_flowlabel_mgr - echo "TEST datapath" ./in_netns.sh \ sh -c 'sysctl -q -w net.ipv6.auto_flowlabels=0 && ./ipv6_flowlabel -l 1' diff --git a/tools/testing/selftests/net/ipv6_flowlabel_mgr.c b/tools/testing/selftests/net/ipv6_flowlabel_mgr.c index af95b48acea9..072fb3a9b121 100644 --- a/tools/testing/selftests/net/ipv6_flowlabel_mgr.c +++ b/tools/testing/selftests/net/ipv6_flowlabel_mgr.c @@ -6,45 +6,36 @@ #include <arpa/inet.h> #include <error.h> #include <errno.h> +#include <fcntl.h> #include <limits.h> #include <linux/in6.h> +#include <net/if.h> +#include <sched.h> #include <stdbool.h> #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> +#include <sys/ioctl.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/time.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> +#include "kselftest_harness.h" /* uapi/glibc weirdness may leave this undefined */ #ifndef IPV6_FLOWLABEL_MGR #define IPV6_FLOWLABEL_MGR 32 #endif +#ifndef IPV6_FLOWINFO_SEND +#define IPV6_FLOWINFO_SEND 33 +#endif /* from net/ipv6/ip6_flowlabel.c */ #define FL_MIN_LINGER 6 -#define explain(x) \ - do { if (cfg_verbose) fprintf(stderr, " " x "\n"); } while (0) - -#define __expect(x) \ - do { \ - if (!(x)) \ - fprintf(stderr, "[OK] " #x "\n"); \ - else \ - error(1, 0, "[ERR] " #x " (line %d)", __LINE__); \ - } while (0) - -#define expect_pass(x) __expect(x) -#define expect_fail(x) __expect(!(x)) - -static bool cfg_long_running; -static bool cfg_verbose; - static int flowlabel_get(int fd, uint32_t label, uint8_t share, uint16_t flags) { struct in6_flowlabel_req req = { @@ -71,129 +62,484 @@ static int flowlabel_put(int fd, uint32_t label) return setsockopt(fd, SOL_IPV6, IPV6_FLOWLABEL_MGR, &req, sizeof(req)); } -static void run_tests(int fd) +static int flowlabel_renew(int fd, uint32_t label, uint8_t share, + uint16_t linger) { - int wstatus; - pid_t pid; + struct in6_flowlabel_req req = { + .flr_action = IPV6_FL_A_RENEW, + .flr_label = htonl(label), + .flr_share = share, + .flr_linger = linger, + }; + + return setsockopt(fd, SOL_IPV6, IPV6_FLOWLABEL_MGR, &req, sizeof(req)); +} + +static struct sockaddr_in6 loopback_addr(void) +{ + struct sockaddr_in6 addr = { + .sin6_family = AF_INET6, + .sin6_addr = IN6ADDR_LOOPBACK_INIT, + .sin6_port = htons(8888), + }; + + return addr; +} + +static int tcp_listen(void) +{ + struct sockaddr_in6 addr = loopback_addr(); + const int one = 1; + int fd; + + fd = socket(PF_INET6, SOCK_STREAM, 0); + if (fd == -1) + error(1, errno, "socket listener"); + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one))) + error(1, errno, "setsockopt SO_REUSEADDR"); + if (bind(fd, (void *)&addr, sizeof(addr))) + error(1, errno, "bind"); + if (listen(fd, 1)) + error(1, errno, "listen"); + + return fd; +} + +static void tcp_connect(int listener, uint32_t flowlabel, + int *client, int *accepted) +{ + struct sockaddr_in6 addr = loopback_addr(); + const int one = 1; + int cfd, afd; + + cfd = socket(PF_INET6, SOCK_STREAM, 0); + if (cfd == -1) + error(1, errno, "socket client"); + + if (flowlabel_get(cfd, flowlabel, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE)) + error(1, errno, "flowlabel_get"); + if (setsockopt(cfd, SOL_IPV6, IPV6_FLOWINFO_SEND, &one, sizeof(one))) + error(1, errno, "setsockopt flowinfo_send"); + addr.sin6_flowinfo = htonl(flowlabel); + + if (connect(cfd, (void *)&addr, sizeof(addr))) + error(1, errno, "connect"); + + afd = accept(listener, NULL, NULL); + if (afd == -1) + error(1, errno, "accept"); + + if (flowlabel_put(cfd, flowlabel)) + error(1, errno, "flowlabel_put"); + + *client = cfd; + *accepted = afd; +} + +static int bringup_loopback(void) +{ + struct ifreq ifr = { + .ifr_name = "lo" + }; + int fd; + + fd = socket(AF_LOCAL, SOCK_STREAM, 0); + if (fd < 0) + return -1; + + if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0) + goto err; + + ifr.ifr_flags = ifr.ifr_flags | IFF_UP; + + if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) + goto err; + + close(fd); + return 0; - explain("cannot get non-existent label"); - expect_fail(flowlabel_get(fd, 1, IPV6_FL_S_ANY, 0)); - - explain("cannot put non-existent label"); - expect_fail(flowlabel_put(fd, 1)); - - explain("cannot create label greater than 20 bits"); - expect_fail(flowlabel_get(fd, 0x1FFFFF, IPV6_FL_S_ANY, - IPV6_FL_F_CREATE)); - - explain("create a new label (FL_F_CREATE)"); - expect_pass(flowlabel_get(fd, 1, IPV6_FL_S_ANY, IPV6_FL_F_CREATE)); - explain("can get the label (without FL_F_CREATE)"); - expect_pass(flowlabel_get(fd, 1, IPV6_FL_S_ANY, 0)); - explain("can get it again with create flag set, too"); - expect_pass(flowlabel_get(fd, 1, IPV6_FL_S_ANY, IPV6_FL_F_CREATE)); - explain("cannot get it again with the exclusive (FL_FL_EXCL) flag"); - expect_fail(flowlabel_get(fd, 1, IPV6_FL_S_ANY, - IPV6_FL_F_CREATE | IPV6_FL_F_EXCL)); - explain("can now put exactly three references"); - expect_pass(flowlabel_put(fd, 1)); - expect_pass(flowlabel_put(fd, 1)); - expect_pass(flowlabel_put(fd, 1)); - expect_fail(flowlabel_put(fd, 1)); - - explain("create a new exclusive label (FL_S_EXCL)"); - expect_pass(flowlabel_get(fd, 2, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE)); - explain("cannot get it again in non-exclusive mode"); - expect_fail(flowlabel_get(fd, 2, IPV6_FL_S_ANY, IPV6_FL_F_CREATE)); - explain("cannot get it again in exclusive mode either"); - expect_fail(flowlabel_get(fd, 2, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE)); - expect_pass(flowlabel_put(fd, 2)); - - if (cfg_long_running) { - explain("cannot reuse the label, due to linger"); - expect_fail(flowlabel_get(fd, 2, IPV6_FL_S_ANY, - IPV6_FL_F_CREATE)); - explain("after sleep, can reuse"); - sleep(FL_MIN_LINGER * 2 + 1); - expect_pass(flowlabel_get(fd, 2, IPV6_FL_S_ANY, - IPV6_FL_F_CREATE)); +err: + close(fd); + return -1; +} + +FIXTURE(flowlabel) {}; + +FIXTURE_SETUP(flowlabel) +{ + int ret; + + ret = unshare(CLONE_NEWNET); + ASSERT_EQ(ret, 0) { + TH_LOG("unshare(CLONE_NEWNET) failed: %s", strerror(errno)); } - explain("create a new user-private label (FL_S_USER)"); - expect_pass(flowlabel_get(fd, 3, IPV6_FL_S_USER, IPV6_FL_F_CREATE)); - explain("cannot get it again in non-exclusive mode"); - expect_fail(flowlabel_get(fd, 3, IPV6_FL_S_ANY, 0)); - explain("cannot get it again in exclusive mode"); - expect_fail(flowlabel_get(fd, 3, IPV6_FL_S_EXCL, 0)); - explain("can get it again in user mode"); - expect_pass(flowlabel_get(fd, 3, IPV6_FL_S_USER, 0)); - explain("child process can get it too, but not after setuid(nobody)"); + ret = bringup_loopback(); + ASSERT_EQ(ret, 0) TH_LOG("Failed to bring up loopback interface"); +} + +FIXTURE_TEARDOWN(flowlabel) +{ +} + +TEST_F(flowlabel, cannot_get_non_existent_label) +{ + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 9, IPV6_FL_S_ANY, 0); + EXPECT_TRUE(err) TH_LOG("expected get of a non-existent label to fail"); + EXPECT_EQ(ENOENT, errno) TH_LOG("expected ENOENT, got %d", errno); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, cannot_put_non_existent_label) +{ + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_put(fd, 10); + EXPECT_TRUE(err) TH_LOG("expected put of a non-existent label to fail"); + EXPECT_EQ(ESRCH, errno) TH_LOG("expected ESRCH, got %d", errno); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, cannot_create_label_greater_than_20_bits) +{ + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 0x1FFFFF, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(err) TH_LOG("expected label > 20 bits to be rejected"); + EXPECT_EQ(EINVAL, errno) TH_LOG("expected EINVAL, got %d", errno); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, can_create_and_get_and_put_labels) +{ + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 1, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) TH_LOG("failed to create label (FL_F_CREATE)"); + + err = flowlabel_get(fd, 1, IPV6_FL_S_ANY, 0); + EXPECT_TRUE(!err) TH_LOG("failed to get the label without FL_F_CREATE"); + + err = flowlabel_get(fd, 1, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) + TH_LOG("failed to get it again with create flag set, too"); + + err = flowlabel_get(fd, 1, IPV6_FL_S_ANY, + IPV6_FL_F_CREATE | IPV6_FL_F_EXCL); + EXPECT_TRUE(err) + TH_LOG("expected FL_F_EXCL to reject existing label"); + EXPECT_EQ(EEXIST, errno) TH_LOG("expected EEXIST, got %d", errno); + + err = flowlabel_put(fd, 1); + EXPECT_TRUE(!err) TH_LOG("failed to put first reference"); + err = flowlabel_put(fd, 1); + EXPECT_TRUE(!err) TH_LOG("failed to put second reference"); + err = flowlabel_put(fd, 1); + EXPECT_TRUE(!err) TH_LOG("failed to put third reference"); + err = flowlabel_put(fd, 1); + EXPECT_TRUE(err) + TH_LOG("expected fourth put to fail, no references left"); + EXPECT_EQ(ESRCH, errno) TH_LOG("expected ESRCH, got %d", errno); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, exclusive_label_share) +{ + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 2, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) + TH_LOG("failed to create a new exclusive label (FL_S_EXCL)"); + + err = flowlabel_get(fd, 2, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(err) TH_LOG("expected reuse in non-exclusive mode to fail"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); + + err = flowlabel_get(fd, 2, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE); + EXPECT_TRUE(err) TH_LOG("expected reuse in exclusive mode to fail too"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); + + err = flowlabel_put(fd, 2); + EXPECT_TRUE(!err) TH_LOG("failed to put the exclusive label"); + + err = flowlabel_get(fd, 2, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(err) TH_LOG("expected reuse to fail, due to linger"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); + + sleep(FL_MIN_LINGER * 2 + 1); + + err = flowlabel_get(fd, 2, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) TH_LOG("expected reuse to succeed after linger"); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, user_private_label_share) +{ + int fd, err, wstatus; + pid_t pid; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 3, IPV6_FL_S_USER, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) + TH_LOG("failed to create a new user-private label (FL_S_USER)"); + + err = flowlabel_get(fd, 3, IPV6_FL_S_ANY, 0); + EXPECT_TRUE(err) TH_LOG("expected get in non-exclusive mode to fail"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); + + err = flowlabel_get(fd, 3, IPV6_FL_S_EXCL, 0); + EXPECT_TRUE(err) TH_LOG("expected get in exclusive mode to fail"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); + + err = flowlabel_get(fd, 3, IPV6_FL_S_USER, 0); + EXPECT_TRUE(!err) TH_LOG("failed to get it again in user mode"); + pid = fork(); - if (pid == -1) - error(1, errno, "fork"); + ASSERT_NE(-1, pid) TH_LOG("fork failed"); if (!pid) { - expect_pass(flowlabel_get(fd, 3, IPV6_FL_S_USER, 0)); + err = flowlabel_get(fd, 3, IPV6_FL_S_USER, 0); + EXPECT_TRUE(!err) + TH_LOG("child failed to get the user-private label"); + if (setuid(USHRT_MAX)) - fprintf(stderr, "[INFO] skip setuid child test\n"); - else - expect_fail(flowlabel_get(fd, 3, IPV6_FL_S_USER, 0)); + exit(KSFT_SKIP); + + err = flowlabel_get(fd, 3, IPV6_FL_S_USER, 0); + EXPECT_TRUE(err) + TH_LOG("child unexpectedly got label after setuid"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); exit(0); } - if (wait(&wstatus) == -1) - error(1, errno, "wait"); - if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) - error(1, errno, "wait: unexpected child result"); - - explain("create a new process-private label (FL_S_PROCESS)"); - expect_pass(flowlabel_get(fd, 4, IPV6_FL_S_PROCESS, IPV6_FL_F_CREATE)); - explain("can get it again"); - expect_pass(flowlabel_get(fd, 4, IPV6_FL_S_PROCESS, 0)); - explain("child process cannot can get it"); + ASSERT_EQ(pid, wait(&wstatus)) TH_LOG("wait failed"); + ASSERT_TRUE(WIFEXITED(wstatus)) TH_LOG("child did not exit normally"); + if (WEXITSTATUS(wstatus) == KSFT_SKIP) + SKIP(return, + "setuid(USHRT_MAX) unavailable (no CAP_SETUID or uid unmapped)"); + EXPECT_EQ(0, WEXITSTATUS(wstatus)) + TH_LOG("child reported unexpected result"); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, process_private_label_share) +{ + int fd, err, wstatus; + pid_t pid; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 4, IPV6_FL_S_PROCESS, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) + TH_LOG("failed to create a new process-private label"); + + err = flowlabel_get(fd, 4, IPV6_FL_S_PROCESS, 0); + EXPECT_TRUE(!err) TH_LOG("failed to get it again"); + pid = fork(); - if (pid == -1) - error(1, errno, "fork"); + ASSERT_NE(-1, pid) TH_LOG("fork failed"); if (!pid) { - expect_fail(flowlabel_get(fd, 4, IPV6_FL_S_PROCESS, 0)); + err = flowlabel_get(fd, 4, IPV6_FL_S_PROCESS, 0); + EXPECT_TRUE(err) + TH_LOG("child unexpectedly got process-private label"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); exit(0); } - if (wait(&wstatus) == -1) - error(1, errno, "wait"); - if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) - error(1, errno, "wait: unexpected child result"); -} - -static void parse_opts(int argc, char **argv) -{ - int c; - - while ((c = getopt(argc, argv, "lv")) != -1) { - switch (c) { - case 'l': - cfg_long_running = true; - break; - case 'v': - cfg_verbose = true; - break; - default: - error(1, 0, "%s: parse error", argv[0]); - } - } + ASSERT_EQ(pid, wait(&wstatus)) TH_LOG("wait failed"); + ASSERT_TRUE(WIFEXITED(wstatus)) TH_LOG("child did not exit normally"); + EXPECT_EQ(0, WEXITSTATUS(wstatus)) + TH_LOG("child reported unexpected result"); + + EXPECT_EQ(0, close(fd)); } -int main(int argc, char **argv) +TEST_F(flowlabel, cannot_renew_non_existent_label) { - int fd; + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_renew(fd, 5, IPV6_FL_S_EXCL, + 2 * (FL_MIN_LINGER * 2 + 1)); + EXPECT_TRUE(err) + TH_LOG("expected renew of a non-existent label to fail"); + EXPECT_EQ(ESRCH, errno) TH_LOG("expected ESRCH, got %d", errno); - parse_opts(argc, argv); + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, can_renew_existing_label) +{ + int fd, err; fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 5, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) + TH_LOG("failed to create a new label for renew validation"); + + err = flowlabel_renew(fd, 5, IPV6_FL_S_EXCL, + 2 * (FL_MIN_LINGER * 2 + 1)); + EXPECT_TRUE(!err) TH_LOG("failed to renew an existing valid label"); + + err = flowlabel_put(fd, 5); + EXPECT_TRUE(!err) TH_LOG("failed to put the label"); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, renew_label_linger) +{ + /* RENEW must extend a label's linger period: putting a renewed + * label and waiting out its original linger time must not be + * enough to allow the label to be recreated. + */ + int fd, err; + + fd = socket(PF_INET6, SOCK_DGRAM, 0); + ASSERT_GE(fd, 0) TH_LOG("socket failed"); + + err = flowlabel_get(fd, 6, IPV6_FL_S_EXCL, IPV6_FL_F_CREATE); + EXPECT_TRUE(!err) + TH_LOG("failed to create label with FL_MIN_LINGER linger time"); + + err = flowlabel_renew(fd, 6, IPV6_FL_S_EXCL, + 2 * (FL_MIN_LINGER * 2 + 1)); + EXPECT_TRUE(!err) + TH_LOG("failed to renew the label to increase its linger time"); + + err = flowlabel_put(fd, 6); + EXPECT_TRUE(!err) TH_LOG("failed to put the label"); + + sleep(FL_MIN_LINGER * 2 + 1); + + err = flowlabel_get(fd, 6, IPV6_FL_S_ANY, IPV6_FL_F_CREATE); + EXPECT_TRUE(err) + TH_LOG("expected reuse to fail, new linger time not over yet"); + EXPECT_EQ(EPERM, errno) TH_LOG("expected EPERM, got %d", errno); + + EXPECT_EQ(0, close(fd)); +} + +TEST_F(flowlabel, remote_flag) +{ + /* The REMOTE flag, used for getsockopt, is expected to retrieve the + * label from the latest received header. + */ + struct in6_flowlabel_req freq = { + .flr_action = IPV6_FL_A_GET, + .flr_flags = IPV6_FL_F_REMOTE, + }; + socklen_t freq_len = sizeof(freq); + int listener, cfd, afd, err; + + listener = tcp_listen(); + tcp_connect(listener, 7, &cfd, &afd); + + err = getsockopt(afd, SOL_IPV6, IPV6_FLOWLABEL_MGR, &freq, &freq_len); + EXPECT_TRUE(!err) TH_LOG("getsockopt with IPV6_FL_F_REMOTE failed"); + EXPECT_EQ(7, ntohl(freq.flr_label)) + TH_LOG("unexpected remote flow label"); + + EXPECT_EQ(0, close(afd)); + EXPECT_EQ(0, close(cfd)); + EXPECT_EQ(0, close(listener)); +} + +static bool disable_flowlabel_consistency(void) +{ + int fd; + + fd = open("/proc/sys/net/ipv6/flowlabel_consistency", O_WRONLY); if (fd == -1) - error(1, errno, "socket"); + return false; - run_tests(fd); + if (write(fd, "0", 1) != 1) { + close(fd); + return false; + } + close(fd); - if (close(fd)) - error(1, errno, "close"); + return true; +} - return 0; +TEST_F(flowlabel, reflect_flag) +{ + /* The REFLECT flag acts as a trigger to the REPFLOW bit. When REPFLOW + * is triggered for a socket, it adopts the label received from the + * connected socket. + */ + struct in6_flowlabel_req reflect_on = { + .flr_action = IPV6_FL_A_GET, + .flr_flags = IPV6_FL_F_REFLECT, + }; + struct in6_flowlabel_req reflect_query = { + .flr_action = IPV6_FL_A_GET, + }; + struct in6_flowlabel_req reflect_off = { + .flr_action = IPV6_FL_A_PUT, + .flr_flags = IPV6_FL_F_REFLECT, + }; + socklen_t reflect_query_len = sizeof(reflect_query); + int listener, cfd, afd, err; + + if (!disable_flowlabel_consistency()) + SKIP(return, + "cannot disable net.ipv6.flowlabel_consistency"); + + listener = tcp_listen(); + err = setsockopt(listener, SOL_IPV6, IPV6_FLOWLABEL_MGR, + &reflect_on, sizeof(reflect_on)); + EXPECT_TRUE(!err) TH_LOG("failed to enable REFLECT on the listener"); + + tcp_connect(listener, 8, &cfd, &afd); + + err = getsockopt(afd, SOL_IPV6, IPV6_FLOWLABEL_MGR, + &reflect_query, &reflect_query_len); + EXPECT_TRUE(!err) + TH_LOG("failed to query the accepted socket's outgoing label"); + EXPECT_EQ(8, ntohl(reflect_query.flr_label)) + TH_LOG("accepted socket did not reflect client's label"); + + err = setsockopt(afd, SOL_IPV6, IPV6_FLOWLABEL_MGR, + &reflect_off, sizeof(reflect_off)); + EXPECT_TRUE(!err) + TH_LOG("failed to disable REFLECT on the accepted socket"); + + err = setsockopt(afd, SOL_IPV6, IPV6_FLOWLABEL_MGR, + &reflect_off, sizeof(reflect_off)); + EXPECT_TRUE(err) TH_LOG("expected disabling REFLECT twice to fail"); + EXPECT_EQ(ESRCH, errno) TH_LOG("expected ESRCH, got %d", errno); + + EXPECT_EQ(0, close(afd)); + EXPECT_EQ(0, close(cfd)); + EXPECT_EQ(0, close(listener)); } + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/lib.sh b/tools/testing/selftests/net/lib.sh index b40694573f4c..d46d2cec89e4 100644 --- a/tools/testing/selftests/net/lib.sh +++ b/tools/testing/selftests/net/lib.sh @@ -70,12 +70,33 @@ ksft_exit_status_merge() $ksft_xfail $ksft_pass $ksft_skip $ksft_fail } +timestamp_ms() +{ + local now + local seconds + local nanoseconds + + now=$(date -u +%s:%N) || return + seconds=${now%:*} + nanoseconds=${now#*:} + + if [[ $nanoseconds =~ ^[0-9]+$ ]]; then + nanoseconds=${nanoseconds:0:9} + else + nanoseconds=0 + fi + + echo $((seconds * 1000 + 10#$nanoseconds / 1000000)) +} + loopy_wait() { local sleep_cmd=$1; shift local timeout_ms=$1; shift + local start_time + local current_time - local start_time="$(date -u +%s%3N)" + start_time=$(timestamp_ms) || return while true do local out @@ -84,7 +105,7 @@ loopy_wait() return 0 fi - local current_time="$(date -u +%s%3N)" + current_time=$(timestamp_ms) || return if ((current_time - start_time > timeout_ms)); then echo -n "$out" return 1 @@ -224,6 +245,19 @@ setup_ns() NS_LIST+=("${ns_list[@]}") } +in_all_ns() +{ + local ret=0 + local ns_list=("${NS_LIST[@]}") + + for ns in "${ns_list[@]}"; do + ip netns exec "${ns}" "$@" + (( ret = ret || $? )) + done + + return "${ret}" +} + # Create netdevsim with given id and net namespace. create_netdevsim() { local id="$1" @@ -514,7 +548,8 @@ mac_get() { local if_name=$1 - ip -j link show dev $if_name | jq -r '.[]["address"]' + run_on "$if_name" \ + ip -j link show dev "$if_name" | jq -r '.[]["address"]' } kill_process() @@ -670,3 +705,8 @@ cmd_jq() # return success only in case of non-empty output [ ! -z "$output" ] } + +run_on() +{ + shift; "$@" +} diff --git a/tools/testing/selftests/net/lib/.gitignore b/tools/testing/selftests/net/lib/.gitignore index bbc97d6bf556..6cd2b762af5d 100644 --- a/tools/testing/selftests/net/lib/.gitignore +++ b/tools/testing/selftests/net/lib/.gitignore @@ -1,3 +1,4 @@ # SPDX-License-Identifier: GPL-2.0-only csum +gro xdp_helper diff --git a/tools/testing/selftests/net/lib/Makefile b/tools/testing/selftests/net/lib/Makefile index 5339f56329e1..ff83603397d0 100644 --- a/tools/testing/selftests/net/lib/Makefile +++ b/tools/testing/selftests/net/lib/Makefile @@ -14,6 +14,7 @@ TEST_FILES := \ TEST_GEN_FILES := \ $(patsubst %.c,%.o,$(wildcard *.bpf.c)) \ csum \ + gro \ xdp_helper \ # end of TEST_GEN_FILES diff --git a/tools/testing/selftests/net/lib/gro.c b/tools/testing/selftests/net/lib/gro.c new file mode 100644 index 000000000000..7a333155de1a --- /dev/null +++ b/tools/testing/selftests/net/lib/gro.c @@ -0,0 +1,1919 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * This testsuite provides conformance testing for GRO coalescing. + * + * Test cases: + * + * data_*: + * Data packets of the same size and same header setup with correct + * sequence numbers coalesce. The one exception being the last data + * packet coalesced: it can be smaller than the rest and coalesced + * as long as it is in the same flow. + * - data_same: same size packets coalesce + * - data_lrg_sml: large then small coalesces + * - data_lrg_1byte: large then 1 byte coalesces (Ethernet padding) + * - data_sml_lrg: small then large doesn't coalesce + * - data_burst: two bursts of two, separated by 100ms + * + * ack: + * Pure ACK does not coalesce. + * + * flags_*: + * No packets with PSH, SYN, URG, RST, CWR set will be coalesced. + * - flags_psh, flags_syn, flags_rst, flags_urg, flags_cwr + * + * tcp_*: + * Packets with incorrect checksum, non-consecutive seqno and + * different TCP header options shouldn't coalesce. Nit: given that + * some extension headers have paddings, such as timestamp, headers + * that are padded differently would not be coalesced. + * - tcp_csum: incorrect checksum + * - tcp_seq: non-consecutive sequence numbers + * - tcp_ts: different timestamps + * - tcp_opt: different TCP options + * + * ip_*: + * Packets with different (ECN, TTL, TOS) header, IP options or + * IP fragments shouldn't coalesce. + * - ip_ecn, ip_tos: shared between IPv4/IPv6 + * - ip_csum: IPv4 only, bad IP header checksum + * - ip_ttl, ip_opt, ip_frag4: IPv4 only + * - ip_id_df*: IPv4 IP ID field coalescing tests + * - ip_frag6, ip_v6ext_*: IPv6 only + * + * large_*: + * Packets larger than GRO_MAX_SIZE packets shouldn't coalesce. + * - large_max: exceeding max size + * - large_rem: remainder handling + * + * single, capacity: + * Boring cases used to test coalescing machinery itself and stats + * more than protocol behavior. + * + * MSS is defined as 4096 - header because if it is too small + * (i.e. 1500 MTU - header), it will result in many packets, + * increasing the "large" test case's flakiness. This is because + * due to time sensitivity in the coalescing window, the receiver + * may not coalesce all of the packets. + * + * Note the timing issue applies to all of the test cases, so some + * flakiness is to be expected. + * + */ + +#define _GNU_SOURCE + +#include <arpa/inet.h> +#include <errno.h> +#include <error.h> +#include <getopt.h> +#include <net/ethernet.h> +#include <net/if.h> +#include <linux/filter.h> +#include <linux/if_packet.h> +#include <linux/if_pppox.h> +#include <linux/ipv6.h> +#include <linux/net_tstamp.h> +#include <linux/ppp_defs.h> +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/ip6.h> +#include <netinet/tcp.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "kselftest.h" +#include "ksft.h" + +#define DPORT 8000 +#define SPORT 1500 +#define PAYLOAD_LEN 100 +#define NUM_PACKETS 4 +#define START_SEQ 100 +#define START_ACK 100 +#define ETH_P_NONE 0 +#define ASSUMED_MTU 4096 +#define MAX_MSS (ASSUMED_MTU - sizeof(struct iphdr) - sizeof(struct tcphdr)) +#define MAX_HDR_LEN \ + (ETH_HLEN + sizeof(struct ipv6hdr) * 2 + sizeof(struct tcphdr)) +#define MAX_LARGE_PKT_CNT ((IP_MAXPACKET - (MAX_HDR_LEN - ETH_HLEN)) / \ + (ASSUMED_MTU - (MAX_HDR_LEN - ETH_HLEN))) +#define MIN_EXTHDR_SIZE 8 +#define L2_HLEN_MAX (ETH_HLEN + PPPOE_SES_HLEN) +#define EXT_PAYLOAD_1 "\x00\x00\x00\x00\x00\x00" +#define EXT_PAYLOAD_2 "\x11\x11\x11\x11\x11\x11" + +#define EXIT_OVER_COALESCE 42 + +#define ipv6_optlen(p) (((p)->hdrlen+1) << 3) /* calculate IPv6 extension header len */ +#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)])) + +enum flush_id_case { + FLUSH_ID_DF1_INC, + FLUSH_ID_DF1_FIXED, + FLUSH_ID_DF0_INC, + FLUSH_ID_DF0_FIXED, + FLUSH_ID_DF1_INC_FIXED, + FLUSH_ID_DF1_FIXED_INC, +}; + +static const char *addr6_src = "fdaa::2"; +static const char *addr6_dst = "fdaa::1"; +static const char *addr4_src = "192.168.1.200"; +static const char *addr4_dst = "192.168.1.100"; +static int proto = -1; +static uint8_t src_mac[ETH_ALEN], dst_mac[ETH_ALEN]; +static char *testname = "data"; +static char *ifname = "eth0"; +static char *smac = "aa:00:00:00:00:02"; +static char *dmac = "aa:00:00:00:00:01"; +static bool verbose; +static bool tx_socket = true; +static int tcp_offset = -1; +static int total_hdr_len = -1; +static int ethhdr_proto = -1; +static bool ipip; +static bool ip6ip6; +static bool pppoe; +static uint64_t txtime_ns; +static int num_flows = 4; +static bool order_check; + +#define CAPACITY_PAYLOAD_LEN 200 + +#define TXTIME_DELAY_MS 5 + +/* Max TCP payload that GRO will coalesce. The outer header overhead + * varies by encapsulation, reducing the effective max payload. + */ +static int max_payload(void) +{ + return IP_MAXPACKET - (total_hdr_len - ETH_HLEN); +} + +static int calc_mss(void) +{ + return ASSUMED_MTU - (total_hdr_len - ETH_HLEN); +} + +static int num_large_pkt(void) +{ + return max_payload() / calc_mss(); +} + +static void vlog(const char *fmt, ...) +{ + va_list args; + + if (verbose) { + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + } +} + +static void fill_pppoelayer(void *buf, int payload_len, uint16_t sid) +{ + struct pppoe_ppp_hdr { + struct pppoe_hdr eh; + __be16 proto; + } *ph = buf; + + payload_len += sizeof(struct tcphdr); + ph->eh.type = 1; + ph->eh.ver = 1; + ph->eh.code = 0; + ph->eh.sid = htons(sid); + ph->eh.length = htons(payload_len + sizeof(ph->proto)); + ph->proto = htons(proto == PF_INET ? PPP_IP : PPP_IPV6); +} + +static void setup_sock_filter(int fd) +{ + const int dport_off = tcp_offset + offsetof(struct tcphdr, dest); + const int ethproto_off = offsetof(struct ethhdr, h_proto); + int optlen = 0; + int ipproto_off, opt_ipproto_off; + + if (proto == PF_INET) + ipproto_off = tcp_offset - sizeof(struct iphdr) + + offsetof(struct iphdr, protocol); + else + ipproto_off = tcp_offset - sizeof(struct ipv6hdr) + + offsetof(struct ipv6hdr, nexthdr); + + /* Overridden later if exthdrs are used: */ + opt_ipproto_off = ipproto_off; + + if (strcmp(testname, "ip_opt") == 0) { + optlen = sizeof(struct ip_timestamp); + } else if (strcmp(testname, "ip_frag6") == 0 || + strcmp(testname, "ip_v6ext_same") == 0 || + strcmp(testname, "ip_v6ext_diff") == 0) { + BUILD_BUG_ON(sizeof(struct ip6_hbh) > MIN_EXTHDR_SIZE); + BUILD_BUG_ON(sizeof(struct ip6_dest) > MIN_EXTHDR_SIZE); + BUILD_BUG_ON(sizeof(struct ip6_frag) > MIN_EXTHDR_SIZE); + + /* same size for HBH and Fragment extension header types */ + optlen = MIN_EXTHDR_SIZE; + opt_ipproto_off = ETH_HLEN + sizeof(struct ipv6hdr) + + offsetof(struct ip6_ext, ip6e_nxt); + } + + /* this filter validates the following: + * - packet is IPv4/IPv6 according to the running test. + * - packet is TCP. Also handles the case of one extension header and then TCP. + * - checks the packet tcp dport equals to DPORT. Also handles the case of one + * extension header and then TCP. + */ + struct sock_filter filter[] = { + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, ethproto_off), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ntohs(ethhdr_proto), 0, 9), + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, ipproto_off), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_TCP, 2, 0), + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, opt_ipproto_off), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_TCP, 0, 5), + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, dport_off), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, DPORT, 2, 0), + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, dport_off + optlen), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, DPORT, 0, 1), + BPF_STMT(BPF_RET + BPF_K, 0xFFFFFFFF), + BPF_STMT(BPF_RET + BPF_K, 0), + }; + + struct sock_fprog bpf = { + .len = ARRAY_SIZE(filter), + .filter = filter, + }; + + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf)) < 0) + error(1, errno, "error setting filter"); +} + +static uint32_t checksum_nofold(void *data, size_t len, uint32_t sum) +{ + uint16_t *words = data; + int i; + + for (i = 0; i < len / 2; i++) + sum += words[i]; + if (len & 1) + sum += ((char *)data)[len - 1]; + return sum; +} + +static uint16_t checksum_fold(void *data, size_t len, uint32_t sum) +{ + sum = checksum_nofold(data, len, sum); + while (sum > 0xFFFF) + sum = (sum & 0xFFFF) + (sum >> 16); + return ~sum; +} + +static uint16_t tcp_checksum(void *buf, int payload_len) +{ + struct pseudo_header6 { + struct in6_addr saddr; + struct in6_addr daddr; + uint16_t protocol; + uint16_t payload_len; + } ph6; + struct pseudo_header4 { + struct in_addr saddr; + struct in_addr daddr; + uint16_t protocol; + uint16_t payload_len; + } ph4; + uint32_t sum = 0; + + if (proto == PF_INET6) { + if (inet_pton(AF_INET6, addr6_src, &ph6.saddr) != 1) + error(1, errno, "inet_pton6 source ip pseudo"); + if (inet_pton(AF_INET6, addr6_dst, &ph6.daddr) != 1) + error(1, errno, "inet_pton6 dest ip pseudo"); + ph6.protocol = htons(IPPROTO_TCP); + ph6.payload_len = htons(sizeof(struct tcphdr) + payload_len); + + sum = checksum_nofold(&ph6, sizeof(ph6), 0); + } else if (proto == PF_INET) { + if (inet_pton(AF_INET, addr4_src, &ph4.saddr) != 1) + error(1, errno, "inet_pton source ip pseudo"); + if (inet_pton(AF_INET, addr4_dst, &ph4.daddr) != 1) + error(1, errno, "inet_pton dest ip pseudo"); + ph4.protocol = htons(IPPROTO_TCP); + ph4.payload_len = htons(sizeof(struct tcphdr) + payload_len); + + sum = checksum_nofold(&ph4, sizeof(ph4), 0); + } + + return checksum_fold(buf, sizeof(struct tcphdr) + payload_len, sum); +} + +static void read_MAC(uint8_t *mac_addr, char *mac) +{ + if (sscanf(mac, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &mac_addr[0], &mac_addr[1], &mac_addr[2], + &mac_addr[3], &mac_addr[4], &mac_addr[5]) != 6) + error(1, 0, "sscanf"); +} + +static void fill_datalinklayer(void *buf) +{ + struct ethhdr *eth = buf; + + memcpy(eth->h_dest, dst_mac, ETH_ALEN); + memcpy(eth->h_source, src_mac, ETH_ALEN); + eth->h_proto = ethhdr_proto; +} + +static void fill_networklayer(void *buf, int payload_len, int protocol) +{ + struct ipv6hdr *ip6h = buf; + struct iphdr *iph = buf; + + if (proto == PF_INET6) { + memset(ip6h, 0, sizeof(*ip6h)); + + ip6h->version = 6; + ip6h->payload_len = htons(sizeof(struct tcphdr) + payload_len); + ip6h->nexthdr = protocol; + ip6h->hop_limit = 8; + if (inet_pton(AF_INET6, addr6_src, &ip6h->saddr) != 1) + error(1, errno, "inet_pton source ip6"); + if (inet_pton(AF_INET6, addr6_dst, &ip6h->daddr) != 1) + error(1, errno, "inet_pton dest ip6"); + } else if (proto == PF_INET) { + memset(iph, 0, sizeof(*iph)); + + iph->version = 4; + iph->ihl = 5; + iph->ttl = 8; + iph->protocol = protocol; + iph->tot_len = htons(sizeof(struct tcphdr) + + payload_len + sizeof(struct iphdr)); + iph->frag_off = htons(0x4000); /* DF = 1, MF = 0 */ + if (inet_pton(AF_INET, addr4_src, &iph->saddr) != 1) + error(1, errno, "inet_pton source ip"); + if (inet_pton(AF_INET, addr4_dst, &iph->daddr) != 1) + error(1, errno, "inet_pton dest ip"); + iph->check = checksum_fold(buf, sizeof(struct iphdr), 0); + } +} + +static void fill_transportlayer(void *buf, int seq_offset, int ack_offset, + int payload_len, int fin) +{ + struct tcphdr *tcph = buf; + + memset(tcph, 0, sizeof(*tcph)); + + tcph->source = htons(SPORT); + tcph->dest = htons(DPORT); + tcph->seq = ntohl(START_SEQ + seq_offset); + tcph->ack_seq = ntohl(START_ACK + ack_offset); + tcph->ack = 1; + tcph->fin = fin; + tcph->doff = 5; + tcph->window = htons(TCP_MAXWIN); + tcph->urg_ptr = 0; + tcph->check = tcp_checksum(tcph, payload_len); +} + +static void write_packet(int fd, char *buf, int len, struct sockaddr_ll *daddr) +{ + char control[CMSG_SPACE(sizeof(uint64_t))]; + struct msghdr msg = {}; + struct iovec iov = {}; + struct cmsghdr *cm; + int ret = -1; + + iov.iov_base = buf; + iov.iov_len = len; + + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_name = daddr; + msg.msg_namelen = sizeof(*daddr); + + if (txtime_ns) { + memset(control, 0, sizeof(control)); + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + cm = CMSG_FIRSTHDR(&msg); + cm->cmsg_level = SOL_SOCKET; + cm->cmsg_type = SCM_TXTIME; + cm->cmsg_len = CMSG_LEN(sizeof(uint64_t)); + memcpy(CMSG_DATA(cm), &txtime_ns, sizeof(txtime_ns)); + } + + ret = sendmsg(fd, &msg, 0); + if (ret == -1) + error(1, errno, "sendmsg failure"); + if (ret != len) + error(1, 0, "sendmsg wrong length: %d vs %d", ret, len); +} + +static void create_packet(void *buf, int seq_offset, int ack_offset, + int payload_len, int fin) +{ + int ip_hdr_len = (proto == PF_INET) ? + sizeof(struct iphdr) : sizeof(struct ipv6hdr); + int inner_ip_off = tcp_offset - ip_hdr_len; + + memset(buf, 0, total_hdr_len); + memset(buf + total_hdr_len, 'a', payload_len); + + fill_transportlayer(buf + tcp_offset, seq_offset, ack_offset, + payload_len, fin); + + fill_networklayer(buf + inner_ip_off, payload_len, IPPROTO_TCP); + if (inner_ip_off > ETH_HLEN) { + if (pppoe) { + fill_pppoelayer(buf + ETH_HLEN, payload_len + ip_hdr_len, 0x1234); + } else { + int encap_proto = (proto == PF_INET) ? + IPPROTO_IPIP : IPPROTO_IPV6; + + fill_networklayer(buf + ETH_HLEN, + payload_len + ip_hdr_len, encap_proto); + } + } + + fill_datalinklayer(buf); +} + +static void create_capacity_packet(void *buf, int flow_id, int pkt_idx, int psh) +{ + int seq_offset = pkt_idx * CAPACITY_PAYLOAD_LEN; + struct tcphdr *tcph; + + create_packet(buf, seq_offset, 0, CAPACITY_PAYLOAD_LEN, 0); + + /* Customize for this flow id */ + memset(buf + total_hdr_len, 'a' + flow_id, CAPACITY_PAYLOAD_LEN); + + tcph = buf + tcp_offset; + tcph->source = htons(SPORT + flow_id); + tcph->psh = psh; + tcph->check = 0; + tcph->check = tcp_checksum(tcph, CAPACITY_PAYLOAD_LEN); +} + +/* Send a capacity test, 2 packets per flow, all first packets then all second: + * A1 B1 C1 D1 ... A2 B2 C2 D2 ... + */ +static void send_capacity(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + CAPACITY_PAYLOAD_LEN]; + int pkt_size = total_hdr_len + CAPACITY_PAYLOAD_LEN; + int i; + + /* Send first packet of each flow (no PSH) */ + for (i = 0; i < num_flows; i++) { + create_capacity_packet(buf, i, 0, 0); + write_packet(fd, buf, pkt_size, daddr); + } + + /* Send second packet of each flow (with PSH to flush) */ + for (i = 0; i < num_flows; i++) { + create_capacity_packet(buf, i, 1, 1); + write_packet(fd, buf, pkt_size, daddr); + } +} + +#ifndef TH_CWR +#define TH_CWR 0x80 +#endif +static void set_flags(struct tcphdr *tcph, int payload_len, int psh, int syn, + int rst, int urg, int cwr) +{ + tcph->psh = psh; + tcph->syn = syn; + tcph->rst = rst; + tcph->urg = urg; + if (cwr) + tcph->th_flags |= TH_CWR; + else + tcph->th_flags &= ~TH_CWR; + tcph->check = 0; + tcph->check = tcp_checksum(tcph, payload_len); +} + +/* send extra flags of the (NUM_PACKETS / 2) and (NUM_PACKETS / 2 - 1) + * pkts, not first and not last pkt + */ +static void send_flags(int fd, struct sockaddr_ll *daddr, int psh, int syn, + int rst, int urg, int cwr) +{ + static char flag_buf[2][MAX_HDR_LEN + PAYLOAD_LEN]; + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + int payload_len, pkt_size, i; + struct tcphdr *tcph; + int flag[2]; + + payload_len = PAYLOAD_LEN * (psh || cwr); + pkt_size = total_hdr_len + payload_len; + flag[0] = NUM_PACKETS / 2; + flag[1] = NUM_PACKETS / 2 - 1; + + /* Create and configure packets with flags + */ + for (i = 0; i < 2; i++) { + if (flag[i] > 0) { + create_packet(flag_buf[i], flag[i] * payload_len, 0, + payload_len, 0); + tcph = (struct tcphdr *)(flag_buf[i] + tcp_offset); + set_flags(tcph, payload_len, psh, syn, rst, urg, cwr); + } + } + + for (i = 0; i < NUM_PACKETS + 1; i++) { + if (i == flag[0]) { + write_packet(fd, flag_buf[0], pkt_size, daddr); + continue; + } else if (i == flag[1] && cwr) { + write_packet(fd, flag_buf[1], pkt_size, daddr); + continue; + } + create_packet(buf, i * PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, total_hdr_len + PAYLOAD_LEN, daddr); + } +} + +/* Test for data of same length, smaller than previous + * and of different lengths + */ +static void send_data_pkts(int fd, struct sockaddr_ll *daddr, + int payload_len1, int payload_len2) +{ + static char buf[L2_HLEN_MAX + IP_MAXPACKET]; + + create_packet(buf, 0, 0, payload_len1, 0); + write_packet(fd, buf, total_hdr_len + payload_len1, daddr); + create_packet(buf, payload_len1, 0, payload_len2, 0); + write_packet(fd, buf, total_hdr_len + payload_len2, daddr); +} + +/* If incoming segments make tracked segment length exceed + * legal IP datagram length, do not coalesce + */ +static void send_large(int fd, struct sockaddr_ll *daddr, int remainder) +{ + static char pkts[MAX_LARGE_PKT_CNT][MAX_HDR_LEN + MAX_MSS]; + static char new_seg[MAX_HDR_LEN + MAX_MSS]; + static char last[MAX_HDR_LEN + MAX_MSS]; + const int num_pkt = num_large_pkt(); + const int mss = calc_mss(); + int i; + + for (i = 0; i < num_pkt; i++) + create_packet(pkts[i], i * mss, 0, mss, 0); + create_packet(last, num_pkt * mss, 0, remainder, 0); + create_packet(new_seg, (num_pkt + 1) * mss, 0, remainder, 0); + + for (i = 0; i < num_pkt; i++) + write_packet(fd, pkts[i], total_hdr_len + mss, daddr); + write_packet(fd, last, total_hdr_len + remainder, daddr); + write_packet(fd, new_seg, total_hdr_len + remainder, daddr); +} + +/* Pure acks and dup acks don't coalesce */ +static void send_ack(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN]; + + create_packet(buf, 0, 0, 0, 0); + write_packet(fd, buf, total_hdr_len, daddr); + write_packet(fd, buf, total_hdr_len, daddr); + create_packet(buf, 0, 1, 0, 0); + write_packet(fd, buf, total_hdr_len, daddr); +} + +static void recompute_packet(char *buf, char *no_ext, int extlen) +{ + struct tcphdr *tcphdr = (struct tcphdr *)(buf + tcp_offset); + int off; + + memmove(buf, no_ext, total_hdr_len); + memmove(buf + total_hdr_len + extlen, + no_ext + total_hdr_len, PAYLOAD_LEN); + + tcphdr->doff = tcphdr->doff + (extlen / 4); + tcphdr->check = 0; + tcphdr->check = tcp_checksum(tcphdr, PAYLOAD_LEN + extlen); + if (proto == PF_INET) { + for (off = ETH_HLEN; off < tcp_offset; + off += sizeof(struct iphdr)) { + struct iphdr *iph = (struct iphdr *)(buf + off); + + iph->tot_len = htons(ntohs(iph->tot_len) + extlen); + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); + } + } else { + for (off = ETH_HLEN; off < tcp_offset; + off += sizeof(struct ipv6hdr)) { + struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + off); + + ip6h->payload_len = + htons(ntohs(ip6h->payload_len) + extlen); + } + } +} + +static void tcp_write_options(char *buf, int kind, int ts) +{ + struct tcp_option_ts { + uint8_t kind; + uint8_t len; + uint32_t tsval; + uint32_t tsecr; + } *opt_ts = (void *)buf; + struct tcp_option_window { + uint8_t kind; + uint8_t len; + uint8_t shift; + } *opt_window = (void *)buf; + + switch (kind) { + case TCPOPT_NOP: + buf[0] = TCPOPT_NOP; + break; + case TCPOPT_WINDOW: + memset(opt_window, 0, sizeof(struct tcp_option_window)); + opt_window->kind = TCPOPT_WINDOW; + opt_window->len = TCPOLEN_WINDOW; + opt_window->shift = 0; + break; + case TCPOPT_TIMESTAMP: + memset(opt_ts, 0, sizeof(struct tcp_option_ts)); + opt_ts->kind = TCPOPT_TIMESTAMP; + opt_ts->len = TCPOLEN_TIMESTAMP; + opt_ts->tsval = ts; + opt_ts->tsecr = 0; + break; + default: + error(1, 0, "unimplemented TCP option"); + break; + } +} + +/* TCP with options is always a permutation of {TS, NOP, NOP}. + * Implement different orders to verify coalescing stops. + */ +static void add_standard_tcp_options(char *buf, char *no_ext, int ts, int order) +{ + switch (order) { + case 0: + tcp_write_options(buf + total_hdr_len, TCPOPT_NOP, 0); + tcp_write_options(buf + total_hdr_len + 1, TCPOPT_NOP, 0); + tcp_write_options(buf + total_hdr_len + 2 /* two NOP opts */, + TCPOPT_TIMESTAMP, ts); + break; + case 1: + tcp_write_options(buf + total_hdr_len, TCPOPT_NOP, 0); + tcp_write_options(buf + total_hdr_len + 1, + TCPOPT_TIMESTAMP, ts); + tcp_write_options(buf + total_hdr_len + 1 + TCPOLEN_TIMESTAMP, + TCPOPT_NOP, 0); + break; + case 2: + tcp_write_options(buf + total_hdr_len, TCPOPT_TIMESTAMP, ts); + tcp_write_options(buf + total_hdr_len + TCPOLEN_TIMESTAMP + 1, + TCPOPT_NOP, 0); + tcp_write_options(buf + total_hdr_len + TCPOLEN_TIMESTAMP + 2, + TCPOPT_NOP, 0); + break; + default: + error(1, 0, "unknown order"); + break; + } + recompute_packet(buf, no_ext, TCPOLEN_TSTAMP_APPA); +} + +/* Packets with invalid checksum don't coalesce. */ +static void send_changed_checksum(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct tcphdr *tcph = (struct tcphdr *)(buf + tcp_offset); + int pkt_size = total_hdr_len + PAYLOAD_LEN; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + tcph->check = tcph->check - 1; + write_packet(fd, buf, pkt_size, daddr); +} + +/* Packets with incorrect IPv4 header checksum don't coalesce. */ +static void send_changed_ip_checksum(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + int pkt_size = total_hdr_len + PAYLOAD_LEN; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + iph->check = iph->check - 1; + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); +} + + /* Packets with non-consecutive sequence number don't coalesce.*/ +static void send_changed_seq(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct tcphdr *tcph = (struct tcphdr *)(buf + tcp_offset); + int pkt_size = total_hdr_len + PAYLOAD_LEN; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + tcph->seq = ntohl(htonl(tcph->seq) + 1); + tcph->check = 0; + tcph->check = tcp_checksum(tcph, PAYLOAD_LEN); + write_packet(fd, buf, pkt_size, daddr); +} + + /* Packet with different timestamp option or different timestamps + * don't coalesce. + */ +static void send_changed_ts(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + static char extpkt[sizeof(buf) + TCPOLEN_TSTAMP_APPA]; + int pkt_size = total_hdr_len + PAYLOAD_LEN + TCPOLEN_TSTAMP_APPA; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt, buf, 0, 0); + write_packet(fd, extpkt, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt, buf, 0, 0); + write_packet(fd, extpkt, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt, buf, 100, 0); + write_packet(fd, extpkt, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 3, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt, buf, 100, 1); + write_packet(fd, extpkt, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 4, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt, buf, 100, 2); + write_packet(fd, extpkt, pkt_size, daddr); +} + +/* Packet with different tcp options don't coalesce. */ +static void send_diff_opt(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + static char extpkt1[sizeof(buf) + TCPOLEN_TSTAMP_APPA]; + static char extpkt2[sizeof(buf) + TCPOLEN_MAXSEG]; + int extpkt1_size = total_hdr_len + PAYLOAD_LEN + TCPOLEN_TSTAMP_APPA; + int extpkt2_size = total_hdr_len + PAYLOAD_LEN + TCPOLEN_MAXSEG; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt1, buf, 0, 0); + write_packet(fd, extpkt1, extpkt1_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + add_standard_tcp_options(extpkt1, buf, 0, 0); + write_packet(fd, extpkt1, extpkt1_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + tcp_write_options(extpkt2 + MAX_HDR_LEN, TCPOPT_NOP, 0); + tcp_write_options(extpkt2 + MAX_HDR_LEN + 1, TCPOPT_WINDOW, 0); + recompute_packet(extpkt2, buf, TCPOLEN_WINDOW + 1); + write_packet(fd, extpkt2, extpkt2_size, daddr); +} + +static void add_ipv4_ts_option(void *buf, void *optpkt) +{ + struct ip_timestamp *ts = (struct ip_timestamp *)(optpkt + tcp_offset); + int optlen = sizeof(struct ip_timestamp); + struct iphdr *iph; + + if (optlen % 4) + error(1, 0, "ipv4 timestamp length is not a multiple of 4B"); + + ts->ipt_code = IPOPT_TS; + ts->ipt_len = optlen; + ts->ipt_ptr = 5; + ts->ipt_flg = IPOPT_TS_TSONLY; + + memcpy(optpkt, buf, tcp_offset); + memcpy(optpkt + tcp_offset + optlen, buf + tcp_offset, + sizeof(struct tcphdr) + PAYLOAD_LEN); + + iph = (struct iphdr *)(optpkt + ETH_HLEN); + iph->ihl = 5 + (optlen / 4); + iph->tot_len = htons(ntohs(iph->tot_len) + optlen); + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr) + optlen, 0); +} + +static void add_ipv6_exthdr(void *buf, void *optpkt, __u8 exthdr_type, char *ext_payload) +{ + struct ipv6_opt_hdr *exthdr = (struct ipv6_opt_hdr *)(optpkt + tcp_offset); + struct ipv6hdr *iph = (struct ipv6hdr *)(optpkt + ETH_HLEN); + char *exthdr_payload_start = (char *)(exthdr + 1); + + exthdr->hdrlen = 0; + exthdr->nexthdr = IPPROTO_TCP; + + memcpy(exthdr_payload_start, ext_payload, MIN_EXTHDR_SIZE - sizeof(*exthdr)); + + memcpy(optpkt, buf, tcp_offset); + memcpy(optpkt + tcp_offset + MIN_EXTHDR_SIZE, buf + tcp_offset, + sizeof(struct tcphdr) + PAYLOAD_LEN); + + iph->nexthdr = exthdr_type; + iph->payload_len = htons(ntohs(iph->payload_len) + MIN_EXTHDR_SIZE); +} + +static void fix_ip4_checksum(struct iphdr *iph) +{ + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); +} + +static void send_flush_id_case(int fd, struct sockaddr_ll *daddr, + enum flush_id_case tcase) +{ + static char buf1[MAX_HDR_LEN + PAYLOAD_LEN]; + static char buf2[MAX_HDR_LEN + PAYLOAD_LEN]; + static char buf3[MAX_HDR_LEN + PAYLOAD_LEN]; + bool send_three = false; + struct iphdr *iph1; + struct iphdr *iph2; + struct iphdr *iph3; + + iph1 = (struct iphdr *)(buf1 + ETH_HLEN); + iph2 = (struct iphdr *)(buf2 + ETH_HLEN); + iph3 = (struct iphdr *)(buf3 + ETH_HLEN); + + create_packet(buf1, 0, 0, PAYLOAD_LEN, 0); + create_packet(buf2, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + create_packet(buf3, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + + switch (tcase) { + case FLUSH_ID_DF1_INC: /* DF=1, Incrementing - should coalesce */ + iph1->frag_off |= htons(IP_DF); + iph1->id = htons(8); + + iph2->frag_off |= htons(IP_DF); + iph2->id = htons(9); + break; + + case FLUSH_ID_DF1_FIXED: /* DF=1, Fixed - should coalesce */ + iph1->frag_off |= htons(IP_DF); + iph1->id = htons(8); + + iph2->frag_off |= htons(IP_DF); + iph2->id = htons(8); + break; + + case FLUSH_ID_DF0_INC: /* DF=0, Incrementing - should coalesce */ + iph1->frag_off &= ~htons(IP_DF); + iph1->id = htons(8); + + iph2->frag_off &= ~htons(IP_DF); + iph2->id = htons(9); + break; + + case FLUSH_ID_DF0_FIXED: /* DF=0, Fixed - should coalesce */ + iph1->frag_off &= ~htons(IP_DF); + iph1->id = htons(8); + + iph2->frag_off &= ~htons(IP_DF); + iph2->id = htons(8); + break; + + case FLUSH_ID_DF1_INC_FIXED: /* DF=1, two packets incrementing, and + * one fixed - should coalesce only the + * first two packets + */ + iph1->frag_off |= htons(IP_DF); + iph1->id = htons(8); + + iph2->frag_off |= htons(IP_DF); + iph2->id = htons(9); + + iph3->frag_off |= htons(IP_DF); + iph3->id = htons(9); + send_three = true; + break; + + case FLUSH_ID_DF1_FIXED_INC: /* DF=1, two packets fixed, and one + * incrementing - should coalesce only + * the first two packets + */ + iph1->frag_off |= htons(IP_DF); + iph1->id = htons(8); + + iph2->frag_off |= htons(IP_DF); + iph2->id = htons(8); + + iph3->frag_off |= htons(IP_DF); + iph3->id = htons(9); + send_three = true; + break; + } + + fix_ip4_checksum(iph1); + fix_ip4_checksum(iph2); + write_packet(fd, buf1, total_hdr_len + PAYLOAD_LEN, daddr); + write_packet(fd, buf2, total_hdr_len + PAYLOAD_LEN, daddr); + + if (send_three) { + fix_ip4_checksum(iph3); + write_packet(fd, buf3, total_hdr_len + PAYLOAD_LEN, daddr); + } +} + +static void send_ipv6_exthdr(int fd, struct sockaddr_ll *daddr, char *ext_data1, char *ext_data2) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + static char exthdr_pck[sizeof(buf) + MIN_EXTHDR_SIZE]; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + add_ipv6_exthdr(buf, exthdr_pck, IPPROTO_DSTOPTS, ext_data1); + write_packet(fd, exthdr_pck, total_hdr_len + PAYLOAD_LEN + MIN_EXTHDR_SIZE, daddr); + + create_packet(buf, PAYLOAD_LEN * 1, 0, PAYLOAD_LEN, 0); + add_ipv6_exthdr(buf, exthdr_pck, IPPROTO_DSTOPTS, ext_data2); + write_packet(fd, exthdr_pck, total_hdr_len + PAYLOAD_LEN + MIN_EXTHDR_SIZE, daddr); +} + +/* IPv4 options shouldn't coalesce */ +static void send_ip_options(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + static char optpkt[sizeof(buf) + sizeof(struct ip_timestamp)]; + int optlen = sizeof(struct ip_timestamp); + int pkt_size = total_hdr_len + PAYLOAD_LEN + optlen; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, total_hdr_len + PAYLOAD_LEN, daddr); + + create_packet(buf, PAYLOAD_LEN * 1, 0, PAYLOAD_LEN, 0); + add_ipv4_ts_option(buf, optpkt); + write_packet(fd, optpkt, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, total_hdr_len + PAYLOAD_LEN, daddr); +} + +/* IPv4 fragments shouldn't coalesce */ +static void send_fragment4(int fd, struct sockaddr_ll *daddr) +{ + static char buf[IP_MAXPACKET]; + struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + int pkt_size = total_hdr_len + PAYLOAD_LEN; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + /* Once fragmented, packet would retain the total_len. + * Tcp header is prepared as if rest of data is in follow-up frags, + * but follow up frags aren't actually sent. + */ + memset(buf + total_hdr_len, 'a', PAYLOAD_LEN * 2); + fill_transportlayer(buf + tcp_offset, PAYLOAD_LEN, 0, PAYLOAD_LEN * 2, 0); + fill_networklayer(buf + ETH_HLEN, PAYLOAD_LEN, IPPROTO_TCP); + fill_datalinklayer(buf); + + iph->frag_off = htons(0x6000); // DF = 1, MF = 1 + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); + write_packet(fd, buf, pkt_size, daddr); +} + +/* IPv4 packets with different ttl don't coalesce.*/ +static void send_changed_ttl(int fd, struct sockaddr_ll *daddr) +{ + int pkt_size = total_hdr_len + PAYLOAD_LEN; + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + iph->ttl = 7; + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); + write_packet(fd, buf, pkt_size, daddr); +} + +/* Packets with different tos don't coalesce.*/ +static void send_changed_tos(int fd, struct sockaddr_ll *daddr) +{ + int pkt_size = total_hdr_len + PAYLOAD_LEN; + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + ETH_HLEN); + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + if (proto == PF_INET) { + iph->tos = 1; + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); + } else if (proto == PF_INET6) { + ip6h->priority = 0xf; + } + write_packet(fd, buf, pkt_size, daddr); +} + +/* Packets with different ECN don't coalesce.*/ +static void send_changed_ECN(int fd, struct sockaddr_ll *daddr) +{ + int pkt_size = total_hdr_len + PAYLOAD_LEN; + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + struct iphdr *iph = (struct iphdr *)(buf + ETH_HLEN); + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + if (proto == PF_INET) { + buf[ETH_HLEN + 1] ^= 0x2; // ECN set to 10 + iph->check = 0; + iph->check = checksum_fold(iph, sizeof(struct iphdr), 0); + } else { + buf[ETH_HLEN + 1] ^= 0x20; // ECN set to 10 + } + write_packet(fd, buf, pkt_size, daddr); +} + +/* IPv6 fragments and packets with extensions don't coalesce.*/ +static void send_fragment6(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + static char extpkt[MAX_HDR_LEN + PAYLOAD_LEN + + sizeof(struct ip6_frag)]; + struct ipv6hdr *ip6h = (struct ipv6hdr *)(buf + ETH_HLEN); + struct ip6_frag *frag = (void *)(extpkt + tcp_offset); + int extlen = sizeof(struct ip6_frag); + int bufpkt_len = total_hdr_len + PAYLOAD_LEN; + int extpkt_len = bufpkt_len + extlen; + int i; + + for (i = 0; i < 2; i++) { + create_packet(buf, PAYLOAD_LEN * i, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, bufpkt_len, daddr); + } + sleep(1); + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + memset(extpkt, 0, extpkt_len); + + ip6h->nexthdr = IPPROTO_FRAGMENT; + ip6h->payload_len = htons(ntohs(ip6h->payload_len) + extlen); + frag->ip6f_nxt = IPPROTO_TCP; + + memcpy(extpkt, buf, tcp_offset); + memcpy(extpkt + tcp_offset + extlen, buf + tcp_offset, + sizeof(struct tcphdr) + PAYLOAD_LEN); + write_packet(fd, extpkt, extpkt_len, daddr); + + create_packet(buf, PAYLOAD_LEN * 3, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, bufpkt_len, daddr); +} + +static void send_changed_pppoe_sid(int fd, struct sockaddr_ll *daddr) +{ + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + int pkt_size = total_hdr_len + PAYLOAD_LEN; + struct pppoe_hdr *hdr = (struct pppoe_hdr *)(buf + ETH_HLEN); + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(fd, buf, pkt_size, daddr); + + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + hdr->sid = htons(0x4321); + write_packet(fd, buf, pkt_size, daddr); +} + +static void bind_packetsocket(int fd) +{ + struct sockaddr_ll daddr = {}; + + daddr.sll_family = AF_PACKET; + daddr.sll_protocol = ethhdr_proto; + daddr.sll_ifindex = if_nametoindex(ifname); + if (daddr.sll_ifindex == 0) + error(1, errno, "if_nametoindex"); + + if (bind(fd, (void *)&daddr, sizeof(daddr)) < 0) + error(1, errno, "could not bind socket"); +} + +static void set_timeout(int fd) +{ + struct timeval timeout; + + timeout.tv_sec = 3; + timeout.tv_usec = 0; + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, + sizeof(timeout)) < 0) + error(1, errno, "cannot set timeout, setsockopt failed"); +} + +static void set_rcvbuf(int fd) +{ + int bufsize = 1 * 1024 * 1024; /* 1 MB */ + + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize))) + error(1, errno, "cannot set rcvbuf size, setsockopt failed"); +} + +static void recv_error(int fd, int rcv_errno) +{ + struct tpacket_stats stats; + socklen_t len; + + len = sizeof(stats); + if (getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &stats, &len)) + error(1, errno, "can't get stats"); + + fprintf(stderr, "Socket stats: packets=%u, drops=%u\n", + stats.tp_packets, stats.tp_drops); + error(1, rcv_errno, "could not receive"); +} + +static void check_recv_pkts(int fd, int *correct_payload, + int correct_num_pkts) +{ + static char buffer[IP_MAXPACKET + L2_HLEN_MAX + 1]; + int nhoff = ETH_HLEN + (pppoe ? PPPOE_SES_HLEN : 0); + struct iphdr *iph = (struct iphdr *)(buffer + nhoff); + struct ipv6hdr *ip6h = (struct ipv6hdr *)(buffer + nhoff); + struct tcphdr *tcph; + bool bad_packet = false; + int bytes_expected = 0; + int bytes_received = 0; + int tcp_ext_len = 0; + int ip_ext_len = 0; + int pkt_size = -1; + int data_len = 0; + int num_pkt = 0; + int i; + + vlog("Expected {"); + for (i = 0; i < correct_num_pkts; i++) { + vlog("%d ", correct_payload[i]); + bytes_expected += correct_payload[i]; + } + vlog("}, Total %d packets\nReceived {", correct_num_pkts); + + while (1) { + ip_ext_len = 0; + pkt_size = recv(fd, buffer, sizeof(buffer), 0); + if (pkt_size < 0) + recv_error(fd, errno); + + if (iph->version == 4) + ip_ext_len = (iph->ihl - 5) * 4; + else if (ip6h->version == 6 && !ip6ip6 && + ip6h->nexthdr != IPPROTO_TCP) + ip_ext_len = MIN_EXTHDR_SIZE; + + tcph = (struct tcphdr *)(buffer + tcp_offset + ip_ext_len); + + if (tcph->fin) + break; + + tcp_ext_len = (tcph->doff - 5) * 4; + data_len = pkt_size - total_hdr_len - tcp_ext_len - ip_ext_len; + /* Min ethernet frame payload is 46(ETH_ZLEN - ETH_HLEN) by RFC 802.3. + * Ipv4/tcp packets without at least 6 bytes of data will be padded. + * Packet sockets are protocol agnostic, and will not trim the padding. + */ + if (pkt_size == ETH_ZLEN && iph->version == 4) { + data_len = ntohs(iph->tot_len) + - sizeof(struct tcphdr) - sizeof(struct iphdr); + } + vlog("%d ", data_len); + if (data_len != correct_payload[num_pkt]) { + vlog("[!=%d]", correct_payload[num_pkt]); + bad_packet = true; + } + bytes_received += data_len; + num_pkt++; + } + vlog("}, Total %d packets.\n", num_pkt); + /* Signal over-coalescing explicitly, it's a hard failure, unlike + * under-coalescing which could be timing- or loss-related. + */ + if (num_pkt < correct_num_pkts && bytes_received == bytes_expected) + error(EXIT_OVER_COALESCE, 0, + "over-coalesced: got %d pkts vs expected %d (%d B)", + num_pkt, correct_num_pkts, bytes_received); + if (num_pkt != correct_num_pkts) + error(1, 0, "incorrect number of packets"); + if (bad_packet) + error(1, 0, "incorrect packet geometry"); + + printf("Test succeeded\n\n"); +} + +static void check_capacity_pkts(int fd) +{ + static char buffer[IP_MAXPACKET + L2_HLEN_MAX + 1]; + int nhoff = ETH_HLEN + (pppoe ? PPPOE_SES_HLEN : 0); + struct iphdr *iph = (struct iphdr *)(buffer + nhoff); + struct ipv6hdr *ip6h = (struct ipv6hdr *)(buffer + nhoff); + int num_pkt = 0, num_coal = 0, pkt_idx; + const char *fail_reason = NULL; + int flow_order[num_flows * 2]; + int coalesced[num_flows]; + struct tcphdr *tcph; + int ip_ext_len = 0; + int total_data = 0; + int pkt_size = -1; + int data_len = 0; + int flow_id; + int sport; + + memset(coalesced, 0, sizeof(coalesced)); + memset(flow_order, -1, sizeof(flow_order)); + + while (1) { + ip_ext_len = 0; + pkt_size = recv(fd, buffer, sizeof(buffer), 0); + if (pkt_size < 0) + recv_error(fd, errno); + + if (iph->version == 4) + ip_ext_len = (iph->ihl - 5) * 4; + else if (ip6h->version == 6 && !ip6ip6 && + ip6h->nexthdr != IPPROTO_TCP) + ip_ext_len = MIN_EXTHDR_SIZE; + + tcph = (struct tcphdr *)(buffer + tcp_offset + ip_ext_len); + + if (tcph->fin) + break; + + sport = ntohs(tcph->source); + flow_id = sport - SPORT; + + if (flow_id < 0 || flow_id >= num_flows) { + vlog("Invalid flow_id %d from sport %d\n", + flow_id, sport); + fail_reason = fail_reason ?: "invalid packet"; + continue; + } + + /* Calculate payload length */ + if (pkt_size == ETH_ZLEN && iph->version == 4) { + data_len = ntohs(iph->tot_len) + - sizeof(struct tcphdr) - sizeof(struct iphdr); + } else { + data_len = pkt_size - total_hdr_len - ip_ext_len; + } + + if (num_pkt < num_flows * 2) { + flow_order[num_pkt] = flow_id; + } else if (num_pkt == num_flows * 2) { + vlog("More packets than expected (%d)\n", + num_flows * 2); + fail_reason = fail_reason ?: "too many packets"; + } + coalesced[flow_id] = data_len; + + if (data_len == CAPACITY_PAYLOAD_LEN * 2) { + num_coal++; + } else { + vlog("Pkt %d: flow %d, sport %d, len %d (expected %d)\n", + num_pkt, flow_id, sport, data_len, + CAPACITY_PAYLOAD_LEN * 2); + fail_reason = fail_reason ?: "not coalesced"; + } + + num_pkt++; + total_data += data_len; + } + + /* Check flow ordering. We expect to see all non-coalesced first segs + * then interleaved coalesced and non-coalesced second frames. + */ + pkt_idx = 0; + for (flow_id = 0; order_check && flow_id < num_flows; flow_id++) { + bool coaled = coalesced[flow_id] > CAPACITY_PAYLOAD_LEN; + + if (coaled) + continue; + + if (flow_order[pkt_idx] != flow_id) { + vlog("Flow order mismatch (non-coalesced) at position %d: expected flow %d, got flow %d\n", + pkt_idx, flow_id, flow_order[pkt_idx]); + fail_reason = fail_reason ?: "bad packet order (1)"; + } + pkt_idx++; + } + for (flow_id = 0; order_check && flow_id < num_flows; flow_id++) { + bool coaled = coalesced[flow_id] > CAPACITY_PAYLOAD_LEN; + + if (flow_order[pkt_idx] != flow_id) { + vlog("Flow order mismatch at position %d: expected flow %d, got flow %d, coalesced: %d\n", + pkt_idx, flow_id, flow_order[pkt_idx], coaled); + fail_reason = fail_reason ?: "bad packet order (2)"; + } + pkt_idx++; + } + + if (!fail_reason) { + vlog("All %d flows coalesced correctly\n", num_flows); + printf("Test succeeded\n\n"); + } else { + printf("FAILED\n"); + } + + /* Always print stats for external validation */ + printf("STATS: received=%d wire=%d coalesced=%d\n", + num_pkt, num_pkt + num_coal, num_coal); + + if (fail_reason) + error(1, 0, "capacity test failed %s", fail_reason); +} + +static void gro_sender(void) +{ + int bufsize = 4 * 1024 * 1024; /* 4 MB */ + const int fin_delay_us = 100 * 1000; + static char fin_pkt[MAX_HDR_LEN]; + struct sockaddr_ll daddr = {}; + int txfd = -1; + + txfd = socket(PF_PACKET, SOCK_RAW, IPPROTO_RAW); + if (txfd < 0) + error(1, errno, "socket creation"); + + if (setsockopt(txfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize))) + error(1, errno, "cannot set sndbuf size, setsockopt failed"); + + /* Enable SO_TXTIME unless test case generates more than one flow + * SO_TXTIME could result in qdisc layer sorting the packets at sender. + */ + if (strcmp(testname, "single") && strcmp(testname, "capacity")) { + struct sock_txtime so_txtime = { .clockid = CLOCK_MONOTONIC, }; + struct timespec ts; + + if (setsockopt(txfd, SOL_SOCKET, SO_TXTIME, + &so_txtime, sizeof(so_txtime))) + error(1, errno, "setsockopt SO_TXTIME"); + + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + error(1, errno, "clock_gettime"); + + txtime_ns = ts.tv_sec * 1000000000ULL + ts.tv_nsec; + txtime_ns += TXTIME_DELAY_MS * 1000000ULL; + } + + memset(&daddr, 0, sizeof(daddr)); + daddr.sll_ifindex = if_nametoindex(ifname); + if (daddr.sll_ifindex == 0) + error(1, errno, "if_nametoindex"); + daddr.sll_family = AF_PACKET; + memcpy(daddr.sll_addr, dst_mac, ETH_ALEN); + daddr.sll_halen = ETH_ALEN; + create_packet(fin_pkt, PAYLOAD_LEN * 2, 0, 0, 1); + + /* data sub-tests */ + if (strcmp(testname, "data_same") == 0) { + send_data_pkts(txfd, &daddr, PAYLOAD_LEN, PAYLOAD_LEN); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "data_lrg_sml") == 0) { + send_data_pkts(txfd, &daddr, PAYLOAD_LEN, PAYLOAD_LEN / 2); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "data_lrg_1byte") == 0) { + send_data_pkts(txfd, &daddr, PAYLOAD_LEN, 1); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "data_sml_lrg") == 0) { + send_data_pkts(txfd, &daddr, PAYLOAD_LEN / 2, PAYLOAD_LEN); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "data_burst") == 0) { + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + create_packet(buf, PAYLOAD_LEN, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + + usleep(100 * 1000); /* 100ms */ + create_packet(buf, PAYLOAD_LEN * 2, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + create_packet(buf, PAYLOAD_LEN * 3, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* ack test */ + } else if (strcmp(testname, "ack") == 0) { + send_ack(txfd, &daddr); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* flags sub-tests */ + } else if (strcmp(testname, "flags_psh") == 0) { + send_flags(txfd, &daddr, 1, 0, 0, 0, 0); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "flags_syn") == 0) { + send_flags(txfd, &daddr, 0, 1, 0, 0, 0); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "flags_rst") == 0) { + send_flags(txfd, &daddr, 0, 0, 1, 0, 0); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "flags_urg") == 0) { + send_flags(txfd, &daddr, 0, 0, 0, 1, 0); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "flags_cwr") == 0) { + send_flags(txfd, &daddr, 0, 0, 0, 0, 1); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* tcp sub-tests */ + } else if (strcmp(testname, "tcp_csum") == 0) { + send_changed_checksum(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "tcp_seq") == 0) { + send_changed_seq(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "tcp_ts") == 0) { + send_changed_ts(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "tcp_opt") == 0) { + send_diff_opt(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* ip sub-tests - shared between IPv4 and IPv6 */ + } else if (strcmp(testname, "ip_ecn") == 0) { + send_changed_ECN(txfd, &daddr); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_tos") == 0) { + send_changed_tos(txfd, &daddr); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* ip sub-tests - IPv4 only */ + } else if (strcmp(testname, "ip_csum") == 0) { + send_changed_ip_checksum(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_ttl") == 0) { + send_changed_ttl(txfd, &daddr); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_opt") == 0) { + send_ip_options(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_frag4") == 0) { + send_fragment4(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_id_df1_inc") == 0) { + send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_INC); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_id_df1_fixed") == 0) { + send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_FIXED); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_id_df0_inc") == 0) { + send_flush_id_case(txfd, &daddr, FLUSH_ID_DF0_INC); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_id_df0_fixed") == 0) { + send_flush_id_case(txfd, &daddr, FLUSH_ID_DF0_FIXED); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_id_df1_inc_fixed") == 0) { + send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_INC_FIXED); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_id_df1_fixed_inc") == 0) { + send_flush_id_case(txfd, &daddr, FLUSH_ID_DF1_FIXED_INC); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* ip sub-tests - IPv6 only */ + } else if (strcmp(testname, "ip_frag6") == 0) { + send_fragment6(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_v6ext_same") == 0) { + send_ipv6_exthdr(txfd, &daddr, EXT_PAYLOAD_1, EXT_PAYLOAD_1); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "ip_v6ext_diff") == 0) { + send_ipv6_exthdr(txfd, &daddr, EXT_PAYLOAD_1, EXT_PAYLOAD_2); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* large sub-tests */ + } else if (strcmp(testname, "large_max") == 0) { + int remainder = max_payload() % calc_mss(); + + send_large(txfd, &daddr, remainder); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "large_rem") == 0) { + int remainder = max_payload() % calc_mss(); + + send_large(txfd, &daddr, remainder + 1); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* machinery sub-tests */ + } else if (strcmp(testname, "single") == 0) { + static char buf[MAX_HDR_LEN + PAYLOAD_LEN]; + + create_packet(buf, 0, 0, PAYLOAD_LEN, 0); + write_packet(txfd, buf, total_hdr_len + PAYLOAD_LEN, &daddr); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + } else if (strcmp(testname, "capacity") == 0) { + send_capacity(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + /* PPPoE sub-tests */ + } else if (strcmp(testname, "pppoe_sid") == 0) { + send_changed_pppoe_sid(txfd, &daddr); + usleep(fin_delay_us); + write_packet(txfd, fin_pkt, total_hdr_len, &daddr); + + } else { + error(1, 0, "Unknown testcase: %s", testname); + } + + if (close(txfd)) + error(1, errno, "socket close"); +} + +static void gro_receiver(void) +{ + static int correct_payload[NUM_PACKETS]; + int rxfd = -1; + + rxfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_NONE)); + if (rxfd < 0) + error(1, 0, "socket creation"); + setup_sock_filter(rxfd); + set_timeout(rxfd); + set_rcvbuf(rxfd); + bind_packetsocket(rxfd); + + ksft_ready(); + + memset(correct_payload, 0, sizeof(correct_payload)); + + /* data sub-tests */ + if (strcmp(testname, "data_same") == 0) { + printf("pure data packet of same size: "); + correct_payload[0] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "data_lrg_sml") == 0) { + printf("large data packets followed by a smaller one: "); + correct_payload[0] = PAYLOAD_LEN * 1.5; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "data_lrg_1byte") == 0) { + printf("large data packet followed by a 1 byte one: "); + correct_payload[0] = PAYLOAD_LEN + 1; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "data_sml_lrg") == 0) { + printf("small data packets followed by a larger one: "); + correct_payload[0] = PAYLOAD_LEN / 2; + correct_payload[1] = PAYLOAD_LEN; + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "data_burst") == 0) { + printf("two bursts of two data packets: "); + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 2); + + /* ack test */ + } else if (strcmp(testname, "ack") == 0) { + printf("duplicate ack and pure ack: "); + check_recv_pkts(rxfd, correct_payload, 3); + + /* flags sub-tests */ + } else if (strcmp(testname, "flags_psh") == 0) { + correct_payload[0] = PAYLOAD_LEN * 3; + correct_payload[1] = PAYLOAD_LEN * 2; + printf("psh flag ends coalescing: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "flags_syn") == 0) { + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = 0; + correct_payload[2] = PAYLOAD_LEN * 2; + printf("syn flag ends coalescing: "); + check_recv_pkts(rxfd, correct_payload, 3); + } else if (strcmp(testname, "flags_rst") == 0) { + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = 0; + correct_payload[2] = PAYLOAD_LEN * 2; + printf("rst flag ends coalescing: "); + check_recv_pkts(rxfd, correct_payload, 3); + } else if (strcmp(testname, "flags_urg") == 0) { + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = 0; + correct_payload[2] = PAYLOAD_LEN * 2; + printf("urg flag ends coalescing: "); + check_recv_pkts(rxfd, correct_payload, 3); + } else if (strcmp(testname, "flags_cwr") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN * 2; + correct_payload[2] = PAYLOAD_LEN * 2; + printf("cwr flag ends coalescing: "); + check_recv_pkts(rxfd, correct_payload, 3); + + /* tcp sub-tests */ + } else if (strcmp(testname, "tcp_csum") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("changed checksum does not coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "tcp_seq") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("Wrong Seq number doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "tcp_ts") == 0) { + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN; + correct_payload[2] = PAYLOAD_LEN; + correct_payload[3] = PAYLOAD_LEN; + printf("Different timestamp doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 4); + } else if (strcmp(testname, "tcp_opt") == 0) { + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN; + printf("Different options doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + + /* ip sub-tests - shared between IPv4 and IPv6 */ + } else if (strcmp(testname, "ip_ecn") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("different ECN doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "ip_tos") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("different tos doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + + /* ip sub-tests - IPv4 only */ + } else if (strcmp(testname, "ip_csum") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + correct_payload[2] = PAYLOAD_LEN; + printf("bad ip checksum doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 3); + } else if (strcmp(testname, "ip_ttl") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("different ttl doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "ip_opt") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + correct_payload[2] = PAYLOAD_LEN; + printf("ip options doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 3); + } else if (strcmp(testname, "ip_frag4") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("fragmented ip4 doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "ip_id_df1_inc") == 0) { + printf("DF=1, Incrementing - should coalesce: "); + correct_payload[0] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "ip_id_df1_fixed") == 0) { + printf("DF=1, Fixed - should coalesce: "); + correct_payload[0] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "ip_id_df0_inc") == 0) { + printf("DF=0, Incrementing - should coalesce: "); + correct_payload[0] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "ip_id_df0_fixed") == 0) { + printf("DF=0, Fixed - should coalesce: "); + correct_payload[0] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "ip_id_df1_inc_fixed") == 0) { + printf("DF=1, 2 Incrementing and one fixed - should coalesce only first 2 packets: "); + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN; + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "ip_id_df1_fixed_inc") == 0) { + printf("DF=1, 2 Fixed and one incrementing - should coalesce only first 2 packets: "); + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN; + check_recv_pkts(rxfd, correct_payload, 2); + + /* ip sub-tests - IPv6 only */ + } else if (strcmp(testname, "ip_frag6") == 0) { + /* GRO doesn't check for ipv6 hop limit when flushing. + * Hence no corresponding test to the ipv4 case. + */ + printf("fragmented ip6 doesn't coalesce: "); + correct_payload[0] = PAYLOAD_LEN * 2; + correct_payload[1] = PAYLOAD_LEN; + correct_payload[2] = PAYLOAD_LEN; + check_recv_pkts(rxfd, correct_payload, 3); + } else if (strcmp(testname, "ip_v6ext_same") == 0) { + printf("ipv6 with ext header does coalesce: "); + correct_payload[0] = PAYLOAD_LEN * 2; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "ip_v6ext_diff") == 0) { + printf("ipv6 with ext header with different payloads doesn't coalesce: "); + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + check_recv_pkts(rxfd, correct_payload, 2); + + /* large sub-tests */ + } else if (strcmp(testname, "large_max") == 0) { + int remainder = max_payload() % calc_mss(); + + correct_payload[0] = max_payload(); + correct_payload[1] = remainder; + printf("Shouldn't coalesce if exceed IP max pkt size: "); + check_recv_pkts(rxfd, correct_payload, 2); + } else if (strcmp(testname, "large_rem") == 0) { + int remainder = max_payload() % calc_mss(); + + /* last segment sent individually, doesn't start new segment */ + correct_payload[0] = max_payload() - remainder; + correct_payload[1] = remainder + 1; + correct_payload[2] = remainder + 1; + printf("last segment sent individually: "); + check_recv_pkts(rxfd, correct_payload, 3); + + /* machinery sub-tests */ + } else if (strcmp(testname, "single") == 0) { + printf("single data packet: "); + correct_payload[0] = PAYLOAD_LEN; + check_recv_pkts(rxfd, correct_payload, 1); + } else if (strcmp(testname, "capacity") == 0) { + check_capacity_pkts(rxfd); + + } else if (strcmp(testname, "pppoe_sid") == 0) { + correct_payload[0] = PAYLOAD_LEN; + correct_payload[1] = PAYLOAD_LEN; + printf("different PPPoE session ID doesn't coalesce: "); + check_recv_pkts(rxfd, correct_payload, 2); + + } else { + error(1, 0, "Test case error: unknown testname %s", testname); + } + + if (close(rxfd)) + error(1, 0, "socket close"); +} + +static void parse_args(int argc, char **argv) +{ + static const struct option opts[] = { + { "daddr", required_argument, NULL, 'd' }, + { "dmac", required_argument, NULL, 'D' }, + { "iface", required_argument, NULL, 'i' }, + { "ipv4", no_argument, NULL, '4' }, + { "ipv6", no_argument, NULL, '6' }, + { "ipip", no_argument, NULL, 'e' }, + { "ip6ip6", no_argument, NULL, 'E' }, + { "pppoev4", no_argument, NULL, 'p' }, + { "pppoev6", no_argument, NULL, 'P' }, + { "num-flows", required_argument, NULL, 'n' }, + { "rx", no_argument, NULL, 'r' }, + { "saddr", required_argument, NULL, 's' }, + { "smac", required_argument, NULL, 'S' }, + { "test", required_argument, NULL, 't' }, + { "order-check", no_argument, NULL, 'o' }, + { "verbose", no_argument, NULL, 'v' }, + { 0, 0, 0, 0 } + }; + int c; + + while ((c = getopt_long(argc, argv, "46d:D:eEi:n:pPrs:S:t:ov", opts, NULL)) != -1) { + switch (c) { + case '4': + proto = PF_INET; + ethhdr_proto = htons(ETH_P_IP); + break; + case '6': + proto = PF_INET6; + ethhdr_proto = htons(ETH_P_IPV6); + break; + case 'e': + ipip = true; + proto = PF_INET; + ethhdr_proto = htons(ETH_P_IP); + break; + case 'E': + ip6ip6 = true; + proto = PF_INET6; + ethhdr_proto = htons(ETH_P_IPV6); + break; + case 'p': + pppoe = true; + proto = PF_INET; + ethhdr_proto = htons(ETH_P_PPP_SES); + break; + case 'P': + pppoe = true; + proto = PF_INET6; + ethhdr_proto = htons(ETH_P_PPP_SES); + break; + case 'd': + addr4_dst = addr6_dst = optarg; + break; + case 'D': + dmac = optarg; + break; + case 'i': + ifname = optarg; + break; + case 'n': + num_flows = atoi(optarg); + break; + case 'r': + tx_socket = false; + break; + case 's': + addr4_src = addr6_src = optarg; + break; + case 'S': + smac = optarg; + break; + case 't': + testname = optarg; + break; + case 'o': + order_check = true; + break; + case 'v': + verbose = true; + break; + default: + error(1, 0, "%s invalid option %c\n", __func__, c); + break; + } + } +} + +int main(int argc, char **argv) +{ + parse_args(argc, argv); + + if (ipip) { + tcp_offset = ETH_HLEN + sizeof(struct iphdr) * 2; + total_hdr_len = tcp_offset + sizeof(struct tcphdr); + } else if (ip6ip6) { + tcp_offset = ETH_HLEN + sizeof(struct ipv6hdr) * 2; + total_hdr_len = tcp_offset + sizeof(struct tcphdr); + } else if (pppoe) { + tcp_offset = ETH_HLEN + PPPOE_SES_HLEN + + (proto == PF_INET ? sizeof(struct iphdr) : sizeof(struct ipv6hdr)); + total_hdr_len = tcp_offset + sizeof(struct tcphdr); + } else if (proto == PF_INET) { + tcp_offset = ETH_HLEN + sizeof(struct iphdr); + total_hdr_len = tcp_offset + sizeof(struct tcphdr); + } else if (proto == PF_INET6) { + tcp_offset = ETH_HLEN + sizeof(struct ipv6hdr); + total_hdr_len = tcp_offset + sizeof(struct tcphdr); + } else { + error(1, 0, "Protocol family is not ipv4 or ipv6"); + } + + read_MAC(src_mac, smac); + read_MAC(dst_mac, dmac); + + if (tx_socket) { + gro_sender(); + } else { + /* Only the receiver exit status determines test success. */ + gro_receiver(); + fprintf(stderr, "Gro::%s test passed.\n", testname); + } + + return 0; +} diff --git a/tools/testing/selftests/net/lib/py/__init__.py b/tools/testing/selftests/net/lib/py/__init__.py index f528b67639de..34935886b6ad 100644 --- a/tools/testing/selftests/net/lib/py/__init__.py +++ b/tools/testing/selftests/net/lib/py/__init__.py @@ -10,12 +10,15 @@ from .ksft import KsftFailEx, KsftSkipEx, KsftXfailEx, ksft_pr, ksft_eq, \ ksft_ge, ksft_gt, ksft_lt, ksft_raises, ksft_busy_wait, \ ktap_result, ksft_disruptive, ksft_setup, ksft_run, ksft_exit, \ ksft_variants, KsftNamedVariant -from .netns import NetNS, NetNSEnter +from .netns import NetNS, NetNSEnter, UserNetNS from .nsim import NetdevSim, NetdevSimDev from .utils import CmdExitFailure, fd_read_timeout, cmd, bkg, defer, \ - bpftool, ip, ethtool, bpftrace, rand_port, wait_port_listen, wait_file, tool -from .ynl import NlError, YnlFamily, EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily -from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily + bpftool, ip, ethtool, bpftrace, rand_port, rand_ports, wait_port_listen, \ + wait_file, tool, tc +from .bpf import bpf_map_set, bpf_map_dump, bpf_prog_map_ids +from .ynl import NlError, NlctrlFamily, YnlFamily, \ + EthtoolFamily, NetdevFamily, RtnlFamily, RtnlAddrFamily, RtnlRouteFamily +from .ynl import NetshaperFamily, DevlinkFamily, PSPFamily, Netlink __all__ = ["KSRC", "KsftFailEx", "KsftSkipEx", "KsftXfailEx", "ksft_pr", "ksft_eq", @@ -23,11 +26,12 @@ __all__ = ["KSRC", "ksft_is", "ksft_ge", "ksft_gt", "ksft_lt", "ksft_raises", "ksft_busy_wait", "ktap_result", "ksft_disruptive", "ksft_setup", "ksft_run", "ksft_exit", "ksft_variants", "KsftNamedVariant", - "NetNS", "NetNSEnter", + "NetNS", "NetNSEnter", "UserNetNS", "CmdExitFailure", "fd_read_timeout", "cmd", "bkg", "defer", - "bpftool", "ip", "ethtool", "bpftrace", "rand_port", - "wait_port_listen", "wait_file", "tool", + "bpftool", "ip", "ethtool", "bpftrace", "rand_port", "rand_ports", + "wait_port_listen", "wait_file", "tool", "tc", + "bpf_map_set", "bpf_map_dump", "bpf_prog_map_ids", "NetdevSim", "NetdevSimDev", "NetshaperFamily", "DevlinkFamily", "PSPFamily", "NlError", "YnlFamily", "EthtoolFamily", "NetdevFamily", "RtnlFamily", - "RtnlAddrFamily"] + "NlctrlFamily", "RtnlAddrFamily", "RtnlRouteFamily", "Netlink"] diff --git a/tools/testing/selftests/net/lib/py/bpf.py b/tools/testing/selftests/net/lib/py/bpf.py new file mode 100644 index 000000000000..beb6bf2896a8 --- /dev/null +++ b/tools/testing/selftests/net/lib/py/bpf.py @@ -0,0 +1,68 @@ +# SPDX-License-Identifier: GPL-2.0 + +""" +BPF helper utilities for kernel selftests. + +Provides common operations for interacting with BPF maps and programs +via bpftool, used by XDP and other BPF-based test files. +""" + +from .utils import bpftool + +def _format_hex_bytes(value): + """ + Helper function that converts an integer into a formatted hexadecimal byte string. + + Args: + value: An integer representing the number to be converted. + + Returns: + A string representing hexadecimal equivalent of value, with bytes separated by spaces. + """ + hex_str = value.to_bytes(4, byteorder='little', signed=True) + return ' '.join(f'{byte:02x}' for byte in hex_str) + + +def bpf_map_set(map_name, key, value): + """ + Updates an XDP map with a given key-value pair using bpftool. + + Args: + map_name: The name of the XDP map to update. + key: The key to update in the map, formatted as a hexadecimal string. + value: The value to associate with the key, formatted as a hexadecimal string. + """ + key_formatted = _format_hex_bytes(key) + value_formatted = _format_hex_bytes(value) + bpftool( + f"map update name {map_name} key hex {key_formatted} value hex {value_formatted}" + ) + +def bpf_map_dump(map_id): + """Dump all entries of a BPF array map. + + Args: + map_id: Numeric map ID (as returned by bpftool prog show). + + Returns: + A dict mapping formatted key (int) to formatted value (int). + """ + raw = bpftool(f"map dump id {map_id}", json=True) + return {e["formatted"]["key"]: e["formatted"]["value"] for e in raw} + + +def bpf_prog_map_ids(prog_id): + """Get the map name-to-ID mapping for a loaded BPF program. + + Args: + prog_id: Numeric program ID. + + Returns: + A dict mapping map name (str) to map ID (int). + """ + map_ids = bpftool(f"prog show id {prog_id}", json=True)["map_ids"] + maps = {} + for mid in map_ids: + name = bpftool(f"map show id {mid}", json=True)["name"] + maps[name] = mid + return maps diff --git a/tools/testing/selftests/net/lib/py/ksft.py b/tools/testing/selftests/net/lib/py/ksft.py index 6cdfb8afccb5..81287c2daff0 100644 --- a/tools/testing/selftests/net/lib/py/ksft.py +++ b/tools/testing/selftests/net/lib/py/ksft.py @@ -1,7 +1,10 @@ # SPDX-License-Identifier: GPL-2.0 +import fnmatch import functools +import getopt import inspect +import os import signal import sys import time @@ -31,6 +34,45 @@ class KsftTerminate(KeyboardInterrupt): pass +class _KsftArgs: + def __init__(self): + self.list_tests = False + self.filters = [] + + try: + opts, _ = getopt.getopt(sys.argv[1:], 'hlt:T:') + except getopt.GetoptError as e: + print(e, file=sys.stderr) + sys.exit(1) + + for opt, val in opts: + if opt == '-h': + print(f"Usage: {sys.argv[0]} [-h|-l] [-t|-T name]\n" + f"\t-h print help\n" + f"\t-l list tests (filtered, if filters were specified)\n" + f"\t-t name include test\n" + f"\t-T name exclude test", + file=sys.stderr) + sys.exit(0) + elif opt == '-l': + self.list_tests = True + elif opt == '-t': + self.filters.append((True, val)) + elif opt == '-T': + self.filters.append((False, val)) + + +@functools.lru_cache() +def _ksft_supports_color(): + if os.environ.get("NO_COLOR") is not None: + return False + if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty(): + return False + if os.environ.get("TERM") == "dumb": + return False + return True + + def ksft_pr(*objs, **kwargs): """ Print logs to stdout. @@ -165,6 +207,14 @@ def ktap_result(ok, cnt=1, case_name="", comment=""): res += "." + case_name if comment: res += " # " + comment + if _ksft_supports_color(): + if comment.startswith(("SKIP", "XFAIL")): + color = "\033[33m" + elif ok: + color = "\033[32m" + else: + color = "\033[31m" + res = color + res + "\033[0m" print(res, flush=True) @@ -278,8 +328,26 @@ def _ksft_intr(signum, frame): ksft_pr(f"Ignoring SIGTERM (cnt: {term_cnt}), already exiting...") -def _ksft_generate_test_cases(cases, globs, case_pfx, args): - """Generate a flat list of (func, args, name) tuples""" +def _ksft_name_matches(name, pattern): + if '*' in pattern or '?' in pattern or '[' in pattern: + return fnmatch.fnmatchcase(name, pattern) + return name == pattern + + +def _ksft_test_enabled(name, filters): + has_positive = False + for include, pattern in filters: + has_positive |= include + if _ksft_name_matches(name, pattern): + return include + return not has_positive + + +def _ksft_generate_test_cases(cases, globs, case_pfx, args, cli_args): + """Generate a filtered list of (func, args, name) tuples. + + If -l is given, prints matching test names and exits. + """ cases = cases or [] test_cases = [] @@ -309,11 +377,22 @@ def _ksft_generate_test_cases(cases, globs, case_pfx, args): else: test_cases.append((func, args, func.__name__)) + if cli_args.filters: + test_cases = [tc for tc in test_cases + if _ksft_test_enabled(tc[2], cli_args.filters)] + + if cli_args.list_tests: + for _, _, name in test_cases: + print(name) + sys.exit(0) + return test_cases def ksft_run(cases=None, globs=None, case_pfx=None, args=()): - test_cases = _ksft_generate_test_cases(cases, globs, case_pfx, args) + cli_args = _KsftArgs() + test_cases = _ksft_generate_test_cases(cases, globs, case_pfx, args, + cli_args) global term_cnt term_cnt = 0 @@ -321,10 +400,13 @@ def ksft_run(cases=None, globs=None, case_pfx=None, args=()): totals = {"pass": 0, "fail": 0, "skip": 0, "xfail": 0} + global KSFT_RESULT + if KSFT_RESULT is not None: + raise RuntimeError("ksft_run() can't be called multiple times.") + print("TAP version 13", flush=True) print("1.." + str(len(test_cases)), flush=True) - global KSFT_RESULT cnt = 0 stop = False for func, args, name in test_cases: diff --git a/tools/testing/selftests/net/lib/py/netns.py b/tools/testing/selftests/net/lib/py/netns.py index 8e9317044eef..526f6aa80077 100644 --- a/tools/testing/selftests/net/lib/py/netns.py +++ b/tools/testing/selftests/net/lib/py/netns.py @@ -1,9 +1,14 @@ # SPDX-License-Identifier: GPL-2.0 -from .utils import ip import ctypes +import os import random import string +import subprocess +import time +from pathlib import Path + +from .utils import ip libc = ctypes.cdll.LoadLibrary('libc.so.6') @@ -34,6 +39,74 @@ class NetNS: return f"NetNS({self.name})" +class UserNetNS: + """Network namespace owned by a non-init user namespace.""" + + def __init__(self): + self.name = ''.join( + random.choice(string.ascii_lowercase) for _ in range(8)) + self.user_ns_path = f"/run/userns/{self.name}" + self.net_ns_path = f"/run/netns/{self.name}" + self._user_mounted = False + self._net_mounted = False + + os.makedirs("/run/userns", exist_ok=True) + os.makedirs("/run/netns", exist_ok=True) + + Path(self.user_ns_path).touch() + Path(self.net_ns_path).touch() + + with subprocess.Popen( + ["unshare", "--user", "--net", "--map-root-user", + "sleep", "infinity"]) as proc: + try: + pid = proc.pid + init_user = os.readlink("/proc/self/ns/user") + for _ in range(200): + try: + if os.readlink(f"/proc/{pid}/ns/user") != init_user: + break + except OSError: + pass + time.sleep(0.01) + else: + raise RuntimeError("unshare child did not create userns") + + subprocess.run(["mount", "--bind", f"/proc/{pid}/ns/user", + self.user_ns_path], check=True) + self._user_mounted = True + subprocess.run(["mount", "--bind", f"/proc/{pid}/ns/net", + self.net_ns_path], check=True) + self._net_mounted = True + finally: + proc.kill() + + def __del__(self): + if self._net_mounted: + subprocess.run(["umount", self.net_ns_path], check=False) + self._net_mounted = False + if self._user_mounted: + subprocess.run(["umount", self.user_ns_path], check=False) + self._user_mounted = False + for path in (self.net_ns_path, self.user_ns_path): + try: + os.unlink(path) + except OSError: + pass + + def __enter__(self): + return self + + def __exit__(self, ex_type, ex_value, ex_tb): + self.__del__() + + def __str__(self): + return self.name + + def __repr__(self): + return f"UserNetNS({self.name})" + + class NetNSEnter: def __init__(self, ns_name): self.ns_path = f"/run/netns/{ns_name}" diff --git a/tools/testing/selftests/net/lib/py/utils.py b/tools/testing/selftests/net/lib/py/utils.py index 85884f3e827b..184bb04343f6 100644 --- a/tools/testing/selftests/net/lib/py/utils.py +++ b/tools/testing/selftests/net/lib/py/utils.py @@ -9,12 +9,24 @@ import subprocess import time +class CmdInitFailure(Exception): + """ Command failed to start. Only raised by bkg(). """ + def __init__(self, msg, cmd_obj): + super().__init__(msg + "\n" + repr(cmd_obj)) + self.cmd = cmd_obj + + class CmdExitFailure(Exception): + """ Command failed (returned non-zero exit code). """ def __init__(self, msg, cmd_obj): - super().__init__(msg) + super().__init__(msg + "\n" + repr(cmd_obj)) self.cmd = cmd_obj +class CmdExitZeroFailure(CmdExitFailure): + """ Command succeeded (returned zero exit code), but expected failure. """ + + def fd_read_timeout(fd, timeout): rlist, _, _ = select.select([fd], [], [], timeout) if rlist: @@ -31,10 +43,16 @@ class cmd: Use bkg() instead to run a command in the background. """ - def __init__(self, comm, shell=None, fail=True, ns=None, background=False, - host=None, timeout=5, ksft_ready=None, ksft_wait=None): + def __init__(self, comm, shell=None, fail=True, expect_fail=False, ns=None, + background=False, host=None, timeout=20, ksft_ready=None, + ksft_wait=None): if ns: - comm = f'ip netns exec {ns} ' + comm + if hasattr(ns, 'user_ns_path'): + comm = (f'nsenter --user={ns.user_ns_path} ' + f'--net={ns.net_ns_path} --setuid=0 --setgid=0 -- ' + + comm) + else: + comm = f'ip netns exec {ns} ' + comm self.stdout = None self.stderr = None @@ -76,30 +94,47 @@ class cmd: msg = fd_read_timeout(rfd, ksft_wait) os.close(rfd) if not msg: - raise Exception("Did not receive ready message") + terminate = self.proc.poll() is None + self._process_terminate(terminate=terminate, timeout=1) + raise CmdInitFailure("Did not receive ready message", self) if not background: - self.process(terminate=False, fail=fail, timeout=timeout) + self.process(terminate=False, fail=fail, expect_fail=expect_fail, + timeout=timeout) - def process(self, terminate=True, fail=None, timeout=5): - if fail is None: - fail = not terminate - - if self.ksft_term_fd: - os.write(self.ksft_term_fd, b"1") + def _process_terminate(self, terminate, timeout): if terminate: self.proc.terminate() - stdout, stderr = self.proc.communicate(timeout) + stdout, stderr = self.proc.communicate(timeout=timeout) self.stdout = stdout.decode("utf-8") self.stderr = stderr.decode("utf-8") self.proc.stdout.close() self.proc.stderr.close() self.ret = self.proc.returncode - if self.proc.returncode != 0 and fail: + return stdout, stderr + + def process(self, terminate=True, fail=None, expect_fail=False, timeout=20): + if fail is None: + fail = not terminate + + if self.ksft_term_fd: + os.write(self.ksft_term_fd, b"1") + + stdout, stderr = self._process_terminate(terminate=terminate, + timeout=timeout) + + # Fail on unexpected test failure if fail. + # Fail on unexpected test success if expect_fail. + # Fail on negative returncode if either: + # Set by subprocess on crash or signal, this is never expected failure. + if (self.proc.returncode != 0 and fail or + (self.proc.returncode < 0 and expect_fail)): if len(stderr) > 0 and stderr[-1] == "\n": stderr = stderr[:-1] - raise CmdExitFailure("Command failed: %s\nSTDOUT: %s\nSTDERR: %s" % - (self.proc.args, stdout, stderr), self) + raise CmdExitFailure("Command failed", self) + elif self.proc.returncode == 0 and expect_fail: + raise CmdExitZeroFailure("Command succeeded (expected fail)", self) + def __repr__(self): def str_fmt(name, s): @@ -142,14 +177,17 @@ class bkg(cmd): with bkg("my_binary", ksft_wait=5): """ - def __init__(self, comm, shell=None, fail=None, ns=None, host=None, - exit_wait=False, ksft_ready=None, ksft_wait=None): + def __init__(self, comm, shell=None, fail=None, expect_fail=None, + ns=None, host=None, exit_wait=False, ksft_ready=None, + ksft_wait=None): super().__init__(comm, background=True, - shell=shell, fail=fail, ns=ns, host=host, - ksft_ready=ksft_ready, ksft_wait=ksft_wait) + shell=shell, fail=fail, expect_fail=expect_fail, + ns=ns, host=host, ksft_ready=ksft_ready, + ksft_wait=ksft_wait) self.terminate = not exit_wait and not ksft_wait self._exit_wait = exit_wait self.check_fail = fail + self.expect_fail = expect_fail if shell and self.terminate: print("# Warning: combining shell and terminate is risky!") @@ -159,9 +197,13 @@ class bkg(cmd): return self def __exit__(self, ex_type, ex_value, ex_tb): - # Force termination on exception - terminate = self.terminate or (self._exit_wait and ex_type is not None) - return self.process(terminate=terminate, fail=self.check_fail) + terminate = self.terminate + # Force termination on exception, but only if bkg() didn't already exit + # since forcing termination silences failures with fail=None + if self.proc.poll() is None: + terminate = terminate or (self._exit_wait and ex_type is not None) + return self.process(terminate=terminate, fail=self.check_fail, + expect_fail=self.expect_fail) GLOBAL_DEFER_QUEUE = [] @@ -202,7 +244,10 @@ class defer: def tool(name, args, json=None, ns=None, host=None): cmd_str = name + ' ' if json: - cmd_str += '--json ' + if name == 'tc': + cmd_str += '-json ' + else: + cmd_str += '--json ' cmd_str += args cmd_obj = cmd(cmd_str, ns=ns, host=host) if json: @@ -220,6 +265,13 @@ def ip(args, json=None, ns=None, host=None): return tool('ip', args, json=json, host=host) +def tc(args, json=None, ns=None, host=None): + """ Helper to call tc with standard set of optional args. """ + if ns: + args = f'-netns {ns} ' + args + return tool('tc', args, json=json, host=host) + + def ethtool(args, json=None, ns=None, host=None): return tool('ethtool', args, json=json, ns=ns, host=host) @@ -240,8 +292,9 @@ def bpftrace(expr, json=None, ns=None, host=None, timeout=None): cmd_arr += ['-f', 'json', '-q'] if timeout: expr += ' interval:s:' + str(timeout) + ' { exit(); }' + timeout += 20 cmd_arr += ['-e', expr] - cmd_obj = cmd(cmd_arr, ns=ns, host=host, shell=False) + cmd_obj = cmd(cmd_arr, ns=ns, host=host, shell=False, timeout=timeout) if json: # bpftrace prints objects as lines ret = {} @@ -263,9 +316,27 @@ def rand_port(stype=socket.SOCK_STREAM): """ Get a random unprivileged port. """ - with socket.socket(socket.AF_INET6, stype) as s: - s.bind(("", 0)) - return s.getsockname()[1] + return rand_ports(1, stype)[0] + + +def rand_ports(count, stype=socket.SOCK_STREAM): + """ + Get a unique set of random unprivileged ports. + """ + sockets = [] + ports = [] + + try: + for _ in range(count): + s = socket.socket(socket.AF_INET6, stype) + sockets.append(s) + s.bind(("", 0)) + ports.append(s.getsockname()[1]) + finally: + for s in sockets: + s.close() + + return ports def wait_port_listen(port, proto="tcp", ns=None, host=None, sleep=0.005, deadline=5): diff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py index 32c223e93b2c..08deff756f29 100644 --- a/tools/testing/selftests/net/lib/py/ynl.py +++ b/tools/testing/selftests/net/lib/py/ynl.py @@ -13,20 +13,27 @@ try: SPEC_PATH = KSFT_DIR / "net/lib/specs" sys.path.append(tools_full_path.as_posix()) - from net.lib.ynl.pyynl.lib import YnlFamily, NlError + from net.lib.ynl.pyynl.lib import YnlFamily, NlError, NlPolicy, Netlink else: # Running in tree tools_full_path = KSRC / "tools" SPEC_PATH = KSRC / "Documentation/netlink/specs" sys.path.append(tools_full_path.as_posix()) - from net.ynl.pyynl.lib import YnlFamily, NlError + from net.ynl.pyynl.lib import YnlFamily, NlError, NlPolicy, Netlink except ModuleNotFoundError as e: ksft_pr("Failed importing `ynl` library from kernel sources") ksft_pr(str(e)) ktap_result(True, comment="SKIP") sys.exit(4) +__all__ = [ + "NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH", + "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily", "RtnlRouteFamily", + "NetdevFamily", "NetshaperFamily", "NlctrlFamily", "DevlinkFamily", + "PSPFamily", +] + # # Wrapper classes, loading the right specs # Set schema='' to avoid jsonschema validation, it's slow @@ -47,6 +54,11 @@ class RtnlAddrFamily(YnlFamily): super().__init__((SPEC_PATH / Path('rt-addr.yaml')).as_posix(), schema='', recv_size=recv_size) +class RtnlRouteFamily(YnlFamily): + def __init__(self, recv_size=0): + super().__init__((SPEC_PATH / Path('rt-route.yaml')).as_posix(), + schema='', recv_size=recv_size) + class NetdevFamily(YnlFamily): def __init__(self, recv_size=0): super().__init__((SPEC_PATH / Path('netdev.yaml')).as_posix(), @@ -57,6 +69,13 @@ class NetshaperFamily(YnlFamily): super().__init__((SPEC_PATH / Path('net_shaper.yaml')).as_posix(), schema='', recv_size=recv_size) + +class NlctrlFamily(YnlFamily): + def __init__(self, recv_size=0): + super().__init__((SPEC_PATH / Path('nlctrl.yaml')).as_posix(), + schema='', recv_size=recv_size) + + class DevlinkFamily(YnlFamily): def __init__(self, recv_size=0): super().__init__((SPEC_PATH / Path('devlink.yaml')).as_posix(), diff --git a/tools/testing/selftests/net/lib/xdp_metadata.bpf.c b/tools/testing/selftests/net/lib/xdp_metadata.bpf.c new file mode 100644 index 000000000000..f71f59215239 --- /dev/null +++ b/tools/testing/selftests/net/lib/xdp_metadata.bpf.c @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include <stddef.h> +#include <linux/bpf.h> +#include <linux/in.h> +#include <linux/if_ether.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/udp.h> +#include <linux/tcp.h> +#include <bpf/bpf_endian.h> +#include <bpf/bpf_helpers.h> + +enum { + XDP_PORT = 1, + XDP_PROTO = 4, +} xdp_map_setup_keys; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 5); + __type(key, __u32); + __type(value, __s32); +} map_xdp_setup SEC(".maps"); + +/* RSS hash results: key 0 = hash, key 1 = hash type, + * key 2 = packet count, key 3 = error count. + */ +enum { + RSS_KEY_HASH = 0, + RSS_KEY_TYPE = 1, + RSS_KEY_PKT_CNT = 2, + RSS_KEY_ERR_CNT = 3, +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __type(key, __u32); + __type(value, __u32); + __uint(max_entries, 4); +} map_rss SEC(".maps"); + +/* Mirror of enum xdp_rss_hash_type from include/net/xdp.h. + * Needed because the enum is not part of UAPI headers. + */ +enum xdp_rss_hash_type { + XDP_RSS_L3_IPV4 = 1U << 0, + XDP_RSS_L3_IPV6 = 1U << 1, + XDP_RSS_L3_DYNHDR = 1U << 2, + XDP_RSS_L4 = 1U << 3, + XDP_RSS_L4_TCP = 1U << 4, + XDP_RSS_L4_UDP = 1U << 5, + XDP_RSS_L4_SCTP = 1U << 6, + XDP_RSS_L4_IPSEC = 1U << 7, + XDP_RSS_L4_ICMP = 1U << 8, +}; + +extern int bpf_xdp_metadata_rx_hash(const struct xdp_md *ctx, __u32 *hash, + enum xdp_rss_hash_type *rss_type) __ksym; + +static __always_inline __u16 get_dest_port(void *l4, void *data_end, + __u8 protocol) +{ + if (protocol == IPPROTO_UDP) { + struct udphdr *udp = l4; + + if ((void *)(udp + 1) > data_end) + return 0; + return udp->dest; + } else if (protocol == IPPROTO_TCP) { + struct tcphdr *tcp = l4; + + if ((void *)(tcp + 1) > data_end) + return 0; + return tcp->dest; + } + + return 0; +} + +SEC("xdp") +int xdp_rss_hash(struct xdp_md *ctx) +{ + void *data_end = (void *)(long)ctx->data_end; + void *data = (void *)(long)ctx->data; + enum xdp_rss_hash_type rss_type = 0; + struct ethhdr *eth = data; + __u8 l4_proto = 0; + __u32 hash = 0; + __u32 key, val; + void *l4 = NULL; + __u32 *cnt; + int ret; + + if ((void *)(eth + 1) > data_end) + return XDP_PASS; + + if (eth->h_proto == bpf_htons(ETH_P_IP)) { + struct iphdr *iph = (void *)(eth + 1); + + if ((void *)(iph + 1) > data_end) + return XDP_PASS; + l4_proto = iph->protocol; + l4 = (void *)(iph + 1); + } else if (eth->h_proto == bpf_htons(ETH_P_IPV6)) { + struct ipv6hdr *ip6h = (void *)(eth + 1); + + if ((void *)(ip6h + 1) > data_end) + return XDP_PASS; + l4_proto = ip6h->nexthdr; + l4 = (void *)(ip6h + 1); + } + + if (!l4) + return XDP_PASS; + + /* Filter on the configured protocol (map_xdp_setup key XDP_PROTO). + * When set, only process packets matching the requested L4 protocol. + */ + key = XDP_PROTO; + __s32 *proto_cfg = bpf_map_lookup_elem(&map_xdp_setup, &key); + + if (proto_cfg && *proto_cfg != 0 && l4_proto != (__u8)*proto_cfg) + return XDP_PASS; + + /* Filter on the configured port (map_xdp_setup key XDP_PORT). + * Only applies to protocols with ports (UDP, TCP). + */ + key = XDP_PORT; + __s32 *port_cfg = bpf_map_lookup_elem(&map_xdp_setup, &key); + + if (port_cfg && *port_cfg != 0) { + __u16 dest = get_dest_port(l4, data_end, l4_proto); + + if (!dest || bpf_ntohs(dest) != (__u16)*port_cfg) + return XDP_PASS; + } + + ret = bpf_xdp_metadata_rx_hash(ctx, &hash, &rss_type); + if (ret < 0) { + key = RSS_KEY_ERR_CNT; + cnt = bpf_map_lookup_elem(&map_rss, &key); + if (cnt) + __sync_fetch_and_add(cnt, 1); + return XDP_PASS; + } + + key = RSS_KEY_HASH; + bpf_map_update_elem(&map_rss, &key, &hash, BPF_ANY); + + key = RSS_KEY_TYPE; + val = (__u32)rss_type; + bpf_map_update_elem(&map_rss, &key, &val, BPF_ANY); + + key = RSS_KEY_PKT_CNT; + cnt = bpf_map_lookup_elem(&map_rss, &key); + if (cnt) + __sync_fetch_and_add(cnt, 1); + + return XDP_PASS; +} + +char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/net/lib/xdp_native.bpf.c b/tools/testing/selftests/net/lib/xdp_native.bpf.c index 64f05229ab24..ded3f896e622 100644 --- a/tools/testing/selftests/net/lib/xdp_native.bpf.c +++ b/tools/testing/selftests/net/lib/xdp_native.bpf.c @@ -268,6 +268,17 @@ static int xdp_mode_tx_handler(struct xdp_md *ctx, __u16 port) return XDP_PASS; } +static __always_inline __u16 csum_fold_helper(__u32 csum) +{ + csum = (csum & 0xffff) + (csum >> 16); + return ~((csum & 0xffff) + (csum >> 16)); +} + +static __always_inline __u16 csum_fold_udp_helper(__u32 csum) +{ + return csum_fold_helper(csum) ? : 0xffff; +} + static void *update_pkt(struct xdp_md *ctx, __s16 offset, __u32 *udp_csum) { void *data_end = (void *)(long)ctx->data_end; @@ -281,21 +292,22 @@ static void *update_pkt(struct xdp_md *ctx, __s16 offset, __u32 *udp_csum) if (eth->h_proto == bpf_htons(ETH_P_IP)) { struct iphdr *iph = data + sizeof(*eth); - __u16 total_len; if (iph + 1 > (struct iphdr *)data_end) return NULL; - iph->tot_len = bpf_htons(bpf_ntohs(iph->tot_len) + offset); - udph = (void *)eth + sizeof(*iph) + sizeof(*eth); if (!udph || udph + 1 > (struct udphdr *)data_end) return NULL; - len_new = bpf_htons(bpf_ntohs(udph->len) + offset); + len = iph->tot_len; + len_new = bpf_htons(bpf_ntohs(len) + offset); + iph->tot_len = len_new; + iph->check = csum_fold_helper( + bpf_csum_diff(&len, sizeof(len), &len_new, + sizeof(len_new), ~((__u32)iph->check))); } else if (eth->h_proto == bpf_htons(ETH_P_IPV6)) { struct ipv6hdr *ipv6h = data + sizeof(*eth); - __u16 payload_len; if (ipv6h + 1 > (struct ipv6hdr *)data_end) return NULL; @@ -304,33 +316,27 @@ static void *update_pkt(struct xdp_md *ctx, __s16 offset, __u32 *udp_csum) if (!udph || udph + 1 > (struct udphdr *)data_end) return NULL; - *udp_csum = ~((__u32)udph->check); - len = ipv6h->payload_len; len_new = bpf_htons(bpf_ntohs(len) + offset); ipv6h->payload_len = len_new; - - *udp_csum = bpf_csum_diff(&len, sizeof(len), &len_new, - sizeof(len_new), *udp_csum); - - len = udph->len; - len_new = bpf_htons(bpf_ntohs(udph->len) + offset); - *udp_csum = bpf_csum_diff(&len, sizeof(len), &len_new, - sizeof(len_new), *udp_csum); } else { return NULL; } + len = udph->len; + len_new = bpf_htons(bpf_ntohs(len) + offset); + + *udp_csum = ~((__u32)udph->check); + *udp_csum = bpf_csum_diff(&len, sizeof(len), &len_new, + sizeof(len_new), *udp_csum); + *udp_csum = bpf_csum_diff(&len, sizeof(len), &len_new, + sizeof(len_new), *udp_csum); + udph->len = len_new; return udph; } -static __u16 csum_fold_helper(__u32 csum) -{ - return ~((csum & 0xffff) + (csum >> 16)) ? : 0xffff; -} - static int xdp_adjst_tail_shrnk_data(struct xdp_md *ctx, __u16 offset, unsigned long hdr_len) { @@ -359,7 +365,7 @@ static int xdp_adjst_tail_shrnk_data(struct xdp_md *ctx, __u16 offset, return -1; udp_csum = bpf_csum_diff((__be32 *)tmp_buff, offset, 0, 0, udp_csum); - udph->check = (__u16)csum_fold_helper(udp_csum); + udph->check = (__u16)csum_fold_udp_helper(udp_csum); if (bpf_xdp_adjust_tail(ctx, 0 - offset) < 0) return -1; @@ -403,7 +409,7 @@ static int xdp_adjst_tail_grow_data(struct xdp_md *ctx, __u16 offset) return -1; udp_csum = bpf_csum_diff(0, 0, (__be32 *)tmp_buff, offset, udp_csum); - udph->check = (__u16)csum_fold_helper(udp_csum); + udph->check = (__u16)csum_fold_udp_helper(udp_csum); buff_len = bpf_xdp_get_buff_len(ctx); @@ -484,8 +490,7 @@ static int xdp_adjst_head_shrnk_data(struct xdp_md *ctx, __u64 hdr_len, return -1; udp_csum = bpf_csum_diff((__be32 *)tmp_buff, offset, 0, 0, udp_csum); - - udph->check = (__u16)csum_fold_helper(udp_csum); + udph->check = (__u16)csum_fold_udp_helper(udp_csum); if (bpf_xdp_load_bytes(ctx, 0, tmp_buff, MAX_ADJST_OFFSET) < 0) return -1; @@ -542,7 +547,7 @@ static int xdp_adjst_head_grow_data(struct xdp_md *ctx, __u64 hdr_len, return -1; udp_csum = bpf_csum_diff(0, 0, (__be32 *)data_buff, offset, udp_csum); - udph->check = (__u16)csum_fold_helper(udp_csum); + udph->check = (__u16)csum_fold_udp_helper(udp_csum); if (hdr_len > MAX_ADJST_OFFSET || hdr_len == 0) return -1; diff --git a/tools/testing/selftests/net/link_netns.py b/tools/testing/selftests/net/link_netns.py index aab043c59d69..6d1f863b6262 100755 --- a/tools/testing/selftests/net/link_netns.py +++ b/tools/testing/selftests/net/link_netns.py @@ -3,13 +3,14 @@ import time -from lib.py import ksft_run, ksft_exit, ksft_true +from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_true from lib.py import ip from lib.py import NetNS, NetNSEnter from lib.py import RtnlFamily LINK_NETNSID = 100 +LINK_NETNSID2 = 200 def test_event() -> None: @@ -32,6 +33,57 @@ def test_event() -> None: "Received unexpected link notification") +def test_event_all_nsid() -> None: + """NETLINK_LISTEN_ALL_NSID notifications: local events must not + carry nsid even with a self-referential mapping. Remote events + must carry the correct nsid.""" + + with NetNS() as ns1, NetNS() as ns2: + net1, net2 = str(ns1), str(ns2) + + with NetNSEnter(net1): + rtnl = RtnlFamily() + rtnl.ntf_listen_all_nsid() + rtnl.ntf_subscribe("rtnlgrp-link") + + # Case 1: no nsid assigned, local event, no nsid expected. + ip("link add dummy-lo type dummy", ns=net1) + + # Case 2: self-referential nsid, local event, still no nsid. + ip(f"netns set {net1} {LINK_NETNSID}", ns=net1) + ip("link add dummy-sr type dummy", ns=net1) + + # Case 3: remote event, nsid present. + ip(f"netns set {net2} {LINK_NETNSID2}", ns=net1) + ip("link add dummy-re type dummy", ns=net2) + + # Collect the three newlink events, ignoring unrelated noise. + events = {} + for msg in rtnl.poll_ntf(duration=1): + if msg['name'] == 'getlink': + ifname = msg['msg'].get('ifname') + if ifname in ('dummy-lo', 'dummy-sr', 'dummy-re'): + events[ifname] = msg + if len(events) == 3: + break + + ksft_true('dummy-lo' in events, "missing local event") + ksft_true(events['dummy-lo'].get('nsid') is None, + "local event without nsid should not carry nsid") + + ksft_true('dummy-sr' in events, "missing self-ref event") + ksft_true(events['dummy-sr'].get('nsid') is None, + "local event with self-ref nsid should not carry nsid") + + ksft_true('dummy-re' in events, "missing remote event") + ksft_eq(events['dummy-re'].get('nsid'), LINK_NETNSID2, + "remote event should carry nsid") + + ip("link del dummy-lo", ns=net1) + ip("link del dummy-sr", ns=net1) + ip("link del dummy-re", ns=net2) + + def validate_link_netns(netns, ifname, link_netnsid) -> bool: link_info = ip(f"-d link show dev {ifname}", ns=netns, json=True) if not link_info: @@ -133,7 +185,12 @@ def test_peer_net() -> None: def main() -> None: - ksft_run([test_event, test_link_net, test_peer_net]) + ksft_run([ + test_event, + test_event_all_nsid, + test_link_net, + test_peer_net, + ]) ksft_exit() diff --git a/tools/testing/selftests/net/macvlan_mcast_shared_mac.sh b/tools/testing/selftests/net/macvlan_mcast_shared_mac.sh new file mode 100755 index 000000000000..ff5b89347247 --- /dev/null +++ b/tools/testing/selftests/net/macvlan_mcast_shared_mac.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test multicast delivery to macvlan bridge ports when the source MAC +# matches the macvlan's own MAC address (e.g., VRRP virtual MAC shared +# across multiple hosts). +# +# Topology: +# +# NS_SRC NS_BRIDGE +# veth_src (SHARED_MAC) <-----> veth_dst +# | +# +-- macvlan0 (bridge mode, SHARED_MAC) +# +# A multicast packet sent from NS_SRC with source MAC equal to +# macvlan0's MAC must still be delivered to macvlan0. + +source lib.sh + +SHARED_MAC="00:00:5e:00:01:01" +MCAST_ADDR="239.0.0.1" + +setup() { + setup_ns NS_SRC NS_BRIDGE + + ip -net "${NS_BRIDGE}" link add veth_dst type veth \ + peer name veth_src netns "${NS_SRC}" + + ip -net "${NS_SRC}" link set veth_src address "${SHARED_MAC}" + ip -net "${NS_SRC}" link set veth_src up + ip -net "${NS_SRC}" addr add 192.168.1.1/24 dev veth_src + + ip -net "${NS_BRIDGE}" link set veth_dst up + + ip -net "${NS_BRIDGE}" link add macvlan0 link veth_dst \ + type macvlan mode bridge + ip -net "${NS_BRIDGE}" link set macvlan0 address "${SHARED_MAC}" + ip -net "${NS_BRIDGE}" link set macvlan0 up + ip -net "${NS_BRIDGE}" addr add 192.168.1.2/24 dev macvlan0 + + # Accept all multicast so the mc_filter passes for any group. + ip -net "${NS_BRIDGE}" link set macvlan0 allmulticast on +} + +cleanup() { + rm -f "${CAPFILE}" "${CAPOUT}" + cleanup_ns "${NS_SRC}" "${NS_BRIDGE}" +} + +test_macvlan_mcast_shared_mac() { + CAPFILE=$(mktemp) + CAPOUT=$(mktemp) + + echo "Testing multicast delivery to macvlan with shared source MAC" + + # Listen for one ICMP packet on macvlan0. + timeout 5s ip netns exec "${NS_BRIDGE}" \ + tcpdump -i macvlan0 -c 1 -w "${CAPFILE}" icmp &> "${CAPOUT}" & + local pid=$! + if ! slowwait 1 grep -qs "listening" "${CAPOUT}"; then + echo "[FAIL] tcpdump did not start listening" + return "${ksft_fail}" + fi + + # Send multicast ping from NS_SRC; source MAC equals macvlan0's MAC. + ip netns exec "${NS_SRC}" \ + ping -W 0.1 -c 3 -I veth_src "${MCAST_ADDR}" &> /dev/null + + wait "${pid}" + + local count + count=$(tcpdump -r "${CAPFILE}" 2>/dev/null | wc -l) + if [[ "${count}" -ge 1 ]]; then + echo "[ OK ]" + return "${ksft_pass}" + else + echo "[FAIL] expected at least 1 ICMP packet on macvlan0," \ + "got ${count}" + return "${ksft_fail}" + fi +} + +if [ ! -x "$(command -v tcpdump)" ]; then + echo "SKIP: Could not run test without tcpdump tool" + exit "${ksft_skip}" +fi + +trap cleanup EXIT + +setup +test_macvlan_mcast_shared_mac + +exit $? diff --git a/tools/testing/selftests/net/mptcp/diag.sh b/tools/testing/selftests/net/mptcp/diag.sh index d847ff1737c3..27cbda68144e 100755 --- a/tools/testing/selftests/net/mptcp/diag.sh +++ b/tools/testing/selftests/net/mptcp/diag.sh @@ -322,6 +322,33 @@ wait_connected() done } +chk_sndbuf() +{ + local server_sndbuf client_sndbuf msg + local port=${1} + + msg="....chk sndbuf server/client" + server_sndbuf=$(ss -N "${ns}" -inmHM "sport" "${port}" | \ + sed -n 's/.*tb\([0-9]\+\).*/\1/p') + client_sndbuf=$(ss -N "${ns}" -inmHM "dport" "${port}" | \ + sed -n 's/.*tb\([0-9]\+\).*/\1/p') + + mptcp_lib_print_title "${msg}" + if [ -z "${server_sndbuf}" ] || [ -z "${client_sndbuf}" ]; then + mptcp_lib_pr_fail "sndbuf S=${server_sndbuf} C=${client_sndbuf}" + mptcp_lib_result_fail "${msg}" + ret=${KSFT_FAIL} + elif [ "${server_sndbuf}" != "${client_sndbuf}" ]; then + mptcp_lib_pr_fail "sndbuf S=${server_sndbuf} != C=${client_sndbuf}" + mptcp_lib_result_fail "${msg}" + ret=${KSFT_FAIL} + else + mptcp_lib_pr_ok + mptcp_lib_result_pass "${msg}" + fi +} + + trap cleanup EXIT mptcp_lib_ns_init ns @@ -341,6 +368,7 @@ echo "b" | \ 127.0.0.1 >/dev/null & wait_connected $ns 10000 chk_msk_nr 2 "after MPC handshake" +chk_sndbuf 10000 chk_last_time_info 10000 chk_msk_remote_key_nr 2 "....chk remote_key" chk_msk_fallback_nr 0 "....chk no fallback" diff --git a/tools/testing/selftests/net/mptcp/mptcp_connect.c b/tools/testing/selftests/net/mptcp/mptcp_connect.c index cbe573c4ab3a..178d98d91fea 100644 --- a/tools/testing/selftests/net/mptcp/mptcp_connect.c +++ b/tools/testing/selftests/net/mptcp/mptcp_connect.c @@ -381,6 +381,9 @@ static int sock_connect_mptcp(const char * const remoteaddr, hints.ai_family = pf; + /* Keep the resolved address alive for the whole execution: it is + * used again when reconnecting, and will be released at exit time. + */ xgetaddrinfo(remoteaddr, port, &hints, &addr); for (a = addr; a; a = a->ai_next) { sock = socket(a->ai_family, a->ai_socktype, proto); @@ -421,7 +424,6 @@ static int sock_connect_mptcp(const char * const remoteaddr, sock = -1; } - freeaddrinfo(addr); if (sock != -1) SOCK_TEST_TCPULP(sock, proto); return sock; @@ -1264,7 +1266,7 @@ static void apply_cmsg_types(int fd, const struct cfg_cmsg_types *cmsg) static void parse_cmsg_types(const char *type) { - char *next = strchr(type, ','); + const char *next = strchr(type, ','); unsigned int len = 0; cfg_cmsg_types.cmsg_enabled = 1; @@ -1292,7 +1294,7 @@ static void parse_cmsg_types(const char *type) static void parse_setsock_options(const char *name) { - char *next = strchr(name, ','); + const char *next = strchr(name, ','); unsigned int len = 0; if (next) { diff --git a/tools/testing/selftests/net/mptcp/mptcp_connect.sh b/tools/testing/selftests/net/mptcp/mptcp_connect.sh index a6447f7a31fe..7a2a851fa0ad 100755 --- a/tools/testing/selftests/net/mptcp/mptcp_connect.sh +++ b/tools/testing/selftests/net/mptcp/mptcp_connect.sh @@ -212,8 +212,8 @@ if $checksum; then fi if $capture; then - rndh="${ns1:4}" - mptcp_lib_pr_info "Packet capture files will have this prefix: ${rndh}-" + capprefix="mptcp_connect-${ns1:4}" + mptcp_lib_pr_info "pcap will have this prefix: ${capprefix}-" fi set_ethtool_flags() { @@ -372,8 +372,8 @@ do_transfer() capuser="-Z $SUDO_USER" fi - local capfile="${rndh}-${connector_ns:0:3}-${listener_ns:0:3}-${cl_proto}-${srv_proto}-${connect_addr}-${port}" - local capopt="-i any -s 65535 -B 32768 ${capuser}" + local capfile="${capprefix}-${connector_ns:0:3}-${listener_ns:0:3}-${cl_proto}-${srv_proto}-${connect_addr}-${port}" + local capopt="-i any -s 128 -B 32768 ${capuser}" ip netns exec ${listener_ns} tcpdump ${capopt} \ -w "${capfile}-listener.pcap" >> "${capout}" 2>&1 & @@ -401,7 +401,7 @@ do_transfer() mptcp_lib_wait_local_port_listen "${listener_ns}" "${port}" local start - start=$(date +%s%3N) + start=$(date +%s%N) ip netns exec ${connector_ns} \ ./mptcp_connect -t ${timeout_poll} -p $port -s ${cl_proto} \ $extra_args $connect_addr < "$cin" > "$cout" & @@ -423,7 +423,7 @@ do_transfer() fi local stop - stop=$(date +%s%3N) + stop=$(date +%s%N) if $capture; then sleep 1 @@ -439,7 +439,7 @@ do_transfer() fi local duration - duration=$((stop-start)) + duration=$(((stop-start) / 1000000)) printf "(duration %05sms) " "${duration}" if [ ${rets} -ne 0 ] || [ ${retc} -ne 0 ] || [ ${timeout_pid} -ne 0 ]; then mptcp_lib_pr_fail "client exit code $retc, server $rets" diff --git a/tools/testing/selftests/net/mptcp/mptcp_diag.c b/tools/testing/selftests/net/mptcp/mptcp_diag.c index 5e222ba977e4..3b8d2c8a6216 100644 --- a/tools/testing/selftests/net/mptcp/mptcp_diag.c +++ b/tools/testing/selftests/net/mptcp/mptcp_diag.c @@ -377,7 +377,8 @@ static void get_subflow_info(char *subflow_addrs) int ret; int fd; - ret = sscanf(subflow_addrs, "%[^:]:%d %[^:]:%d", saddr, &sport, daddr, &dport); + ret = sscanf(subflow_addrs, "%63[^:]:%d %63[^:]:%d", + saddr, &sport, daddr, &dport); if (ret != 4) die_perror("IP PORT Pairs has style problems!"); diff --git a/tools/testing/selftests/net/mptcp/mptcp_join.sh b/tools/testing/selftests/net/mptcp/mptcp_join.sh index dc1f200aaa81..18ce7136a2b0 100755 --- a/tools/testing/selftests/net/mptcp/mptcp_join.sh +++ b/tools/testing/selftests/net/mptcp/mptcp_join.sh @@ -63,6 +63,7 @@ unset fastclose unset fullmesh unset speed unset bind_addr +unset ifaces_nr unset join_syn_rej unset join_csum_ns1 unset join_csum_ns2 @@ -74,6 +75,14 @@ unset join_syn_tx unset join_create_err unset join_bind_err unset join_connect_err +unset join_synack_no_mpjoin +unset join_ack_no_mpjoin +unset join_ack_no_ctx +unset join_not_established +unset join_no_id_found + +unset rst_md5sig +unset rst_dss unset fb_ns1 unset fb_ns2 @@ -86,6 +95,10 @@ unset fb_mpc_data unset fb_md5_sig unset fb_dss +unset add_addr_tx_nr +unset add_addr_echo_tx_nr +unset add_addr_drop_tx_nr + # generated using "nfbpf_compile '(ip && (ip[54] & 0xf0) == 0x30) || # (ip6 && (ip6[74] & 0xf0) == 0x30)'" CBPF_MPTCP_SUBOPTION_ADD_ADDR="14, @@ -104,6 +117,24 @@ CBPF_MPTCP_SUBOPTION_ADD_ADDR="14, 6 0 0 65535, 6 0 0 0" +# IPv4: TCP hdr of 48B, a first suboption of 12B (DACK8), the RM_ADDR suboption +# generated using "nfbpf_compile '(ip[32] & 0xf0) == 0xc0 && ip[53] == 0x0c && +# (ip[66] & 0xf0) == 0x40'" +CBPF_MPTCP_SUBOPTION_RM_ADDR="13, + 48 0 0 0, + 84 0 0 240, + 21 0 9 64, + 48 0 0 32, + 84 0 0 240, + 21 0 6 192, + 48 0 0 53, + 21 0 4 12, + 48 0 0 66, + 84 0 0 240, + 21 0 1 64, + 6 0 0 65535, + 6 0 0 0" + init_partial() { capout=$(mktemp) @@ -128,7 +159,7 @@ init_partial() # ns1eth4 ns2eth4 local i - for i in $(seq 1 4); do + for i in $(seq 1 "${ifaces_nr:-4}"); do ip link add ns1eth$i netns "$ns1" type veth peer name ns2eth$i netns "$ns2" ip -net "$ns1" addr add 10.0.$i.1/24 dev ns1eth$i ip -net "$ns1" addr add dead:beef:$i::1/64 dev ns1eth$i nodad @@ -147,7 +178,7 @@ init_partial() init_shapers() { local i - for i in $(seq 1 4); do + for i in $(seq 1 "${ifaces_nr:-4}"); do tc -n $ns1 qdisc add dev ns1eth$i root netem rate 20mbit delay 1ms tc -n $ns2 qdisc add dev ns2eth$i root netem rate 20mbit delay 1ms done @@ -494,6 +525,19 @@ reset_with_tcp_filter() fi } +# For kernel supporting limits above 8 +# $1: title ; $2,4: addrs limit ns1,2 ; $3,5: subflows limit ns1,2 +reset_with_high_limits() +{ + reset "${1}" || return 1 + + if ! pm_nl_set_limits "${ns1}" "${2}" "${3}" 2>/dev/null || + ! pm_nl_set_limits "${ns2}" "${4}" "${5}" 2>/dev/null; then + mark_as_skipped "unable to set the limits to ${*:2}" + return 1 + fi +} + # $1: err msg fail_test() { @@ -548,7 +592,7 @@ check_transfer() mv "$tmpfile" "$out" tmpfile="" fi - cmp -l "$in" "$out" | while read -r i a b; do + while read -r i a b; do local sum=$((0${a} + 0${b})) if [ $check_invert -eq 0 ] || [ $sum -ne $((0xff)) ]; then fail_test "$what does not match (in, out):" @@ -559,7 +603,7 @@ check_transfer() else print_info "$what has inverted byte at ${i}" fi - done + done < <(cmp -l "$in" "$out") return 0 } @@ -935,7 +979,7 @@ cond_start_capture() capfile=$(printf "mp_join-%02u-%s.pcap" "$MPTCP_LIB_TEST_COUNTER" "$ns") echo "Capturing traffic for test $MPTCP_LIB_TEST_COUNTER into $capfile" - ip netns exec "$ns" tcpdump -i any -s 65535 -B 32768 $capuser -w "$capfile" > "$capout" 2>&1 & + ip netns exec "$ns" tcpdump -i any -s 128 -B 32768 $capuser -w "$capfile" > "$capout" 2>&1 & cappid=$! sleep 1 @@ -1317,6 +1361,8 @@ chk_rst_nr() local rst_tx=$1 local rst_rx=$2 local ns_invert=${3:-""} + local md5sig=${rst_md5sig:-0} + local dss=${rst_dss:-0} local count local ns_tx=$ns1 local ns_rx=$ns2 @@ -1353,6 +1399,21 @@ chk_rst_nr() else print_ok fi + + # MPTCP_RST_EMPTCP reset-event counters; default 0, gated on + # availability. Fixed namespaces: MD5SigReset fires on the listener + # (server), DssReset on the data receiver (client). + count=$(mptcp_lib_get_counter ${ns1} "MPTcpExtMD5SigReset") + if [ -n "$count" ] && [ "$count" != "$md5sig" ]; then + print_check "MD5SigReset" + fail_test "got $count MD5SigReset expected $md5sig" + fi + + count=$(mptcp_lib_get_counter ${ns2} "MPTcpExtDssReset") + if [ -n "$count" ] && [ "$count" != "$dss" ]; then + print_check "DssReset" + fail_test "got $count DssReset expected $dss" + fi } chk_infi_nr() @@ -1551,6 +1612,11 @@ chk_join_nr() local rst_nr=${join_rst_nr:-0} local infi_nr=${join_infi_nr:-0} local corrupted_pkts=${join_corrupted_pkts:-0} + local synack_no_mpjoin=${join_synack_no_mpjoin:-0} + local ack_no_mpjoin=${join_ack_no_mpjoin:-0} + local ack_no_ctx=${join_ack_no_ctx:-0} + local not_established=${join_not_established:-0} + local no_id_found=${join_no_id_found:-0} local rc=${KSFT_PASS} local count local with_cookie @@ -1619,6 +1685,44 @@ chk_join_nr() fail_test "got $count JOIN[s] syn rejected expected $syn_rej" fi + # Per-event MPTCP_RST_EMPTCP JOIN counters; default 0, gated on + # availability. Fixed namespaces: the *SynAck* one fires on the + # client receiving the SYN/ACK, the others on the server. + count=$(mptcp_lib_get_counter ${ns2} "MPTcpExtMPJoinSynAckNoMPJoin") + if [ -n "$count" ] && [ "$count" != "$synack_no_mpjoin" ]; then + rc=${KSFT_FAIL} + print_check "synack no mpjoin" + fail_test "got $count JOIN[s] synack no mpjoin expected $synack_no_mpjoin" + fi + + count=$(mptcp_lib_get_counter ${ns1} "MPTcpExtMPJoinAckNoMPJoin") + if [ -n "$count" ] && [ "$count" != "$ack_no_mpjoin" ]; then + rc=${KSFT_FAIL} + print_check "ack no mpjoin" + fail_test "got $count JOIN[s] ack no mpjoin expected $ack_no_mpjoin" + fi + + count=$(mptcp_lib_get_counter ${ns1} "MPTcpExtMPJoinAckNoCtx") + if [ -n "$count" ] && [ "$count" != "$ack_no_ctx" ]; then + rc=${KSFT_FAIL} + print_check "ack no ctx" + fail_test "got $count JOIN[s] ack no ctx expected $ack_no_ctx" + fi + + count=$(mptcp_lib_get_counter ${ns1} "MPTcpExtMPJoinNotEstablished") + if [ -n "$count" ] && [ "$count" != "$not_established" ]; then + rc=${KSFT_FAIL} + print_check "join not established" + fail_test "got $count JOIN[s] not established expected $not_established" + fi + + count=$(mptcp_lib_get_counter ${ns1} "MPTcpExtMPJoinNoIdFound") + if [ -n "$count" ] && [ "$count" != "$no_id_found" ]; then + rc=${KSFT_FAIL} + print_check "join no id found" + fail_test "got $count JOIN[s] no id found expected $no_id_found" + fi + print_results "join Rx" ${rc} join_syn_tx="${join_syn_tx:-${syn_nr}}" \ @@ -1678,6 +1782,9 @@ chk_add_nr() local ack_nr=$port_nr local mis_syn_nr=0 local mis_ack_nr=0 + local add_tx_nr=${add_addr_tx_nr:-${add_nr}} + local echo_tx_nr=${add_addr_echo_tx_nr:-${echo_nr}} + local drop_tx_nr=${add_addr_drop_tx_nr:-0} local ns_tx=$ns1 local ns_rx=$ns2 local tx="" @@ -1779,34 +1886,25 @@ chk_add_nr() print_ok fi fi -} - -chk_add_tx_nr() -{ - local add_tx_nr=$1 - local echo_tx_nr=$2 - local count - print_check "add addr tx" - count=$(mptcp_lib_get_counter ${ns1} "MPTcpExtAddAddrTx") - if [ -z "$count" ]; then - print_skip + count=$(mptcp_lib_get_counter ${ns_tx} "MPTcpExtAddAddrTx") # Tolerate more ADD_ADDR then expected (if any), due to retransmissions - elif [ "$count" != "$add_tx_nr" ] && - { [ "$add_tx_nr" -eq 0 ] || [ "$count" -lt "$add_tx_nr" ]; }; then + if [ -n "$count" ] && [ "$count" != "$add_tx_nr" ] && + { [ "$add_tx_nr" -eq 0 ] || [ "$count" -lt "$add_tx_nr" ]; }; then + print_check "add addr tx" fail_test "got $count ADD_ADDR[s] TX, expected $add_tx_nr" - else - print_ok fi - print_check "add addr echo tx" - count=$(mptcp_lib_get_counter ${ns2} "MPTcpExtEchoAddTx") - if [ -z "$count" ]; then - print_skip - elif [ "$count" != "$echo_tx_nr" ]; then + count=$(mptcp_lib_get_counter ${ns_rx} "MPTcpExtEchoAddTx") + if [ -n "$count" ] && [ "$count" != "$echo_tx_nr" ]; then + print_check "add addr echo tx" fail_test "got $count ADD_ADDR echo[s] TX, expected $echo_tx_nr" - else - print_ok + fi + + count=$(mptcp_lib_get_counter ${ns_tx} "MPTcpExtAddAddrTxDrop") + if [ -n "$count" ] && [ "$count" != "$drop_tx_nr" ]; then + print_check "add addr tx drop" + fail_test "got $count ADD_ADDR drop[s] TX, expected $drop_tx_nr" fi } @@ -2219,7 +2317,6 @@ signal_address_tests() pm_nl_add_endpoint $ns1 10.0.2.1 flags signal run_tests $ns1 $ns2 10.0.1.1 chk_join_nr 0 0 0 - chk_add_tx_nr 1 1 chk_add_nr 1 1 fi @@ -2330,6 +2427,31 @@ signal_address_tests() chk_add_nr 4 4 fi fi + + # signalled address belongs to the client, where a TCP-only + # listener is bound at it: the client's MP_JOIN routes locally + # to the listener and receives a SYN/ACK without MP_JOIN. + # MPJoinSynAckNoMPJoin increments on the client side. + if reset "signal address, TCP-only listener on client"; then + local extra_bind + local port + + pm_nl_set_limits $ns1 0 1 + pm_nl_set_limits $ns2 1 1 + pm_nl_add_endpoint $ns1 10.0.2.2 flags signal + + port=$(get_port) + ip netns exec ${ns2} ./mptcp_connect -l -t -1 -p "$port" \ + -s TCP 10.0.2.2 & + extra_bind=$! + mptcp_lib_wait_local_port_listen "$ns2" "$port" + + run_tests $ns1 $ns2 10.0.1.1 + join_synack_no_mpjoin=1 join_syn_tx=1 \ + chk_join_nr 0 0 0 + + kill ${extra_bind} 2>/dev/null + fi } laminar_endp_tests() @@ -2497,8 +2619,8 @@ add_addr_timeout_tests() speed=slow \ run_tests $ns1 $ns2 10.0.1.1 chk_join_nr 1 1 1 - chk_add_tx_nr 4 4 - chk_add_nr 4 0 + add_addr_echo_tx_nr=4 \ + chk_add_nr 4 0 fi # add_addr timeout IPv6 @@ -2509,7 +2631,8 @@ add_addr_timeout_tests() speed=slow \ run_tests $ns1 $ns2 dead:beef:1::1 chk_join_nr 1 1 1 - chk_add_nr 4 0 + add_addr_echo_tx_nr=4 \ + chk_add_nr 4 0 fi # signal addresses timeout @@ -2521,7 +2644,8 @@ add_addr_timeout_tests() speed=10 \ run_tests $ns1 $ns2 10.0.1.1 chk_join_nr 2 2 2 - chk_add_nr 8 0 + add_addr_echo_tx_nr=8 \ + chk_add_nr 8 0 fi # signal invalid addresses timeout @@ -2534,7 +2658,8 @@ add_addr_timeout_tests() run_tests $ns1 $ns2 10.0.1.1 join_syn_tx=2 \ chk_join_nr 1 1 1 - chk_add_nr 8 0 + add_addr_echo_tx_nr=7 \ + chk_add_nr 8 0 fi } @@ -2608,6 +2733,19 @@ remove_tests() chk_rst_nr 0 0 fi + # signal+subflow with limits, remove + if reset "remove signal+subflow with limits"; then + pm_nl_set_limits $ns1 0 0 + pm_nl_add_endpoint $ns1 10.0.2.1 flags signal,subflow + pm_nl_set_limits $ns2 0 0 + addr_nr_ns1=-1 speed=slow \ + run_tests $ns1 $ns2 10.0.1.1 + chk_join_nr 0 0 0 + chk_add_nr 1 1 + chk_rm_nr 1 0 invert + chk_rst_nr 0 0 + fi + # addresses remove if reset "remove addresses"; then pm_nl_set_limits $ns1 3 3 @@ -3153,6 +3291,17 @@ add_addr_ports_tests() chk_add_nr 1 1 1 fi + # signal address v6 with port + if reset "signal address v6 with port" && + continue_if mptcp_lib_has_file '/proc/sys/net/mptcp/add_addr_v6_port_drop_ts'; then + pm_nl_set_limits $ns1 0 1 + pm_nl_set_limits $ns2 1 1 + pm_nl_add_endpoint $ns1 dead:beef:2::1 flags signal port 10100 + run_tests $ns1 $ns2 dead:beef:1::1 + chk_join_nr 1 1 1 + chk_add_nr 1 1 1 + fi + # subflow and signal with port if reset "subflow and signal with port"; then pm_nl_add_endpoint $ns1 10.0.2.1 flags signal port 10100 @@ -3247,6 +3396,21 @@ add_addr_ports_tests() chk_mpc_endp_attempt ${retl} 1 fi + + # first signal address drops, second one still progresses + if reset "signal addr list progresses after tx drop"; then + pm_nl_set_limits $ns1 0 2 + pm_nl_set_limits $ns2 1 0 + ip netns exec $ns1 sysctl -q net.mptcp.add_addr_v6_port_drop_ts=0 2>/dev/null || true + ip netns exec $ns1 sysctl -q net.ipv4.tcp_timestamps=1 + ip netns exec $ns2 sysctl -q net.ipv4.tcp_timestamps=1 + + pm_nl_add_endpoint $ns1 dead:beef:2::1 flags signal port 10100 + pm_nl_add_endpoint $ns1 dead:beef:3::1 flags signal + run_tests $ns1 $ns2 dead:beef:1::1 + add_addr_drop_tx_nr=1 \ + chk_add_nr 1 1 0 + fi } bind_tests() @@ -3638,6 +3802,21 @@ fullmesh_tests() chk_prio_nr 0 1 1 0 chk_rm_nr 0 1 fi + + # fullmesh in 8x8 to create 63 additional subflows + if ifaces_nr=8 reset_with_high_limits "fullmesh 8x8" 64 64 64 64; then + # higher chance to lose ADD_ADDR: allow retransmissions + ip netns exec $ns1 sysctl -q net.mptcp.add_addr_timeout=1 + local i + for i in $(seq 1 8); do + pm_nl_add_endpoint $ns2 10.0.$i.2 flags subflow,fullmesh + pm_nl_add_endpoint $ns1 10.0.$i.1 flags signal + done + speed=slow \ + run_tests $ns1 $ns2 10.0.1.1 + chk_join_nr 63 63 63 + fi + } fastclose_tests() @@ -4038,6 +4217,10 @@ userspace_tests() chk_rm_nr 0 1 chk_mptcp_info subflows 0 subflows 0 chk_subflows_total 1 1 + # check counters are not affected by errors at creation time + userspace_pm_add_sf $ns2 10.0.12.2 10 2>/dev/null + chk_mptcp_info subflows 0 subflows 0 + chk_subflows_total 1 1 kill_events_pids mptcp_lib_kill_group_wait $tests_pid fi @@ -4217,6 +4400,14 @@ endpoint_tests() chk_subflow_nr "after no reject" 3 chk_mptcp_info subflows 2 subflows 2 + # To make sure RM_ADDR are sent over a different subflow, but + # allow the rest to quickly and cleanly close the subflow + local ipt=1 + ip netns exec "${ns2}" ${iptables} -I OUTPUT -s "10.0.1.2" \ + -p tcp -m tcp --tcp-option 30 \ + -m bpf --bytecode \ + "$CBPF_MPTCP_SUBOPTION_RM_ADDR" \ + -j DROP || ipt=0 local i for i in $(seq 3); do pm_nl_del_endpoint $ns2 1 10.0.1.2 @@ -4229,6 +4420,7 @@ endpoint_tests() chk_subflow_nr "after re-add id 0 ($i)" 3 chk_mptcp_info subflows 3 subflows 3 done + [ ${ipt} = 1 ] && ip netns exec "${ns2}" ${iptables} -D OUTPUT 1 mptcp_lib_kill_group_wait $tests_pid @@ -4288,19 +4480,28 @@ endpoint_tests() chk_mptcp_info subflows 2 subflows 2 chk_mptcp_info add_addr_signal 2 add_addr_accepted 2 + # To make sure RM_ADDR are sent over a different subflow, but + # allow the rest to quickly and cleanly close the subflow + local ipt=1 + ip netns exec "${ns1}" ${iptables} -I OUTPUT -s "10.0.1.1" \ + -p tcp -m tcp --tcp-option 30 \ + -m bpf --bytecode \ + "$CBPF_MPTCP_SUBOPTION_RM_ADDR" \ + -j DROP || ipt=0 pm_nl_del_endpoint $ns1 42 10.0.1.1 sleep 0.5 chk_subflow_nr "after delete ID 0" 2 chk_mptcp_info subflows 2 subflows 2 chk_mptcp_info add_addr_signal 2 add_addr_accepted 2 + [ ${ipt} = 1 ] && ip netns exec "${ns1}" ${iptables} -D OUTPUT 1 - pm_nl_add_endpoint $ns1 10.0.1.1 id 99 flags signal + pm_nl_add_endpoint $ns1 10.0.1.1 id 42 flags signal wait_mpj 4 chk_subflow_nr "after re-add ID 0" 3 chk_mptcp_info subflows 3 subflows 3 chk_mptcp_info add_addr_signal 3 add_addr_accepted 2 - pm_nl_del_endpoint $ns1 99 10.0.1.1 + pm_nl_del_endpoint $ns1 42 10.0.1.1 sleep 0.5 chk_subflow_nr "after re-delete ID 0" 2 chk_mptcp_info subflows 2 subflows 2 diff --git a/tools/testing/selftests/net/mptcp/mptcp_lib.sh b/tools/testing/selftests/net/mptcp/mptcp_lib.sh index 5fea7e7df628..b9d14647f401 100644 --- a/tools/testing/selftests/net/mptcp/mptcp_lib.sh +++ b/tools/testing/selftests/net/mptcp/mptcp_lib.sh @@ -28,7 +28,7 @@ declare -rx MPTCP_LIB_AF_INET6=10 MPTCP_LIB_SUBTESTS=() MPTCP_LIB_SUBTESTS_DUPLICATED=0 MPTCP_LIB_SUBTEST_FLAKY=0 -MPTCP_LIB_SUBTESTS_LAST_TS_MS= +MPTCP_LIB_SUBTESTS_LAST_TS_NS= MPTCP_LIB_TEST_COUNTER=0 MPTCP_LIB_TEST_FORMAT="%02u %-50s" MPTCP_LIB_IP_MPTCP=0 @@ -108,12 +108,14 @@ mptcp_lib_pr_info() { mptcp_lib_pr_nstat() { local ns="${1}" - local hist="/tmp/${ns}.out" + local cache="/tmp/${ns}.out" + local hist="/tmp/${ns}.nstat" - if [ -f "${hist}" ]; then - awk '$2 != 0 { print " "$0 }' "${hist}" + if [ -f "${cache}" ]; then + awk '$2 != 0 { print " "$0 }' "${cache}" else - ip netns exec "${ns}" nstat -as | grep Tcp + NSTAT_HISTORY="${hist}" ip netns exec "${ns}" nstat -s | + grep Tcp fi } @@ -236,7 +238,7 @@ mptcp_lib_kversion_ge() { } mptcp_lib_subtests_last_ts_reset() { - MPTCP_LIB_SUBTESTS_LAST_TS_MS="$(date +%s%3N)" + MPTCP_LIB_SUBTESTS_LAST_TS_NS="$(date +%s%N)" } mptcp_lib_subtests_last_ts_reset @@ -255,7 +257,7 @@ __mptcp_lib_result_check_duplicated() { __mptcp_lib_result_add() { local result="${1}" local time="time=" - local ts_prev_ms + local ts_prev_ns shift local id=$((${#MPTCP_LIB_SUBTESTS[@]} + 1)) @@ -265,9 +267,9 @@ __mptcp_lib_result_add() { # not to add two '#' [[ "${*}" != *"#"* ]] && time="# ${time}" - ts_prev_ms="${MPTCP_LIB_SUBTESTS_LAST_TS_MS}" + ts_prev_ns="${MPTCP_LIB_SUBTESTS_LAST_TS_NS}" mptcp_lib_subtests_last_ts_reset - time+="$((MPTCP_LIB_SUBTESTS_LAST_TS_MS - ts_prev_ms))ms" + time+="$(((MPTCP_LIB_SUBTESTS_LAST_TS_NS - ts_prev_ns) / 1000000))ms" MPTCP_LIB_SUBTESTS+=("${result} ${id} - ${KSFT_TEST}: ${*} ${time}") } @@ -414,19 +416,21 @@ mptcp_lib_nstat_get() { } # $1: ns, $2: MIB counter -# Get the counter from the history (mptcp_lib_nstat_{init,get}()) if available. -# If not, get the counter from nstat ignoring any history. +# Get the counter from the cache (mptcp_lib_nstat_{init,get}()) if available. +# If not, get the counter from nstat ignoring any cache, but using the history. mptcp_lib_get_counter() { local ns="${1}" local counter="${2}" - local hist="/tmp/${ns}.out" + local cache="/tmp/${ns}.out" + local hist="/tmp/${ns}.nstat" local count - if [[ -s "${hist}" && "${counter}" == *"Tcp"* ]]; then - count=$(awk "/^${counter} / {print \$2; exit}" "${hist}") + if [[ -s "${cache}" && "${counter}" == *"Tcp"* ]]; then + count=$(awk "/^${counter} / {print \$2; exit}" "${cache}") else - count=$(ip netns exec "${ns}" nstat -asz "${counter}" | - awk 'NR==1 {next} {print $2}') + count=$(NSTAT_HISTORY="${hist}" ip netns exec "${ns}" \ + nstat -sz "${counter}" | + awk 'NR==1 {next} {print $2}') fi if [ -z "${count}" ]; then mptcp_lib_fail_if_expected_feature "${counter} counter" @@ -474,20 +478,24 @@ mptcp_lib_wait_local_port_listen() { wait_local_port_listen "${@}" "tcp" } +# $1: error file, $2: cmd, $3: expected msg, [$4: expected error] mptcp_lib_check_output() { local err="${1}" local cmd="${2}" local expected="${3}" + local exp_error="${4:-0}" local cmd_ret=0 local out - if ! out=$(${cmd} 2>"${err}"); then - cmd_ret=${?} - fi + out=$(${cmd} 2>"${err}") || cmd_ret=1 - if [ ${cmd_ret} -ne 0 ]; then - mptcp_lib_pr_fail "command execution '${cmd}' stderr" - cat "${err}" + if [ "${cmd_ret}" != "${exp_error}" ]; then + mptcp_lib_pr_fail "unexpected returned code for '${cmd}', info:" + if [ "${exp_error}" = 0 ]; then + cat "${err}" + else + echo "${out}" + fi return 2 elif [ "${out}" = "${expected}" ]; then return 0 diff --git a/tools/testing/selftests/net/mptcp/mptcp_sockopt.sh b/tools/testing/selftests/net/mptcp/mptcp_sockopt.sh index ab8bce06b262..e850a87429b6 100755 --- a/tools/testing/selftests/net/mptcp/mptcp_sockopt.sh +++ b/tools/testing/selftests/net/mptcp/mptcp_sockopt.sh @@ -355,10 +355,10 @@ sin=$(mktemp) sout=$(mktemp) cin=$(mktemp) cout=$(mktemp) +trap cleanup EXIT init make_file "$cin" "client" 1 make_file "$sin" "server" 1 -trap cleanup EXIT mptcp_lib_subtests_last_ts_reset run_tests $ns1 $ns2 10.0.1.1 diff --git a/tools/testing/selftests/net/mptcp/pm_netlink.sh b/tools/testing/selftests/net/mptcp/pm_netlink.sh index 123d9d7a0278..21bfe1311f11 100755 --- a/tools/testing/selftests/net/mptcp/pm_netlink.sh +++ b/tools/testing/selftests/net/mptcp/pm_netlink.sh @@ -66,6 +66,15 @@ get_limits() { fi } +get_limits_nb() { + if mptcp_lib_is_ip_mptcp; then + ip -n "${ns1}" mptcp limits | awk '{ print $2" "$4 }' + else + ip netns exec "${ns1}" ./pm_nl_ctl limits | \ + awk '{ printf "%s ", $2 }' + fi +} + format_endpoints() { mptcp_lib_pm_nl_format_endpoints "${@}" } @@ -122,10 +131,12 @@ check() local cmd="$1" local expected="$2" local msg="$3" + local exp_error="$4" local rc=0 mptcp_lib_print_title "$msg" - mptcp_lib_check_output "${err}" "${cmd}" "${expected}" || rc=${?} + mptcp_lib_check_output "${err}" "${cmd}" "${expected}" "${exp_error}" || + rc=${?} if [ ${rc} -eq 2 ]; then mptcp_lib_result_fail "${msg} # error ${rc}" ret=${KSFT_FAIL} @@ -158,53 +169,62 @@ check "show_endpoints" \ "3,10.0.1.3,signal backup")" "dump addrs" del_endpoint 2 -check "get_endpoint 2" "" "simple del addr" +check "get_endpoint 2" "" "simple del addr" 1 check "show_endpoints" \ "$(format_endpoints "1,10.0.1.1" \ "3,10.0.1.3,signal backup")" "dump addrs after del" +add_endpoint 10.0.1.2 id 2 add_endpoint 10.0.1.3 2>/dev/null -check "get_endpoint 4" "" "duplicate addr" +check "get_endpoint 4" "" "duplicate addr" 1 add_endpoint 10.0.1.4 flags signal check "get_endpoint 4" "$(format_endpoints "4,10.0.1.4,signal")" "id addr increment" -for i in $(seq 5 9); do - add_endpoint "10.0.1.${i}" flags signal >/dev/null 2>&1 -done -check "get_endpoint 9" "$(format_endpoints "9,10.0.1.9,signal")" "hard addr limit" -check "get_endpoint 10" "" "above hard addr limit" +read -r -a default_limits_nb <<< "$(get_limits_nb)" +# limits have been increased: from 8 to 64 for subflows/add_addr & 255 for endp +if mptcp_lib_expect_all_features || set_limits 9 9 2>/dev/null; then + max_endp=255 + max_limits=64 +else + max_endp=8 + max_limits=8 +fi +set_limits "${default_limits_nb[@]}" -del_endpoint 9 -for i in $(seq 10 255); do - add_endpoint 10.0.0.9 id "${i}" - del_endpoint "${i}" +for i in $(seq 5 ${max_endp}); do + add_endpoint "10.0.0.${i}" id "${i}" done -check "show_endpoints" \ - "$(format_endpoints "1,10.0.1.1" \ - "3,10.0.1.3,signal backup" \ - "4,10.0.1.4,signal" \ - "5,10.0.1.5,signal" \ - "6,10.0.1.6,signal" \ - "7,10.0.1.7,signal" \ - "8,10.0.1.8,signal")" "id limit" +check "get_endpoint ${max_endp}" \ + "$(format_endpoints "${max_endp},10.0.0.${max_endp}")" "id limit" + +if add_endpoint '10.0.0.1' &>/dev/null; then + hardlimit="no error" +else + hardlimit="error" +fi +check "echo ${hardlimit}" "error" "above hard addr limit" flush_endpoint check "show_endpoints" "" "flush addrs" -add_endpoint 10.0.1.1 flags unknown -check "show_endpoints" "$(format_endpoints "1,10.0.1.1")" "ignore unknown flags" -flush_endpoint +# "unknown" flag is only supported by pm_nl_ctl +if ! mptcp_lib_is_ip_mptcp; then + add_endpoint 10.0.1.1 flags unknown + check "show_endpoints" "$(format_endpoints "1,10.0.1.1")" \ + "ignore unknown flags" + flush_endpoint +fi -set_limits 9 1 2>/dev/null +set_limits $((max_limits + 1)) 1 2>/dev/null check "get_limits" "${default_limits}" "rcv addrs above hard limit" -set_limits 1 9 2>/dev/null +set_limits 1 $((max_limits + 1)) 2>/dev/null check "get_limits" "${default_limits}" "subflows above hard limit" -set_limits 8 8 +set_limits ${max_limits} ${max_limits} flush_endpoint ## to make sure it doesn't affect the limits -check "get_limits" "$(format_limits 8 8)" "set limits" +check "get_limits" "$(format_limits ${max_limits} ${max_limits})" "set limits" flush_endpoint add_endpoint 10.0.1.1 diff --git a/tools/testing/selftests/net/mptcp/pm_nl_ctl.c b/tools/testing/selftests/net/mptcp/pm_nl_ctl.c index 99eecccbf0c8..78180da1efcc 100644 --- a/tools/testing/selftests/net/mptcp/pm_nl_ctl.c +++ b/tools/testing/selftests/net/mptcp/pm_nl_ctl.c @@ -217,8 +217,6 @@ static int capture_events(int fd, int event_group) /* do a netlink command and, if max > 0, fetch the reply ; nh's size >1024B */ static int do_nl_req(int fd, struct nlmsghdr *nh, int len, int max) { - struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK }; - socklen_t addr_len; void *data = nh; int rem, ret; int err = 0; @@ -230,15 +228,15 @@ static int do_nl_req(int fd, struct nlmsghdr *nh, int len, int max) } nh->nlmsg_len = len; - ret = sendto(fd, data, len, 0, (void *)&nladdr, sizeof(nladdr)); + ret = send(fd, data, len, 0); if (ret != len) error(1, errno, "send netlink: %uB != %uB\n", ret, len); - addr_len = sizeof(nladdr); - rem = ret = recvfrom(fd, data, max, 0, (void *)&nladdr, &addr_len); + ret = recv(fd, data, max, 0); if (ret < 0) error(1, errno, "recv netlink: %uB\n", ret); + rem = ret; /* Beware: the NLMSG_NEXT macro updates the 'rem' argument */ for (; NLMSG_OK(nh, rem); nh = NLMSG_NEXT(nh, rem)) { if (nh->nlmsg_type == NLMSG_DONE) diff --git a/tools/testing/selftests/net/mptcp/simult_flows.sh b/tools/testing/selftests/net/mptcp/simult_flows.sh index 806aaa7d2d61..3ea3d1efe32e 100755 --- a/tools/testing/selftests/net/mptcp/simult_flows.sh +++ b/tools/testing/selftests/net/mptcp/simult_flows.sh @@ -24,6 +24,7 @@ small="" sout="" cout="" capout="" +capprefix="" size=0 usage() { @@ -70,19 +71,24 @@ setup() mptcp_lib_ns_init ns1 ns2 ns3 + if $capture; then + capprefix="simult_flows-${ns1:4}" + mptcp_lib_pr_info "pcap will have this prefix: ${capprefix}-" + fi + ip link add ns1eth1 netns "$ns1" type veth peer name ns2eth1 netns "$ns2" ip link add ns1eth2 netns "$ns1" type veth peer name ns2eth2 netns "$ns2" ip link add ns2eth3 netns "$ns2" type veth peer name ns3eth1 netns "$ns3" ip -net "$ns1" addr add 10.0.1.1/24 dev ns1eth1 ip -net "$ns1" addr add dead:beef:1::1/64 dev ns1eth1 nodad - ip -net "$ns1" link set ns1eth1 up mtu 1500 + ip -net "$ns1" link set ns1eth1 up mtu 1500 gso_max_segs 0 ip -net "$ns1" route add default via 10.0.1.2 ip -net "$ns1" route add default via dead:beef:1::2 ip -net "$ns1" addr add 10.0.2.1/24 dev ns1eth2 ip -net "$ns1" addr add dead:beef:2::1/64 dev ns1eth2 nodad - ip -net "$ns1" link set ns1eth2 up mtu 1500 + ip -net "$ns1" link set ns1eth2 up mtu 1500 gso_max_segs 0 ip -net "$ns1" route add default via 10.0.2.2 metric 101 ip -net "$ns1" route add default via dead:beef:2::2 metric 101 @@ -91,21 +97,21 @@ setup() ip -net "$ns2" addr add 10.0.1.2/24 dev ns2eth1 ip -net "$ns2" addr add dead:beef:1::2/64 dev ns2eth1 nodad - ip -net "$ns2" link set ns2eth1 up mtu 1500 + ip -net "$ns2" link set ns2eth1 up mtu 1500 gso_max_segs 0 ip -net "$ns2" addr add 10.0.2.2/24 dev ns2eth2 ip -net "$ns2" addr add dead:beef:2::2/64 dev ns2eth2 nodad - ip -net "$ns2" link set ns2eth2 up mtu 1500 + ip -net "$ns2" link set ns2eth2 up mtu 1500 gso_max_segs 0 ip -net "$ns2" addr add 10.0.3.2/24 dev ns2eth3 ip -net "$ns2" addr add dead:beef:3::2/64 dev ns2eth3 nodad - ip -net "$ns2" link set ns2eth3 up mtu 1500 + ip -net "$ns2" link set ns2eth3 up mtu 1500 gso_max_segs 0 ip netns exec "$ns2" sysctl -q net.ipv4.ip_forward=1 ip netns exec "$ns2" sysctl -q net.ipv6.conf.all.forwarding=1 ip -net "$ns3" addr add 10.0.3.3/24 dev ns3eth1 ip -net "$ns3" addr add dead:beef:3::3/64 dev ns3eth1 nodad - ip -net "$ns3" link set ns3eth1 up mtu 1500 + ip -net "$ns3" link set ns3eth1 up mtu 1500 gso_max_segs 0 ip -net "$ns3" route add default via 10.0.3.2 ip -net "$ns3" route add default via dead:beef:3::2 @@ -136,15 +142,14 @@ do_transfer() if $capture; then local capuser - local rndh="${ns1:4}" if [ -z $SUDO_USER ] ; then capuser="" else capuser="-Z $SUDO_USER" fi - local capfile="${rndh}-${port}" - local capopt="-i any -s 65535 -B 32768 ${capuser}" + local capfile="${capprefix}-${port}" + local capopt="-i any -s 108 -B 32768 ${capuser}" ip netns exec ${ns3} tcpdump ${capopt} -w "${capfile}-listener.pcap" >> "${capout}" 2>&1 & local cappid_listener=$! @@ -223,9 +228,11 @@ run_test() local rate2=$2 local delay1=$3 local delay2=$4 + local limit1=$5 + local limit2=$6 local lret local dev - shift 4 + shift 6 local msg=$* [ $delay1 -gt 0 ] && delay1="delay ${delay1}ms" || delay1="" @@ -237,10 +244,13 @@ run_test() for dev in ns2eth1 ns2eth2; do tc -n $ns2 qdisc del dev $dev root >/dev/null 2>&1 done - tc -n $ns1 qdisc add dev ns1eth1 root netem rate ${rate1}mbit $delay1 - tc -n $ns1 qdisc add dev ns1eth2 root netem rate ${rate2}mbit $delay2 - tc -n $ns2 qdisc add dev ns2eth1 root netem rate ${rate1}mbit $delay1 - tc -n $ns2 qdisc add dev ns2eth2 root netem rate ${rate2}mbit $delay2 + + # keep the queued pkts number low, or the RTT estimator will see + # increasing latency over time. + tc -n $ns1 qdisc add dev ns1eth1 root netem rate ${rate1}mbit $delay1 limit ${limit1} + tc -n $ns1 qdisc add dev ns1eth2 root netem rate ${rate2}mbit $delay2 limit ${limit2} + tc -n $ns2 qdisc add dev ns2eth1 root netem rate ${rate1}mbit $delay1 limit ${limit1} + tc -n $ns2 qdisc add dev ns2eth2 root netem rate ${rate2}mbit $delay2 limit ${limit2} # time is measured in ms, account for transfer size, aggregated link speed # and header overhead (10%) @@ -298,13 +308,13 @@ done setup mptcp_lib_subtests_last_ts_reset -run_test 10 10 0 0 "balanced bwidth" -run_test 10 10 1 25 "balanced bwidth with unbalanced delay" +run_test 10 10 0 0 20 20 "balanced bwidth" +run_test 10 10 1 25 20 50 "balanced bwidth with unbalanced delay" # we still need some additional infrastructure to pass the following test-cases -MPTCP_LIB_SUBTEST_FLAKY=1 run_test 10 3 0 0 "unbalanced bwidth" -run_test 10 3 1 25 "unbalanced bwidth with unbalanced delay" -run_test 10 3 25 1 "unbalanced bwidth with opposed, unbalanced delay" +MPTCP_LIB_SUBTEST_FLAKY=1 run_test 10 3 0 0 30 20 "unbalanced bwidth" +run_test 10 3 1 25 40 30 "unbalanced bwidth with unbalanced delay" +run_test 10 3 25 1 50 30 "unbalanced bwidth with opposed, unbalanced delay" mptcp_lib_result_print_all_tap exit $ret diff --git a/tools/testing/selftests/net/mptcp/userspace_pm.sh b/tools/testing/selftests/net/mptcp/userspace_pm.sh index e9ae1806ab07..30a809752d1b 100755 --- a/tools/testing/selftests/net/mptcp/userspace_pm.sh +++ b/tools/testing/selftests/net/mptcp/userspace_pm.sh @@ -212,7 +212,7 @@ make_connection() ./mptcp_connect -s MPTCP -w 300 -p $app_port -l $listen_addr > /dev/null 2>&1 & local server_pid=$! - mptcp_lib_wait_local_port_listen "${ns1}" "${port}" + mptcp_lib_wait_local_port_listen "${ns1}" "${app_port}" # Run the client, transfer $file and stay connected to the server # to conduct tests diff --git a/tools/testing/selftests/net/netfilter/Makefile b/tools/testing/selftests/net/netfilter/Makefile index ee2d1a5254f8..f88dd4ef8d26 100644 --- a/tools/testing/selftests/net/netfilter/Makefile +++ b/tools/testing/selftests/net/netfilter/Makefile @@ -26,11 +26,13 @@ TEST_PROGS := \ nft_concat_range.sh \ nft_conntrack_helper.sh \ nft_fib.sh \ + nft_fib_nexthop.sh \ nft_flowtable.sh \ nft_interface_stress.sh \ nft_meta.sh \ nft_nat.sh \ nft_nat_zones.sh \ + nft_offload.sh \ nft_queue.sh \ nft_synproxy.sh \ nft_tproxy_tcp.sh \ diff --git a/tools/testing/selftests/net/netfilter/config b/tools/testing/selftests/net/netfilter/config index 979cff56e1f5..c3c121b6f300 100644 --- a/tools/testing/selftests/net/netfilter/config +++ b/tools/testing/selftests/net/netfilter/config @@ -11,7 +11,12 @@ CONFIG_BRIDGE_NF_EBTABLES_LEGACY=m CONFIG_BRIDGE_VLAN_FILTERING=y CONFIG_CGROUP_BPF=y CONFIG_CRYPTO_SHA1=m +CONFIG_DEBUG_FS=y CONFIG_DUMMY=m +CONFIG_FAIL_FUNCTION=y +CONFIG_FAULT_INJECTION=y +CONFIG_FAULT_INJECTION_DEBUG_FS=y +CONFIG_FUNCTION_ERROR_INJECTION=y CONFIG_INET_DIAG=m CONFIG_INET_ESP=m CONFIG_INET_SCTP_DIAG=m @@ -36,6 +41,7 @@ CONFIG_IP_VS_RR=m CONFIG_MACVLAN=m CONFIG_NAMESPACES=y CONFIG_NET_CLS_U32=m +CONFIG_NETDEVSIM=m CONFIG_NETFILTER=y CONFIG_NETFILTER_ADVANCED=y CONFIG_NETFILTER_NETLINK=m diff --git a/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c b/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c index 5cecb8a1bc94..31b8250ddc53 100644 --- a/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c +++ b/tools/testing/selftests/net/netfilter/conntrack_dump_flush.c @@ -102,7 +102,6 @@ static int conntrack_data_insert(struct mnl_socket *sock, struct nlmsghdr *nlh, uint16_t zone) { char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *rplnlh; unsigned int portid; int ret; @@ -216,12 +215,11 @@ static int count_entries(const struct nlmsghdr *nlh, void *data) return MNL_CB_OK; } -static int conntracK_count_zone(struct mnl_socket *sock, uint16_t zone) +static int conntrack_count_zone(struct mnl_socket *sock, uint16_t zone) { char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *nlh, *rplnlh; + struct nlmsghdr *nlh; struct nfgenmsg *nfh; - struct nlattr *nest; unsigned int portid; int ret; @@ -266,9 +264,8 @@ static int conntracK_count_zone(struct mnl_socket *sock, uint16_t zone) static int conntrack_flush_zone(struct mnl_socket *sock, uint16_t zone) { char buf[MNL_SOCKET_BUFFER_SIZE]; - struct nlmsghdr *nlh, *rplnlh; + struct nlmsghdr *nlh; struct nfgenmsg *nfh; - struct nlattr *nest; unsigned int portid; int ret; @@ -326,7 +323,7 @@ FIXTURE_SETUP(conntrack_dump_flush) ret = mnl_socket_bind(self->sock, 0, MNL_SOCKET_AUTOPID); EXPECT_EQ(ret, 0); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); if (ret < 0 && errno == EPERM) SKIP(return, "Needs to be run as root"); else if (ret < 0 && errno == EOPNOTSUPP) @@ -423,7 +420,7 @@ FIXTURE_SETUP(conntrack_dump_flush) NF_CT_DEFAULT_ZONE_ID); EXPECT_EQ(ret, 0); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); EXPECT_GE(ret, 2); if (ret > 2) SKIP(return, "kernel does not support filtering by zone"); @@ -437,7 +434,7 @@ TEST_F(conntrack_dump_flush, test_dump_by_zone) { int ret; - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); EXPECT_EQ(ret, 2); } @@ -447,13 +444,13 @@ TEST_F(conntrack_dump_flush, test_flush_by_zone) ret = conntrack_flush_zone(self->sock, TEST_ZONE_ID); EXPECT_EQ(ret, 0); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); EXPECT_EQ(ret, 0); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID + 1); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 1); EXPECT_EQ(ret, 2); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID + 2); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 2); EXPECT_EQ(ret, 2); - ret = conntracK_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); + ret = conntrack_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); EXPECT_EQ(ret, 2); } @@ -463,13 +460,13 @@ TEST_F(conntrack_dump_flush, test_flush_by_zone_default) ret = conntrack_flush_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); EXPECT_EQ(ret, 0); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID); EXPECT_EQ(ret, 2); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID + 1); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 1); EXPECT_EQ(ret, 2); - ret = conntracK_count_zone(self->sock, TEST_ZONE_ID + 2); + ret = conntrack_count_zone(self->sock, TEST_ZONE_ID + 2); EXPECT_EQ(ret, 2); - ret = conntracK_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); + ret = conntrack_count_zone(self->sock, NF_CT_DEFAULT_ZONE_ID); EXPECT_EQ(ret, 0); } diff --git a/tools/testing/selftests/net/netfilter/conntrack_sctp_collision.sh b/tools/testing/selftests/net/netfilter/conntrack_sctp_collision.sh index d860f7d9744b..7261975957ef 100755 --- a/tools/testing/selftests/net/netfilter/conntrack_sctp_collision.sh +++ b/tools/testing/selftests/net/netfilter/conntrack_sctp_collision.sh @@ -2,18 +2,32 @@ # SPDX-License-Identifier: GPL-2.0 # # Testing For SCTP COLLISION SCENARIO as Below: -# +# 1. Stale INIT_ACK capture: # 14:35:47.655279 IP CLIENT_IP.PORT > SERVER_IP.PORT: sctp (1) [INIT] [init tag: 2017837359] # 14:35:48.353250 IP SERVER_IP.PORT > CLIENT_IP.PORT: sctp (1) [INIT] [init tag: 1187206187] # 14:35:48.353275 IP CLIENT_IP.PORT > SERVER_IP.PORT: sctp (1) [INIT ACK] [init tag: 2017837359] # 14:35:48.353283 IP SERVER_IP.PORT > CLIENT_IP.PORT: sctp (1) [COOKIE ECHO] # 14:35:48.353977 IP CLIENT_IP.PORT > SERVER_IP.PORT: sctp (1) [COOKIE ACK] # 14:35:48.855335 IP SERVER_IP.PORT > CLIENT_IP.PORT: sctp (1) [INIT ACK] [init tag: 164579970] +# (Delayed) +# +# 2. Stale INIT capture: +# 14:35:48.353250 IP SERVER_IP.PORT > CLIENT_IP.PORT: sctp (1) [INIT] [init tag: 1187206187] +# 14:35:48.353275 IP CLIENT_IP.PORT > SERVER_IP.PORT: sctp (1) [INIT ACK] [init tag: 2017837359] +# 14:35:48.353283 IP SERVER_IP.PORT > CLIENT_IP.PORT: sctp (1) [COOKIE ECHO] +# 14:35:48.353977 IP CLIENT_IP.PORT > SERVER_IP.PORT: sctp (1) [COOKIE ACK] +# 14:35:47.655279 IP CLIENT_IP.PORT > SERVER_IP.PORT: sctp (1) [INIT] [init tag: 2017837359] +# (Delayed) +# 14:35:48.855335 IP SERVER_IP.PORT > CLIENT_IP.PORT: sctp (1) [INIT ACK] [init tag: 164579970] # # TOPO: SERVER_NS (link0)<--->(link1) ROUTER_NS (link2)<--->(link3) CLIENT_NS source lib.sh +checktool "nft --version" "run test without nft" +checktool "tc -h" "run test without tc" +checktool "modprobe -q sctp" "load sctp module" + CLIENT_IP="198.51.200.1" CLIENT_PORT=1234 @@ -24,7 +38,8 @@ CLIENT_GW="198.51.200.2" SERVER_GW="198.51.100.2" # setup the topo -setup() { +topo_setup() { + # setup_ns cleans up existing net namespaces first. setup_ns CLIENT_NS SERVER_NS ROUTER_NS ip -n "$SERVER_NS" link add link0 type veth peer name link1 netns "$ROUTER_NS" ip -n "$CLIENT_NS" link add link3 type veth peer name link2 netns "$ROUTER_NS" @@ -38,35 +53,53 @@ setup() { ip -n "$ROUTER_NS" addr add $SERVER_GW/24 dev link1 ip -n "$ROUTER_NS" addr add $CLIENT_GW/24 dev link2 ip net exec "$ROUTER_NS" sysctl -wq net.ipv4.ip_forward=1 + sysctl -wq net.netfilter.nf_log_all_netns=1 ip -n "$CLIENT_NS" link set link3 up ip -n "$CLIENT_NS" addr add $CLIENT_IP/24 dev link3 ip -n "$CLIENT_NS" route add $SERVER_IP dev link3 via $CLIENT_GW +} + +conf_delay() +{ + # simulate the delay on OVS upcall by setting up a delay for INIT_ACK/INIT with + local ns=$1 + local link=$2 + local chunk_type=$3 - # simulate the delay on OVS upcall by setting up a delay for INIT_ACK with - # tc on $SERVER_NS side - tc -n "$SERVER_NS" qdisc add dev link0 root handle 1: htb r2q 64 - tc -n "$SERVER_NS" class add dev link0 parent 1: classid 1:1 htb rate 100mbit - tc -n "$SERVER_NS" filter add dev link0 parent 1: protocol ip u32 match ip protocol 132 \ - 0xff match u8 2 0xff at 32 flowid 1:1 - if ! tc -n "$SERVER_NS" qdisc add dev link0 parent 1:1 handle 10: netem delay 1200ms; then + # use a smaller number for assoc's max_retrans to reproduce the issue + ip net exec "$CLIENT_NS" sysctl -wq net.sctp.association_max_retrans=3 + + tc -n "$ns" qdisc add dev "$link" root handle 1: htb r2q 64 + tc -n "$ns" class add dev "$link" parent 1: classid 1:1 htb rate 100mbit + tc -n "$ns" filter add dev "$link" parent 1: protocol ip \ + u32 match ip protocol 132 0xff match u8 "$chunk_type" 0xff at 32 flowid 1:1 + if ! tc -n "$ns" qdisc add dev "$link" parent 1:1 handle 10: netem delay 1200ms; then echo "SKIP: Cannot add netem qdisc" - exit $ksft_skip + return $ksft_skip fi # simulate the ctstate check on OVS nf_conntrack - ip net exec "$ROUTER_NS" iptables -A FORWARD -m state --state INVALID,UNTRACKED -j DROP - ip net exec "$ROUTER_NS" iptables -A INPUT -p sctp -j DROP - - # use a smaller number for assoc's max_retrans to reproduce the issue - modprobe -q sctp - ip net exec "$CLIENT_NS" sysctl -wq net.sctp.association_max_retrans=3 + ip net exec "$ROUTER_NS" nft -f - <<-EOF + table ip t { + chain forward { + type filter hook forward priority filter; policy accept; + meta l4proto icmp counter accept + ct state new counter accept + ct state established,related counter accept + ct state invalid log flags all counter drop comment \ + "Expect to drop stale INIT/INIT_ACK chunks" + counter + } + } + EOF + return 0 } cleanup() { - ip net exec "$CLIENT_NS" pkill sctp_collision >/dev/null 2>&1 - ip net exec "$SERVER_NS" pkill sctp_collision >/dev/null 2>&1 + # cleanup_all_ns terminates running processes in the namespaces. cleanup_all_ns + sysctl -wq net.netfilter.nf_log_all_netns=0 } do_test() { @@ -81,7 +114,19 @@ do_test() { # run the test case trap cleanup EXIT -setup && \ -echo "Test for SCTP Collision in nf_conntrack:" && \ -do_test && echo "PASS!" -exit $? + +echo "Test for SCTP INIT_ACK Collision in nf_conntrack:" +topo_setup || exit $? +conf_delay $SERVER_NS link0 2 || exit $? + +if ! do_test; then + exit $ksft_fail +fi + +echo "Test for SCTP INIT Collision in nf_conntrack:" +topo_setup || exit $? +conf_delay $CLIENT_NS link3 1 || exit $? + +if ! do_test; then + exit $ksft_fail +fi diff --git a/tools/testing/selftests/net/netfilter/nf_queue.c b/tools/testing/selftests/net/netfilter/nf_queue.c index 9e56b9d47037..8bbec37f5356 100644 --- a/tools/testing/selftests/net/netfilter/nf_queue.c +++ b/tools/testing/selftests/net/netfilter/nf_queue.c @@ -18,6 +18,9 @@ struct options { bool count_packets; bool gso_enabled; + bool failopen; + bool out_of_order; + bool bogus_verdict; int verbose; unsigned int queue_num; unsigned int timeout; @@ -30,7 +33,7 @@ static struct options opts; static void help(const char *p) { - printf("Usage: %s [-c|-v [-vv] ] [-t timeout] [-q queue_num] [-Qdst_queue ] [ -d ms_delay ] [-G]\n", p); + printf("Usage: %s [-c|-v [-vv] ] [-o] [-O] [-b] [-t timeout] [-q queue_num] [-Qdst_queue ] [ -d ms_delay ] [-G]\n", p); } static int parse_attr_cb(const struct nlattr *attr, void *data) @@ -236,6 +239,8 @@ struct mnl_socket *open_queue(void) flags = opts.gso_enabled ? NFQA_CFG_F_GSO : 0; flags |= NFQA_CFG_F_UID_GID; + if (opts.failopen) + flags |= NFQA_CFG_F_FAIL_OPEN; mnl_attr_put_u32(nlh, NFQA_CFG_FLAGS, htonl(flags)); mnl_attr_put_u32(nlh, NFQA_CFG_MASK, htonl(flags)); @@ -272,7 +277,9 @@ static int mainloop(void) unsigned int buflen = 64 * 1024 + MNL_SOCKET_BUFFER_SIZE; struct mnl_socket *nl; struct nlmsghdr *nlh; + uint32_t ooo_ids[16]; unsigned int portid; + int ooo_count = 0; char *buf; int ret; @@ -305,6 +312,9 @@ static int mainloop(void) ret = mnl_cb_run(buf, ret, 0, portid, queue_cb, NULL); if (ret < 0) { + /* bogus verdict mode will generate ENOENT error messages */ + if (opts.bogus_verdict && errno == ENOENT) + continue; perror("mnl_cb_run"); exit(EXIT_FAILURE); } @@ -313,10 +323,35 @@ static int mainloop(void) if (opts.delay_ms) sleep_ms(opts.delay_ms); - nlh = nfq_build_verdict(buf, id, opts.queue_num, opts.verdict); - if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) { - perror("mnl_socket_sendto"); - exit(EXIT_FAILURE); + if (opts.bogus_verdict) { + for (int i = 0; i < 50; i++) { + nlh = nfq_build_verdict(buf, id + 0x7FFFFFFF + i, + opts.queue_num, opts.verdict); + mnl_socket_sendto(nl, nlh, nlh->nlmsg_len); + } + } + + if (opts.out_of_order) { + ooo_ids[ooo_count] = id; + if (ooo_count >= 15) { + for (ooo_count; ooo_count >= 0; ooo_count--) { + nlh = nfq_build_verdict(buf, ooo_ids[ooo_count], + opts.queue_num, opts.verdict); + if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) { + perror("mnl_socket_sendto"); + exit(EXIT_FAILURE); + } + } + ooo_count = 0; + } else { + ooo_count++; + } + } else { + nlh = nfq_build_verdict(buf, id, opts.queue_num, opts.verdict); + if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) { + perror("mnl_socket_sendto"); + exit(EXIT_FAILURE); + } } } @@ -329,7 +364,7 @@ static void parse_opts(int argc, char **argv) { int c; - while ((c = getopt(argc, argv, "chvt:q:Q:d:G")) != -1) { + while ((c = getopt(argc, argv, "chvoObt:q:Q:d:G")) != -1) { switch (c) { case 'c': opts.count_packets = true; @@ -366,9 +401,18 @@ static void parse_opts(int argc, char **argv) case 'G': opts.gso_enabled = false; break; + case 'o': + opts.failopen = true; + break; case 'v': opts.verbose++; break; + case 'O': + opts.out_of_order = true; + break; + case 'b': + opts.bogus_verdict = true; + break; } } diff --git a/tools/testing/selftests/net/netfilter/nft_concat_range.sh b/tools/testing/selftests/net/netfilter/nft_concat_range.sh index 394166f224a4..ffdc6ccc6511 100755 --- a/tools/testing/selftests/net/netfilter/nft_concat_range.sh +++ b/tools/testing/selftests/net/netfilter/nft_concat_range.sh @@ -29,7 +29,8 @@ TYPES="net_port port_net net6_port port_proto net6_port_mac net6_port_mac_proto net6_port_net6_port net_port_mac_proto_net" # Reported bugs, also described by TYPE_ variables below -BUGS="flush_remove_add reload net_port_proto_match avx2_mismatch doublecreate insert_overlap" +BUGS="flush_remove_add reload net_port_proto_match avx2_mismatch doublecreate + insert_overlap load_flush_load4 load_flush_load8" # List of possible paths to pktgen script from kernel tree for performance tests PKTGEN_SCRIPT_PATHS=" @@ -432,6 +433,30 @@ race_repeat 0 perf_duration 0 " +TYPE_load_flush_load4=" +display reload with flush, 4bit groups +type_spec ipv4_addr . ipv4_addr +chain_spec ip saddr . ip daddr +dst addr4 +proto icmp + +race_repeat 0 + +perf_duration 0 +" + +TYPE_load_flush_load8=" +display reload with flush, 8bit groups +type_spec ipv4_addr . ipv4_addr +chain_spec ip saddr . ip daddr +dst addr4 +proto icmp + +race_repeat 0 + +perf_duration 0 +" + # Set template for all tests, types and rules are filled in depending on test set_template=' flush ruleset @@ -1997,6 +2022,49 @@ test_bug_insert_overlap() return 0 } +test_bug_load_flush_load4() +{ + local i + + setup veth send_"${proto}" set || return ${ksft_skip} + + for i in $(seq 0 255); do + local addelem="add element inet filter test" + local j + + for j in $(seq 0 20); do + echo "$addelem { 10.$j.0.$i . 10.$j.1.$i }" + echo "$addelem { 10.$j.0.$i . 10.$j.2.$i }" + done + done > "$tmp" + + nft -f "$tmp" || return 1 + + ( echo "flush set inet filter test";cat "$tmp") | nft -f - + [ $? -eq 0 ] || return 1 + + return 0 +} + +test_bug_load_flush_load8() +{ + local i + + setup veth send_"${proto}" set || return ${ksft_skip} + + for i in $(seq 1 100); do + echo "add element inet filter test { 10.0.0.$i . 10.0.1.$i }" + echo "add element inet filter test { 10.0.0.$i . 10.0.2.$i }" + done > "$tmp" + + nft -f "$tmp" || return 1 + + ( echo "flush set inet filter test";cat "$tmp") | nft -f - + [ $? -eq 0 ] || return 1 + + return 0 +} + test_reported_issues() { eval test_bug_"${subtest}" } diff --git a/tools/testing/selftests/net/netfilter/nft_fib_nexthop.sh b/tools/testing/selftests/net/netfilter/nft_fib_nexthop.sh new file mode 100755 index 000000000000..c4f203057382 --- /dev/null +++ b/tools/testing/selftests/net/netfilter/nft_fib_nexthop.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2154 +# +# Exercise nft_fib6_eval()'s sibling/nh enumeration on three route shapes: +# 1) route via a single external nexthop (nhid) +# 2) route via an external nexthop group (nhid -> group, two members) +# 3) route via old-style multipath (nexthop ... nexthop ...) +# +# In each scenario the route's nexthop set contains veth0 (the iif of the +# test packet). nft_fib6_info_nh_uses_dev() must walk the set and report +# veth0 as a valid oif. For (2) and (3) the matching nexthop is the second +# member, so the walk has to traverse beyond the primary nh. +# +# After sending $PKTS ICMPv6 echo requests from ns1, check two counters on +# nsrouter: +# nf_ok -- `fib daddr . iif oif eq "veth0"` must equal $PKTS +# nf_bad -- `fib daddr . iif oif missing` must stay at 0 +# Both rules also match on iif veth0 and ip6 daddr dead:dead::/64 so that +# kernel-generated ND/MLD/RA traffic cannot pollute the counters. +# +# Topology similar to nft_fib.sh, without ns2; two dummy interfaces on +# nsrouter host extra nh devices: +# +# dead:1::99 dead:1::1 +# ns1 <----veth----> nsrouter --- dummy0 dead:2::1 +# \-- dummy1 dead:9::1 + +source lib.sh + +ret=0 +PKTS=3 + +checktool "nft --version" "run test without nft" +checktool "ip -V" "run test without iproute2" + +setup_ns nsrouter ns1 +trap cleanup_all_ns EXIT + +if ! ip link add veth0 netns "$nsrouter" type veth peer name eth0 netns "$ns1" \ + > /dev/null 2>&1; then + echo "SKIP: No virtual ethernet pair device support in kernel" + exit $ksft_skip +fi + +ip -net "$ns1" link set lo up +ip -net "$ns1" link set eth0 up +ip -net "$ns1" -6 addr add dead:1::99/64 dev eth0 nodad +ip -net "$ns1" -6 route add default via dead:1::1 + +ip -net "$nsrouter" link set lo up +ip -net "$nsrouter" link set veth0 up +ip -net "$nsrouter" -6 addr add dead:1::1/64 dev veth0 nodad + +if ! ip -net "$nsrouter" link add dummy0 type dummy 2>/dev/null; then + echo "SKIP: dummy netdev not available" + exit $ksft_skip +fi +ip -net "$nsrouter" link set dummy0 up +ip -net "$nsrouter" -6 addr add dead:2::1/64 dev dummy0 nodad + +ip -net "$nsrouter" link add dummy1 type dummy +ip -net "$nsrouter" link set dummy1 up +ip -net "$nsrouter" -6 addr add dead:9::1/64 dev dummy1 nodad + +ip netns exec "$nsrouter" sysctl -q net.ipv6.conf.all.forwarding=1 + +load_fib_rule() { + # filter on iif + daddr so the counters only see our test packets + ip netns exec "$nsrouter" nft -f /dev/stdin <<EOF +flush ruleset +table ip6 t { + counter nf_ok { } + counter nf_bad { } + chain c { + type filter hook prerouting priority 0; policy accept; + iif "veth0" ip6 daddr dead:dead::/64 fib daddr . iif oif eq "veth0" counter name nf_ok + iif "veth0" ip6 daddr dead:dead::/64 fib daddr . iif oif missing counter name nf_bad + } +} +EOF +} + +bad_counter() { + local counter=$1 + local expect=$2 + local tag=$3 + + echo "FAIL ($tag): counter $counter has unexpected value (expected \"$expect\")" 1>&2 + ip netns exec "$nsrouter" nft list counter ip6 t "$counter" 1>&2 +} + +run_scenario() { + local what="$1"; shift + # counter output format is "packets PACKET_NUM bytes BYTES_NUM"; + # we only care about the packet count + local expect_ok="packets $PKTS bytes" + local expect_bad="packets 0 bytes" + local lret=0 + + # reset route + nexthop state between scenarios + ip -net "$nsrouter" -6 route del dead:dead::/64 > /dev/null 2>&1 || true + ip -net "$nsrouter" nexthop flush > /dev/null 2>&1 || true + + # run the scenario function passed by the caller + "$@" || echo "WARN ($what): scenario setup returned non-zero" + + load_fib_rule || { echo "FAIL ($what): nft load"; ret=1; return; } + + # ping a daddr inside dead:dead::/64 so fib has to walk the nh set + ip netns exec "$ns1" ping -6 -c "$PKTS" -i 0.1 -W 1 dead:dead::1 \ + > /dev/null 2>&1 || true + + # verify the packets went through the expected fib path + if ! ip netns exec "$nsrouter" nft list counter ip6 t nf_ok | grep -q "$expect_ok"; then + bad_counter nf_ok "$expect_ok" "$what" + lret=1 + fi + if ! ip netns exec "$nsrouter" nft list counter ip6 t nf_bad | grep -q "$expect_bad"; then + bad_counter nf_bad "$expect_bad" "$what" + lret=1 + fi + + if [ $lret -eq 0 ]; then + echo "PASS: $what" + else + ret=1 + fi +} + +scenario_single_nh() { + ip -net "$nsrouter" nexthop add id 1 via dead:1::99 dev veth0 + ip -net "$nsrouter" -6 route add dead:dead::/64 nhid 1 +} +run_scenario "single external nexthop (nhid -> veth0)" scenario_single_nh + +scenario_nh_group() { + ip -net "$nsrouter" nexthop add id 1 via dead:2::2 dev dummy0 + ip -net "$nsrouter" nexthop add id 2 via dead:1::99 dev veth0 + ip -net "$nsrouter" nexthop add id 100 group 1/2 + ip -net "$nsrouter" -6 route add dead:dead::/64 nhid 100 +} +run_scenario "nexthop group (dummy0 + veth0)" scenario_nh_group + +scenario_old_multipath() { + ip -net "$nsrouter" -6 route add dead:dead::/64 \ + nexthop via dead:2::2 dev dummy0 \ + nexthop via dead:1::99 dev veth0 +} +run_scenario "old-style multipath (sibling on veth0)" scenario_old_multipath + +exit $ret diff --git a/tools/testing/selftests/net/netfilter/nft_flowtable.sh b/tools/testing/selftests/net/netfilter/nft_flowtable.sh index 7a34ef468975..449c518bd947 100755 --- a/tools/testing/selftests/net/netfilter/nft_flowtable.sh +++ b/tools/testing/selftests/net/netfilter/nft_flowtable.sh @@ -592,7 +592,7 @@ ip -net "$nsr1" link set tun0 up ip -net "$nsr1" addr add 192.168.100.1/24 dev tun0 ip netns exec "$nsr1" sysctl net.ipv4.conf.tun0.forwarding=1 > /dev/null -ip -net "$nsr1" link add name tun6 type ip6tnl local fee1:2::1 remote fee1:2::2 +ip -net "$nsr1" link add name tun6 type ip6tnl local fee1:2::1 remote fee1:2::2 encaplimit none ip -net "$nsr1" link set tun6 up ip -net "$nsr1" addr add fee1:3::1/64 dev tun6 nodad @@ -601,7 +601,7 @@ ip -net "$nsr2" link set tun0 up ip -net "$nsr2" addr add 192.168.100.2/24 dev tun0 ip netns exec "$nsr2" sysctl net.ipv4.conf.tun0.forwarding=1 > /dev/null -ip -net "$nsr2" link add name tun6 type ip6tnl local fee1:2::2 remote fee1:2::1 || ret=1 +ip -net "$nsr2" link add name tun6 type ip6tnl local fee1:2::2 remote fee1:2::1 encaplimit none || ret=1 ip -net "$nsr2" link set tun6 up ip -net "$nsr2" addr add fee1:3::2/64 dev tun6 nodad @@ -617,7 +617,11 @@ ip -6 -net "$nsr2" route add default via fee1:3::1 ip -net "$ns2" route add default via 10.0.2.1 ip -6 -net "$ns2" route add default via dead:2::1 +ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif tun0 tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun0 accept' +ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif tun6 tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun6 accept' ip netns exec "$nsr1" nft -a insert rule inet filter forward \ 'meta oif "veth0" tcp sport 12345 ct mark set 1 flow add @f1 counter name routed_repl accept' @@ -629,7 +633,7 @@ if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "IPIP tunnel"; then fi if test_tcp_forwarding "$ns1" "$ns2" 1 6 "[dead:2::99]" 12345; then - echo "PASS: flow offload for ns1/ns2 IP6IP6 tunnel" + check_counters "flow offload for ns1/ns2 IP6IP6 tunnel" else echo "FAIL: flow offload for ns1/ns2 with IP6IP6 tunnel" 1>&2 ip netns exec "$nsr1" nft list ruleset @@ -642,6 +646,8 @@ ip -net "$nsr1" link set veth1.10 up ip -net "$nsr1" addr add 192.168.20.1/24 dev veth1.10 ip -net "$nsr1" addr add fee1:4::1/64 dev veth1.10 nodad ip netns exec "$nsr1" sysctl net.ipv4.conf.veth1/10.forwarding=1 > /dev/null +ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif veth1.10 tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif veth1.10 accept' ip -net "$nsr1" link add name tun0.10 type ipip local 192.168.20.1 remote 192.168.20.2 @@ -649,13 +655,17 @@ ip -net "$nsr1" link set tun0.10 up ip -net "$nsr1" addr add 192.168.200.1/24 dev tun0.10 ip -net "$nsr1" route change default via 192.168.200.2 ip netns exec "$nsr1" sysctl net.ipv4.conf.tun0/10.forwarding=1 > /dev/null +ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif tun0.10 tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun0.10 accept' -ip -net "$nsr1" link add name tun6.10 type ip6tnl local fee1:4::1 remote fee1:4::2 +ip -net "$nsr1" link add name tun6.10 type ip6tnl local fee1:4::1 remote fee1:4::2 encaplimit none ip -net "$nsr1" link set tun6.10 up ip -net "$nsr1" addr add fee1:5::1/64 dev tun6.10 nodad ip -6 -net "$nsr1" route delete default ip -6 -net "$nsr1" route add default via fee1:5::2 +ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif tun6.10 tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' ip netns exec "$nsr1" nft -a insert rule inet filter forward 'meta oif tun6.10 accept' ip -net "$nsr2" link add link veth0 name veth0.10 type vlan id 10 @@ -670,7 +680,7 @@ ip -net "$nsr2" addr add 192.168.200.2/24 dev tun0.10 ip -net "$nsr2" route change default via 192.168.200.1 ip netns exec "$nsr2" sysctl net.ipv4.conf.tun0/10.forwarding=1 > /dev/null -ip -net "$nsr2" link add name tun6.10 type ip6tnl local fee1:4::2 remote fee1:4::1 || ret=1 +ip -net "$nsr2" link add name tun6.10 type ip6tnl local fee1:4::2 remote fee1:4::1 encaplimit none || ret=1 ip -net "$nsr2" link set tun6.10 up ip -net "$nsr2" addr add fee1:5::2/64 dev tun6.10 nodad ip -6 -net "$nsr2" route delete default @@ -683,7 +693,7 @@ if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "IPIP tunnel over vlan"; then fi if test_tcp_forwarding "$ns1" "$ns2" 1 6 "[dead:2::99]" 12345; then - echo "PASS: flow offload for ns1/ns2 IP6IP6 tunnel over vlan" + check_counters "flow offload for ns1/ns2 IP6IP6 tunnel over vlan" else echo "FAIL: flow offload for ns1/ns2 with IP6IP6 tunnel over vlan" 1>&2 ip netns exec "$nsr1" nft list ruleset @@ -736,6 +746,61 @@ if ! test_tcp_forwarding_nat "$ns1" "$ns2" 1 "on bridge"; then ret=1 fi +if ip -net "$nsr1" link show tun0 > /dev/null 2>&1 && + ip -net "$nsr2" link show tun0 > /dev/null 2>&1; then + ip -net "$nsr1" route change default via 192.168.100.2 + ip -net "$nsr2" route change default via 192.168.100.1 + ip -6 -net "$nsr1" route delete default + ip -6 -net "$nsr1" route add default via fee1:3::2 + ip -6 -net "$nsr2" route delete default + ip -6 -net "$nsr2" route add default via fee1:3::1 + ip -net "$ns2" route add default via 10.0.2.1 + ip -6 -net "$ns2" route add default via dead:2::1 + + ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif "tun0" tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' + ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif "tun6" tcp dport 12345 ct mark set 1 flow add @f1 counter name routed_orig accept' + ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif "veth0" tcp sport 12345 ct mark set 1 flow add @f1 counter name routed_repl accept' + ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif "br0" tcp sport 12345 ct mark set 1 flow add @f1 counter name routed_repl accept' + ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif "tun0" accept' + ip netns exec "$nsr1" nft -a insert rule inet filter forward \ + 'meta oif "tun6" accept' + + ip netns exec "$nsr1" nft reset counters table inet filter >/dev/null + + if test_tcp_forwarding "$ns1" "$ns2" 1 4 10.0.2.99 12345; then + check_counters "bridge + IPIP tunnel" + else + echo "FAIL: flow offload for ns1/ns2 with bridge + IPIP tunnel" 1>&2 + ip netns exec "$nsr1" nft list ruleset + ret=1 + fi + + if test_tcp_forwarding "$ns1" "$ns2" 1 6 "[dead:2::99]" 12345; then + check_counters "bridge + IP6IP6 tunnel" + else + echo "FAIL: flow offload for ns1/ns2 with bridge + IP6IP6 tunnel" 1>&2 + ip netns exec "$nsr1" nft list ruleset + ret=1 + fi + + ip -net "$nsr1" route change default via 192.168.10.2 + ip -net "$nsr2" route change default via 192.168.10.1 + ip -net "$ns2" route del default via 10.0.2.1 + ip -6 -net "$nsr1" route delete default + ip -6 -net "$nsr1" route add default via fee1:2::2 + ip -6 -net "$nsr2" route delete default + ip -6 -net "$nsr2" route add default via fee1:2::1 + ip -6 -net "$ns2" route del default via dead:2::1 +else + echo "SKIP: bridge + tunnel flowtable regression (tun0 missing)" + [ "$ret" -eq 0 ] && ret=$ksft_skip +fi + # Another test: # Add bridge interface br0 to Router1, with NAT and VLAN. diff --git a/tools/testing/selftests/net/netfilter/nft_offload.sh b/tools/testing/selftests/net/netfilter/nft_offload.sh new file mode 100755 index 000000000000..859bdedf1a51 --- /dev/null +++ b/tools/testing/selftests/net/netfilter/nft_offload.sh @@ -0,0 +1,132 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +source lib.sh + +checktool "nft --version" "run test without nft tool" +modprobe -q netdevsim + +sysfs="/sys/kernel/debug/fail_function" +failname="/proc/self/make-it-fail" +duration=30 +fault=0 +ret=0 +file_ft="" +file_rs="" +id=$((RANDOM%65536)) + +read -r t < /proc/sys/kernel/tainted +if [ "$t" -ne 0 ];then + echo SKIP: kernel is tainted + exit $ksft_skip +fi + +cleanup() { + cleanup_netdevsim "$id" "$NS" + cleanup_ns "$NS" + [ "$fault" -eq 1 ] && echo '!nsim_setup_tc' > "$sysfs/inject" + rm -f "$file_ft" "$file_rs" +} +trap cleanup EXIT + +skip() { + echo "SKIP: $*" + [ $ret -eq 0 ] && exit 4 + + exit $ret +} + +set -e +setup_ns NS + +create_netdevsim "$id" "$NS" >/dev/null +nsim_port=$(create_netdevsim_port "$id" "$NS" 2) + +file_ft=$(mktemp) +cat > "$file_ft" <<EOF +flush ruleset +table inet t { + flowtable f { + flags offload + hook ingress priority filter + 10 + devices = { "$nsim_port", "dummyf1" } + } + + chain cf { + type filter hook forward priority 0; policy accept; + ct state new meta l4proto tcp flow add @f + } +} +EOF + +if ip netns exec "$NS" nft -f "$file_ft"; then + echo "PASS: flowtable offload" +else + echo "FAIL: flowtable offload" + ret=1 +fi + +file_rs=$(mktemp) +cat > "$file_rs" <<EOF +table netdev t { + chain c { + type filter hook ingress device $nsim_port priority 1 + flags offload + ip saddr 10.2.1.1 ip daddr 10.2.1.2 ip protocol icmp accept + ip saddr 10.2.1.1 ip daddr 10.2.1.3 ip protocol icmp drop + ip saddr 10.2.1.0/24 ip daddr 10.2.1.0/24 ip protocol icmp accept + ip6 saddr dead:beef::1 ip6 daddr dead:beef::2 meta l4proto ipv6-icmp accept + ip6 saddr dead:beef::1 ip6 daddr dead:beef::3 meta l4proto ipv6-icmp drop + ip6 saddr dead:beef::/64 ip6 daddr dead:beef::/64 meta l4proto ipv6-icmp accept + } +} +EOF +if ip netns exec "$NS" nft -f "$file_rs"; then + echo "PASS: ruleset offload" +else + echo "FAIL: ruleset offload" + ret=1 +fi + +test -d "$sysfs" || skip "$sysfs not present" +grep -q nsim_setup_tc "$sysfs/injectable" || skip "nsim_setup_tc fault injection not available" + +echo Y > "$sysfs/task-filter" +echo 0 > "$sysfs/verbose" +echo "nsim_setup_tc" > "$sysfs/inject" +fault=1 + +p=$(((RANDOM%90) + 10)) +echo $p > "$sysfs/probability" +echo -1 > "$sysfs/times" + +count=0 +ok=0 + +now=$(date +%s) +stop=$((now+duration)) + +# fault-injection enabled rule loads are expected to fail. +set +e +while [ "$now" -le "$stop" ]; do + for f in "$file_ft" "$file_rs"; do + if ip netns exec "$NS" bash -c "echo 1 > $failname ; ip netns exec \"$NS\" nft -f $f" 2> /dev/null;then + ok=$((ok+1)) + fi + count=$((count+1)) + done + now=$(date +%s) +done + +sleep 5 + +read -r t < /proc/sys/kernel/tainted +if [ "$t" -eq 0 ];then + echo "PASS: Not tainted. $count rounds, $ok successful ruleset loads with P $p." +else + echo "ERROR: Tainted. $count rounds, $ok successful ruleset loads with P $p." + dmesg + ret=1 +fi + +exit $ret diff --git a/tools/testing/selftests/net/netfilter/nft_queue.sh b/tools/testing/selftests/net/netfilter/nft_queue.sh index 139bc1211878..7c857a2e0f34 100755 --- a/tools/testing/selftests/net/netfilter/nft_queue.sh +++ b/tools/testing/selftests/net/netfilter/nft_queue.sh @@ -11,6 +11,7 @@ ret=0 timeout=5 SCTP_TEST_TIMEOUT=60 +STRESS_TEST_TIMEOUT=30 cleanup() { @@ -84,11 +85,12 @@ ip -net "$ns3" route add default via 10.0.3.1 ip -net "$ns3" route add default via dead:3::1 load_ruleset() { - local name=$1 - local prio=$2 + local family=$1 + local name=$2 + local prio=$3 ip netns exec "$nsrouter" nft -f /dev/stdin <<EOF -table inet $name { +table $family $name { chain nfq { ip protocol icmp queue bypass icmpv6 type { "echo-request", "echo-reply" } queue num 1 bypass @@ -227,6 +229,7 @@ nf_queue_wait() test_queue() { local expected="$1" + local family="$2" local last="" # spawn nf_queue listeners @@ -254,11 +257,13 @@ test_queue() if [ x"$last" != x"$expected packets total" ]; then echo "FAIL: Expected $expected packets total, but got $last" 1>&2 ip netns exec "$nsrouter" nft list ruleset + echo -n "$TMPFILE0: ";cat "$TMPFILE0" + echo -n "$TMPFILE1: ";cat "$TMPFILE1" exit 1 fi done - echo "PASS: Expected and received $last" + echo "PASS: Expected and received $last ($family)" } listener_ready() @@ -399,6 +404,8 @@ EOF kill "$nfqpid" echo "PASS: icmp+nfqueue via vrf" + ip -net "$ns1" link del tvrf + ip netns exec "$ns1" nft flush ruleset } sctp_listener_ready() @@ -591,6 +598,7 @@ EOF test_udp_gro_ct() { local errprefix="FAIL: test_udp_gro_ct:" + local timeout=5 ip netns exec "$nsrouter" conntrack -F 2>/dev/null @@ -630,10 +638,10 @@ table inet udpq { } } EOF - timeout 10 ip netns exec "$ns2" socat UDP-LISTEN:12346,fork,pf=ipv4 OPEN:"$TMPFILE1",trunc & + timeout "$timeout" ip netns exec "$ns2" socat UDP-LISTEN:12346,fork,pf=ipv4 OPEN:"$TMPFILE1",trunc & local rpid=$! - ip netns exec "$nsrouter" ./nf_queue -G -c -q 1 -t 2 > "$TMPFILE2" & + ip netns exec "$nsrouter" nice -n -19 ./nf_queue -G -c -q 1 -o -t 2 > "$TMPFILE2" & local nfqpid=$! ip netns exec "$nsrouter" ethtool -K "veth0" rx-udp-gro-forwarding on rx-gro-list on generic-receive-offload on @@ -643,8 +651,12 @@ EOF local bs=512 local count=$(((32 * 1024 * 1024) / bs)) - dd if=/dev/zero bs="$bs" count="$count" 2>/dev/null | for i in $(seq 1 16); do - timeout 5 ip netns exec "$ns1" \ + + local nprocs=$(nproc) + [ $nprocs -gt 1 ] && nprocs=$((nprocs - 1)) + + dd if=/dev/zero bs="$bs" count="$count" 2>/dev/null | for i in $(seq 1 $nprocs); do + timeout "$timeout" nice -n 19 ip netns exec "$ns1" \ socat -u -b 512 STDIN UDP-DATAGRAM:10.0.2.99:12346,reuseport,bind=0.0.0.0:55221 & done @@ -714,6 +726,74 @@ EOF fi } +check_tainted() +{ + local msg="$1" + + if [ "$tainted_then" -ne 0 ];then + return + fi + + read tainted_now < /proc/sys/kernel/tainted + if [ "$tainted_now" -eq 0 ];then + echo "PASS: $msg" + else + echo "TAINT: $msg" + dmesg + ret=1 + fi +} + +test_queue_stress() +{ + read tainted_then < /proc/sys/kernel/tainted + local i + + ip netns exec "$nsrouter" nft -f /dev/stdin <<EOF +flush ruleset +table inet t { + chain forward { + type filter hook forward priority 0; policy accept; + + queue flags bypass to numgen random mod 8 + } +} +EOF + timeout "$STRESS_TEST_TIMEOUT" ip netns exec "$ns2" \ + socat -u UDP-LISTEN:12345,fork,pf=ipv4 STDOUT > /dev/null & + + timeout "$STRESS_TEST_TIMEOUT" ip netns exec "$ns3" \ + socat -u UDP-LISTEN:12345,fork,pf=ipv4 STDOUT > /dev/null & + + for i in $(seq 0 7); do + ip netns exec "$nsrouter" timeout "$STRESS_TEST_TIMEOUT" \ + ./nf_queue -q $i -t 2 -O -b > /dev/null & + done + + ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \ + ping -q -f 10.0.2.99 > /dev/null 2>&1 & + ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \ + ping -q -f 10.0.3.99 > /dev/null 2>&1 & + ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \ + ping -q -f "dead:2::99" > /dev/null 2>&1 & + ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \ + ping -q -f "dead:3::99" > /dev/null 2>&1 & + + busywait "$BUSYWAIT_TIMEOUT" udp_listener_ready "$ns2" 12345 + busywait "$BUSYWAIT_TIMEOUT" udp_listener_ready "$ns3" 12345 + + for i in $(seq 1 4);do + ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \ + socat -u STDIN UDP-DATAGRAM:10.0.2.99:12345 < /dev/zero > /dev/null & + ip netns exec "$ns1" timeout "$STRESS_TEST_TIMEOUT" \ + socat -u STDIN UDP-DATAGRAM:10.0.3.99:12345 < /dev/zero > /dev/null & + done + + wait + + check_tainted "concurrent queueing" +} + test_queue_removal() { read tainted_then < /proc/sys/kernel/tainted @@ -737,18 +817,48 @@ EOF ip netns exec "$ns1" nft flush ruleset - if [ "$tainted_then" -ne 0 ];then - return - fi + check_tainted "queue program exiting while packets queued" +} - read tainted_now < /proc/sys/kernel/tainted - if [ "$tainted_now" -eq 0 ];then - echo "PASS: queue program exiting while packets queued" - else - echo "TAINT: queue program exiting while packets queued" - dmesg - ret=1 +test_queue_bridge() +{ + ip -net "$nsrouter" addr flush dev veth0 + ip -net "$nsrouter" addr flush dev veth1 + + ip -net "$nsrouter" link add br0 type bridge + ip -net "$nsrouter" link set veth0 master br0 + ip -net "$nsrouter" link set veth1 master br0 + + ip -net "$nsrouter" link set br0 up + + ip -net "$nsrouter" addr add 10.0.2.1/16 dev br0 + ip -net "$nsrouter" addr add dead:2::1/64 dev br0 nodad + + ip -net "$ns1" addr flush dev eth0 + ip -net "$ns2" addr flush dev eth0 + + ip -net "$ns1" addr add 10.0.1.1/16 dev eth0 + ip -net "$ns1" addr add dead:2::2/64 dev eth0 nodad + + ip -net "$ns2" addr add 10.0.2.99/16 dev eth0 + ip -net "$ns2" addr add dead:2::99/64 dev eth0 nodad + + ip netns exec "$nsrouter" nft flush ruleset + + ip netns exec "$nsrouter" sysctl net.ipv6.conf.all.forwarding=0 > /dev/null + ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth0.forwarding=0 > /dev/null + ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth1.forwarding=0 > /dev/null + + if ! test_ping;then + echo "FAIL: netns bridge connectivity" 1>&2 + exit $ret fi + + load_ruleset "bridge" "filter" 10 + test_queue 10 "bridge" + + load_ruleset "bridge" "filter2" 20 + test_queue 20 "bridge" } ip netns exec "$nsrouter" sysctl net.ipv6.conf.all.forwarding=1 > /dev/null @@ -756,7 +866,7 @@ ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth0.forwarding=1 > /dev/null ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth1.forwarding=1 > /dev/null ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth2.forwarding=1 > /dev/null -load_ruleset "filter" 0 +load_ruleset "inet" "filter" 0 if test_ping; then # queue bypass works (rules were skipped, no listener) @@ -779,11 +889,11 @@ load_counter_ruleset 10 # 1x icmp prerouting,forward,postrouting -> 3 queue events (6 incl. reply). # 1x icmp prerouting,input,output postrouting -> 4 queue events incl. reply. # so we expect that userspace program receives 10 packets. -test_queue 10 +test_queue 10 "inet" # same. We queue to a second program as well. -load_ruleset "filter2" 20 -test_queue 20 +load_ruleset "inet" "filter2" 20 +test_queue 20 "inet" ip netns exec "$ns1" nft flush ruleset test_tcp_forward @@ -794,9 +904,13 @@ test_sctp_forward test_sctp_output test_udp_nat_race test_udp_gro_ct +test_queue_stress # should be last, adds vrf device in ns1 and changes routes test_icmp_vrf test_queue_removal +# turns router into a bridge +test_queue_bridge + exit $ret diff --git a/tools/testing/selftests/net/netfilter/nft_tproxy_udp.sh b/tools/testing/selftests/net/netfilter/nft_tproxy_udp.sh index d16de13fe5a7..1dc7b0450145 100755 --- a/tools/testing/selftests/net/netfilter/nft_tproxy_udp.sh +++ b/tools/testing/selftests/net/netfilter/nft_tproxy_udp.sh @@ -190,13 +190,13 @@ table inet filter { } EOF - timeout "$timeout" ip netns exec "$nsrouter" socat -u "$socat_ipproto" udp-listen:12345,fork,ip-transparent,reuseport udp:"$ns1_ip_port",ip-transparent,reuseport,bind="$ns2_ip_port" 2>/dev/null & + timeout "$timeout" ip netns exec "$nsrouter" socat -u "$socat_ipproto" udp-listen:12345,fork,ip-transparent,reuseport,shut-none udp:"$ns1_ip_port",ip-transparent,reuseport,bind="$ns2_ip_port",shut-none 2>/dev/null & local tproxy_pid=$! - timeout "$timeout" ip netns exec "$ns2" socat "$socat_ipproto" udp-listen:8080,fork SYSTEM:"echo PONG_NS2" 2>/dev/null & + timeout "$timeout" ip netns exec "$ns2" socat "$socat_ipproto" udp-listen:8080,fork,shut-none SYSTEM:"echo PONG_NS2" 2>/dev/null & local server2_pid=$! - timeout "$timeout" ip netns exec "$ns3" socat "$socat_ipproto" udp-listen:8080,fork SYSTEM:"echo PONG_NS3" 2>/dev/null & + timeout "$timeout" ip netns exec "$ns3" socat "$socat_ipproto" udp-listen:8080,fork,shut-none SYSTEM:"echo PONG_NS3" 2>/dev/null & local server3_pid=$! busywait "$BUSYWAIT_TIMEOUT" listener_ready "$nsrouter" 12345 "-u" @@ -205,7 +205,7 @@ EOF local result # request from ns1 to ns2 (forwarded traffic) - result=$(echo I_M_PROXIED | ip netns exec "$ns1" socat -t 2 -T 2 STDIO udp:"$ns2_ip_port",sourceport=18888) + result=$(echo I_M_PROXIED | ip netns exec "$ns1" socat -t 2 -T 2 STDIO udp:"$ns2_ip_port",sourceport=18888,shut-none) if [ "$result" == "$expect_ns1_ns2" ] ;then echo "PASS: tproxy test $testname: ns1 got reply \"$result\" connecting to ns2" else @@ -214,7 +214,7 @@ EOF fi # request from ns1 to ns3 (forwarded traffic) - result=$(echo I_M_PROXIED | ip netns exec "$ns1" socat -t 2 -T 2 STDIO udp:"$ns3_ip_port") + result=$(echo I_M_PROXIED | ip netns exec "$ns1" socat -t 2 -T 2 STDIO udp:"$ns3_ip_port",shut-none) if [ "$result" = "$expect_ns1_ns3" ] ;then echo "PASS: tproxy test $testname: ns1 got reply \"$result\" connecting to ns3" else @@ -223,7 +223,7 @@ EOF fi # request from nsrouter to ns2 (localy originated traffic) - result=$(echo I_M_PROXIED | ip netns exec "$nsrouter" socat -t 2 -T 2 STDIO udp:"$ns2_ip_port") + result=$(echo I_M_PROXIED | ip netns exec "$nsrouter" socat -t 2 -T 2 STDIO udp:"$ns2_ip_port",shut-none) if [ "$result" == "$expect_nsrouter_ns2" ] ;then echo "PASS: tproxy test $testname: nsrouter got reply \"$result\" connecting to ns2" else @@ -232,7 +232,7 @@ EOF fi # request from nsrouter to ns3 (localy originated traffic) - result=$(echo I_M_PROXIED | ip netns exec "$nsrouter" socat -t 2 -T 2 STDIO udp:"$ns3_ip_port") + result=$(echo I_M_PROXIED | ip netns exec "$nsrouter" socat -t 2 -T 2 STDIO udp:"$ns3_ip_port",shut-none) if [ "$result" = "$expect_nsrouter_ns3" ] ;then echo "PASS: tproxy test $testname: nsrouter got reply \"$result\" connecting to ns3" else diff --git a/tools/testing/selftests/net/nk_qlease.py b/tools/testing/selftests/net/nk_qlease.py new file mode 100755 index 000000000000..a84a73ff4eda --- /dev/null +++ b/tools/testing/selftests/net/nk_qlease.py @@ -0,0 +1,2109 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +import errno +import time +from lib.py import ( + ksft_run, + ksft_exit, + ksft_eq, + ksft_ne, + ksft_in, + ksft_not_in, + ksft_raises, +) +from lib.py import ( + NetNS, + NetNSEnter, + EthtoolFamily, + NetdevFamily, + RtnlFamily, + NetdevSimDev, +) +from lib.py import ( + NlError, + Netlink, + cmd, + defer, + ip, +) + + +def wait_until(cond, timeout=2.0, interval=0.05): + deadline = time.monotonic() + timeout + while not cond(): + if time.monotonic() >= deadline: + return + time.sleep(interval) + + +def create_netkit(rxqueues, mode="l2"): + all_links = ip("-d link show", json=True) + old_idxs = { + link["ifindex"] + for link in all_links + if link.get("linkinfo", {}).get("info_kind") == "netkit" + } + + rtnl = RtnlFamily() + rtnl.newlink( + { + "linkinfo": { + "kind": "netkit", + "data": { + "mode": mode, + "policy": "forward", + "peer-policy": "forward", + }, + }, + "num-rx-queues": rxqueues, + }, + flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], + ) + + all_links = ip("-d link show", json=True) + nk_links = [ + link + for link in all_links + if link.get("linkinfo", {}).get("info_kind") == "netkit" + and link["ifindex"] not in old_idxs + ] + nk_links.sort(key=lambda x: x["ifindex"]) + return ( + nk_links[1]["ifname"], + nk_links[1]["ifindex"], + nk_links[0]["ifname"], + nk_links[0]["ifindex"], + ) + + +def create_netkit_single(rxqueues): + rtnl = RtnlFamily() + rtnl.newlink( + { + "linkinfo": { + "kind": "netkit", + "data": { + "mode": "l2", + "pairing": "single", + }, + }, + "num-rx-queues": rxqueues, + }, + flags=[Netlink.NLM_F_CREATE, Netlink.NLM_F_EXCL], + ) + + all_links = ip("-d link show", json=True) + nk_links = [ + link + for link in all_links + if link.get("linkinfo", {}).get("info_kind") == "netkit" + and "UP" not in link.get("flags", []) + ] + return nk_links[0]["ifname"], nk_links[0]["ifindex"] + + +def test_remove_phys(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + nk_queue_id = result["id"] + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["ifindex"], nk_guest_idx) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + nsimdev.remove() + wait_until(lambda: cmd(f"ip link show dev {nk_host}", fail=False).ret != 0) + ret = cmd(f"ip link show dev {nk_host}", fail=False) + ksft_ne(ret.ret, 0) + + +def test_double_lease(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3) + defer(cmd, f"ip link del dev {nk_host}") + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EBUSY) + + +def test_virtual_lessor(netns) -> None: + nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_a}") + ip(f"link set dev {nk_host_a} up") + ip(f"link set dev {nk_guest_a} up") + + nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_b}") + + ip(f"link set dev {nk_guest_b} netns {netns.name}") + ip(f"link set dev {nk_host_b} up") + ip(f"link set dev {nk_guest_b} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_b_idx, + "type": "rx", + "lease": { + "ifindex": nk_guest_a_idx, + "queue": {"id": 0, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_phys_lessee(_netns) -> None: + nsimdev_a = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev_a.remove) + nsim_a = nsimdev_a.nsims[0] + ip(f"link set dev {nsim_a.ifname} up") + + nsimdev_b = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev_b.remove) + nsim_b = nsimdev_b.nsims[0] + ip(f"link set dev {nsim_b.ifname} up") + + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nsim_a.ifindex, + "type": "rx", + "lease": { + "ifindex": nsim_b.ifindex, + "queue": {"id": 0, "type": "rx"}, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_different_lessors(netns) -> None: + nsimdev_a = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev_a.remove) + nsim_a = nsimdev_a.nsims[0] + ip(f"link set dev {nsim_a.ifname} up") + + nsimdev_b = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev_b.remove) + nsim_b = nsimdev_b.nsims[0] + ip(f"link set dev {nsim_b.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim_a.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim_b.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP) + + +def test_queue_out_of_range(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.ERANGE) + + +def test_resize_leased(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + ethnl = EthtoolFamily() + with ksft_raises(NlError) as e: + ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1}) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_self_lease(_netns) -> None: + nk_host, _, _, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nk_guest_idx, + "queue": {"id": 0, "type": "rx"}, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_veth_queue_create(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + ip("link add veth0 type veth peer name veth1") + defer(cmd, "ip link del dev veth0", fail=False) + + all_links = ip("-d link show", json=True) + veth_peer = [ + link + for link in all_links + if link.get("ifname") == "veth1" + ] + veth_peer_idx = veth_peer[0]["ifindex"] + + ip(f"link set dev veth1 netns {netns.name}") + ip("link set dev veth0 up") + ip("link set dev veth1 up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": veth_peer_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_create_tx_type(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "tx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_create_primary(_netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, nk_host_idx, _, _ = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_host} up") + + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_host_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP) + + +def test_create_limit(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=1) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_link_flap_phys(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}") + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + nk_queue_id = result["id"] + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + # Link flap the physical device + ip(f"link set dev {nsim.ifname} down") + ip(f"link set dev {nsim.ifname} up") + + # Verify lease survives the flap + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + +def test_queue_get_virtual(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}") + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + nk_queue_id = result["id"] + + # queue-get on virtual device's leased queue should not show lease + # info (lease info is only shown from the physical device's side) + queue_info = netdevnl.queue_get( + {"ifindex": nk_guest_idx, "id": nk_queue_id, "type": "rx"} + ) + ksft_eq(queue_info["id"], nk_queue_id) + ksft_eq(queue_info["ifindex"], nk_guest_idx) + ksft_not_in("lease", queue_info) + + # Default queue (not leased) also has no lease info + queue_info = netdevnl.queue_get( + {"ifindex": nk_guest_idx, "id": 0, "type": "rx"} + ) + ksft_not_in("lease", queue_info) + + +def test_remove_virt_first(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], result["id"]) + + # Delete netkit (virtual device removed first, physical stays) + cmd(f"ip link del dev {nk_host}") + + # Verify lease is cleaned up on physical device + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_not_in("lease", queue_info) + + +def test_multiple_leases(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=3) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=4) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + r1 = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + r2 = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + ksft_eq(r1["id"], 1) + ksft_eq(r2["id"], 2) + + # Verify both leases visible on physical device + netdevnl = NetdevFamily() + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_in("lease", q1) + ksft_in("lease", q2) + ksft_eq(q1["lease"]["ifindex"], nk_guest_idx) + ksft_eq(q2["lease"]["ifindex"], nk_guest_idx) + ksft_eq(q1["lease"]["queue"]["id"], r1["id"]) + ksft_eq(q2["lease"]["queue"]["id"], r2["id"]) + + +def test_lease_queue_tx_type(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "tx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_invalid_netns(netns) -> None: + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": 1, + "queue": {"id": 0, "type": "rx"}, + "netns-id": 999, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.ENONET) + + +def test_invalid_phys_ifindex(netns) -> None: + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": 99999, + "queue": {"id": 0, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.ENODEV) + + +def test_multi_netkit_remove_phys(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=3) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + # Create two netkit pairs, each leasing a different physical queue + nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_a}", fail=False) + + nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_b}", fail=False) + + ip(f"link set dev {nk_guest_a} netns {netns.name}") + ip(f"link set dev {nk_host_a} up") + ip(f"link set dev {nk_guest_a} up", ns=netns) + + ip(f"link set dev {nk_guest_b} netns {netns.name}") + ip(f"link set dev {nk_host_b} up") + ip(f"link set dev {nk_guest_b} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_guest_a_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + netdevnl.queue_create( + { + "ifindex": nk_guest_b_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + # Removing the physical device should take down both netkit pairs + nsimdev.remove() + wait_until(lambda: cmd(f"ip link show dev {nk_host_a}", fail=False).ret != 0 + and cmd(f"ip link show dev {nk_host_b}", fail=False).ret != 0) + ret = cmd(f"ip link show dev {nk_host_a}", fail=False) + ksft_ne(ret.ret, 0) + ret = cmd(f"ip link show dev {nk_host_b}", fail=False) + ksft_ne(ret.ret, 0) + + +def test_single_remove_phys(_netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_name, nk_idx = create_netkit_single(rxqueues=2) + defer(cmd, f"ip link del dev {nk_name}", fail=False) + + ip(f"link set dev {nk_name} up") + + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + + # Removing the physical device should take down the single netkit device + nsimdev.remove() + wait_until(lambda: cmd(f"ip link show dev {nk_name}", fail=False).ret != 0) + ret = cmd(f"ip link show dev {nk_name}", fail=False) + ksft_ne(ret.ret, 0) + + +def test_link_flap_virt(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}") + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + nk_queue_id = result["id"] + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + # Link flap the virtual (netkit) device + ip(f"link set dev {nk_guest} down", ns=netns) + ip(f"link set dev {nk_guest} up", ns=netns) + + # Verify lease survives the virtual device flap + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + +def test_phys_queue_no_lease(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}") + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + # Physical queue 0 (not leased) should have no lease info + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 0, "type": "rx"} + ) + ksft_not_in("lease", queue_info) + + # Physical queue 1 (leased) should have lease info + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + + +def test_same_ns_lease(_netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_name, nk_idx = create_netkit_single(rxqueues=2) + defer(cmd, f"ip link del dev {nk_name}", fail=False) + + ip(f"link set dev {nk_name} up") + + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + ksft_eq(result["id"], 1) + + # Same namespace: lease info should NOT have netns-id + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["ifindex"], nk_idx) + ksft_eq(queue_info["lease"]["queue"]["id"], result["id"]) + ksft_not_in("netns-id", queue_info["lease"]) + + +def test_resize_after_unlease(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + # Resize should fail while lease is active + ethnl = EthtoolFamily() + with ksft_raises(NlError) as e: + ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1}) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + # Delete netkit, clearing the lease + cmd(f"ip link del dev {nk_host}") + + # Resize should now succeed + ethnl.channels_set({"header": {"dev-index": nsim.ifindex}, "combined-count": 1}) + + +def test_lease_queue_zero(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 0, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 0, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], result["id"]) + + +def test_release_and_reuse(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + src_queue = 1 + + # First lease + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + + # Delete netkit, freeing the lease + cmd(f"ip link del dev {nk_host}") + + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_not_in("lease", queue_info) + + # Re-create netkit and lease the same physical queue again + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)): + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], result["id"]) + + +def test_two_netkits_same_queue(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_a}", fail=False) + + nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_b}", fail=False) + + ip(f"link set dev {nk_guest_a} netns {netns.name}") + ip(f"link set dev {nk_host_a} up") + ip(f"link set dev {nk_guest_a} up", ns=netns) + + ip(f"link set dev {nk_guest_b} netns {netns.name}") + ip(f"link set dev {nk_host_b} up") + ip(f"link set dev {nk_guest_b} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_a_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + with ksft_raises(NlError) as e: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_b_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EBUSY) + + +def test_l3_mode_lease(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2, mode="l3") + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + result = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["ifindex"], nk_guest_idx) + ksft_eq(queue_info["lease"]["queue"]["id"], result["id"]) + + +def test_single_double_lease(_netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_name, nk_idx = create_netkit_single(rxqueues=3) + defer(cmd, f"ip link del dev {nk_name}", fail=False) + + ip(f"link set dev {nk_name} up") + + netdevnl = NetdevFamily() + result = netdevnl.queue_create( + { + "ifindex": nk_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + ksft_eq(result["id"], 1) + + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EBUSY) + + +def test_single_different_lessors(_netns) -> None: + nsimdev_a = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev_a.remove) + nsim_a = nsimdev_a.nsims[0] + ip(f"link set dev {nsim_a.ifname} up") + + nsimdev_b = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev_b.remove) + nsim_b = nsimdev_b.nsims[0] + ip(f"link set dev {nsim_b.ifname} up") + + nk_name, nk_idx = create_netkit_single(rxqueues=3) + defer(cmd, f"ip link del dev {nk_name}", fail=False) + + ip(f"link set dev {nk_name} up") + + netdevnl = NetdevFamily() + netdevnl.queue_create( + { + "ifindex": nk_idx, + "type": "rx", + "lease": { + "ifindex": nsim_a.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + + with ksft_raises(NlError) as e: + netdevnl.queue_create( + { + "ifindex": nk_idx, + "type": "rx", + "lease": { + "ifindex": nsim_b.ifindex, + "queue": {"id": 1, "type": "rx"}, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP) + + +def test_cross_ns_netns_id(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_in("netns-id", queue_info["lease"]) + + +def test_delete_guest_netns(_netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + test_ns = NetNS() + ip("netns set init 0", ns=test_ns) + ip("link set lo up", ns=test_ns) + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {test_ns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=test_ns) + + src_queue = 1 + with NetNSEnter(str(test_ns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + + del test_ns + wait_until(lambda: "lease" not in netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"})) + + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_not_in("lease", queue_info) + + ret = cmd(f"ip link show dev {nk_host}", fail=False) + ksft_ne(ret.ret, 0) + + +def test_move_guest_netns(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + result = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + nk_queue_id = result["id"] + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + new_ns = NetNS() + defer(new_ns.__del__) + ip(f"link set dev {nk_guest} netns {new_ns.name}", ns=netns) + + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + +def test_resize_phys_no_reduction(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + ethnl = EthtoolFamily() + ethnl.channels_set( + {"header": {"dev-index": nsim.ifindex}, "combined-count": 2} + ) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + + +def test_delete_one_netkit_of_two(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=3) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_a}", fail=False) + + nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_b}", fail=False) + + ip(f"link set dev {nk_guest_a} netns {netns.name}") + ip(f"link set dev {nk_host_a} up") + ip(f"link set dev {nk_guest_a} up", ns=netns) + + ip(f"link set dev {nk_guest_b} netns {netns.name}") + ip(f"link set dev {nk_host_b} up") + ip(f"link set dev {nk_guest_b} up", ns=netns) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_a_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_b_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + netdevnl = NetdevFamily() + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_in("lease", q1) + ksft_in("lease", q2) + + cmd(f"ip link del dev {nk_host_a}") + + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_not_in("lease", q1) + ksft_in("lease", q2) + + +def test_bind_rx_leased_phys_queue(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + netdevnl = NetdevFamily() + with ksft_raises(NlError) as e: + netdevnl.bind_rx( + { + "ifindex": nsim.ifindex, + "fd": 0, + "queues": [ + {"id": 0, "type": "rx"}, + {"id": 1, "type": "rx"}, + ], + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP) + + +def test_resize_phys_shrink_past_leased(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=4) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + ethnl = EthtoolFamily() + + # Shrink past the leased queue — only queue 3 removed, queue 1 untouched + ethnl.channels_set( + {"header": {"dev-index": nsim.ifindex}, "combined-count": 3} + ) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + + # Shrink further — queue 2 removed, queue 1 still untouched + ethnl.channels_set( + {"header": {"dev-index": nsim.ifindex}, "combined-count": 2} + ) + + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + + # Shrink into the leased queue — queue 1 is busy, must fail + with ksft_raises(NlError) as e: + ethnl.channels_set( + {"header": {"dev-index": nsim.ifindex}, "combined-count": 1} + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + +def test_resize_virt_not_supported(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, nk_host_idx, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + # Channel resize on the netkit host must fail — not supported + ethnl = EthtoolFamily() + with ksft_raises(NlError) as e: + ethnl.channels_set( + {"header": {"dev-index": nk_host_idx}, "combined-count": 1} + ) + ksft_eq(e.exception.nl_msg.error, -errno.EOPNOTSUPP) + + # Lease must be intact + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + + +def test_lease_devices_down(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + + # Create lease while both physical and virtual devices are down + src_queue = 1 + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + result = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + # Bring devices up before queue_get: netdevsim only instantiates NAPIs in + # ndo_open, and netdev-genl queue_get returns -ENOENT without a NAPI. + ip(f"link set dev {nsim.ifname} up") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], result["id"]) + + +def test_lease_capacity_exhaustion(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=4) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + # rxqueues=3 means num_rx_queues=3, real_num_rx_queues starts at 1. + # Can create 2 leased queues (real goes 1->2->3) but not a 3rd (3->4 > 3). + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=3) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + r1 = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(r1["id"], 1) + + r2 = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(r2["id"], 2) + + # Third lease fails — netkit queue capacity exhausted + with ksft_raises(NlError) as e: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 3, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(e.exception.nl_msg.error, -errno.EINVAL) + + # Verify the two successful leases are intact + netdevnl = NetdevFamily() + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_in("lease", q1) + ksft_in("lease", q2) + + +def test_resize_phys_up(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=3) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + # Shrink nsim first so we have room to grow + ethnl = EthtoolFamily() + ethnl.channels_set( + {"header": {"dev-index": nsim.ifindex}, "combined-count": 2} + ) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + # Grow channels — should succeed since leased queue is not removed + ethnl.channels_set( + {"header": {"dev-index": nsim.ifindex}, "combined-count": 3} + ) + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", queue_info) + + # New queue 2 should exist without a lease + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_not_in("lease", queue_info) + + +def test_multi_ns_lease(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=3) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + ns_b = NetNS() + defer(ns_b.__del__) + ip("netns set init 0", ns=ns_b) + ip("link set lo up", ns=ns_b) + + # First netkit pair, guest in netns + nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_a}", fail=False) + ip(f"link set dev {nk_guest_a} netns {netns.name}") + ip(f"link set dev {nk_host_a} up") + ip(f"link set dev {nk_guest_a} up", ns=netns) + + # Second netkit pair, guest in ns_b + nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_b}", fail=False) + ip(f"link set dev {nk_guest_b} netns {ns_b.name}") + ip(f"link set dev {nk_host_b} up") + ip(f"link set dev {nk_guest_b} up", ns=ns_b) + + # Lease from netns + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + result = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_a_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + # Lease from ns_b (different namespace, same physical device) + with NetNSEnter(str(ns_b)), NetdevFamily() as netdevnl_ns: + result = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_b_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + ksft_eq(result["id"], 1) + + # Verify both leases from the physical side + netdevnl = NetdevFamily() + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_in("lease", q1) + ksft_in("lease", q2) + ksft_eq(q1["lease"]["ifindex"], nk_guest_a_idx) + ksft_eq(q2["lease"]["ifindex"], nk_guest_b_idx) + + +def test_multi_ns_delete_one(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=3) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + ns_b = NetNS() + ip("netns set init 0", ns=ns_b) + ip("link set lo up", ns=ns_b) + + # First netkit pair, guest in netns (ns_a) + nk_host_a, _, nk_guest_a, nk_guest_a_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_a}", fail=False) + ip(f"link set dev {nk_guest_a} netns {netns.name}") + ip(f"link set dev {nk_host_a} up") + ip(f"link set dev {nk_guest_a} up", ns=netns) + + # Second netkit pair, guest in ns_b + nk_host_b, _, nk_guest_b, nk_guest_b_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host_b}", fail=False) + + ip(f"link set dev {nk_guest_b} netns {ns_b.name}") + ip(f"link set dev {nk_host_b} up") + ip(f"link set dev {nk_guest_b} up", ns=ns_b) + + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_a_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 1, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + with NetNSEnter(str(ns_b)), NetdevFamily() as netdevnl_ns: + netdevnl_ns.queue_create( + { + "ifindex": nk_guest_b_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": 2, "type": "rx"}, + "netns-id": 0, + }, + } + ) + + netdevnl = NetdevFamily() + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_in("lease", q1) + ksft_in("lease", q2) + + # Delete ns_b — destroys nk_guest_b, triggers unlease of queue 2 + del ns_b + wait_until(lambda: "lease" not in netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"})) + + # ns_a's lease on queue 1 must survive + q1 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 1, "type": "rx"} + ) + ksft_in("lease", q1) + ksft_eq(q1["lease"]["ifindex"], nk_guest_a_idx) + + # ns_b's lease on queue 2 must be gone + q2 = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": 2, "type": "rx"} + ) + ksft_not_in("lease", q2) + + # nk_host_b should be gone too (phys removal cascades to netkit pair) + ret = cmd(f"ip link show dev {nk_host_b}", fail=False) + ksft_ne(ret.ret, 0) + + +def test_move_phys_netns(netns) -> None: + nsimdev = NetdevSimDev(port_count=1, queue_count=2) + defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + ip(f"link set dev {nsim.ifname} up") + + nk_host, _, nk_guest, nk_guest_idx = create_netkit(rxqueues=2) + defer(cmd, f"ip link del dev {nk_host}", fail=False) + + ip(f"link set dev {nk_guest} netns {netns.name}") + ip(f"link set dev {nk_host} up") + ip(f"link set dev {nk_guest} up", ns=netns) + + src_queue = 1 + with NetNSEnter(str(netns)), NetdevFamily() as netdevnl_ns: + nk_queue_id = netdevnl_ns.queue_create( + { + "ifindex": nk_guest_idx, + "type": "rx", + "lease": { + "ifindex": nsim.ifindex, + "queue": {"id": src_queue, "type": "rx"}, + "netns-id": 0, + }, + } + )["id"] + + netdevnl = NetdevFamily() + queue_info = netdevnl.queue_get( + {"ifindex": nsim.ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + + # Move the physical device to a new namespace. Move it back to init_net + # on cleanup before the other defers fire (new_ns deletion, nsimdev.remove) + # so nsim lives in a stable namespace when they run. + new_ns = NetNS() + defer(new_ns.__del__) + ip(f"link set dev {nsim.ifname} netns {new_ns.name}") + defer(ip, f"link set dev {nsim.ifname} netns init", ns=new_ns) + + # Physical device is now in new_ns — find its ifindex there + all_links = ip("-d link show", json=True, ns=new_ns) + nsim_in_new = [lnk for lnk in all_links if lnk.get("ifname") == nsim.ifname] + new_ifindex = nsim_in_new[0]["ifindex"] + + # Moving a device across netns brings it admin-down; bring it back up so + # netdevsim re-creates the NAPI (netdev-genl queue_get needs it). + ip(f"link set dev {nsim.ifname} up", ns=new_ns) + + # Verify lease survived the namespace move + with NetNSEnter(str(new_ns)), NetdevFamily() as netdevnl_ns: + queue_info = netdevnl_ns.queue_get( + {"ifindex": new_ifindex, "id": src_queue, "type": "rx"} + ) + ksft_in("lease", queue_info) + ksft_eq(queue_info["lease"]["queue"]["id"], nk_queue_id) + + +def main() -> None: + netns = NetNS() + cmd("ip netns attach init 1") + ip("netns set init 0", ns=netns) + ip("link set lo up", ns=netns) + + ksft_run( + [ + test_remove_phys, + test_double_lease, + test_virtual_lessor, + test_phys_lessee, + test_different_lessors, + test_queue_out_of_range, + test_resize_leased, + test_self_lease, + test_create_tx_type, + test_create_primary, + test_create_limit, + test_link_flap_phys, + test_queue_get_virtual, + test_remove_virt_first, + test_multiple_leases, + test_lease_queue_tx_type, + test_invalid_netns, + test_invalid_phys_ifindex, + test_multi_netkit_remove_phys, + test_single_remove_phys, + test_link_flap_virt, + test_phys_queue_no_lease, + test_same_ns_lease, + test_resize_after_unlease, + test_lease_queue_zero, + test_release_and_reuse, + test_veth_queue_create, + test_two_netkits_same_queue, + test_l3_mode_lease, + test_single_double_lease, + test_single_different_lessors, + test_cross_ns_netns_id, + test_delete_guest_netns, + test_move_guest_netns, + test_resize_phys_no_reduction, + test_delete_one_netkit_of_two, + test_bind_rx_leased_phys_queue, + test_resize_phys_shrink_past_leased, + test_resize_virt_not_supported, + test_lease_devices_down, + test_lease_capacity_exhaustion, + test_resize_phys_up, + test_multi_ns_lease, + test_multi_ns_delete_one, + test_move_phys_netns, + ], + args=(netns,), + ) + + cmd("ip netns del init", fail=False) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/net/nl_netdev.py b/tools/testing/selftests/net/nl_netdev.py index 5c66421ab8aa..ceb44c8e1fec 100755 --- a/tools/testing/selftests/net/nl_netdev.py +++ b/tools/testing/selftests/net/nl_netdev.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 -import time +""" +Tests for the netdev netlink family. +""" + +import errno from os import system -from lib.py import ksft_run, ksft_exit, ksft_pr -from lib.py import ksft_eq, ksft_ge, ksft_ne, ksft_busy_wait -from lib.py import NetdevFamily, NetdevSimDev, ip +from lib.py import ksft_run, ksft_exit +from lib.py import ksft_eq, ksft_ge, ksft_ne, ksft_raises, ksft_busy_wait +from lib.py import NetdevFamily, NetdevSimDev, NlError, defer, ip def empty_check(nf) -> None: @@ -19,6 +23,15 @@ def lo_check(nf) -> None: ksft_eq(len(lo_info['xdp-rx-metadata-features']), 0) +def dev_dump_reject_attr(nf) -> None: + """Test that dev-get dump rejects attributes (no dump request policy).""" + with ksft_raises(NlError) as cm: + nf.dev_get({'ifindex': 1}, dump=True) + ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL) + ksft_eq(cm.exception.nl_msg.extack['msg'], 'Unknown attribute type') + ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.ifindex') + + def napi_list_check(nf) -> None: with NetdevSimDev(queue_count=100) as nsimdev: nsim = nsimdev.nsims[0] @@ -242,10 +255,132 @@ def page_pool_check(nf) -> None: nsim.dfs_write("pp_hold", "y") +def page_pool_dump_ifindex(nf) -> None: + """Test page pool dump filtering by ifindex.""" + nsimdev1 = NetdevSimDev(queue_count=3) + rm_nsim1 = defer(nsimdev1.remove) + nsimdev2 = NetdevSimDev(queue_count=5) + defer(nsimdev2.remove) + + nsim1 = nsimdev1.nsims[0] + nsim2 = nsimdev2.nsims[0] + + ip(f"link set dev {nsim1.ifname} up") + ip(f"link set dev {nsim2.ifname} up") + + # Unfiltered dump should have pools from both devices + all_pp = nf.page_pool_get({}, dump=True) + pp1_all = [pp for pp in all_pp + if pp.get("ifindex") == nsim1.ifindex] + pp2_all = [pp for pp in all_pp + if pp.get("ifindex") == nsim2.ifindex] + ksft_ge(len(pp1_all), 1) + ksft_ge(len(pp2_all), 1) + + # Filtered dump should only return pools for that device + pp1_flt = nf.page_pool_get({'ifindex': nsim1.ifindex}, dump=True) + ksft_eq(pp1_flt, pp1_all) + + pp2_flt = nf.page_pool_get({'ifindex': nsim2.ifindex}, dump=True) + ksft_eq(pp2_flt, pp2_all) + + # Non-existent ifindex should return empty dump + pp_none = nf.page_pool_get({'ifindex': 12345678}, dump=True) + ksft_eq(len(pp_none), 0) + + # Device down - no pools for that ifindex + ip(f"link set dev {nsim1.ifname} down") + pp1_down = nf.page_pool_get({'ifindex': nsim1.ifindex}, dump=True) + ksft_eq(len(pp1_down), 0) + + # Remove device, dump by its old ifindex should return empty + old_ifindex = nsim1.ifindex + rm_nsim1.exec() + pp1_gone = nf.page_pool_get({'ifindex': old_ifindex}, dump=True) + ksft_eq(len(pp1_gone), 0) + + +def page_pool_ifindex_leak_check(nf) -> None: + """Test that zombie page pools don't show up under the original ifindex.""" + nsimdev = NetdevSimDev() + rm_nsim = defer(nsimdev.remove) + nsim = nsimdev.nsims[0] + + ip(f"link set dev {nsim.ifname} up") + nsim.dfs_write("pp_hold", "y") + + pp_up = nf.page_pool_get({'ifindex': nsim.ifindex}, dump=True) + ksft_ge(len(pp_up), 1) + + # Remove device with leaked page - pool becomes zombie (orphaned to lo) + old_ifindex = nsim.ifindex + rm_nsim.exec() + + # Zombie pool should NOT appear under the original device + pp_down = nf.page_pool_get({'ifindex': old_ifindex}, dump=True) + ksft_eq(len(pp_down), 0) + + # But it should appear in an unfiltered dump (under loopback) + pp_all = nf.page_pool_get({}, dump=True) + orphans = [pp for pp in pp_all + if "detach-time" in pp and "ifindex" not in pp] + ksft_ge(len(orphans), 1) + + +def page_pool_stats_ifindex_check(nf) -> None: + """Test page pool stats dump filtering by ifindex.""" + nsimdev1 = NetdevSimDev(queue_count=3) + defer(nsimdev1.remove) + nsimdev2 = NetdevSimDev(queue_count=5) + defer(nsimdev2.remove) + + nsim1 = nsimdev1.nsims[0] + nsim2 = nsimdev2.nsims[0] + + ip(f"link set dev {nsim1.ifname} up") + ip(f"link set dev {nsim2.ifname} up") + + # Unfiltered stats dump + all_stats = nf.page_pool_stats_get({}, dump=True) + s1_all = [s for s in all_stats + if s.get("info", {}).get("ifindex") == nsim1.ifindex] + s2_all = [s for s in all_stats + if s.get("info", {}).get("ifindex") == nsim2.ifindex] + ksft_ge(len(s1_all), 1) + ksft_ge(len(s2_all), 1) + + # Filtered stats dump + s1_flt = nf.page_pool_stats_get({'info': {'ifindex': nsim1.ifindex}}, + dump=True) + ksft_eq(s1_flt, s1_all) + + # Non-existent ifindex should return empty + s_none = nf.page_pool_stats_get({'info': {'ifindex': 12345678}}, dump=True) + ksft_eq(len(s_none), 0) + + # info.id should be rejected for stats dump + with ksft_raises(NlError) as cm: + nf.page_pool_stats_get({'info': {'id': s1_all[0]['info']['id']}}, + dump=True) + ksft_eq(cm.exception.nl_msg.error, -errno.EINVAL) + ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.info.id') + + def main() -> None: + """ Ksft boiler plate main """ nf = NetdevFamily() - ksft_run([empty_check, lo_check, page_pool_check, napi_list_check, - dev_set_threaded, napi_set_threaded, nsim_rxq_reset_down], + ksft_run([empty_check, + lo_check, + dev_dump_reject_attr, + napi_list_check, + napi_set_threaded, + dev_set_threaded, + nsim_rxq_reset_down, + page_pool_check, + page_pool_dump_ifindex, + page_pool_ifindex_leak_check, + page_pool_stats_ifindex_check + ], args=(nf, )) ksft_exit() diff --git a/tools/testing/selftests/net/nl_nlctrl.py b/tools/testing/selftests/net/nl_nlctrl.py new file mode 100755 index 000000000000..237b3d273260 --- /dev/null +++ b/tools/testing/selftests/net/nl_nlctrl.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +""" +Tests for the nlctrl genetlink family (family info and policy dumps). +""" + +from lib.py import ksft_run, ksft_exit +from lib.py import ksft_eq, ksft_ge, ksft_true, ksft_in, ksft_not_in +from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily + +# Families we can expect to always be around, and which between them +# cover ops with a do, with a dump, and with both. +FAMILIES = ('nlctrl', 'netdev') + + +def _get_ops(ctrl, name): + """Get the ops of a family, keyed by command id.""" + fam = ctrl.getfamily({'family-name': name}) + ksft_eq(fam['family-name'], name) + ksft_true(fam['family-id'] > 0) + + # The format of ops is quite odd, [{$idx: {"id"...}}, {$idx: {"id"...}}] + # Discard the indices and re-key by command id. + ops_by_id = {v['id']: v for op in fam['ops'] for v in op.values()} + ksft_eq(len(ops_by_id), len(fam['ops']), + comment=f"{name} lists a command twice") + return ops_by_id + + +def _get_policy_map(ctrl, req): + """ + The policy map in the Netlink replies looks like this: + + [{'family-id': 16, 'op-policy': {'do': 0, 'dump': 0, 'op-id': 3}}, + {'family-id': 16, 'op-policy': {'dump': 1, 'op-id': 4}}, ...] + + Return the mapping: + + {3:{'do','dump'}, 4:{'dump'}} + + The policy itself is discarded here, only return which command has policy. + """ + pol_map = {} + for msg in ctrl.getpolicy(req, dump=True): + if 'op-policy' not in msg: + continue + modes = dict(msg['op-policy']) + cmd = modes.pop('op-id') + ksft_not_in(cmd, pol_map, comment=f"command {cmd} reported twice") + pol_map[cmd] = set(modes.keys()) + return pol_map + + +def getfamily_do(ctrl) -> None: + """Query single families by name and validate their ops.""" + ops = {name: _get_ops(ctrl, name) for name in FAMILIES} + + for name, ops_by_id in ops.items(): + for op in ops_by_id.values(): + # All ops in nlctrl and netdev have a policy + ksft_in('cmd-cap-haspol', op['flags'], + comment=f"{name} op {op['id']} missing haspol") + ksft_true(op['flags'] & {'cmd-cap-do', 'cmd-cap-dump'}, + comment=f"{name} op {op['id']} has no handler") + + # nlctrl getfamily (id 3) does both, getpolicy (id 10) is dump-only + ksft_in('cmd-cap-do', ops['nlctrl'][3]['flags']) + ksft_in('cmd-cap-dump', ops['nlctrl'][3]['flags']) + ksft_not_in('cmd-cap-do', ops['nlctrl'][10]['flags']) + ksft_in('cmd-cap-dump', ops['nlctrl'][10]['flags']) + + netdev = ops['netdev'] + + # dev-get (id 1) should support both do and dump + ksft_in('cmd-cap-do', netdev[1]['flags']) + ksft_in('cmd-cap-dump', netdev[1]['flags']) + + # qstats-get (id 12) is dump-only + ksft_not_in('cmd-cap-do', netdev[12]['flags']) + ksft_in('cmd-cap-dump', netdev[12]['flags']) + + # napi-set (id 14) is do-only and requires admin + ksft_in('cmd-cap-do', netdev[14]['flags']) + ksft_not_in('cmd-cap-dump', netdev[14]['flags']) + ksft_in('admin-perm', netdev[14]['flags']) + + # Notification-only commands (dev-add/del/change-ntf etc.) must + # not appear in the ops list since they have no do/dump handlers. + for ntf_id in [2, 3, 4, 6, 7, 8]: + ksft_not_in(ntf_id, netdev, + comment=f"ntf-only cmd {ntf_id} should not be in ops") + + +def getfamily_dump(ctrl) -> None: + """Dump all families and verify expected entries.""" + families = ctrl.getfamily({}, dump=True) + ksft_ge(len(families), 2) + + names = [f['family-name'] for f in families] + ksft_in('nlctrl', names, comment="nlctrl not found in family dump") + ksft_in('netdev', names, comment="netdev not found in family dump") + + +def getpolicy_dump(_ctrl) -> None: + """Dump policies for ops using get_policy() and validate results. + + Test with netdev (split ops) where do and dump can have different + policies, and with ethtool (full ops) where they always share one. + """ + # -- netdev (split ops) -- + ndev = NetdevFamily() + + # dev-get: do has a real policy with ifindex, dump has no policy + # (only the reject-all policy with maxattr=0) + pol = ndev.get_policy('dev-get', 'do') + ksft_in('ifindex', pol, comment="dev-get do policy should have ifindex") + ksft_eq(pol['ifindex'].type, 'u32') + + pol_dump = ndev.get_policy('dev-get', 'dump') + ksft_eq(len(pol_dump), 0, comment="dev-get should not accept any attrs") + + # napi-get: both do and dump have real policies + pol_do = ndev.get_policy('napi-get', 'do') + ksft_ge(len(pol_do), 1) + + pol_dump = ndev.get_policy('napi-get', 'dump') + ksft_ge(len(pol_dump), 1) + + # -- ethtool (full ops) -- + et = EthtoolFamily() + + # strset-get (has both do and dump, full ops share policy) + pol_do = et.get_policy('strset-get', 'do') + ksft_ge(len(pol_do), 1, comment="strset-get should have a do policy") + + pol_dump = et.get_policy('strset-get', 'dump') + ksft_ge(len(pol_dump), 1, comment="strset-get should have a dump policy") + + # Same policy means same attribute names + ksft_eq(set(pol_do.keys()), set(pol_dump.keys())) + + # linkinfo-set is do-only (SET command), no dump + pol_do = et.get_policy('linkinfo-set', 'do') + ksft_ge(len(pol_do), 1, comment="linkinfo-set should have a do policy") + + pol_dump = et.get_policy('linkinfo-set', 'dump') + ksft_eq(pol_dump, None, + comment="linkinfo-set should not have a dump policy") + + +def getpolicy_op_map(ctrl) -> None: + """Check the op-to-policy map consistency. Each op with 'haspol' flag + has to have a policy. The policy back-references must name only + real ops that exist, have given modes (do vs dump) and have 'haspol'. + """ + for name in FAMILIES: + ops_by_id = _get_ops(ctrl, name) + haspol = {cmd for cmd, op in ops_by_id.items() + if 'cmd-cap-haspol' in op['flags']} + + pol_map = _get_policy_map(ctrl, {'family-name': name}) + ksft_eq(set(pol_map), haspol, + comment=f"{name} policy map does not match the op list") + + # Walk the op list rather than the map, the map may be missing + # the very op we are after. Asking for a command the family does + # not have is an error, so it must not come from the map either. + for cmd in sorted(haspol): + modes = pol_map.get(cmd, set()) + + # The kernel only reports a mode the op actually has. + if 'do' in modes: + ksft_in('cmd-cap-do', ops_by_id[cmd]['flags'], + comment=f"{name} cmd {cmd} has no do") + if 'dump' in modes: + ksft_in('cmd-cap-dump', ops_by_id[cmd]['flags'], + comment=f"{name} cmd {cmd} has no dump") + + # Asking for one op builds the map in a different place in + # the kernel, it has to report what the full dump did. + single = _get_policy_map(ctrl, {'family-name': name, 'op': cmd}) + ksft_eq(single, {cmd: modes}, + comment=f"{name} cmd {cmd} policy differs from the dump") + + +def getpolicy_by_op(_ctrl) -> None: + """Query policy for specific ops, check attr names are resolved.""" + ndev = NetdevFamily() + + # dev-get do policy should have named attributes from the spec + pol = ndev.get_policy('dev-get', 'do') + ksft_ge(len(pol), 1) + # All attr names should be resolved (no 'attr-N' fallbacks) + for name in pol: + ksft_true(not name.startswith('attr-'), + comment=f"unresolved attr name: {name}") + + +def main() -> None: + """ Ksft boiler plate main """ + ctrl = NlctrlFamily() + ksft_run([getfamily_do, + getfamily_dump, + getpolicy_dump, + getpolicy_op_map, + getpolicy_by_op], + args=(ctrl, )) + ksft_exit() + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/net/openvswitch/config b/tools/testing/selftests/net/openvswitch/config new file mode 100644 index 000000000000..a825e0b5c88e --- /dev/null +++ b/tools/testing/selftests/net/openvswitch/config @@ -0,0 +1,14 @@ +CONFIG_GENEVE=m +CONFIG_INET_DIAG=y +CONFIG_IP_SCTP=y +CONFIG_IPV6=y +CONFIG_NETFILTER=y +CONFIG_NET_IPGRE=m +CONFIG_NET_IPGRE_DEMUX=m +CONFIG_NF_CONNTRACK=m +CONFIG_NF_CONNTRACK_OVS=y +CONFIG_OPENVSWITCH=m +CONFIG_PSAMPLE=m +CONFIG_VETH=y +CONFIG_VLAN_8021Q=y +CONFIG_VXLAN=m diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools/testing/selftests/net/openvswitch/openvswitch.sh index b327d3061ed5..a31f7fb6882d 100755 --- a/tools/testing/selftests/net/openvswitch/openvswitch.sh +++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh @@ -27,6 +27,13 @@ tests=" upcall_interfaces ovs: test the upcall interfaces tunnel_metadata ovs: test extraction of tunnel metadata drop_reason drop: test drop reasons are emitted + pop_vlan vlan: POP_VLAN action strips tag + dec_ttl ttl: dec_ttl decrements IP TTL + flow_set flow-set: Flow modify + action_set set: SET action rewrites fields + trunc trunc: output truncation + icmpv6 icmpv6: ICMPv6 echo type match + sctp_connect_v4 sctp: SCTP flow key matching psample psample: Sampling packets with psample" info() { @@ -190,6 +197,23 @@ ovs_add_flow () { return 0 } +ovs_mod_flow () { + if [ -n "$4" ]; then + info "Modifying flow: sbx:$1 br:$2 flow:$3 act:$4" + ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py \ + mod-flow "$2" "$3" "$4" + else + info "Modifying flow (no actions): sbx:$1 br:$2 flow:$3" + ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py \ + mod-flow "$2" "$3" + fi + if [ $? -ne 0 ]; then + info "Flow modify [ $3 ] failed" + return 1 + fi + return 0 +} + ovs_del_flows () { info "Deleting all flows from DP: sbx:$1 br:$2" ovs_sbx "$1" python3 $ovs_base/ovs-dpctl.py del-flows "$2" @@ -243,6 +267,439 @@ usage() { } +test_dec_ttl() { + sbx_add "test_dec_ttl" || return $? + ovs_add_dp "test_dec_ttl" decttl || return 1 + + info "create namespaces" + for ns in client server; do + ovs_add_netns_and_veths "test_dec_ttl" "decttl" "$ns" \ + "${ns:0:1}0" "${ns:0:1}1" || return 1 + done + + ip netns exec client ip addr add 10.0.0.1/24 dev c1 + ip netns exec client ip link set c1 up + ip netns exec server ip addr add 10.0.0.2/24 dev s1 + ip netns exec server ip link set s1 up + + # Probe: check if kernel supports dec_ttl action. + ovs_add_flow "test_dec_ttl" decttl \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + 'dec_ttl(le_1())' &>/dev/null + if [ $? -ne 0 ]; then + info "no support for dec_ttl - skipping" + ovs_exit_sig + return $ksft_skip + fi + + ovs_del_flows "test_dec_ttl" decttl + + # ARP flows (bidirectional) + ovs_add_flow "test_dec_ttl" decttl \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_dec_ttl" decttl \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + + # IP flows with dec_ttl action + ovs_add_flow "test_dec_ttl" decttl \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + 'dec_ttl(le_1()),2' || return 1 + ovs_add_flow "test_dec_ttl" decttl \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' \ + 'dec_ttl(le_1()),1' || return 1 + + info "verify connectivity with dec_ttl" + ovs_sbx "test_dec_ttl" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 || return 1 + + info "verify TTL=1 is dropped by dec_ttl" + ovs_sbx "test_dec_ttl" ip netns exec client ping -c 1 -W 2 \ + -t 1 10.0.0.2 >/dev/null 2>&1 \ + && { info "FAIL: ping should fail with TTL=1 and dec_ttl" + return 1; } + + return 0 +} + +test_flow_set() { + sbx_add "test_flow_set" || return $? + ovs_add_dp "test_flow_set" flowset || return 1 + + info "create namespaces" + for ns in client server; do + ovs_add_netns_and_veths "test_flow_set" "flowset" "$ns" \ + "${ns:0:1}0" "${ns:0:1}1" || return 1 + done + + ip netns exec client ip addr add 10.0.0.1/24 dev c1 + ip netns exec client ip link set c1 up + ip netns exec server ip addr add 10.0.0.2/24 dev s1 + ip netns exec server ip link set s1 up + + ovs_add_flow "test_flow_set" flowset \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_flow_set" flowset \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + + local fwd_flow="ufid:00000001-0002-0003-0004-000500060007" + fwd_flow="$fwd_flow,in_port(1),eth(),eth_type(0x0800),ipv4()" + + ovs_add_flow "test_flow_set" flowset "$fwd_flow" '2' \ + || return 1 + ovs_add_flow "test_flow_set" flowset \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1 + + info "verify initial forwarding" + ovs_sbx "test_flow_set" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 || return 1 + + info "mod-flow with new actions (change to drop)" + ovs_mod_flow "test_flow_set" flowset "$fwd_flow" 'drop' \ + || return 1 + + info "verify traffic is now dropped" + ovs_sbx "test_flow_set" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 >/dev/null 2>&1 \ + && { info "FAIL: ping should fail after mod-flow to drop" + return 1; } + + info "mod-flow without actions" + ovs_mod_flow "test_flow_set" flowset "$fwd_flow" || return 1 + + info "verify flow retained drop action via dump" + python3 "$ovs_base/ovs-dpctl.py" dump-flows flowset \ + | grep -q "actions:drop" || \ + { info "FAIL: flow not showing drop action"; return 1; } + + info "verify drop actions unchanged" + ovs_sbx "test_flow_set" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 >/dev/null 2>&1 \ + && { info "FAIL: ping should still fail after no-actions set" + return 1; } + + return 0 +} + +test_action_set() { + sbx_add "test_action_set" || return $? + ovs_add_dp "test_action_set" settest || return 1 + + info "create namespaces" + for ns in client server; do + ovs_add_netns_and_veths "test_action_set" "settest" "$ns" \ + "${ns:0:1}0" "${ns:0:1}1" || return 1 + done + + ip netns exec client ip addr add 10.0.0.1/24 dev c1 + ip netns exec client ip link set c1 up + ip netns exec server ip addr add 10.0.0.2/24 dev s1 + ip netns exec server ip link set s1 up + + ovs_add_flow "test_action_set" settest \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + + ovs_add_flow "test_action_set" settest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1 + + info "verify connectivity without SET" + ovs_sbx "test_action_set" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 || return 1 + + ovs_del_flows "test_action_set" settest + ovs_add_flow "test_action_set" settest \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + + info "set ipv4 dst to unreachable address" + ovs_add_flow "test_action_set" settest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + 'set(ipv4(dst=10.0.0.99)),2' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1 + + info "verify ping fails with rewritten dst" + ovs_sbx "test_action_set" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 >/dev/null 2>&1 \ + && { info "FAIL: ping should fail with dst rewritten" + return 1; } + + ovs_del_flows "test_action_set" settest + ovs_add_flow "test_action_set" settest \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1 + ovs_add_flow "test_action_set" settest \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1 + + info "verify connectivity restored without SET" + ovs_sbx "test_action_set" ip netns exec client ping -c 1 -W 2 \ + 10.0.0.2 || return 1 + + return 0 +} + +# trunc test +# - trunc(14): truncate to ETH_HLEN, strips IP payload, ping fails +# - trunc(1) and trunc(13): kernel rejects below ETH_HLEN (EINVAL) +# - restore normal forwarding and verify recovery +test_trunc() { + sbx_add "test_trunc" || return $? + ovs_add_dp "test_trunc" trunctest || return 1 + + info "create namespaces" + for ns in client server; do + ovs_add_netns_and_veths "test_trunc" "trunctest" \ + "$ns" "${ns:0:1}0" "${ns:0:1}1" || return 1 + done + + ip netns exec client ip addr add 10.0.0.1/24 dev c1 + ip netns exec client ip link set c1 up + ip netns exec server ip addr add 10.0.0.2/24 dev s1 + ip netns exec server ip link set s1 up + + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + '2' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' \ + '1' || return 1 + + info "verify connectivity without truncation" + ovs_sbx "test_trunc" ip netns exec client \ + ping -c 1 -W 2 10.0.0.2 || return 1 + + # trunc below ETH_HLEN must be rejected by the kernel + info "verify trunc(1) is rejected" + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + 'trunc(1),2' &> /dev/null \ + && { info "trunc(1) should be rejected"; return 1; } + + info "verify trunc(13) is rejected" + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + 'trunc(13),2' &> /dev/null \ + && { info "trunc(13) should be rejected"; return 1; } + + ovs_del_flows "test_trunc" trunctest + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + + info "add trunc(14) forwarding flow" + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + 'trunc(14),2' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' \ + '1' || return 1 + + info "verify ping fails with trunc(14)" + ovs_sbx "test_trunc" ip netns exec client \ + ping -c 1 -W 2 10.0.0.2 >/dev/null 2>&1 \ + && { info "ping should fail with trunc(14)" + return 1; } + + ovs_del_flows "test_trunc" trunctest + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' \ + '2' || return 1 + ovs_add_flow "test_trunc" trunctest \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' \ + '1' || return 1 + + info "verify connectivity restored" + ovs_sbx "test_trunc" ip netns exec client \ + ping -c 1 -W 2 10.0.0.2 || return 1 + + return 0 +} + +# icmpv6 test +# - static neighbours to bypass NDP (nud permanent) +# - icmpv6(type=128) echo request, icmpv6(type=129) echo reply +# - remove flows and verify ping fails, reinstall and recover +test_icmpv6() { + local t="test_icmpv6" + local v6="eth_type(0x86dd),ipv6(proto=58)" + + sbx_add "$t" || return $? + ovs_add_dp "$t" icmpv6 || return 1 + + info "create namespaces" + for ns in client server; do + ovs_add_netns_and_veths "$t" "icmpv6" \ + "$ns" "${ns:0:1}0" "${ns:0:1}1" || return 1 + done + + ip netns exec client ip addr add fd00::1/64 dev c1 nodad + ip netns exec client ip link set c1 up + ip netns exec server ip addr add fd00::2/64 dev s1 nodad + ip netns exec server ip link set s1 up + + local cl_mac sl_mac + cl_mac=$(ip netns exec client ip link show c1 \ + | awk '/link\/ether/ {print $2}') + [ -z "$cl_mac" ] && \ + { info "failed to get c1 hwaddr"; return 1; } + sl_mac=$(ip netns exec server ip link show s1 \ + | awk '/link\/ether/ {print $2}') + [ -z "$sl_mac" ] && \ + { info "failed to get s1 hwaddr"; return 1; } + ip netns exec client ip -6 neigh add fd00::2 \ + lladdr "$sl_mac" nud permanent dev c1 || return 1 + ip netns exec server ip -6 neigh add fd00::1 \ + lladdr "$cl_mac" nud permanent dev s1 || return 1 + + # Probe: check if kernel supports icmpv6 flow key. + ovs_add_flow "$t" icmpv6 \ + "in_port(1),eth(),$v6,icmpv6(type=128)" \ + '2' &>/dev/null + if [ $? -ne 0 ]; then + info "no support for icmpv6 key - skipping" + ovs_exit_sig + return $ksft_skip + fi + ovs_del_flows "$t" icmpv6 + + ovs_add_flow "$t" icmpv6 \ + "in_port(1),eth(),$v6,icmpv6(type=128)" \ + '2' || return 1 + ovs_add_flow "$t" icmpv6 \ + "in_port(2),eth(),$v6,icmpv6(type=129)" \ + '1' || return 1 + + info "verify ICMPv6 echo with type-specific flows" + ovs_sbx "$t" ip netns exec client \ + ping -6 -c 1 -W 2 fd00::2 || return 1 + + ovs_del_flows "$t" icmpv6 + + info "verify ping fails without echo flows" + ovs_sbx "$t" ip netns exec client \ + ping -6 -c 1 -W 2 fd00::2 >/dev/null 2>&1 \ + && { info "ping should fail without flows" + return 1; } + + ovs_add_flow "$t" icmpv6 \ + "in_port(1),eth(),$v6,icmpv6(type=128)" \ + '2' || return 1 + ovs_add_flow "$t" icmpv6 \ + "in_port(2),eth(),$v6,icmpv6(type=129)" \ + '1' || return 1 + + info "verify connectivity restored" + ovs_sbx "$t" ip netns exec client \ + ping -6 -c 1 -W 2 fd00::2 || return 1 + + return 0 +} + +# Check for an SCTP endpoint via /proc, which works without sctp_diag. +sctp_eps_has() { + ip netns exec "$1" awk -v p="$2" '$6==p' /proc/net/sctp/eps | grep -q . +} + +# sctp_connect_v4 test +# - sctp(dst=4443) matches client-to-server INIT +# - sctp(src=4443) matches server-to-client INIT-ACK +# - remove flows and verify connection fails, reinstall and recover +test_sctp_connect_v4() { + local t="test_sctp_connect_v4" + local srv_ip=172.31.110.20 + + modprobe -q sctp 2>/dev/null || return "$ksft_skip" + socat -V 2>&1 | grep -q "define WITH_SCTP" || return "$ksft_skip" + + sbx_add "$t" || return $? + ovs_add_dp "$t" sctp4 || return 1 + + info "create namespaces" + for ns in client server; do + ovs_add_netns_and_veths "$t" "sctp4" "$ns" \ + "${ns:0:1}0" "${ns:0:1}1" || return 1 + done + + ip netns exec client ip addr add 172.31.110.10/24 dev c1 + ip netns exec client ip link set c1 up + ip netns exec server ip addr add "${srv_ip}/24" dev s1 + ip netns exec server ip link set s1 up + + # ARP forwarding + ovs_add_flow "$t" sctp4 \ + 'in_port(1),eth(),eth_type(0x0806),arp()' \ + '2' || return 1 + ovs_add_flow "$t" sctp4 \ + 'in_port(2),eth(),eth_type(0x0806),arp()' \ + '1' || return 1 + + # SCTP port matching: dst for request, src for reply + ovs_add_flow "$t" sctp4 \ + 'in_port(1),eth(),eth_type(0x0800),ipv4(proto=132),sctp(dst=4443)' \ + '2' || return 1 + ovs_add_flow "$t" sctp4 \ + 'in_port(2),eth(),eth_type(0x0800),ipv4(proto=132),sctp(src=4443)' \ + '1' || return 1 + + # The listener forks a child per association, so one instance serves + # the whole test and the flows stay the only variable. -t 1 bounds + # how long a child lingers after its association closes. + ovs_netns_spawn_daemon "$t" "server" \ + socat -u -t 1 SCTP4-LISTEN:4443,fork STDOUT + ovs_wait sctp_eps_has server 4443 || return 1 + + info "verify SCTP association with port-keyed flows" + ovs_sbx "$t" ip netns exec client \ + timeout 3 socat -u STDIN "SCTP4-CONNECT:${srv_ip}:4443" </dev/null \ + || return 1 + + ovs_del_flows "$t" sctp4 + + info "verify connection fails without flows" + ovs_add_flow "$t" sctp4 \ + 'in_port(1),eth(),eth_type(0x0806),arp()' \ + '2' || return 1 + ovs_add_flow "$t" sctp4 \ + 'in_port(2),eth(),eth_type(0x0806),arp()' \ + '1' || return 1 + + ovs_sbx "$t" ip netns exec client \ + timeout 3 socat -u STDIN "SCTP4-CONNECT:${srv_ip}:4443" </dev/null \ + >/dev/null 2>&1 \ + && { info "connection should fail without flows" + return 1; } + + info "reinstall flows and verify recovery" + ovs_add_flow "$t" sctp4 \ + 'in_port(1),eth(),eth_type(0x0800),ipv4(proto=132),sctp(dst=4443)' \ + '2' || return 1 + ovs_add_flow "$t" sctp4 \ + 'in_port(2),eth(),eth_type(0x0800),ipv4(proto=132),sctp(src=4443)' \ + '1' || return 1 + + ovs_sbx "$t" ip netns exec client \ + timeout 3 socat -u STDIN "SCTP4-CONNECT:${srv_ip}:4443" </dev/null \ + || return 1 + + return 0 +} + # psample test # - use psample to observe packets test_psample() { @@ -303,6 +760,8 @@ test_psample() { # sFlow / IPFIX. nlpid=$(grep -E "listening on upcall packet handler" \ $ovs_dir/s0.out | cut -d ":" -f 2 | tr -d ' ') + [ -z "$nlpid" ] && \ + { info "failed to get upcall PID"; return 1; } ovs_add_flow "test_psample" psample \ "in_port(2),eth(),eth_type(0x0800),ipv4()" \ @@ -336,6 +795,10 @@ test_drop_reason() { ovs_drop_subsys=$(pahole -C skb_drop_reason_subsys | awk '/OPENVSWITCH/ { print $3; }' | tr -d ,) + if [ -z "$ovs_drop_subsys" ]; then + info "failed to get OVS drop subsys ID" + return $ksft_skip + fi sbx_add "test_drop_reason" || return $? @@ -434,13 +897,19 @@ test_arp_ping () { # Setup client namespace ip netns exec client ip addr add 172.31.110.10/24 dev c1 ip netns exec client ip link set c1 up - HW_CLIENT=`ip netns exec client ip link show dev c1 | grep -E 'link/ether [0-9a-f:]+' | awk '{print $2;}'` + HW_CLIENT=$(ip netns exec client ip link show dev c1 \ + | awk '/link\/ether/ {print $2}') + [ -z "$HW_CLIENT" ] && \ + { info "failed to get client hwaddr"; return 1; } info "Client hwaddr: $HW_CLIENT" # Setup server namespace ip netns exec server ip addr add 172.31.110.20/24 dev s1 ip netns exec server ip link set s1 up - HW_SERVER=`ip netns exec server ip link show dev s1 | grep -E 'link/ether [0-9a-f:]+' | awk '{print $2;}'` + HW_SERVER=$(ip netns exec server ip link show dev s1 \ + | awk '/link\/ether/ {print $2}') + [ -z "$HW_SERVER" ] && \ + { info "failed to get server hwaddr"; return 1; } info "Server hwaddr: $HW_SERVER" ovs_add_flow "test_arp_ping" arpping \ @@ -830,6 +1299,82 @@ test_tunnel_metadata() { return 0 } +test_pop_vlan() { + local sbx="test_pop_vlan" + sbx_add "$sbx" || return $? + ovs_add_dp "$sbx" vlandp || return 1 + + ovs_add_netns_and_veths "$sbx" vlandp \ + ns1 veth1 ns1veth 192.0.2.1/24 || return 1 + ovs_add_netns_and_veths "$sbx" vlandp \ + ns2 veth2 ns2veth 192.0.2.2/24 || return 1 + + # Baseline: untagged bidirectional forwarding + ovs_add_flow "$sbx" vlandp \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1 + ovs_sbx "$sbx" ip netns exec ns1 ping -c 3 -W 2 \ + 192.0.2.2 || return 1 + + # VLAN topology: ns1 uses VLAN sub-interface, ns2 is plain + ip -n ns1 link add link ns1veth name ns1veth.10 \ + type vlan id 10 || return 1 + on_exit "ip -n ns1 link del ns1veth.10 2>/dev/null" + ip -n ns1 addr add 198.51.100.1/24 dev ns1veth.10 || return 1 + ip -n ns1 link set ns1veth.10 up || return 1 + ip -n ns2 addr add 198.51.100.2/24 dev ns2veth || return 1 + + ovs_del_flows "$sbx" vlandp + + # Static ARP: avoids VLAN-tagged ARP complexity + local ns1veth10mac ns2mac + ns1veth10mac=$(ip -n ns1 link show ns1veth.10 \ + | awk '/link\/ether/ {print $2}') + [ -z "$ns1veth10mac" ] && \ + { info "failed to get ns1veth10mac"; return 1; } + ns2mac=$(ip -n ns2 link show ns2veth \ + | awk '/link\/ether/ {print $2}') + [ -z "$ns2mac" ] && \ + { info "failed to get ns2mac"; return 1; } + ip -n ns1 neigh replace 198.51.100.2 lladdr "$ns2mac" \ + dev ns1veth.10 nud permanent || return 1 + ip -n ns2 neigh replace 198.51.100.1 \ + lladdr "$ns1veth10mac" \ + dev ns2veth nud permanent || return 1 + + local vlan_match='in_port(1),eth(),eth_type(0x8100),' + vlan_match+='vlan(vid=10),' + vlan_match+='encap(eth_type(0x0800),' + vlan_match+='ipv4(src=198.51.100.1,proto=1),icmp())' + + # Negative: forward without pop_vlan -- tagged frame + # is invisible to ns2 (no VLAN sub-interface), ping fails + ovs_add_flow "$sbx" vlandp "$vlan_match" '2' || return 1 + ovs_sbx "$sbx" ip netns exec ns1 ping -I ns1veth.10 \ + -c 3 -W 1 198.51.100.2 >/dev/null 2>&1 \ + && { info "FAIL: ping should fail without pop_vlan" + return 1; } + + ovs_del_flows "$sbx" vlandp + + # Positive: pop_vlan strips tag on forward path, + # push_vlan restores tag on return path -- ping succeeds + ovs_add_flow "$sbx" vlandp \ + "$vlan_match" 'pop_vlan,2' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' \ + 'push_vlan(vid=10,pcp=0,tpid=0x8100),1' || return 1 + ovs_sbx "$sbx" ip netns exec ns1 ping -I ns1veth.10 \ + -c 3 -W 2 198.51.100.2 || return 1 + + return 0 +} + run_test() { ( tname="$1" diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py index b521e0dea506..1615843c225e 100644 --- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py +++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py @@ -11,7 +11,6 @@ import logging import math import multiprocessing import re -import socket import struct import sys import time @@ -370,7 +369,7 @@ class ovsactions(nla): ("OVS_ACTION_ATTR_OUTPUT", "uint32"), ("OVS_ACTION_ATTR_USERSPACE", "userspace"), ("OVS_ACTION_ATTR_SET", "ovskey"), - ("OVS_ACTION_ATTR_PUSH_VLAN", "none"), + ("OVS_ACTION_ATTR_PUSH_VLAN", "push_vlan"), ("OVS_ACTION_ATTR_POP_VLAN", "flag"), ("OVS_ACTION_ATTR_SAMPLE", "sample"), ("OVS_ACTION_ATTR_RECIRC", "uint32"), @@ -389,11 +388,21 @@ class ovsactions(nla): ("OVS_ACTION_ATTR_CLONE", "recursive"), ("OVS_ACTION_ATTR_CHECK_PKT_LEN", "none"), ("OVS_ACTION_ATTR_ADD_MPLS", "none"), - ("OVS_ACTION_ATTR_DEC_TTL", "none"), + ("OVS_ACTION_ATTR_DEC_TTL", "dec_ttl"), ("OVS_ACTION_ATTR_DROP", "uint32"), ("OVS_ACTION_ATTR_PSAMPLE", "psample"), ) + class dec_ttl(nla): # pylint: disable=invalid-name + """Nested OVS_DEC_TTL_ATTR_* sub-attributes.""" + + nla_flags = NLA_F_NESTED + + nla_map = ( + ("OVS_DEC_TTL_ATTR_UNSPEC", "none"), + ("OVS_DEC_TTL_ATTR_ACTION", "actions"), + ) + class psample(nla): nla_flags = NLA_F_NESTED @@ -427,6 +436,9 @@ class ovsactions(nla): return actstr + class push_vlan(nla): + fields = (("vlan_tpid", "!H"), ("vlan_tci", "!H")) + class sample(nla): nla_flags = NLA_F_NESTED @@ -633,6 +645,21 @@ class ovsactions(nla): print_str += "ct_clear" elif field[0] == "OVS_ACTION_ATTR_POP_VLAN": print_str += "pop_vlan" + elif field[0] == "OVS_ACTION_ATTR_DEC_TTL": + datum = self.get_attr(field[0]) + print_str += "dec_ttl(le_1(" + subacts = datum.get_attr("OVS_DEC_TTL_ATTR_ACTION") + if subacts and subacts.get("attrs"): + print_str += subacts.dpstr(more) + print_str += "))" + elif field[0] == "OVS_ACTION_ATTR_PUSH_VLAN": + datum = self.get_attr(field[0]) + tpid = datum["vlan_tpid"] + tci = datum["vlan_tci"] + vid = tci & 0x0FFF + pcp = (tci >> 13) & 0x7 + print_str += "push_vlan(vid=%d,pcp=%d" \ + ",tpid=0x%04x)" % (vid, pcp, tpid) elif field[0] == "OVS_ACTION_ATTR_POP_ETH": print_str += "pop_eth" elif field[0] == "OVS_ACTION_ATTR_POP_NSH": @@ -726,7 +753,71 @@ class ovsactions(nla): actstr = actstr[strspn(actstr, ", ") :] parsed = True - if parse_starts_block(actstr, "clone(", False): + if parse_starts_block(actstr, "push_vlan(", False): + actstr = actstr[len("push_vlan("):] + vid = 0 + pcp = 0 + tpid = 0x8100 + if ")" not in actstr: + raise ValueError( + "push_vlan(): missing ')'") + paren = actstr.index(")") + if not actstr[:paren].strip(): + raise ValueError("push_vlan(): no fields") + for kv in actstr[:paren].split(","): + if "=" not in kv: + raise ValueError( + "push_vlan(): bad field '%s'" + % kv.strip()) + k = kv[:kv.index("=")].strip() + v = kv[kv.index("=") + 1:].strip() + if k == "vid": + vid = int(v, 0) + if vid < 0 or vid > 0xFFF: + raise ValueError( + "push_vlan(): vid=%d out of " + "range (0-4095)" % vid) + elif k == "pcp": + pcp = int(v, 0) + if pcp < 0 or pcp > 7: + raise ValueError( + "push_vlan(): pcp=%d out of " + "range (0-7)" % pcp) + elif k == "tpid": + tpid = int(v, 0) + if tpid < 0 or tpid > 0xFFFF: + raise ValueError( + "push_vlan(): tpid=0x%x out " + "of range (0-0xffff)" % tpid) + else: + raise ValueError( + "push_vlan(): unknown key '%s'" + % k) + tci = (vid & 0x0FFF) | ((pcp & 0x7) << 13) \ + | 0x1000 + pvact = self.push_vlan() + pvact["vlan_tpid"] = tpid + pvact["vlan_tci"] = tci + self["attrs"].append( + ["OVS_ACTION_ATTR_PUSH_VLAN", pvact]) + actstr = actstr[paren + 1:] + parsed = True + + elif parse_starts_block(actstr, "dec_ttl(le_1(", False): + parencount += 2 + subacts = ovsactions() + actstr = actstr[len("dec_ttl(le_1("):] + parsed_len = subacts.parse(actstr) + decttl = ovsactions.dec_ttl() + decttl["attrs"].append( + ("OVS_DEC_TTL_ATTR_ACTION", subacts) + ) + self["attrs"].append( + ("OVS_ACTION_ATTR_DEC_TTL", decttl) + ) + actstr = actstr[parsed_len:] + parsed = True + elif parse_starts_block(actstr, "clone(", False): parencount += 1 subacts = ovsactions() actstr = actstr[len("clone("):] @@ -897,15 +988,21 @@ class ovsactions(nla): return (totallen - len(actstr)) +# pyroute2 resolves nla_map types via getattr(self, name). +# dec_ttl needs "actions" to resolve to ovsactions, but +# ovsactions is not defined when dec_ttl class body runs. +ovsactions.dec_ttl.actions = ovsactions + + class ovskey(nla): nla_flags = NLA_F_NESTED nla_map = ( ("OVS_KEY_ATTR_UNSPEC", "none"), - ("OVS_KEY_ATTR_ENCAP", "none"), + ("OVS_KEY_ATTR_ENCAP", "encap_ovskey"), ("OVS_KEY_ATTR_PRIORITY", "uint32"), ("OVS_KEY_ATTR_IN_PORT", "uint32"), ("OVS_KEY_ATTR_ETHERNET", "ethaddr"), - ("OVS_KEY_ATTR_VLAN", "uint16"), + ("OVS_KEY_ATTR_VLAN", "be16"), ("OVS_KEY_ATTR_ETHERTYPE", "be16"), ("OVS_KEY_ATTR_IPV4", "ovs_key_ipv4"), ("OVS_KEY_ATTR_IPV6", "ovs_key_ipv6"), @@ -1158,11 +1255,16 @@ class ovskey(nla): lambda x: ipaddress.IPv6Address(x).packed if x else 0, convert_ipv6, ), - ("label", "label", "%d", lambda x: int(x) if x else 0), - ("proto", "proto", "%d", lambda x: int(x) if x else 0), - ("tclass", "tclass", "%d", lambda x: int(x) if x else 0), - ("hlimit", "hlimit", "%d", lambda x: int(x) if x else 0), - ("frag", "frag", "%d", lambda x: int(x) if x else 0), + ("label", "label", "%d", lambda x: int(x) if x else 0, + convert_int(20)), + ("proto", "proto", "%d", lambda x: int(x) if x else 0, + convert_int(8)), + ("tclass", "tclass", "%d", lambda x: int(x) if x else 0, + convert_int(8)), + ("hlimit", "hlimit", "%d", lambda x: int(x) if x else 0, + convert_int(8)), + ("frag", "frag", "%d", lambda x: int(x) if x else 0, + convert_int(8)), ) def __init__( @@ -1247,8 +1349,10 @@ class ovskey(nla): ) fields_map = ( - ("type", "type", "%d", lambda x: int(x) if x else 0), - ("code", "code", "%d", lambda x: int(x) if x else 0), + ("type", "type", "%d", lambda x: int(x) if x else 0, + convert_int(8)), + ("code", "code", "%d", lambda x: int(x) if x else 0, + convert_int(8)), ) def __init__( @@ -1636,6 +1740,194 @@ class ovskey(nla): class ovs_key_mpls(nla): fields = (("lse", ">I"),) + # 802.1Q CFI (Canonical Format Indicator) bit, always set for Ethernet + _VLAN_CFI_MASK = 0x1000 + + @staticmethod + def _vlan_dpstr(tci): + """Format VLAN TCI as vid=X,pcp=Y,cfi=Z or tci=0xNNNN. + + When cfi=1 (standard Ethernet VLAN), outputs decomposed + vid/pcp/cfi fields. When cfi=0 (truncated VLAN header), + falls back to raw tci=0x%04x to ensure round-trip + correctness: the parser auto-adds cfi=1 for vid/pcp + format, so cfi=0 would be lost on re-parse.""" + vid = tci & 0x0FFF + pcp = (tci >> 13) & 0x7 + cfi = (tci >> 12) & 0x1 + if cfi: + return "vid=%d,pcp=%d,cfi=%d" % (vid, pcp, cfi) + return "tci=0x%04x" % tci + + @staticmethod + def _parse_vlan_from_flowstr(flowstr): + """Parse vlan(tci=X) or vlan(vid=X[,pcp=Y,cfi=Z]) from flowstr. + + Returns (remaining_flowstr, key_tci, mask_tci). + TCI values use standard bit layout (VID bits 0-11, + CFI bit 12, PCP bits 13-15); byte order conversion to + big-endian happens in pyroute2 be16 NLA serialization. + The mask covers only the fields the caller specified: + vid -> 0x0FFF, pcp -> 0xE000, cfi -> 0x1000, tci -> 0xFFFF. + + The tci= key sets the raw TCI bitfield (no CFI validation) to allow + non-Ethernet use cases. Use cfi=1 for standard Ethernet VLAN matching. + """ + tci = 0 + mask = 0 + has_tci = False + has_vid = has_pcp = has_cfi = False + _tci_mix_err = "vlan(): 'tci' cannot be mixed " \ + "with 'vid'/'pcp'/'cfi'" + first = True + while True: + flowstr = flowstr.lstrip() + if not flowstr: + raise ValueError("vlan(): missing ')'") + if flowstr[0] == ')': + break + if not first: + flowstr = flowstr[1:] # skip ',' + if not flowstr: + raise ValueError("vlan(): missing ')' after trailing comma") + flowstr = flowstr.lstrip() + if flowstr and flowstr[0] == ')': + break + if flowstr and flowstr[0] == ',': + raise ValueError( + "vlan(): empty or extra comma in field list") + first = False + + eq = flowstr.find('=') + if eq == -1: + raise ValueError( + "vlan(): expected key=value, got '%s'" % flowstr) + key = flowstr[:eq].strip() + flowstr = flowstr[eq + 1:] + + end = flowstr.find(',') + end2 = flowstr.find(')') + if end == -1 and end2 == -1: + raise ValueError("vlan(): missing ')'") + if end == -1 or (end2 != -1 and end2 < end): + end = end2 + val = flowstr[:end].strip() + flowstr = flowstr[end:] + + if not val: + raise ValueError("vlan(): empty value for key '%s'" % key) + try: + v = int(val, 0) + except ValueError as exc: + raise ValueError( + "vlan(): invalid value '%s' for key '%s'" + % (val, key)) from exc + + if key == 'tci': + if has_tci: + raise ValueError("vlan(): duplicate 'tci'") + if has_vid or has_pcp or has_cfi: + raise ValueError(_tci_mix_err) + if v > 0xFFFF or v < 0: + raise ValueError("vlan(): tci=0x%x out of range" % v) + tci = v + mask = 0xFFFF + has_tci = True + elif key == 'vid': + if has_tci: + raise ValueError(_tci_mix_err) + if has_vid: + raise ValueError("vlan(): duplicate 'vid'") + if v < 0 or v > 0xFFF: + raise ValueError("vlan(): vid=%d out of range (0-4095)" % v) + tci |= v + mask |= 0x0FFF + has_vid = True + elif key == 'pcp': + if has_tci: + raise ValueError(_tci_mix_err) + if has_pcp: + raise ValueError("vlan(): duplicate 'pcp'") + if v < 0 or v > 7: + raise ValueError("vlan(): pcp=%d out of range (0-7)" % v) + tci |= (v & 0x7) << 13 + mask |= 0xE000 + has_pcp = True + elif key == 'cfi': + if has_tci: + raise ValueError(_tci_mix_err) + if has_cfi: + raise ValueError("vlan(): duplicate 'cfi'") + if v != 1: + raise ValueError("vlan(): cfi must be 1 for Ethernet") + tci |= ovskey._VLAN_CFI_MASK + mask |= ovskey._VLAN_CFI_MASK + has_cfi = True + else: + raise ValueError("vlan(): unknown key '%s'" % key) + + flowstr = flowstr[1:] # skip ')' + # Catch immediate '))' (user error). A ')' after ',' is consumed + # by parse()'s strspn(flowstr, "), ") inter-field separator stripping. + if flowstr.lstrip().startswith(')'): + raise ValueError("vlan(): unmatched ')'") + # parse() strips trailing ',', ')', ' ' as inter-field separators, + # so we do not need to call strspn here. + + if mask == 0: + raise ValueError("vlan(): no fields specified, " + "use vlan(vid=X[,pcp=Y,cfi=Z]) or vlan(tci=X)") + if not has_tci: + tci |= ovskey._VLAN_CFI_MASK + mask |= ovskey._VLAN_CFI_MASK + return flowstr, tci, mask + + @staticmethod + def _parse_encap_from_flowstr(flowstr): + """Parse encap(inner_flow) from flowstr. + + Returns (remaining_flowstr, inner_key_dict, inner_mask_dict) + where each dict has an 'attrs' key for recursive NLA encoding. + Parenthesis-depth tracking handles nested encap() calls but not + quoted strings containing literal parentheses. + """ + depth = 1 + end = -1 + for i, c in enumerate(flowstr): + if c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth < 0: + raise ValueError( + "encap(): unmatched ')' at position %d" % i) + if depth == 0: + end = i + break + + if end == -1: + if depth > 1: + raise ValueError("encap(): missing ')' in nested encap") + raise ValueError("encap(): missing ')'") + + inner_str = flowstr[:end].strip() + if not inner_str: + raise ValueError("encap(): empty inner flow") + + flowstr = flowstr[end + 1:] + if flowstr.lstrip().startswith(')'): + raise ValueError("encap(): unmatched ')' after encap()") + + inner_key = encap_ovskey() + inner_mask = encap_ovskey() + remaining = inner_key.parse(inner_str, inner_mask) + if remaining and re.search(r'[^\s,)]', remaining): + raise ValueError( + "encap(): unrecognized trailing " + "content '%s'" % remaining.strip()) + + return flowstr, inner_key, inner_mask + def parse(self, flowstr, mask=None): for field in ( ("OVS_KEY_ATTR_PRIORITY", "skb_priority", intparse), @@ -1658,6 +1950,16 @@ class ovskey(nla): lambda x: intparse(x, "0xffff"), ), ( + "OVS_KEY_ATTR_VLAN", + "vlan", + ovskey._parse_vlan_from_flowstr, + ), + ( + "OVS_KEY_ATTR_ENCAP", + "encap", + ovskey._parse_encap_from_flowstr, + ), + ( "OVS_KEY_ATTR_IPV4", "ipv4", ovskey.ovs_key_ipv4, @@ -1683,11 +1985,21 @@ class ovskey(nla): ovskey.ovs_key_udp, ), ( + "OVS_KEY_ATTR_SCTP", + "sctp", + ovskey.ovs_key_sctp, + ), + ( "OVS_KEY_ATTR_ICMP", "icmp", ovskey.ovs_key_icmp, ), ( + "OVS_KEY_ATTR_ICMPV6", + "icmpv6", + ovskey.ovs_key_icmpv6, + ), + ( "OVS_KEY_ATTR_TCP_FLAGS", "tcp_flags", lambda x: parse_flags(x, None), @@ -1794,6 +2106,9 @@ class ovskey(nla): True, ), ("OVS_KEY_ATTR_ETHERNET", None, None, False, False), + ("OVS_KEY_ATTR_VLAN", "vlan", ovskey._vlan_dpstr, + lambda x: False, True), + ("OVS_KEY_ATTR_ENCAP", None, None, False, False), ( "OVS_KEY_ATTR_ETHERTYPE", "eth_type", @@ -1821,22 +2136,63 @@ class ovskey(nla): v = self.get_attr(field[0]) if v is not None: m = None if mask is None else mask.get_attr(field[0]) + fmt = field[2] # str format or callable if field[4] is False: print_str += v.dpstr(m, more) print_str += "," else: if m is None or field[3](m): - print_str += field[1] + "(" - print_str += field[2] % v - print_str += ")," + val = fmt(v) if callable(fmt) else fmt % v + print_str += field[1] + "(" + val + ")," elif more or m != 0: - print_str += field[1] + "(" - print_str += (field[2] % v) + "/" + (field[2] % m) - print_str += ")," + if field[0] == "OVS_KEY_ATTR_VLAN": + val = "tci=0x%04x/0x%04x" % (v, m) + elif callable(fmt): + val = fmt(v) + "/" + fmt(m) + else: + val = (fmt % v) + "/" + (fmt % m) + print_str += field[1] + "(" + val + ")," return print_str +class encap_ovskey(ovskey): + """Inner flow key attributes valid inside 802.1Q ENCAP. + + Only L2-L4 key attributes (slots 0-21) appear inside ENCAP. + Metadata-only attributes (SKB_MARK, DP_HASH, RECIRC_ID, etc.) + are set to "none" -- they never appear inside ENCAP per + ovs_nla_put_vlan() in net/openvswitch/flow_netlink.c. + + nla_map indexes must match OVS_KEY_ATTR_* enum values in + include/uapi/linux/openvswitch.h. + """ + nla_map = ( + ("OVS_KEY_ATTR_UNSPEC", "none"), + ("OVS_KEY_ATTR_ENCAP", "none"), # placeholder, parsed by ovskey + ("OVS_KEY_ATTR_PRIORITY", "none"), # skb metadata, not in ENCAP + ("OVS_KEY_ATTR_IN_PORT", "none"), # skb metadata, not in ENCAP + ("OVS_KEY_ATTR_ETHERNET", "ethaddr"), + ("OVS_KEY_ATTR_VLAN", "be16"), + ("OVS_KEY_ATTR_ETHERTYPE", "be16"), + ("OVS_KEY_ATTR_IPV4", "ovs_key_ipv4"), + ("OVS_KEY_ATTR_IPV6", "ovs_key_ipv6"), + ("OVS_KEY_ATTR_TCP", "ovs_key_tcp"), + ("OVS_KEY_ATTR_UDP", "ovs_key_udp"), + ("OVS_KEY_ATTR_ICMP", "ovs_key_icmp"), + ("OVS_KEY_ATTR_ICMPV6", "ovs_key_icmpv6"), + ("OVS_KEY_ATTR_ARP", "ovs_key_arp"), + ("OVS_KEY_ATTR_ND", "ovs_key_nd"), + ("OVS_KEY_ATTR_SKB_MARK", "none"), # metadata, not in ENCAP + ("OVS_KEY_ATTR_TUNNEL", "none"), # tunnel metadata, not in ENCAP + ("OVS_KEY_ATTR_SCTP", "ovs_key_sctp"), + ("OVS_KEY_ATTR_TCP_FLAGS", "be16"), + ("OVS_KEY_ATTR_DP_HASH", "none"), # metadata, not in ENCAP + ("OVS_KEY_ATTR_RECIRC_ID", "none"), # metadata, not in ENCAP + ("OVS_KEY_ATTR_MPLS", "array(ovs_key_mpls)"), + ) + + class OvsPacket(GenericNetlinkSocket): OVS_PACKET_CMD_MISS = 1 # Flow table miss OVS_PACKET_CMD_ACTION = 2 # USERSPACE action @@ -2013,9 +2369,6 @@ class OvsDatapath(GenericNetlinkSocket): class OvsVport(GenericNetlinkSocket): OVS_VPORT_TYPE_NETDEV = 1 OVS_VPORT_TYPE_INTERNAL = 2 - OVS_VPORT_TYPE_GRE = 3 - OVS_VPORT_TYPE_VXLAN = 4 - OVS_VPORT_TYPE_GENEVE = 5 class ovs_vport_msg(ovs_dp_msg): nla_map = ( @@ -2023,7 +2376,7 @@ class OvsVport(GenericNetlinkSocket): ("OVS_VPORT_ATTR_PORT_NO", "uint32"), ("OVS_VPORT_ATTR_TYPE", "uint32"), ("OVS_VPORT_ATTR_NAME", "asciiz"), - ("OVS_VPORT_ATTR_OPTIONS", "vportopts"), + ("OVS_VPORT_ATTR_OPTIONS", "none"), ("OVS_VPORT_ATTR_UPCALL_PID", "array(uint32)"), ("OVS_VPORT_ATTR_STATS", "vportstats"), ("OVS_VPORT_ATTR_PAD", "none"), @@ -2031,13 +2384,6 @@ class OvsVport(GenericNetlinkSocket): ("OVS_VPORT_ATTR_NETNSID", "uint32"), ) - class vportopts(nla): - nla_map = ( - ("OVS_TUNNEL_ATTR_UNSPEC", "none"), - ("OVS_TUNNEL_ATTR_DST_PORT", "uint16"), - ("OVS_TUNNEL_ATTR_EXTENSION", "none"), - ) - class vportstats(nla): fields = ( ("rx_packets", "=Q"), @@ -2055,25 +2401,13 @@ class OvsVport(GenericNetlinkSocket): return "netdev" elif vport_type == OvsVport.OVS_VPORT_TYPE_INTERNAL: return "internal" - elif vport_type == OvsVport.OVS_VPORT_TYPE_GRE: - return "gre" - elif vport_type == OvsVport.OVS_VPORT_TYPE_VXLAN: - return "vxlan" - elif vport_type == OvsVport.OVS_VPORT_TYPE_GENEVE: - return "geneve" raise ValueError("Unknown vport type:%d" % vport_type) def str_to_type(vport_type): - if vport_type == "netdev": + if vport_type in ["netdev", "gre", "vxlan", "geneve"]: return OvsVport.OVS_VPORT_TYPE_NETDEV elif vport_type == "internal": return OvsVport.OVS_VPORT_TYPE_INTERNAL - elif vport_type == "gre": - return OvsVport.OVS_VPORT_TYPE_INTERNAL - elif vport_type == "vxlan": - return OvsVport.OVS_VPORT_TYPE_VXLAN - elif vport_type == "geneve": - return OvsVport.OVS_VPORT_TYPE_GENEVE raise ValueError("Unknown vport type: '%s'" % vport_type) def __init__(self, packet=OvsPacket()): @@ -2106,21 +2440,24 @@ class OvsVport(GenericNetlinkSocket): raise ne return reply - def attach(self, dpindex, vport_ifname, ptype, dport, lwt): + def attach(self, dpindex, vport_ifname, ptype, dport): msg = OvsVport.ovs_vport_msg() msg["cmd"] = OVS_VPORT_CMD_NEW msg["version"] = OVS_DATAPATH_VERSION msg["reserved"] = 0 msg["dpifindex"] = dpindex - port_type = OvsVport.str_to_type(ptype) msg["attrs"].append(["OVS_VPORT_ATTR_NAME", vport_ifname]) msg["attrs"].append( + ["OVS_VPORT_ATTR_TYPE", OvsVport.str_to_type(ptype)] + ) + msg["attrs"].append( ["OVS_VPORT_ATTR_UPCALL_PID", [self.upcall_packet.epid]] ) TUNNEL_DEFAULTS = [("geneve", 6081), + ("gre", 0), ("vxlan", 4789)] for tnl in TUNNEL_DEFAULTS: @@ -2128,29 +2465,21 @@ class OvsVport(GenericNetlinkSocket): if not dport: dport = tnl[1] - if not lwt: - vportopt = OvsVport.ovs_vport_msg.vportopts() - vportopt["attrs"].append( - ["OVS_TUNNEL_ATTR_DST_PORT", socket.htons(dport)] - ) - msg["attrs"].append( - ["OVS_VPORT_ATTR_OPTIONS", vportopt] - ) - else: - port_type = OvsVport.OVS_VPORT_TYPE_NETDEV - ipr = pyroute2.iproute.IPRoute() - - if tnl[0] == "geneve": - ipr.link("add", ifname=vport_ifname, kind=tnl[0], - geneve_port=dport, - geneve_collect_metadata=True, - geneve_udp_zero_csum6_rx=1) - elif tnl[0] == "vxlan": - ipr.link("add", ifname=vport_ifname, kind=tnl[0], - vxlan_learning=0, vxlan_collect_metadata=1, - vxlan_udp_zero_csum6_rx=1, vxlan_port=dport) + ipr = pyroute2.iproute.IPRoute() + + if tnl[0] == "geneve": + ipr.link("add", ifname=vport_ifname, kind=tnl[0], + geneve_port=dport, + geneve_collect_metadata=True, + geneve_udp_zero_csum6_rx=1) + elif tnl[0] == "gre": + ipr.link("add", ifname=vport_ifname, kind="gretap", + gre_collect_metadata=True) + elif tnl[0] == "vxlan": + ipr.link("add", ifname=vport_ifname, kind=tnl[0], + vxlan_learning=0, vxlan_collect_metadata=1, + vxlan_udp_zero_csum6_rx=1, vxlan_port=dport) break - msg["attrs"].append(["OVS_VPORT_ATTR_TYPE", port_type]) try: reply = self.nlm_request( @@ -2355,9 +2684,10 @@ class OvsFlow(GenericNetlinkSocket): self["attrs"].append(["OVS_FLOW_ATTR_KEY", k]) self["attrs"].append(["OVS_FLOW_ATTR_MASK", m]) - a = ovsactions() - a.parse(actstr) - self["attrs"].append(["OVS_FLOW_ATTR_ACTIONS", a]) + if actstr is not None: + a = ovsactions() + a.parse(actstr) + self["attrs"].append(["OVS_FLOW_ATTR_ACTIONS", a]) def __init__(self): GenericNetlinkSocket.__init__(self) @@ -2391,6 +2721,25 @@ class OvsFlow(GenericNetlinkSocket): raise ne return reply + def mod_flow(self, dpifindex, flowmsg): + """Modify an existing flow in the kernel.""" + flowmsg["cmd"] = OVS_FLOW_CMD_SET + flowmsg["version"] = OVS_DATAPATH_VERSION + flowmsg["reserved"] = 0 + flowmsg["dpifindex"] = dpifindex + + try: + reply = self.nlm_request( + flowmsg, + msg_type=self.prid, + msg_flags=NLM_F_REQUEST | NLM_F_ACK, + ) + reply = reply[0] + except NetlinkError as ne: + print(flowmsg) + raise ne + return reply + def del_flows(self, dpifindex): """ Send a del message to the kernel that will drop all flows. @@ -2558,24 +2907,18 @@ def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=NDB(), vpl=OvsVport()): for iface in ndb.interfaces: rep = vpl.info(iface.ifname, ifindex) if rep is not None: - opts = "" - vpo = rep.get_attr("OVS_VPORT_ATTR_OPTIONS") - if vpo: - dpo = vpo.get_attr("OVS_TUNNEL_ATTR_DST_PORT") - if dpo: - opts += " tnl-dport:%s" % socket.ntohs(dpo) print( - " port %d: %s (%s%s)" + " port %d: %s (%s)" % ( rep.get_attr("OVS_VPORT_ATTR_PORT_NO"), rep.get_attr("OVS_VPORT_ATTR_NAME"), OvsVport.type_to_str(rep.get_attr("OVS_VPORT_ATTR_TYPE")), - opts, ) ) def main(argv): + nlmsg_atoms.encap_ovskey = encap_ovskey nlmsg_atoms.ovskey = ovskey nlmsg_atoms.ovsactions = ovsactions @@ -2583,7 +2926,7 @@ def main(argv): prverscheck = pyroute2.__version__.split(".") if int(prverscheck[0]) == 0 and int(prverscheck[1]) < 6: print("Need to upgrade the python pyroute2 package to >= 0.6.") - sys.exit(0) + sys.exit(1) parser = argparse.ArgumentParser() parser.add_argument( @@ -2632,7 +2975,7 @@ def main(argv): "--ptype", type=str, default="netdev", - choices=["netdev", "internal", "geneve", "vxlan"], + choices=["netdev", "internal", "gre", "geneve", "vxlan"], help="Interface type (default netdev)", ) addifcmd.add_argument( @@ -2642,13 +2985,6 @@ def main(argv): default=0, help="Destination port (0 for default)" ) - addifcmd.add_argument( - "-l", - "--lwt", - type=bool, - default=True, - help="Use LWT infrastructure instead of vport (default true)." - ) delifcmd = subparsers.add_parser("del-if") delifcmd.add_argument("dpname", help="Datapath Name") delifcmd.add_argument("delif", help="Interface name for adding") @@ -2665,6 +3001,12 @@ def main(argv): addflcmd.add_argument("flow", help="Flow specification") addflcmd.add_argument("acts", help="Flow actions") + modflcmd = subparsers.add_parser("mod-flow") + modflcmd.add_argument("modbr", help="Datapath name") + modflcmd.add_argument("modflow", help="Flow specification") + modflcmd.add_argument("modacts", help="Flow actions", + nargs="?", default=None) + delfscmd = subparsers.add_parser("del-flows") delfscmd.add_argument("flsbr", help="Datapath name") @@ -2722,7 +3064,7 @@ def main(argv): return 1 dpindex = rep["dpifindex"] rep = ovsvp.attach(rep["dpifindex"], args.addif, args.ptype, - args.dport, args.lwt) + args.dport) msg = "vport '%s'" % args.addif if rep and rep["header"]["error"] is None: msg += " added." @@ -2762,6 +3104,14 @@ def main(argv): flow = OvsFlow.ovs_flow_msg() flow.parse(args.flow, args.acts, rep["dpifindex"]) ovsflow.add_flow(rep["dpifindex"], flow) + elif hasattr(args, "modbr"): + rep = ovsdp.info(args.modbr, 0) + if rep is None: + print(f"DP '{args.modbr}' not found.") + return 1 + flow = OvsFlow.ovs_flow_msg() + flow.parse(args.modflow, args.modacts, rep["dpifindex"]) + ovsflow.mod_flow(rep["dpifindex"], flow) elif hasattr(args, "flsbr"): rep = ovsdp.info(args.flsbr, 0) if rep is None: diff --git a/tools/testing/selftests/net/ovpn/Makefile b/tools/testing/selftests/net/ovpn/Makefile index dbe0388c8512..169f0464ac3a 100644 --- a/tools/testing/selftests/net/ovpn/Makefile +++ b/tools/testing/selftests/net/ovpn/Makefile @@ -2,22 +2,35 @@ # Copyright (C) 2020-2025 OpenVPN, Inc. # CFLAGS = -pedantic -Wextra -Wall -Wl,--no-as-needed -g -O0 -ggdb $(KHDR_INCLUDES) +CFLAGS += $(shell pkg-config --cflags mbedcrypto-3 mbedtls-3 2>/dev/null) + VAR_CFLAGS = $(shell pkg-config --cflags libnl-3.0 libnl-genl-3.0 2>/dev/null) ifeq ($(VAR_CFLAGS),) VAR_CFLAGS = -I/usr/include/libnl3 endif CFLAGS += $(VAR_CFLAGS) +MTLS_LDLIBS= $(shell pkg-config --libs mbedcrypto-3 mbedtls-3 2>/dev/null) +ifeq ($(MTLS_LDLIBS),) +MTLS_LDLIBS = -lmbedtls -lmbedcrypto +endif +LDLIBS += $(MTLS_LDLIBS) -LDLIBS = -lmbedtls -lmbedcrypto -VAR_LDLIBS = $(shell pkg-config --libs libnl-3.0 libnl-genl-3.0 2>/dev/null) -ifeq ($(VAR_LDLIBS),) -VAR_LDLIBS = -lnl-genl-3 -lnl-3 +NL_LDLIBS = $(shell pkg-config --libs libnl-3.0 libnl-genl-3.0 2>/dev/null) +ifeq ($(NL_LDLIBS),) +NL_LDLIBS = -lnl-genl-3 -lnl-3 endif -LDLIBS += $(VAR_LDLIBS) +LDLIBS += $(NL_LDLIBS) -TEST_FILES = common.sh +TEST_FILES = \ + common.sh \ + data64.key \ + json \ + tcp_peers.txt \ + udp_peers.txt \ + ../../../../net/ynl/pyynl/cli.py \ +# end of TEST_FILES TEST_PROGS := \ test-chachapoly.sh \ @@ -25,6 +38,10 @@ TEST_PROGS := \ test-close-socket.sh \ test-float.sh \ test-large-mtu.sh \ + test-mark.sh \ + test-symmetric-id-float.sh \ + test-symmetric-id-tcp.sh \ + test-symmetric-id.sh \ test-tcp.sh \ test.sh \ # end of TEST_PROGS diff --git a/tools/testing/selftests/net/ovpn/common.sh b/tools/testing/selftests/net/ovpn/common.sh index 88869c675d03..5e9c81e885e6 100644 --- a/tools/testing/selftests/net/ovpn/common.sh +++ b/tools/testing/selftests/net/ovpn/common.sh @@ -4,105 +4,374 @@ # # Author: Antonio Quartulli <antonio@openvpn.net> -UDP_PEERS_FILE=${UDP_PEERS_FILE:-udp_peers.txt} -TCP_PEERS_FILE=${TCP_PEERS_FILE:-tcp_peers.txt} -OVPN_CLI=${OVPN_CLI:-./ovpn-cli} -ALG=${ALG:-aes} -PROTO=${PROTO:-UDP} -FLOAT=${FLOAT:-0} +OVPN_COMMON_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") +source "$OVPN_COMMON_DIR/../../kselftest/ktap_helpers.sh" -LAN_IP="11.11.11.11" +OVPN_UDP_PEERS_FILE=${OVPN_UDP_PEERS_FILE:-udp_peers.txt} +OVPN_TCP_PEERS_FILE=${OVPN_TCP_PEERS_FILE:-tcp_peers.txt} +OVPN_CLI=${OVPN_CLI:-${OVPN_COMMON_DIR}/ovpn-cli} +OVPN_YNL=${OVPN_YNL:-${OVPN_COMMON_DIR}/../../../../net/ynl/pyynl/cli.py} +OVPN_ALG=${OVPN_ALG:-aes} +OVPN_PROTO=${OVPN_PROTO:-UDP} +OVPN_FLOAT=${OVPN_FLOAT:-0} +OVPN_SYMMETRIC_ID=${OVPN_SYMMETRIC_ID:-0} +OVPN_VERBOSE=${OVPN_VERBOSE:-0} -create_ns() { - ip netns add peer${1} +export OVPN_ID_OFFSET=$(( 9 * (OVPN_SYMMETRIC_ID == 0) )) + +OVPN_JQ_FILTER='map(if type == "array" then .[] else . end) | + map(select(.msg.peer | has("remote-ipv6") | not)) | + map(del(.msg.ifindex)) | sort_by(.msg.peer.id)[]' +OVPN_LAN_IP="11.11.11.11" + +declare -A OVPN_TMP_JSONS=() +declare -A OVPN_LISTENER_PIDS=() +OVPN_CURRENT_STAGE="" + +ovpn_is_verbose() { + [[ "${OVPN_VERBOSE}" == "1" ]] } -setup_ns() { +ovpn_log() { + ovpn_is_verbose || return 0 + printf '%s\n' "$*" +} + +ovpn_print_cmd_output() { + local output_file="$1" + local line + + [[ -s "${output_file}" ]] || return 0 + + while IFS= read -r line; do + ovpn_log "${line}" + done < "${output_file}" +} + +ovpn_cmd_run() { + local mode="$1" + local label="$2" + local output_file + local rc + local ret=0 + + shift 2 + + output_file=$(mktemp) + if "$@" >"${output_file}" 2>&1; then + rc=0 + else + rc=$? + fi + + case "${mode}" in + ok) + if [[ "${rc}" -ne 0 ]]; then + cat "${output_file}" + printf '%s\n' \ + "${label}: command failed with rc=${rc}: $*" + ret="${rc}" + fi + ;; + mayfail) + ;; + fail) + [[ "${rc}" -eq 0 ]] && ret=1 + ;; + esac + + if ovpn_is_verbose && [[ "${rc}" -eq 0 || "${mode}" != "ok" ]]; then + ovpn_print_cmd_output "${output_file}" + fi + + rm -f "${output_file}" + return "${ret}" +} + +ovpn_cmd_ok() { + ovpn_cmd_run ok "$@" +} + +ovpn_cmd_mayfail() { + ovpn_cmd_run mayfail "$@" +} + +ovpn_cmd_fail() { + ovpn_cmd_run fail "$@" +} + +ovpn_run_bg() { + local pid_var="$1" + + shift + if ovpn_is_verbose; then + "$@" & + else + "$@" >/dev/null 2>&1 & + fi + + printf -v "${pid_var}" '%s' "$!" +} + +ovpn_run_stage() { + local label="$1" + + shift + OVPN_CURRENT_STAGE="${label}" + "$@" + OVPN_CURRENT_STAGE="" + ktap_test_pass "${label}" +} + +ovpn_stage_err() { + # ERR trap is global under set -eE: only report failures that happen + # while ovpn_run_stage() is actively executing a stage body. + if [[ -n "${OVPN_CURRENT_STAGE}" ]]; then + ktap_test_fail "${OVPN_CURRENT_STAGE}" + OVPN_CURRENT_STAGE="" + fi +} + +ovpn_create_ns() { + ip netns add "ovpn_peer${1}" +} + +ovpn_peer_vpn_addr() { + local peer="$1" + local file + + if [ "${OVPN_PROTO}" == "UDP" ]; then + file="${OVPN_UDP_PEERS_FILE}" + else + file="${OVPN_TCP_PEERS_FILE}" + fi + + awk -v peer="${peer}" '$1 == peer {print $NF; exit}' "${file}" +} + +ovpn_setup_ns() { + local peer="ovpn_peer${1}" + local server_ns="ovpn_peer0" + local peer_ns MODE="P2P" if [ ${1} -eq 0 ]; then MODE="MP" - for p in $(seq 1 ${NUM_PEERS}); do - ip link add veth${p} netns peer0 type veth peer name veth${p} netns peer${p} + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ip link add veth${p} netns "${server_ns}" type veth \ + peer name veth${p} netns "${peer_ns}" - ip -n peer0 addr add 10.10.${p}.1/24 dev veth${p} - ip -n peer0 addr add fd00:0:0:${p}::1/64 dev veth${p} - ip -n peer0 link set veth${p} up + ip -n "${server_ns}" addr add 10.10.${p}.1/24 dev \ + veth${p} + ip -n "${server_ns}" addr add fd00:0:0:${p}::1/64 dev \ + veth${p} + ip -n "${server_ns}" link set veth${p} up - ip -n peer${p} addr add 10.10.${p}.2/24 dev veth${p} - ip -n peer${p} addr add fd00:0:0:${p}::2/64 dev veth${p} - ip -n peer${p} link set veth${p} up + ip -n "${peer_ns}" addr add 10.10.${p}.2/24 dev veth${p} + ip -n "${peer_ns}" addr add fd00:0:0:${p}::2/64 dev \ + veth${p} + ip -n "${peer_ns}" link set veth${p} up done fi - ip netns exec peer${1} ${OVPN_CLI} new_iface tun${1} $MODE - ip -n peer${1} addr add ${2} dev tun${1} + ip netns exec "${peer}" ${OVPN_CLI} new_iface tun${1} $MODE + ip -n "${peer}" addr add ${2} dev tun${1} # add a secondary IP to peer 1, to test a LAN behind a client - if [ ${1} -eq 1 -a -n "${LAN_IP}" ]; then - ip -n peer${1} addr add ${LAN_IP} dev tun${1} - ip -n peer0 route add ${LAN_IP} via $(echo ${2} |sed -e s'!/.*!!') dev tun0 + if [ ${1} -eq 1 -a -n "${OVPN_LAN_IP}" ]; then + ip -n "${peer}" addr add ${OVPN_LAN_IP} dev tun${1} + ip -n "${server_ns}" route add ${OVPN_LAN_IP} via \ + $(echo ${2} |sed -e s'!/.*!!') dev tun0 fi if [ -n "${3}" ]; then - ip -n peer${1} link set mtu ${3} dev tun${1} + ip -n "${peer}" link set mtu ${3} dev tun${1} + fi + ip -n "${peer}" link set tun${1} up +} + +ovpn_build_capture_filter() { + # match the first four bytes of the openvpn data payload + if [ "${OVPN_PROTO}" == "UDP" ]; then + # For UDP, libpcap transport indexing only works for IPv4, so + # use an explicit IPv4 or IPv6 expression based on the peer + # address. The IPv6 branch assumes there are no extension + # headers in the outer packet. + if [[ "${2}" == *:* ]]; then + printf "ip6 and ip6[6] = 17 and ip6[48:4] = %s" "${1}" + else + printf "ip and udp[8:4] = %s" "${1}" + fi + else + # openvpn over TCP prepends a 2-byte packet length ahead of the + # DATA_V2 opcode, so skip it before matching the payload header + printf "ip and tcp[(((tcp[12] & 0xf0) >> 2) + 2):4] = %s" "${1}" fi - ip -n peer${1} link set tun${1} up } -add_peer() { - if [ "${PROTO}" == "UDP" ]; then +ovpn_setup_listener() { + local peer="$1" + local file + local peer_ns="ovpn_peer${peer}" + + file=$(mktemp) + PYTHONUNBUFFERED=1 ip netns exec "${peer_ns}" "${OVPN_YNL}" --family \ + ovpn --subscribe peers --output-json > "${file}" \ + 2>/dev/null & + OVPN_LISTENER_PIDS["${peer}"]=$! + OVPN_TMP_JSONS["${peer}"]="${file}" +} + +ovpn_add_peer() { + labels=("ASYMM" "SYMM") + local peer_ns + local server_ns="ovpn_peer0" + M_ID=${labels[OVPN_SYMMETRIC_ID]} + + if [ "${OVPN_PROTO}" == "UDP" ]; then if [ ${1} -eq 0 ]; then - ip netns exec peer0 ${OVPN_CLI} new_multi_peer tun0 1 ${UDP_PEERS_FILE} + ip netns exec "${server_ns}" ${OVPN_CLI} \ + new_multi_peer tun0 1 ${M_ID} \ + ${OVPN_UDP_PEERS_FILE} - for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ${OVPN_CLI} new_key tun0 ${p} 1 0 ${ALG} 0 \ + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + ip netns exec "${server_ns}" ${OVPN_CLI} \ + new_key tun0 ${p} 1 0 ${OVPN_ALG} 0 \ data64.key done else - RADDR=$(awk "NR == ${1} {print \$2}" ${UDP_PEERS_FILE}) - RPORT=$(awk "NR == ${1} {print \$3}" ${UDP_PEERS_FILE}) - LPORT=$(awk "NR == ${1} {print \$5}" ${UDP_PEERS_FILE}) - ip netns exec peer${1} ${OVPN_CLI} new_peer tun${1} ${1} ${LPORT} \ - ${RADDR} ${RPORT} - ip netns exec peer${1} ${OVPN_CLI} new_key tun${1} ${1} 1 0 ${ALG} 1 \ - data64.key + peer_ns="ovpn_peer${1}" + if [ "${OVPN_SYMMETRIC_ID}" -eq 1 ]; then + PEER_ID=${1} + TX_ID="none" + else + PEER_ID=$(awk "NR == ${1} {print \$2}" \ + ${OVPN_UDP_PEERS_FILE}) + TX_ID=${1} + fi + RADDR=$(awk "NR == ${1} {print \$3}" \ + ${OVPN_UDP_PEERS_FILE}) + RPORT=$(awk "NR == ${1} {print \$4}" \ + ${OVPN_UDP_PEERS_FILE}) + LPORT=$(awk "NR == ${1} {print \$6}" \ + ${OVPN_UDP_PEERS_FILE}) + ip netns exec "${peer_ns}" ${OVPN_CLI} new_peer \ + tun${1} ${PEER_ID} ${TX_ID} ${LPORT} ${RADDR} \ + ${RPORT} + ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${1} \ + ${PEER_ID} 1 0 ${OVPN_ALG} 1 data64.key fi else if [ ${1} -eq 0 ]; then - (ip netns exec peer0 ${OVPN_CLI} listen tun0 1 ${TCP_PEERS_FILE} && { - for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ${OVPN_CLI} new_key tun0 ${p} 1 0 \ - ${ALG} 0 data64.key + (ip netns exec "${server_ns}" ${OVPN_CLI} listen tun0 \ + 1 ${M_ID} ${OVPN_TCP_PEERS_FILE} && { + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + ip netns exec "${server_ns}" \ + ${OVPN_CLI} new_key tun0 ${p} \ + 1 0 ${OVPN_ALG} 0 data64.key done }) & sleep 5 else - ip netns exec peer${1} ${OVPN_CLI} connect tun${1} ${1} 10.10.${1}.1 1 \ - data64.key + peer_ns="ovpn_peer${1}" + if [ "${OVPN_SYMMETRIC_ID}" -eq 1 ]; then + PEER_ID=${1} + TX_ID="none" + else + PEER_ID=$(awk "NR == ${1} {print \$2}" \ + ${OVPN_TCP_PEERS_FILE}) + TX_ID=${1} + fi + ip netns exec "${peer_ns}" ${OVPN_CLI} connect tun${1} \ + ${PEER_ID} ${TX_ID} 10.10.${1}.1 1 data64.key + fi + fi +} + +ovpn_compare_ntfs() { + local diff_rc=0 + local diff_file + + if [ ${#OVPN_TMP_JSONS[@]} -gt 0 ]; then + suffix="" + [ "${OVPN_SYMMETRIC_ID}" -eq 1 ] && suffix="${suffix}-symm" + [ "$OVPN_FLOAT" == 1 ] && suffix="${suffix}-float" + expected="json/peer${1}${suffix}.json" + received="${OVPN_TMP_JSONS[$1]}" + diff_file=$(mktemp) + + ovpn_stop_listener "${1}" 1 + printf "Checking notifications for peer ${1}... " + if diff <(jq -s "${OVPN_JQ_FILTER}" ${expected}) \ + <(jq -s "${OVPN_JQ_FILTER}" ${received}) \ + >"${diff_file}" 2>&1; then + echo "OK" + else + diff_rc=$? + echo "failed" + cat "${diff_file}" fi + + rm -f "${diff_file}" || true + rm -f "${received}" || true + unset "OVPN_TMP_JSONS[$1]" + fi + + return "${diff_rc}" +} + +ovpn_stop_listener() { + local peer="$1" + local keep_json="${2:-0}" + local pid="${OVPN_LISTENER_PIDS[$peer]:-}" + local json="${OVPN_TMP_JSONS[$peer]:-}" + + if [[ -n "${pid}" ]]; then + kill -TERM "${pid}" 2>/dev/null || true + wait "${pid}" 2>/dev/null || true + unset "OVPN_LISTENER_PIDS[$peer]" + fi + + if [[ -n "${json}" && "${keep_json}" -eq 0 ]]; then + rm -f "${json}" || true + unset "OVPN_TMP_JSONS[$peer]" fi } -cleanup() { +ovpn_cleanup_peer_ns() { + local peer="$1" + local peer_id="${peer#ovpn_peer}" + + ip -n "${peer}" link set tun${peer_id} down 2>/dev/null || true + ip netns exec "${peer}" ${OVPN_CLI} del_iface tun${peer_id} \ + 1>/dev/null 2>&1 || true + ip netns del "${peer}" 2>/dev/null || true +} + +ovpn_cleanup() { + local peer + # some ovpn-cli processes sleep in background so they need manual poking - killall $(basename ${OVPN_CLI}) 2>/dev/null || true + killall "$(basename "${OVPN_CLI}")" 2>/dev/null || true - # netns peer0 is deleted without erasing ifaces first - for p in $(seq 1 10); do - ip -n peer${p} link set tun${p} down 2>/dev/null || true - ip netns exec peer${p} ${OVPN_CLI} del_iface tun${p} 2>/dev/null || true + for peer in "${!OVPN_LISTENER_PIDS[@]}"; do + ovpn_stop_listener "${peer}" 2>/dev/null done + for p in $(seq 1 10); do - ip -n peer0 link del veth${p} 2>/dev/null || true - done - for p in $(seq 0 10); do - ip netns del peer${p} 2>/dev/null || true + ip -n ovpn_peer0 link del veth${p} 2>/dev/null || true done + + # remove from ovpn's netns pool + while IFS= read -r peer; do + [[ -n "${peer}" ]] || continue + ovpn_cleanup_peer_ns "${peer}" 2>/dev/null + done < <(ip netns list 2>/dev/null | awk '/^ovpn_/ {print $1}') } -if [ "${PROTO}" == "UDP" ]; then - NUM_PEERS=${NUM_PEERS:-$(wc -l ${UDP_PEERS_FILE} | awk '{print $1}')} +if [ "${OVPN_PROTO}" == "UDP" ]; then + OVPN_NUM_PEERS=${OVPN_NUM_PEERS:-$(wc -l ${OVPN_UDP_PEERS_FILE} | \ + awk '{print $1}')} else - NUM_PEERS=${NUM_PEERS:-$(wc -l ${TCP_PEERS_FILE} | awk '{print $1}')} + OVPN_NUM_PEERS=${OVPN_NUM_PEERS:-$(wc -l ${OVPN_TCP_PEERS_FILE} | \ + awk '{print $1}')} fi - - diff --git a/tools/testing/selftests/net/ovpn/config b/tools/testing/selftests/net/ovpn/config index 42699740936d..6b424762e46e 100644 --- a/tools/testing/selftests/net/ovpn/config +++ b/tools/testing/selftests/net/ovpn/config @@ -4,7 +4,12 @@ CONFIG_CRYPTO_CHACHA20POLY1305=y CONFIG_CRYPTO_GCM=y CONFIG_DST_CACHE=y CONFIG_INET=y +CONFIG_IPV6=y CONFIG_NET=y +CONFIG_NETFILTER=y CONFIG_NET_UDP_TUNNEL=y +CONFIG_NF_TABLES=m +CONFIG_NF_TABLES_INET=y CONFIG_OVPN=m CONFIG_STREAM_PARSER=y +CONFIG_VETH=y diff --git a/tools/testing/selftests/net/ovpn/data64.key b/tools/testing/selftests/net/ovpn/data64.key index a99e88c4e290..d04febcdf5a2 100644 --- a/tools/testing/selftests/net/ovpn/data64.key +++ b/tools/testing/selftests/net/ovpn/data64.key @@ -1,5 +1 @@ -jRqMACN7d7/aFQNT8S7jkrBD8uwrgHbG5OQZP2eu4R1Y7tfpS2bf5RHv06Vi163CGoaIiTX99R3B -ia9ycAH8Wz1+9PWv51dnBLur9jbShlgZ2QHLtUc4a/gfT7zZwULXuuxdLnvR21DDeMBaTbkgbai9 -uvAa7ne1liIgGFzbv+Bas4HDVrygxIxuAnP5Qgc3648IJkZ0QEXPF+O9f0n5+QIvGCxkAUVx+5K6 -KIs+SoeWXnAopELmoGSjUpFtJbagXK82HfdqpuUxT2Tnuef0/14SzVE/vNleBNu2ZbyrSAaah8tE -BofkPJUBFY+YQcfZNM5Dgrw3i+Bpmpq/gpdg5w== +jRqMACN7d7/aFQNT8S7jkrBD8uwrgHbG5OQZP2eu4R1Y7tfpS2bf5RHv06Vi163CGoaIiTX99R3Bia9ycAH8Wz1+9PWv51dnBLur9jbShlgZ2QHLtUc4a/gfT7zZwULXuuxdLnvR21DDeMBaTbkgbai9uvAa7ne1liIgGFzbv+Bas4HDVrygxIxuAnP5Qgc3648IJkZ0QEXPF+O9f0n5+QIvGCxkAUVx+5K6KIs+SoeWXnAopELmoGSjUpFtJbagXK82HfdqpuUxT2Tnuef0/14SzVE/vNleBNu2ZbyrSAaah8tEBofkPJUBFY+YQcfZNM5Dgrw3i+Bpmpq/gpdg5w== diff --git a/tools/testing/selftests/net/ovpn/json/peer0-float.json b/tools/testing/selftests/net/ovpn/json/peer0-float.json new file mode 100644 index 000000000000..682fa58ad4ea --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer0-float.json @@ -0,0 +1,9 @@ +{"name": "peer-float-ntf", "msg": {"ifindex": 0, "peer": {"id": 1, "remote-ipv4": "10.10.1.3", "remote-port": 1}}} +{"name": "peer-float-ntf", "msg": {"ifindex": 0, "peer": {"id": 2, "remote-ipv4": "10.10.2.3", "remote-port": 1}}} +{"name": "peer-float-ntf", "msg": {"ifindex": 0, "peer": {"id": 3, "remote-ipv4": "10.10.3.3", "remote-port": 1}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 1}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 2}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 3}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 4}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 5}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 6}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer0-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer0-symm-float.json new file mode 120000 index 000000000000..e31a5bd59863 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer0-symm-float.json @@ -0,0 +1 @@ +peer0-float.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer0-symm.json b/tools/testing/selftests/net/ovpn/json/peer0-symm.json new file mode 120000 index 000000000000..57a163048eed --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer0-symm.json @@ -0,0 +1 @@ +peer0.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer0.json b/tools/testing/selftests/net/ovpn/json/peer0.json new file mode 100644 index 000000000000..7c46a33d5ecd --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer0.json @@ -0,0 +1,6 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 1}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 2}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 3}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 4}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 5}}} +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 6}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer1-float.json b/tools/testing/selftests/net/ovpn/json/peer1-float.json new file mode 120000 index 000000000000..d28c328d1452 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer1-float.json @@ -0,0 +1 @@ +peer1.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer1-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer1-symm-float.json new file mode 120000 index 000000000000..b3615dcc523d --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer1-symm-float.json @@ -0,0 +1 @@ +peer1-symm.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer1-symm.json b/tools/testing/selftests/net/ovpn/json/peer1-symm.json new file mode 100644 index 000000000000..5da4ea9d51fb --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer1-symm.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 1}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer1.json b/tools/testing/selftests/net/ovpn/json/peer1.json new file mode 100644 index 000000000000..1009d26dc14a --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer1.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 10}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer2-float.json b/tools/testing/selftests/net/ovpn/json/peer2-float.json new file mode 120000 index 000000000000..b9f09980aaa0 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer2-float.json @@ -0,0 +1 @@ +peer2.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer2-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer2-symm-float.json new file mode 120000 index 000000000000..28a895cb5170 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer2-symm-float.json @@ -0,0 +1 @@ +peer2-symm.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer2-symm.json b/tools/testing/selftests/net/ovpn/json/peer2-symm.json new file mode 100644 index 000000000000..8f6db4f8c2ac --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer2-symm.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 2}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer2.json b/tools/testing/selftests/net/ovpn/json/peer2.json new file mode 100644 index 000000000000..44e9fad2b622 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer2.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "userspace", "id": 11}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer3-float.json b/tools/testing/selftests/net/ovpn/json/peer3-float.json new file mode 120000 index 000000000000..2700b55bcf2e --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer3-float.json @@ -0,0 +1 @@ +peer3.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer3-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer3-symm-float.json new file mode 120000 index 000000000000..ee8b9719c2fd --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer3-symm-float.json @@ -0,0 +1 @@ +peer3-symm.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer3-symm.json b/tools/testing/selftests/net/ovpn/json/peer3-symm.json new file mode 100644 index 000000000000..bdabd6fa2e64 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer3-symm.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 3}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer3.json b/tools/testing/selftests/net/ovpn/json/peer3.json new file mode 100644 index 000000000000..d4be8ba130ae --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer3.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 12}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer4-float.json b/tools/testing/selftests/net/ovpn/json/peer4-float.json new file mode 120000 index 000000000000..460f6c14cd60 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer4-float.json @@ -0,0 +1 @@ +peer4.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer4-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer4-symm-float.json new file mode 120000 index 000000000000..7d34ff7305da --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer4-symm-float.json @@ -0,0 +1 @@ +peer4-symm.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer4-symm.json b/tools/testing/selftests/net/ovpn/json/peer4-symm.json new file mode 100644 index 000000000000..c3734bb9251b --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer4-symm.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 4}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer4.json b/tools/testing/selftests/net/ovpn/json/peer4.json new file mode 100644 index 000000000000..67d27e2d48ac --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer4.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 13}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer5-float.json b/tools/testing/selftests/net/ovpn/json/peer5-float.json new file mode 120000 index 000000000000..0f725c50ce19 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer5-float.json @@ -0,0 +1 @@ +peer5.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer5-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer5-symm-float.json new file mode 120000 index 000000000000..afc0f5f9f13b --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer5-symm-float.json @@ -0,0 +1 @@ +peer5-symm.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer5-symm.json b/tools/testing/selftests/net/ovpn/json/peer5-symm.json new file mode 100644 index 000000000000..46c4a348299d --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer5-symm.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 5}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer5.json b/tools/testing/selftests/net/ovpn/json/peer5.json new file mode 100644 index 000000000000..ecd9bd0b2f37 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer5.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 14}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer6-float.json b/tools/testing/selftests/net/ovpn/json/peer6-float.json new file mode 120000 index 000000000000..4d9ded3e0a84 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer6-float.json @@ -0,0 +1 @@ +peer6.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer6-symm-float.json b/tools/testing/selftests/net/ovpn/json/peer6-symm-float.json new file mode 120000 index 000000000000..e39203204d8c --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer6-symm-float.json @@ -0,0 +1 @@ +peer6-symm.json
\ No newline at end of file diff --git a/tools/testing/selftests/net/ovpn/json/peer6-symm.json b/tools/testing/selftests/net/ovpn/json/peer6-symm.json new file mode 100644 index 000000000000..aa30f2cff625 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer6-symm.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 6}}} diff --git a/tools/testing/selftests/net/ovpn/json/peer6.json b/tools/testing/selftests/net/ovpn/json/peer6.json new file mode 100644 index 000000000000..7fded29c5804 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/json/peer6.json @@ -0,0 +1 @@ +{"name": "peer-del-ntf", "msg": {"ifindex": 0, "peer": {"del-reason": "expired", "id": 15}}} diff --git a/tools/testing/selftests/net/ovpn/ovpn-cli.c b/tools/testing/selftests/net/ovpn/ovpn-cli.c index 0f3babf19fd0..3b612a8a18fe 100644 --- a/tools/testing/selftests/net/ovpn/ovpn-cli.c +++ b/tools/testing/selftests/net/ovpn/ovpn-cli.c @@ -6,6 +6,7 @@ * Author: Antonio Quartulli <antonio@openvpn.net> */ +#include <stdint.h> #include <stdio.h> #include <inttypes.h> #include <stdbool.h> @@ -103,7 +104,7 @@ struct ovpn_ctx { sa_family_t sa_family; - unsigned long peer_id; + unsigned long peer_id, tx_id; unsigned long lport; union { @@ -133,6 +134,9 @@ struct ovpn_ctx { enum ovpn_key_slot key_slot; int key_id; + uint32_t mark; + bool asymm_id; + const char *peers_file; }; @@ -521,6 +525,15 @@ static int ovpn_socket(struct ovpn_ctx *ctx, sa_family_t family, int proto) return ret; } + if (ctx->mark != 0) { + ret = setsockopt(s, SOL_SOCKET, SO_MARK, (void *)&ctx->mark, + sizeof(ctx->mark)); + if (ret < 0) { + perror("setsockopt for SO_MARK"); + return ret; + } + } + if (family == AF_INET6) { opt = 0; if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &opt, @@ -637,6 +650,26 @@ err: return ret; } +static int ovpn_nl_put_vpn_addr(struct nl_msg *msg, + const struct ovpn_ctx *ovpn) +{ + if (!ovpn->peer_ip_set) + return 0; + + switch (ovpn->peer_ip.in4.sin_family) { + case AF_INET: + return nla_put_u32(msg, OVPN_A_PEER_VPN_IPV4, + ovpn->peer_ip.in4.sin_addr.s_addr); + case AF_INET6: + return nla_put(msg, OVPN_A_PEER_VPN_IPV6, + sizeof(struct in6_addr), + &ovpn->peer_ip.in6.sin6_addr); + default: + fprintf(stderr, "Invalid family for peer address\n"); + return -EAFNOSUPPORT; + } +} + static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp) { struct nlattr *attr; @@ -649,6 +682,8 @@ static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp) attr = nla_nest_start(ctx->nl_msg, OVPN_A_PEER); NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_ID, ovpn->peer_id); + if (ovpn->asymm_id) + NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_TX_ID, ovpn->tx_id); NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_SOCKET, ovpn->socket); if (!is_tcp) { @@ -676,22 +711,9 @@ static int ovpn_new_peer(struct ovpn_ctx *ovpn, bool is_tcp) } } - if (ovpn->peer_ip_set) { - switch (ovpn->peer_ip.in4.sin_family) { - case AF_INET: - NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_VPN_IPV4, - ovpn->peer_ip.in4.sin_addr.s_addr); - break; - case AF_INET6: - NLA_PUT(ctx->nl_msg, OVPN_A_PEER_VPN_IPV6, - sizeof(struct in6_addr), - &ovpn->peer_ip.in6.sin6_addr); - break; - default: - fprintf(stderr, "Invalid family for peer address\n"); - goto nla_put_failure; - } - } + ret = ovpn_nl_put_vpn_addr(ctx->nl_msg, ovpn); + if (ret) + goto nla_put_failure; nla_nest_end(ctx->nl_msg, attr); @@ -717,6 +739,10 @@ static int ovpn_set_peer(struct ovpn_ctx *ovpn) ovpn->keepalive_interval); NLA_PUT_U32(ctx->nl_msg, OVPN_A_PEER_KEEPALIVE_TIMEOUT, ovpn->keepalive_timeout); + + ret = ovpn_nl_put_vpn_addr(ctx->nl_msg, ovpn); + if (ret) + goto nla_put_failure; nla_nest_end(ctx->nl_msg, attr); ret = ovpn_nl_msg_send(ctx, NULL); @@ -767,6 +793,10 @@ static int ovpn_handle_peer(struct nl_msg *msg, void (*arg)__always_unused) fprintf(stderr, "* Peer %u\n", nla_get_u32(pattrs[OVPN_A_PEER_ID])); + if (pattrs[OVPN_A_PEER_TX_ID]) + fprintf(stderr, "\tTX peer ID %u\n", + nla_get_u32(pattrs[OVPN_A_PEER_TX_ID])); + if (pattrs[OVPN_A_PEER_SOCKET_NETNSID]) fprintf(stderr, "\tsocket NetNS ID: %d\n", nla_get_s32(pattrs[OVPN_A_PEER_SOCKET_NETNSID])); @@ -1516,6 +1546,9 @@ static int ovpn_handle_msg(struct nl_msg *msg, void *arg) case OVPN_CMD_PEER_DEL_NTF: fprintf(stdout, "received CMD_PEER_DEL_NTF\n"); break; + case OVPN_CMD_PEER_FLOAT_NTF: + fprintf(stdout, "received CMD_PEER_FLOAT_NTF\n"); + break; case OVPN_CMD_KEY_SWAP_NTF: fprintf(stdout, "received CMD_KEY_SWAP_NTF\n"); break; @@ -1654,50 +1687,68 @@ static void usage(const char *cmd) fprintf(stderr, "\tiface: ovpn interface name\n"); fprintf(stderr, - "* listen <iface> <lport> <peers_file> [ipv6]: listen for incoming peer TCP connections\n"); + "* listen <iface> <lport> <id_type> <peers_file> [ipv6]: listen for incoming peer TCP connections\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); fprintf(stderr, "\tlport: TCP port to listen to\n"); + fprintf(stderr, "\tid_type:\n"); + fprintf(stderr, + "\t\t- SYMM for ignoring the TX peer ID from the peers_file\n"); + fprintf(stderr, + "\t\t- ASYMM for using the TX peer ID from the peers_file\n"); fprintf(stderr, "\tpeers_file: file containing one peer per line: Line format:\n"); - fprintf(stderr, "\t\t<peer_id> <vpnaddr>\n"); + fprintf(stderr, "\t\t<peer_id> <tx_id> <vpnaddr>\n"); fprintf(stderr, "\tipv6: whether the socket should listen to the IPv6 wildcard address\n"); fprintf(stderr, - "* connect <iface> <peer_id> <raddr> <rport> [key_file]: start connecting peer of TCP-based VPN session\n"); + "* connect <iface> <peer_id> <tx_id> <raddr> <rport> [key_file]: start connecting peer of TCP-based VPN session\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); - fprintf(stderr, "\tpeer_id: peer ID of the connecting peer\n"); + fprintf(stderr, + "\tpeer_id: peer ID found in data packets received from this peer\n"); + fprintf(stderr, + "\ttx_id: peer ID to be used when sending to this peer, 'none' for symmetric peer ID\n"); fprintf(stderr, "\traddr: peer IP address to connect to\n"); fprintf(stderr, "\trport: peer TCP port to connect to\n"); fprintf(stderr, "\tkey_file: file containing the symmetric key for encryption\n"); fprintf(stderr, - "* new_peer <iface> <peer_id> <lport> <raddr> <rport> [vpnaddr]: add new peer\n"); + "* new_peer <iface> <peer_id> <tx_id> <lport> <raddr> <rport> [vpnaddr]: add new peer\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); - fprintf(stderr, "\tlport: local UDP port to bind to\n"); fprintf(stderr, - "\tpeer_id: peer ID to be used in data packets to/from this peer\n"); + "\tpeer_id: peer ID found in data packets received from this peer\n"); + fprintf(stderr, + "\ttx_id: peer ID to be used when sending to this peer, 'none' for symmetric peer ID\n"); + fprintf(stderr, "\tlport: local UDP port to bind to\n"); fprintf(stderr, "\traddr: peer IP address\n"); fprintf(stderr, "\trport: peer UDP port\n"); fprintf(stderr, "\tvpnaddr: peer VPN IP\n"); fprintf(stderr, - "* new_multi_peer <iface> <lport> <peers_file>: add multiple peers as listed in the file\n"); + "* new_multi_peer <iface> <lport> <id_type> <peers_file> [mark]: add multiple peers as listed in the file\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); fprintf(stderr, "\tlport: local UDP port to bind to\n"); + fprintf(stderr, "\tid_type:\n"); + fprintf(stderr, + "\t\t- SYMM for ignoring the TX peer ID from the peers_file\n"); + fprintf(stderr, + "\t\t- ASYMM for using the TX peer ID from the peers_file\n"); fprintf(stderr, "\tpeers_file: text file containing one peer per line. Line format:\n"); - fprintf(stderr, "\t\t<peer_id> <raddr> <rport> <vpnaddr>\n"); + fprintf(stderr, + "\t\t<peer_id> <tx_id> <raddr> <rport> <laddr> <lport> <vpnaddr>\n"); + fprintf(stderr, "\tmark: socket FW mark value\n"); fprintf(stderr, - "* set_peer <iface> <peer_id> <keepalive_interval> <keepalive_timeout>: set peer attributes\n"); + "* set_peer <iface> <peer_id> <keepalive_interval> <keepalive_timeout> [vpnaddr]: set peer attributes\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); fprintf(stderr, "\tpeer_id: peer ID of the peer to modify\n"); fprintf(stderr, "\tkeepalive_interval: interval for sending ping messages\n"); fprintf(stderr, "\tkeepalive_timeout: time after which a peer is timed out\n"); + fprintf(stderr, "\tvpnaddr: peer VPN IP\n"); fprintf(stderr, "* del_peer <iface> <peer_id>: delete peer\n"); fprintf(stderr, "\tiface: ovpn interface name\n"); @@ -1746,7 +1797,7 @@ static int ovpn_parse_remote(struct ovpn_ctx *ovpn, const char *host, const char *service, const char *vpnip) { int ret; - struct addrinfo *result; + struct addrinfo *result = NULL; struct addrinfo hints = { .ai_family = ovpn->sa_family, .ai_socktype = SOCK_DGRAM, @@ -1770,6 +1821,8 @@ static int ovpn_parse_remote(struct ovpn_ctx *ovpn, const char *host, } memcpy(&ovpn->remote, result->ai_addr, result->ai_addrlen); + freeaddrinfo(result); + result = NULL; } if (vpnip) { @@ -1801,15 +1854,23 @@ out: } static int ovpn_parse_new_peer(struct ovpn_ctx *ovpn, const char *peer_id, - const char *raddr, const char *rport, - const char *vpnip) + const char *tx_id, const char *raddr, + const char *rport, const char *vpnip) { ovpn->peer_id = strtoul(peer_id, NULL, 10); if (errno == ERANGE || ovpn->peer_id > PEER_ID_UNDEF) { - fprintf(stderr, "peer ID value out of range\n"); + fprintf(stderr, "rx peer ID value out of range\n"); return -1; } + if (ovpn->asymm_id) { + ovpn->tx_id = strtoul(tx_id, NULL, 10); + if (errno == ERANGE || ovpn->tx_id > PEER_ID_UNDEF) { + fprintf(stderr, "tx peer ID value out of range\n"); + return -1; + } + } + return ovpn_parse_remote(ovpn, raddr, rport, vpnip); } @@ -1936,8 +1997,8 @@ static void ovpn_waitbg(void) static int ovpn_run_cmd(struct ovpn_ctx *ovpn) { - char peer_id[10], vpnip[INET6_ADDRSTRLEN], laddr[128], lport[10]; - char raddr[128], rport[10]; + char peer_id[10], tx_id[10], vpnip[INET6_ADDRSTRLEN], laddr[128]; + char lport[10], raddr[128], rport[10]; int n, ret; FILE *fp; @@ -1964,7 +2025,8 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn) int num_peers = 0; - while ((n = fscanf(fp, "%s %s\n", peer_id, vpnip)) == 2) { + while ((n = fscanf(fp, "%s %s %s\n", peer_id, tx_id, + vpnip)) == 3) { struct ovpn_ctx peer_ctx = { 0 }; if (num_peers == MAX_PEERS) { @@ -1974,6 +2036,7 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn) peer_ctx.ifindex = ovpn->ifindex; peer_ctx.sa_family = ovpn->sa_family; + peer_ctx.asymm_id = ovpn->asymm_id; peer_ctx.socket = ovpn_accept(ovpn); if (peer_ctx.socket < 0) { @@ -1984,8 +2047,8 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn) /* store peer sockets to test TCP I/O */ ovpn->cli_sockets[num_peers] = peer_ctx.socket; - ret = ovpn_parse_new_peer(&peer_ctx, peer_id, NULL, - NULL, vpnip); + ret = ovpn_parse_new_peer(&peer_ctx, peer_id, tx_id, + NULL, NULL, vpnip); if (ret < 0) { fprintf(stderr, "error while parsing line\n"); return -1; @@ -2039,6 +2102,8 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn) return ret; ret = ovpn_new_peer(ovpn, false); + if (ret < 0) + return ret; ovpn_waitbg(); break; case CMD_NEW_MULTI_PEER: @@ -2053,16 +2118,17 @@ static int ovpn_run_cmd(struct ovpn_ctx *ovpn) return -1; } - while ((n = fscanf(fp, "%s %s %s %s %s %s\n", peer_id, laddr, - lport, raddr, rport, vpnip)) == 6) { + while ((n = fscanf(fp, "%s %s %s %s %s %s %s\n", peer_id, tx_id, + laddr, lport, raddr, rport, vpnip)) == 7) { struct ovpn_ctx peer_ctx = { 0 }; peer_ctx.ifindex = ovpn->ifindex; peer_ctx.socket = ovpn->socket; peer_ctx.sa_family = AF_UNSPEC; + peer_ctx.asymm_id = ovpn->asymm_id; - ret = ovpn_parse_new_peer(&peer_ctx, peer_id, raddr, - rport, vpnip); + ret = ovpn_parse_new_peer(&peer_ctx, peer_id, tx_id, + raddr, rport, vpnip); if (ret < 0) { fprintf(stderr, "error while parsing line\n"); return -1; @@ -2158,7 +2224,7 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[]) case CMD_DEL_IFACE: break; case CMD_LISTEN: - if (argc < 5) + if (argc < 6) return -EINVAL; ovpn->lport = strtoul(argv[3], NULL, 10); @@ -2167,55 +2233,67 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[]) return -1; } - ovpn->peers_file = argv[4]; + if (strcmp(argv[4], "SYMM") == 0) { + ovpn->asymm_id = false; + } else if (strcmp(argv[4], "ASYMM") == 0) { + ovpn->asymm_id = true; + } else { + fprintf(stderr, "Cannot parse id type: %s\n", argv[4]); + return -1; + } + + ovpn->peers_file = argv[5]; ovpn->sa_family = AF_INET; - if (argc > 5 && !strcmp(argv[5], "ipv6")) + if (argc > 6 && !strcmp(argv[6], "ipv6")) ovpn->sa_family = AF_INET6; break; case CMD_CONNECT: - if (argc < 6) + if (argc < 7) return -EINVAL; ovpn->sa_family = AF_INET; + ovpn->asymm_id = strcmp(argv[4], "none"); ret = ovpn_parse_new_peer(ovpn, argv[3], argv[4], argv[5], - NULL); + argv[6], NULL); if (ret < 0) { fprintf(stderr, "Cannot parse remote peer data\n"); return -1; } - if (argc > 6) { + if (argc > 7) { ovpn->key_slot = OVPN_KEY_SLOT_PRIMARY; ovpn->key_id = 0; ovpn->cipher = OVPN_CIPHER_ALG_AES_GCM; ovpn->key_dir = KEY_DIR_OUT; - ret = ovpn_parse_key(argv[6], ovpn); + ret = ovpn_parse_key(argv[7], ovpn); if (ret) return -1; } break; case CMD_NEW_PEER: - if (argc < 7) + if (argc < 8) return -EINVAL; - ovpn->lport = strtoul(argv[4], NULL, 10); + ovpn->asymm_id = strcmp(argv[4], "none"); + + ovpn->lport = strtoul(argv[5], NULL, 10); if (errno == ERANGE || ovpn->lport > 65535) { fprintf(stderr, "lport value out of range\n"); return -1; } - const char *vpnip = (argc > 7) ? argv[7] : NULL; + const char *vpnip = (argc > 8) ? argv[8] : NULL; - ret = ovpn_parse_new_peer(ovpn, argv[3], argv[5], argv[6], - vpnip); + ret = ovpn_parse_new_peer(ovpn, argv[3], argv[4], argv[6], + argv[7], vpnip); if (ret < 0) return -1; break; case CMD_NEW_MULTI_PEER: - if (argc < 5) + if (argc < 6) return -EINVAL; ovpn->lport = strtoul(argv[3], NULL, 10); @@ -2224,7 +2302,25 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[]) return -1; } - ovpn->peers_file = argv[4]; + if (!strcmp(argv[4], "SYMM")) { + ovpn->asymm_id = false; + } else if (!strcmp(argv[4], "ASYMM")) { + ovpn->asymm_id = true; + } else { + fprintf(stderr, "Cannot parse id type: %s\n", argv[4]); + return -1; + } + + ovpn->peers_file = argv[5]; + + ovpn->mark = 0; + if (argc > 6) { + ovpn->mark = strtoul(argv[6], NULL, 10); + if (errno == ERANGE || ovpn->mark > UINT32_MAX) { + fprintf(stderr, "mark value out of range\n"); + return -1; + } + } break; case CMD_SET_PEER: if (argc < 6) @@ -2249,6 +2345,12 @@ static int ovpn_parse_cmd_args(struct ovpn_ctx *ovpn, int argc, char *argv[]) "keepalive interval value out of range\n"); return -1; } + + if (argc > 6) { + ret = ovpn_parse_remote(ovpn, NULL, NULL, argv[6]); + if (ret < 0) + return -1; + } break; case CMD_DEL_PEER: if (argc < 4) diff --git a/tools/testing/selftests/net/ovpn/settings b/tools/testing/selftests/net/ovpn/settings new file mode 100644 index 000000000000..ba4d85f74cd6 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/settings @@ -0,0 +1 @@ +timeout=90 diff --git a/tools/testing/selftests/net/ovpn/tcp_peers.txt b/tools/testing/selftests/net/ovpn/tcp_peers.txt index d753eebe8716..3cb67b560705 100644 --- a/tools/testing/selftests/net/ovpn/tcp_peers.txt +++ b/tools/testing/selftests/net/ovpn/tcp_peers.txt @@ -1,5 +1,6 @@ -1 5.5.5.2 -2 5.5.5.3 -3 5.5.5.4 -4 5.5.5.5 -5 5.5.5.6 +1 10 5.5.5.2 +2 11 5.5.5.3 +3 12 5.5.5.4 +4 13 5.5.5.5 +5 14 5.5.5.6 +6 15 5.5.5.7 diff --git a/tools/testing/selftests/net/ovpn/test-chachapoly.sh b/tools/testing/selftests/net/ovpn/test-chachapoly.sh index 32504079a2b8..cd3d94355d58 100755 --- a/tools/testing/selftests/net/ovpn/test-chachapoly.sh +++ b/tools/testing/selftests/net/ovpn/test-chachapoly.sh @@ -4,6 +4,6 @@ # # Author: Antonio Quartulli <antonio@openvpn.net> -ALG="chachapoly" +OVPN_ALG="chachapoly" source test.sh diff --git a/tools/testing/selftests/net/ovpn/test-close-socket-tcp.sh b/tools/testing/selftests/net/ovpn/test-close-socket-tcp.sh index 093d44772ffd..392d269bada5 100755 --- a/tools/testing/selftests/net/ovpn/test-close-socket-tcp.sh +++ b/tools/testing/selftests/net/ovpn/test-close-socket-tcp.sh @@ -4,6 +4,6 @@ # # Author: Antonio Quartulli <antonio@openvpn.net> -PROTO="TCP" +OVPN_PROTO="TCP" source test-close-socket.sh diff --git a/tools/testing/selftests/net/ovpn/test-close-socket.sh b/tools/testing/selftests/net/ovpn/test-close-socket.sh index 5e48a8b67928..ec9a51bbf3c9 100755 --- a/tools/testing/selftests/net/ovpn/test-close-socket.sh +++ b/tools/testing/selftests/net/ovpn/test-close-socket.sh @@ -5,41 +5,81 @@ # Author: Antonio Quartulli <antonio@openvpn.net> #set -x -set -e +set -eE source ./common.sh -cleanup +ovpn_test_finished=0 -modprobe -q ovpn || true +ovpn_test_exit() { + ovpn_cleanup + modprobe -r ovpn || true + + if [ "${ovpn_test_finished}" -eq 0 ]; then + ktap_print_totals + fi +} + +ovpn_prepare_network() { + local p + local peer_ns + + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "create namespace peer${p}" ovpn_create_ns "${p}" + done -for p in $(seq 0 ${NUM_PEERS}); do - create_ns ${p} -done + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "configure peer${p} namespace" ovpn_setup_ns \ + "${p}" 5.5.5.$((p + 1))/24 + done -for p in $(seq 0 ${NUM_PEERS}); do - setup_ns ${p} 5.5.5.$((${p} + 1))/24 -done + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "register peer${p} in overlay" ovpn_add_peer "${p}" + done -for p in $(seq 0 ${NUM_PEERS}); do - add_peer ${p} -done + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "set peer0 timeout for peer ${p}" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} set_peer tun0 \ + ${p} 60 120 + ovpn_cmd_ok "set peer${p} timeout for peer ${p}" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} set_peer \ + tun${p} $((p + OVPN_ID_OFFSET)) 60 120 + done +} -for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ${OVPN_CLI} set_peer tun0 ${p} 60 120 - ip netns exec peer${p} ${OVPN_CLI} set_peer tun${p} ${p} 60 120 -done +ovpn_run_ping_traffic() { + local p -sleep 1 + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "send ping traffic to peer ${p}" \ + ip netns exec ovpn_peer0 ping -qfc 100 -w 3 \ + 5.5.5.$((p + 1)) + done +} -for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ping -qfc 500 -w 3 5.5.5.$((${p} + 1)) -done +ovpn_run_iperf() { + local iperf_pid -ip netns exec peer0 iperf3 -1 -s & -sleep 1 -ip netns exec peer1 iperf3 -Z -t 3 -c 5.5.5.1 + ovpn_run_bg iperf_pid ip netns exec ovpn_peer0 iperf3 -1 -s + sleep 1 + ovpn_cmd_ok "run iperf throughput flow" \ + ip netns exec ovpn_peer1 iperf3 -Z -t 3 -c 5.5.5.1 + wait "${iperf_pid}" || return 1 +} + +trap ovpn_test_exit EXIT +trap ovpn_stage_err ERR + +ktap_print_header +ktap_set_plan 3 + +ovpn_cleanup +modprobe -q ovpn || true -cleanup +ovpn_run_stage "setup network topology" ovpn_prepare_network +ovpn_run_stage "run ping traffic" ovpn_run_ping_traffic +ovpn_run_stage "run iperf throughput" ovpn_run_iperf -modprobe -r ovpn || true +ovpn_test_finished=1 +ktap_finished diff --git a/tools/testing/selftests/net/ovpn/test-float.sh b/tools/testing/selftests/net/ovpn/test-float.sh index ba5d725e18b0..91f8e113718e 100755 --- a/tools/testing/selftests/net/ovpn/test-float.sh +++ b/tools/testing/selftests/net/ovpn/test-float.sh @@ -4,6 +4,6 @@ # # Author: Antonio Quartulli <antonio@openvpn.net> -FLOAT="1" +OVPN_FLOAT="1" source test.sh diff --git a/tools/testing/selftests/net/ovpn/test-mark.sh b/tools/testing/selftests/net/ovpn/test-mark.sh new file mode 100755 index 000000000000..7c1d56e9c525 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/test-mark.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2020-2025 OpenVPN, Inc. +# +# Author: Ralf Lici <ralf@mandelbit.com> +# Antonio Quartulli <antonio@openvpn.net> + +#set -x +set -eE + +MARK=1056 +MARK_DROP_COUNTER=0 + +source ./common.sh + +ovpn_test_finished=0 + +ovpn_test_exit() { + ovpn_cleanup + modprobe -r ovpn || true + + if [ "${ovpn_test_finished}" -eq 0 ]; then + ktap_print_totals + fi +} + +ovpn_mark_prepare_network() { + local p + local peer_ns + + for p in $(seq 0 "${OVPN_NUM_PEERS}"); do + ovpn_cmd_ok "create namespace peer${p}" ovpn_create_ns "${p}" + done + + for p in $(seq 0 3); do + ovpn_cmd_ok "configure peer${p} namespace" ovpn_setup_ns \ + "${p}" 5.5.5.$((p + 1))/24 + done + + ovpn_cmd_ok "create server-side multi-peer with fwmark" \ + ip netns exec ovpn_peer0 "${OVPN_CLI}" new_multi_peer tun0 1 \ + ASYMM "${OVPN_UDP_PEERS_FILE}" "${MARK}" + for p in $(seq 1 3); do + ovpn_cmd_ok "install server key for peer ${p}" \ + ip netns exec ovpn_peer0 "${OVPN_CLI}" new_key tun0 \ + "${p}" 1 0 "${OVPN_ALG}" 0 data64.key + done + + for p in $(seq 1 3); do + ovpn_cmd_ok "register peer${p} in overlay" ovpn_add_peer "${p}" + done + + for p in $(seq 1 3); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "set peer0 timeout for peer ${p}" \ + ip netns exec ovpn_peer0 "${OVPN_CLI}" set_peer tun0 \ + "${p}" 60 120 + ovpn_cmd_ok "set peer${p} timeout for peer ${p}" \ + ip netns exec "${peer_ns}" "${OVPN_CLI}" set_peer \ + tun"${p}" $((p + OVPN_ID_OFFSET)) 60 120 + done +} + +ovpn_mark_run_baseline_traffic() { + local p + + for p in $(seq 1 3); do + ovpn_cmd_ok "send baseline traffic to peer ${p}" \ + ip netns exec ovpn_peer0 ping -qfc 100 -w 3 \ + 5.5.5.$((p + 1)) + done +} + +ovpn_mark_add_drop_rule() { + ovpn_log "Adding an nftables drop rule based on mark value ${MARK}" + + ovpn_cmd_ok "flush nft ruleset" ip netns exec ovpn_peer0 nft flush \ + ruleset + ovpn_cmd_ok "create nft filter table" ip netns exec ovpn_peer0 nft \ + "add table inet filter" + ovpn_cmd_ok "create nft filter output chain" \ + ip netns exec ovpn_peer0 nft "add chain inet filter output { \ + type filter hook output priority 0; policy accept; }" + ovpn_cmd_ok "add nft drop rule for mark ${MARK}" \ + ip netns exec ovpn_peer0 nft add rule inet filter output \ + meta mark == "${MARK}" \ + counter drop + + MARK_DROP_COUNTER=$(ip netns exec ovpn_peer0 nft list chain inet \ + filter output | sed -n 's/.*packets \([0-9]*\).*/\1/p') + if [ -z "${MARK_DROP_COUNTER}" ]; then + printf '%s\n' "unable to read nft drop counter" + return 1 + fi +} + +ovpn_mark_verify_drop_traffic() { + local p + local ping_output + local lost_packets + local total_count + + for p in $(seq 1 3); do + if ping_output=$(ip netns exec ovpn_peer0 ping -qfc 100 -w 1 \ + 5.5.5.$((p + 1)) 2>&1); then + printf '%s\n' "expected ping to peer ${p} to fail \ + after nft drop rule" + return 1 + fi + ovpn_log "${ping_output}" + lost_packets=$(echo "${ping_output}" | \ + awk '/packets transmitted/ { print $1 }') + if [ -z "${lost_packets}" ]; then + printf '%s\n' "unable to parse lost packets for peer \ + ${p}" + return 1 + fi + MARK_DROP_COUNTER=$((MARK_DROP_COUNTER + lost_packets)) + done + + total_count=$(ip netns exec ovpn_peer0 nft list chain inet filter \ + output | sed -n 's/.*packets \([0-9]*\).*/\1/p') + if [ -z "${total_count}" ]; then + printf '%s\n' "unable to read final nft drop counter" + return 1 + fi + if [ "${MARK_DROP_COUNTER}" -ne "${total_count}" ]; then + printf '%s\n' "expected ${MARK_DROP_COUNTER} drops, got \ + ${total_count}" + return 1 + fi +} + +ovpn_mark_remove_drop_rule() { + ovpn_log "Removing the drop rule" + + ovpn_cmd_ok "flush nft ruleset" ip netns exec ovpn_peer0 nft flush \ + ruleset +} + +ovpn_mark_verify_traffic_recovery() { + local p + + sleep 1 + for p in $(seq 1 3); do + ovpn_cmd_ok "send recovery traffic to peer ${p}" \ + ip netns exec ovpn_peer0 ping -qfc 100 -w 3 \ + 5.5.5.$((p + 1)) + done +} + +trap ovpn_test_exit EXIT +trap ovpn_stage_err ERR + +ktap_print_header +ktap_set_plan 6 + +ovpn_cleanup +modprobe -q ovpn || true + +ovpn_run_stage "setup marked network topology" ovpn_mark_prepare_network +ovpn_run_stage "run baseline traffic" ovpn_mark_run_baseline_traffic +ovpn_run_stage "install nft mark drop rule" ovpn_mark_add_drop_rule +ovpn_run_stage "drop marked traffic and count packets" \ + ovpn_mark_verify_drop_traffic +ovpn_run_stage "remove nft drop rule" ovpn_mark_remove_drop_rule +ovpn_run_stage "traffic recovers after drop removal" \ + ovpn_mark_verify_traffic_recovery + +ovpn_test_finished=1 +ktap_finished diff --git a/tools/testing/selftests/net/ovpn/test-symmetric-id-float.sh b/tools/testing/selftests/net/ovpn/test-symmetric-id-float.sh new file mode 100755 index 000000000000..75296fe72c39 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/test-symmetric-id-float.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2025 OpenVPN, Inc. +# +# Author: Ralf Lici <ralf@mandelbit.com> +# Antonio Quartulli <antonio@openvpn.net> + +OVPN_SYMMETRIC_ID="1" +OVPN_FLOAT="1" + +source test.sh diff --git a/tools/testing/selftests/net/ovpn/test-symmetric-id-tcp.sh b/tools/testing/selftests/net/ovpn/test-symmetric-id-tcp.sh new file mode 100755 index 000000000000..680a465c49d2 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/test-symmetric-id-tcp.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2025 OpenVPN, Inc. +# +# Author: Ralf Lici <ralf@mandelbit.com> +# Antonio Quartulli <antonio@openvpn.net> + +OVPN_PROTO="TCP" +OVPN_SYMMETRIC_ID=1 + +source test.sh diff --git a/tools/testing/selftests/net/ovpn/test-symmetric-id.sh b/tools/testing/selftests/net/ovpn/test-symmetric-id.sh new file mode 100755 index 000000000000..a2e2808959d9 --- /dev/null +++ b/tools/testing/selftests/net/ovpn/test-symmetric-id.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2025 OpenVPN, Inc. +# +# Author: Ralf Lici <ralf@mandelbit.com> +# Antonio Quartulli <antonio@openvpn.net> + +OVPN_SYMMETRIC_ID="1" + +source test.sh diff --git a/tools/testing/selftests/net/ovpn/test-tcp.sh b/tools/testing/selftests/net/ovpn/test-tcp.sh index ba3f1f315a34..27cc6e7b98bc 100755 --- a/tools/testing/selftests/net/ovpn/test-tcp.sh +++ b/tools/testing/selftests/net/ovpn/test-tcp.sh @@ -4,6 +4,6 @@ # # Author: Antonio Quartulli <antonio@openvpn.net> -PROTO="TCP" +OVPN_PROTO="TCP" source test.sh diff --git a/tools/testing/selftests/net/ovpn/test.sh b/tools/testing/selftests/net/ovpn/test.sh index e8acdc303307..392109d5e14e 100755 --- a/tools/testing/selftests/net/ovpn/test.sh +++ b/tools/testing/selftests/net/ovpn/test.sh @@ -5,113 +5,387 @@ # Author: Antonio Quartulli <antonio@openvpn.net> #set -x -set -e +set -eE source ./common.sh -cleanup +ovpn_test_finished=0 -modprobe -q ovpn || true +ovpn_test_exit() { + ovpn_cleanup + modprobe -r ovpn || true + + if [ "${ovpn_test_finished}" -eq 0 ]; then + ktap_print_totals + fi +} + +ovpn_prepare_network() { + local p + local peer_ns + + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "create namespace peer${p}" ovpn_create_ns "${p}" + done + + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "start notification listener peer${p}" \ + ovpn_setup_listener "${p}" + # starting all YNL listeners back-to-back can intermittently + # stall their startup so serialize launches a bit + sleep 0.5 + done + + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "configure peer${p} namespace" ovpn_setup_ns \ + "${p}" 5.5.5.$((p + 1))/24 "${MTU}" + done + + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "register peer${p} in overlay" ovpn_add_peer "${p}" + done + + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "set peer0 timeout for peer ${p}" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} set_peer tun0 \ + ${p} 60 120 + ovpn_cmd_ok "set peer${p} timeout for peer ${p}" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} set_peer \ + tun${p} $((p + OVPN_ID_OFFSET)) 60 120 + done +} + +ovpn_new_test_peer() { + local peer_id="$1" + + shift + ip netns exec ovpn_peer0 "${OVPN_CLI}" new_peer tun0 \ + "${peer_id}" none 65000 10.10.1.2 1 "$@" +} + +ovpn_set_peer_vpn_addr() { + ip netns exec ovpn_peer0 "${OVPN_CLI}" set_peer tun0 \ + "$1" 60 120 "$2" +} + +ovpn_run_vpn_addr_validation() { + local addr + local peer1_addr4 + local test_peer_id=$((OVPN_NUM_PEERS + 1)) + local test_peer_addr6="2001:db8::2" + # Do not include 0.0.0.0 or :: here. They are invalid on creation, but + # clear one address family on update and are valid if the other remains. + local -a invalid_addrs=( + "127.0.0.1" + "224.0.0.1" + "255.255.255.255" + "::1" + "::192.0.2.1" + "::ffff:192.0.2.1" + "ff02::1" + ) + + peer1_addr4=$(ovpn_peer_vpn_addr 1) -for p in $(seq 0 ${NUM_PEERS}); do - create_ns ${p} -done + ovpn_cmd_fail "reject peer without VPN address" \ + ovpn_new_test_peer "${test_peer_id}" -for p in $(seq 0 ${NUM_PEERS}); do - setup_ns ${p} 5.5.5.$((${p} + 1))/24 ${MTU} -done + for addr in "0.0.0.0" "::" "${invalid_addrs[@]}"; do + ovpn_cmd_fail "reject new peer VPN address ${addr}" \ + ovpn_new_test_peer "${test_peer_id}" "${addr}" + done + + ovpn_cmd_fail "reject duplicate IPv4 address on peer creation" \ + ovpn_new_test_peer "${test_peer_id}" "${peer1_addr4}" + ovpn_cmd_fail "reject clearing the last peer VPN address" \ + ovpn_set_peer_vpn_addr 1 0.0.0.0 + + for addr in "${invalid_addrs[@]}"; do + ovpn_cmd_fail "reject updated peer VPN address ${addr}" \ + ovpn_set_peer_vpn_addr 1 "${addr}" + done + + ovpn_cmd_fail "reject duplicate IPv4 address on peer update" \ + ovpn_set_peer_vpn_addr 2 "${peer1_addr4}" + + ovpn_cmd_ok "add peer IPv6 address" \ + ovpn_set_peer_vpn_addr 1 "${test_peer_addr6}" + ovpn_cmd_fail "reject duplicate IPv6 address on peer creation" \ + ovpn_new_test_peer "${test_peer_id}" "${test_peer_addr6}" + ovpn_cmd_fail "reject duplicate IPv6 address on peer update" \ + ovpn_set_peer_vpn_addr 2 "${test_peer_addr6}" + + ovpn_cmd_ok "clear peer IPv4 address" \ + ovpn_set_peer_vpn_addr 1 0.0.0.0 + ovpn_cmd_fail "reject clearing the remaining peer IPv6 address" \ + ovpn_set_peer_vpn_addr 1 :: + ovpn_cmd_ok "restore peer IPv4 address" \ + ovpn_set_peer_vpn_addr 1 "${peer1_addr4}" + ovpn_cmd_ok "clear peer IPv6 address" \ + ovpn_set_peer_vpn_addr 1 :: +} + +ovpn_run_basic_traffic() { + local p + local header1 + local header2 + local peer_ns + local raddr + local tcpdump_pid1 + local tcpdump_pid2 + local tcpdump_timeout="1.5s" + + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + # The first part of the data packet header consists of: + # - TCP only: 2 bytes for the packet length + # - 5 bits for opcode ("9" for DATA_V2) + # - 3 bits for key-id ("0" at this point) + # - 12 bytes for peer-id: + # - with asymmetric ID: "${p}" one way and "${p} + 9" the + # other way + # - with symmetric ID: "${p}" both ways + header1=$(printf "0x4800000%x" ${p}) + header2=$(printf "0x4800000%x" $((p + OVPN_ID_OFFSET))) + raddr="" + if [ "${OVPN_PROTO}" == "UDP" ]; then + raddr=$(awk "NR == ${p} {print \$3}" \ + "${OVPN_UDP_PEERS_FILE}") + fi + peer_ns="ovpn_peer${p}" -for p in $(seq 0 ${NUM_PEERS}); do - add_peer ${p} -done + timeout ${tcpdump_timeout} ip netns exec "${peer_ns}" \ + tcpdump --immediate-mode -p -ni veth${p} -c 1 \ + "$(ovpn_build_capture_filter "${header1}" "${raddr}")" \ + >/dev/null 2>&1 & + tcpdump_pid1=$! + timeout ${tcpdump_timeout} ip netns exec "${peer_ns}" \ + tcpdump --immediate-mode -p -ni veth${p} -c 1 \ + "$(ovpn_build_capture_filter "${header2}" "${raddr}")" \ + >/dev/null 2>&1 & + tcpdump_pid2=$! -for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ${OVPN_CLI} set_peer tun0 ${p} 60 120 - ip netns exec peer${p} ${OVPN_CLI} set_peer tun${p} ${p} 60 120 -done + sleep 0.3 + ovpn_cmd_ok "send baseline traffic to peer ${p}" \ + ip netns exec ovpn_peer0 \ + ping -qfc 100 -w 3 5.5.5.$((p + 1)) + ovpn_cmd_ok "send large-payload traffic to peer ${p}" \ + ip netns exec ovpn_peer0 \ + ping -qfc 100 -s 3000 -w 3 5.5.5.$((p + 1)) -sleep 1 + wait "${tcpdump_pid1}" || return 1 + wait "${tcpdump_pid2}" || return 1 + done +} -for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ping -qfc 500 -w 3 5.5.5.$((${p} + 1)) - ip netns exec peer0 ping -qfc 500 -s 3000 -w 3 5.5.5.$((${p} + 1)) -done +ovpn_run_lan_traffic() { + ovpn_cmd_ok "ping LAN behind peer1" \ + ip netns exec ovpn_peer0 ping -qfc 100 -w 3 "${OVPN_LAN_IP}" +} -# ping LAN behind client 1 -ip netns exec peer0 ping -qfc 500 -w 3 ${LAN_IP} +ovpn_run_float_mode() { + local p + local peer_ns -if [ "$FLOAT" == "1" ]; then - # make clients float.. - for p in $(seq 1 ${NUM_PEERS}); do - ip -n peer${p} addr del 10.10.${p}.2/24 dev veth${p} - ip -n peer${p} addr add 10.10.${p}.3/24 dev veth${p} + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "float: remove old transport address on peer${p}" \ + ip -n "${peer_ns}" addr del 10.10.${p}.2/24 dev veth${p} + ovpn_cmd_ok "float: add new transport address on peer${p}" \ + ip -n "${peer_ns}" addr add 10.10.${p}.3/24 dev veth${p} done - for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer${p} ping -qfc 500 -w 3 5.5.5.1 + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "ping tunnel after float peer ${p}" \ + ip netns exec "${peer_ns}" ping -qfc 100 -w 3 5.5.5.1 done +} + +ovpn_run_iperf() { + local iperf_pid + + ovpn_run_bg iperf_pid ip netns exec ovpn_peer0 iperf3 -1 -s + sleep 1 + + ovpn_cmd_ok "run iperf throughput flow" \ + ip netns exec ovpn_peer1 iperf3 -Z -t 3 -c 5.5.5.1 + wait "${iperf_pid}" || return 1 +} + +ovpn_run_key_rollover() { + local p + local peer_ns + + ovpn_log "Adding secondary key and then swap:" + + for p in $(seq 1 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "add secondary key on peer0 for peer ${p}" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} new_key tun0 \ + ${p} 2 1 ${OVPN_ALG} 0 data64.key + ovpn_cmd_ok "add secondary key on peer${p} for peer ${p}" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${p} \ + $((p + OVPN_ID_OFFSET)) 2 1 ${OVPN_ALG} 1 \ + data64.key + ovpn_cmd_ok "swap keys on peer${p}" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} swap_keys \ + tun${p} $((p + OVPN_ID_OFFSET)) + done +} + +ovpn_run_queries() { + ovpn_log "Querying all peers:" + + ovpn_cmd_ok "query all peers from peer0" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} get_peer tun0 + ovpn_cmd_ok "query all peers from peer1" \ + ip netns exec ovpn_peer1 ${OVPN_CLI} get_peer tun1 + + ovpn_log "Querying peer 1:" + + ovpn_cmd_ok "query peer 1 from peer0" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} get_peer tun0 1 +} + +ovpn_query_peer_missing() { + ovpn_log "Querying non-existent peer 20:" + + ovpn_cmd_fail "query missing peer 20 on peer0" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} get_peer tun0 20 +} + +ovpn_run_peer_cleanup() { + local p + local peer_ns + + ovpn_log "Deleting peer 1:" + + ovpn_cmd_ok "delete peer1 on peer0" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} del_peer tun0 1 + ovpn_cmd_ok "delete peer1 on peer1" \ + ip netns exec ovpn_peer1 ${OVPN_CLI} del_peer tun1 \ + $((1 + OVPN_ID_OFFSET)) + + ovpn_log "Querying keys:" + + for p in $(seq 2 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "query peer${p} key 1" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} get_key tun${p} \ + $((p + OVPN_ID_OFFSET)) 1 + ovpn_cmd_ok "query peer${p} key 2" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} get_key tun${p} \ + $((p + OVPN_ID_OFFSET)) 2 + done +} + +ovpn_run_traffic_delete_peer() { + local ping_pid + + ovpn_log "Deleting peer while sending traffic:" + + ovpn_run_bg ping_pid ip netns exec ovpn_peer2 ping -qf -w 4 5.5.5.1 + sleep 2 + ovpn_cmd_ok "delete peer0 peer 2" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} del_peer tun0 2 + + if [ "${OVPN_PROTO}" == "TCP" ]; then + # In TCP mode this command is expected to fail for both peers. + ovpn_cmd_mayfail "delete peer2 peer 2 (TCP non-fatal)" \ + ip netns exec ovpn_peer2 ${OVPN_CLI} del_peer tun2 \ + $((2 + OVPN_ID_OFFSET)) + else + ovpn_cmd_ok "delete peer2 peer 2" ip netns exec ovpn_peer2 \ + ${OVPN_CLI} del_peer tun2 $((2 + OVPN_ID_OFFSET)) + fi + + wait "${ping_pid}" || true +} + +ovpn_run_key_cleanup() { + local p + local peer_ns + + ovpn_log "Deleting keys:" + + for p in $(seq 3 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "delete key 1 for peer${p}" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} del_key tun${p} \ + $((p + OVPN_ID_OFFSET)) 1 + ovpn_cmd_ok "delete key 2 for peer${p}" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} del_key tun${p} \ + $((p + OVPN_ID_OFFSET)) 2 + done +} + +ovpn_run_timeouts() { + local p + local peer_ns + + ovpn_log "Setting timeout to 3s MP:" + + for p in $(seq 3 ${OVPN_NUM_PEERS}); do + # Non-fatal: this may fail in some protocol modes. + ovpn_cmd_mayfail "set peer0 timeout for peer ${p} (non-fatal)" \ + ip netns exec ovpn_peer0 ${OVPN_CLI} set_peer tun0 \ + ${p} 3 3 + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "disable timeout on peer${p} while peer0 adjusts \ + state" ip netns exec "${peer_ns}" ${OVPN_CLI} set_peer \ + tun${p} $((p + OVPN_ID_OFFSET)) 0 0 + done + # wait for peers to timeout + sleep 5 + + ovpn_log "Setting timeout to 3s P2P:" + + for p in $(seq 3 ${OVPN_NUM_PEERS}); do + peer_ns="ovpn_peer${p}" + ovpn_cmd_ok "set peer${p} P2P timeout" \ + ip netns exec "${peer_ns}" ${OVPN_CLI} set_peer \ + tun${p} $((p + OVPN_ID_OFFSET)) 3 3 + done + sleep 5 +} + +ovpn_run_notifications() { + local p + + for p in $(seq 0 ${OVPN_NUM_PEERS}); do + ovpn_cmd_ok "validate listener output for peer ${p}" \ + ovpn_compare_ntfs "${p}" + done +} + +trap ovpn_test_exit EXIT +trap ovpn_stage_err ERR + +ktap_print_header +if [ "${OVPN_FLOAT}" == "1" ]; then + ktap_set_plan 14 +else + ktap_set_plan 13 fi -ip netns exec peer0 iperf3 -1 -s & -sleep 1 -ip netns exec peer1 iperf3 -Z -t 3 -c 5.5.5.1 - -echo "Adding secondary key and then swap:" -for p in $(seq 1 ${NUM_PEERS}); do - ip netns exec peer0 ${OVPN_CLI} new_key tun0 ${p} 2 1 ${ALG} 0 data64.key - ip netns exec peer${p} ${OVPN_CLI} new_key tun${p} ${p} 2 1 ${ALG} 1 data64.key - ip netns exec peer${p} ${OVPN_CLI} swap_keys tun${p} ${p} -done - -sleep 1 - -echo "Querying all peers:" -ip netns exec peer0 ${OVPN_CLI} get_peer tun0 -ip netns exec peer1 ${OVPN_CLI} get_peer tun1 - -echo "Querying peer 1:" -ip netns exec peer0 ${OVPN_CLI} get_peer tun0 1 - -echo "Querying non-existent peer 10:" -ip netns exec peer0 ${OVPN_CLI} get_peer tun0 10 || true - -echo "Deleting peer 1:" -ip netns exec peer0 ${OVPN_CLI} del_peer tun0 1 -ip netns exec peer1 ${OVPN_CLI} del_peer tun1 1 - -echo "Querying keys:" -for p in $(seq 2 ${NUM_PEERS}); do - ip netns exec peer${p} ${OVPN_CLI} get_key tun${p} ${p} 1 - ip netns exec peer${p} ${OVPN_CLI} get_key tun${p} ${p} 2 -done - -echo "Deleting peer while sending traffic:" -(ip netns exec peer2 ping -qf -w 4 5.5.5.1)& -sleep 2 -ip netns exec peer0 ${OVPN_CLI} del_peer tun0 2 -# following command fails in TCP mode -# (both ends get conn reset when one peer disconnects) -ip netns exec peer2 ${OVPN_CLI} del_peer tun2 2 || true - -echo "Deleting keys:" -for p in $(seq 3 ${NUM_PEERS}); do - ip netns exec peer${p} ${OVPN_CLI} del_key tun${p} ${p} 1 - ip netns exec peer${p} ${OVPN_CLI} del_key tun${p} ${p} 2 -done - -echo "Setting timeout to 3s MP:" -for p in $(seq 3 ${NUM_PEERS}); do - ip netns exec peer0 ${OVPN_CLI} set_peer tun0 ${p} 3 3 || true - ip netns exec peer${p} ${OVPN_CLI} set_peer tun${p} ${p} 0 0 -done -# wait for peers to timeout -sleep 5 - -echo "Setting timeout to 3s P2P:" -for p in $(seq 3 ${NUM_PEERS}); do - ip netns exec peer${p} ${OVPN_CLI} set_peer tun${p} ${p} 3 3 -done -sleep 5 - -cleanup - -modprobe -r ovpn || true +ovpn_cleanup +modprobe -q ovpn || true + +ovpn_run_stage "setup network topology" ovpn_prepare_network +ovpn_run_stage "validate peer VPN addresses" ovpn_run_vpn_addr_validation +ovpn_run_stage "run baseline data traffic" ovpn_run_basic_traffic +ovpn_run_stage "run LAN traffic behind peer1" ovpn_run_lan_traffic +[ "${OVPN_FLOAT}" == "1" ] && ovpn_run_stage "run floating peer checks" \ + ovpn_run_float_mode +ovpn_run_stage "run iperf throughput" ovpn_run_iperf +ovpn_run_stage "run key rollout" ovpn_run_key_rollover +ovpn_run_stage "query peers" ovpn_run_queries +ovpn_run_stage "query missing peer fails" ovpn_query_peer_missing +ovpn_run_stage "peer lifecycle and key queries" ovpn_run_peer_cleanup +ovpn_run_stage "delete peer while traffic" ovpn_run_traffic_delete_peer +ovpn_run_stage "delete stale keys" ovpn_run_key_cleanup +ovpn_run_stage "check timeout behavior" ovpn_run_timeouts +ovpn_run_stage "validate notification output" ovpn_run_notifications + +ovpn_test_finished=1 +ktap_finished diff --git a/tools/testing/selftests/net/ovpn/udp_peers.txt b/tools/testing/selftests/net/ovpn/udp_peers.txt index e9773ddf875c..93de6465353c 100644 --- a/tools/testing/selftests/net/ovpn/udp_peers.txt +++ b/tools/testing/selftests/net/ovpn/udp_peers.txt @@ -1,6 +1,6 @@ -1 10.10.1.1 1 10.10.1.2 1 5.5.5.2 -2 10.10.2.1 1 10.10.2.2 1 5.5.5.3 -3 10.10.3.1 1 10.10.3.2 1 5.5.5.4 -4 fd00:0:0:4::1 1 fd00:0:0:4::2 1 5.5.5.5 -5 fd00:0:0:5::1 1 fd00:0:0:5::2 1 5.5.5.6 -6 fd00:0:0:6::1 1 fd00:0:0:6::2 1 5.5.5.7 +1 10 10.10.1.1 1 10.10.1.2 1 5.5.5.2 +2 11 10.10.2.1 1 10.10.2.2 1 5.5.5.3 +3 12 10.10.3.1 1 10.10.3.2 1 5.5.5.4 +4 13 fd00:0:0:4::1 1 fd00:0:0:4::2 1 5.5.5.5 +5 14 fd00:0:0:5::1 1 fd00:0:0:5::2 1 5.5.5.6 +6 15 fd00:0:0:6::1 1 fd00:0:0:6::2 1 5.5.5.7 diff --git a/tools/testing/selftests/net/packetdrill/config b/tools/testing/selftests/net/packetdrill/config index c4a19a785521..b7df8bf30920 100644 --- a/tools/testing/selftests/net/packetdrill/config +++ b/tools/testing/selftests/net/packetdrill/config @@ -4,8 +4,8 @@ CONFIG_IPV6=y CONFIG_NET_NS=y CONFIG_NET_SCH_FIFO=y CONFIG_NET_SCH_FQ=y -CONFIG_PROC_SYSCTL=y CONFIG_SYN_COOKIES=y +CONFIG_SYSCTL=y CONFIG_TCP_CONG_CUBIC=y CONFIG_TCP_MD5SIG=y CONFIG_TUN=y diff --git a/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv4.pkt b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv4.pkt new file mode 100644 index 000000000000..f2ef931b77a1 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv4.pkt @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Test that IPv4 advertised MSS in SYN-ACK is derived from the configured +// interface MTU (1500 -> MSS 1460), not the ICMP-learned Path MTU. + +--ip_version=ipv4 + +`./defaults.sh +ethtool -K tun0 tso off +` + +// +// Connection 1: Learn PMTU exception (MTU 1200 -> MSS 1160) +// + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8> + +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8> + +.1 < . 1:1(0) ack 1 win 257 + +0 accept(3, ..., ...) = 4 + +// Send a full 1460-byte segment + +0 write(4, ..., 1460) = 1460 + +0 > P. 1:1461(1460) ack 1 + +// ICMP Fragmentation Needed arrives indicating next-hop MTU 1200 + +0 < icmp unreachable frag_needed mtu 1200 [1:1461(1460)] + +// Local host retransmits using the learned MTU 1200 (MSS = 1200 - 40 = 1160) + +0 > . 1:1161(1160) ack 1 + +0 > P. 1161:1461(300) ack 1 + +0 < R 1:1(0) ack 1461 win 0 + +// Close connection 1 and listener + +0 close(4) = 0 + +0 close(3) = 0 + +// +// Connection 2: New connection from the same peer +// + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8> + +// Verify: SYN-ACK MUST advertise configured MSS 1460, NOT the learned PMTU MSS 1160 + +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8> + +0 < . 1:1(0) ack 1 win 257 + +0 accept(3, ..., ...) = 4 + +// Verify: Outgoing transmit MSS is still constrained by the learned PMTU 1200 + +0 write(4, ..., 1460) = 1460 + +0 > . 1:1161(1160) ack 1 + +0 > P. 1161:1461(300) ack 1 + +0 < . 1:1(0) ack 1461 win 257 + +// Clean up + +0 close(4) = 0 + +0 > F. 1461:1461(0) ack 1 + +0 < F. 1:1(0) ack 1462 win 257 + +0 > . 1462:1462(0) ack 2 + +0 close(3) = 0 diff --git a/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv6.pkt b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv6.pkt new file mode 100644 index 000000000000..c7638b11a815 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_advmss_pmtu_ipv6.pkt @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Test that IPv6 advertised MSS in SYN-ACK is derived from the configured +// interface MTU (1520 -> MSS 1460), not the ICMPv6-learned Path MTU. + +--ip_version=ipv6 + +`./defaults.sh +ethtool -K tun0 tso off +` + +// +// Connection 1: Learn PMTU exception (MTU 1280 -> MSS 1220) +// + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8> + +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8> + +.1 < . 1:1(0) ack 1 win 257 + +0 accept(3, ..., ...) = 4 + +// Send a full 1460-byte segment + +0 write(4, ..., 1460) = 1460 + +0 > P. 1:1461(1460) ack 1 + +// ICMPv6 Packet Too Big arrives indicating next-hop MTU 1280 + +0 < icmp packet_too_big mtu 1280 [1:1461(1460)] + +// Local host retransmits using the learned MTU 1280 (MSS = 1280 - 40 - 20 = 1220) + +0 > . 1:1221(1220) ack 1 + +0 > P. 1221:1461(240) ack 1 + +0 < R 1:1(0) ack 1461 win 0 + +// Close connection 1 and listener + +0 close(4) = 0 + +0 close(3) = 0 + +// +// Connection 2: New connection from the same peer +// + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 65535 <mss 1460,sackOK,nop,nop,nop,wscale 8> + +// Verify: SYN-ACK MUST advertise configured MSS 1460, NOT the learned PMTU MSS 1220 + +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8> + +0 < . 1:1(0) ack 1 win 257 + +0 accept(3, ..., ...) = 4 + +// Verify: Outgoing transmit MSS is still constrained by the learned PMTU 1280 + +0 write(4, ..., 1460) = 1460 + +0 > . 1:1221(1220) ack 1 + +0 > P. 1221:1461(240) ack 1 + +0 < . 1:1(0) ack 1461 win 257 + +// Clean up + +0 close(4) = 0 + +0 > F. 1461:1461(0) ack 1 + +0 < F. 1:1(0) ack 1462 win 257 + +0 > . 1462:1462(0) ack 2 + +0 close(3) = 0 diff --git a/tools/testing/selftests/net/packetdrill/tcp_disorder_fin_in_FIN_WAIT.pkt b/tools/testing/selftests/net/packetdrill/tcp_disorder_fin_in_FIN_WAIT.pkt new file mode 100644 index 000000000000..336cbf7815c8 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_disorder_fin_in_FIN_WAIT.pkt @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0 + +// Check fix in 795a7dfbc3d9 ("net: tcp: accept old ack during closing") + +// Set up config. +`./defaults.sh` + +// Initialize a server socket. + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 65535 <mss 1000,sackOK,nop,nop,nop,wscale 7> + * > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8> + +0 < . 1:1(0) ack 1 win 257 + + +0 accept(3, ..., ...) = 4 + + +0 shutdown(4, SHUT_WR) = 0 + * > F. 1:1(0) ack 1 + +// We expect to receive one ACK. +// But what happens if a FIN was already in transmt and received out-of-order ? + + +0 < . 2:2(0) ack 2 win 257 + +// This FIN packet was sent before the prior ACK (see ack 1). + +0 < F. 1:1(0) ack 1 win 257 + +// Even if the FIN is received out-of-order, we should ACK it. + + * > . 2:2(0) ack 2 diff --git a/tools/testing/selftests/net/packetdrill/tcp_rcv_big_endseq.pkt b/tools/testing/selftests/net/packetdrill/tcp_rcv_big_endseq.pkt index 3848b419e68c..12882be10f2e 100644 --- a/tools/testing/selftests/net/packetdrill/tcp_rcv_big_endseq.pkt +++ b/tools/testing/selftests/net/packetdrill/tcp_rcv_big_endseq.pkt @@ -36,9 +36,9 @@ +0 read(4, ..., 100000) = 4000 -// If queue is empty, accept a packet even if its end_seq is above wup + rcv_wnd +// If queue is empty, accept a packet even if its end_seq is above rcv_mwnd_seq +0 < P. 4001:54001(50000) ack 1 win 257 - +0 > . 1:1(0) ack 54001 win 0 + * > . 1:1(0) ack 54001 win 0 // Check LINUX_MIB_BEYOND_WINDOW has been incremented 3 times. +0 `nstat | grep TcpExtBeyondWindow | grep -q " 3 "` diff --git a/tools/testing/selftests/net/packetdrill/tcp_rcv_neg_window.pkt b/tools/testing/selftests/net/packetdrill/tcp_rcv_neg_window.pkt new file mode 100644 index 000000000000..b9ab264b2a11 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rcv_neg_window.pkt @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0 +// Test maximum advertised window limit when rcv_nxt advances past +// rcv_mwnd_seq. The "usable window" must be properly clamped to zero +// rather than becoming negative. + +--mss=1000 + +`./defaults.sh` + +// Establish a connection. + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [20000], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 32792 <mss 1000,nop,wscale 7> + +0 > S. 0:0(0) ack 1 win 18980 <mss 1460,nop,wscale 0> + +.1 < . 1:1(0) ack 1 win 257 + + +0 accept(3, ..., ...) = 4 + +// A too big packet is accepted if the receive queue is empty. It +// does not trigger an immediate ACK. + +0 < P. 1:20001(20000) ack 1 win 257 + +0 %{ assert tcpi_bytes_received == 20000, tcpi_bytes_received; }% + +// Send a RST immediately so that there is no rcv_wup/rcv_mwnd_seq update yet + +0 < R. 20001:20001(0) ack 1 win 257 + +// Verify that the RST was accepted. Indirectly this also verifies that no +// immediate ACK was sent for the data packet above. + +0 < . 20001:20001(0) ack 1 win 257 + +0 > R 1:1(0) diff --git a/tools/testing/selftests/net/packetdrill/tcp_rcv_toobig.pkt b/tools/testing/selftests/net/packetdrill/tcp_rcv_toobig.pkt deleted file mode 100644 index f575c0ff89da..000000000000 --- a/tools/testing/selftests/net/packetdrill/tcp_rcv_toobig.pkt +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 - ---mss=1000 - -`./defaults.sh` - - 0 `nstat -n` - -// Establish a connection. - +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 - +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 - +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [20000], 4) = 0 - +0 bind(3, ..., ...) = 0 - +0 listen(3, 1) = 0 - - +0 < S 0:0(0) win 32792 <mss 1000,nop,wscale 7> - +0 > S. 0:0(0) ack 1 win 18980 <mss 1460,nop,wscale 0> - +.1 < . 1:1(0) ack 1 win 257 - - +0 accept(3, ..., ...) = 4 - - +0 < P. 1:20001(20000) ack 1 win 257 - +.04 > . 1:1(0) ack 20001 win 18000 - - +0 setsockopt(4, SOL_SOCKET, SO_RCVBUF, [12000], 4) = 0 - +0 < P. 20001:80001(60000) ack 1 win 257 - +0 > . 1:1(0) ack 20001 win 18000 - - +0 read(4, ..., 20000) = 20000 -// A too big packet is accepted if the receive queue is empty - +0 < P. 20001:80001(60000) ack 1 win 257 - +0 > . 1:1(0) ack 80001 win 0 - diff --git a/tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_allowed.pkt b/tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_allowed.pkt new file mode 100644 index 000000000000..6af0e0eb183a --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_allowed.pkt @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0 + +--mss=1000 + +`./defaults.sh +sysctl -q net.ipv4.tcp_shrink_window=1 +sysctl -q net.ipv4.tcp_rmem="4096 32768 $((32*1024*1024))"` + + 0 `nstat -n` + +// Establish a connection. + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 32792 <mss 1000,nop,wscale 7> + +0 > S. 0:0(0) ack 1 <mss 1460,nop,wscale 10> + +0 < . 1:1(0) ack 1 win 257 + + +0 accept(3, ..., ...) = 4 + + +0 < P. 1:10001(10000) ack 1 win 257 + * > . 1:1(0) ack 10001 win 15 + + +0 < P. 10001:11024(1023) ack 1 win 257 + * > . 1:1(0) ack 11024 win 13 + +// Max window seq advertised 10001 + 15*1024 = 25361, last advertised: 11024 + 13*1024 = 24336 + +// Segment beyond the max window is dropped + +0 < P. 11024:25362(14338) ack 1 win 257 + * > . 1:1(0) ack 11024 win 13 + +// Segment using the max window is accepted + +0 < P. 11024:25361(14337) ack 1 win 257 + * > . 1:1(0) ack 25361 win 0 + +// Check LINUX_MIB_BEYOND_WINDOW has been incremented once + +0 `nstat | grep TcpExtBeyondWindow | grep -q " 1 "` diff --git a/tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_nomem.pkt b/tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_nomem.pkt new file mode 100644 index 000000000000..a80eb55dc69a --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rcv_wnd_shrink_nomem.pkt @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-2.0 +// When tcp_receive_window() < tcp_max_receive_window(), tcp_sequence() accepts +// packets that would be dropped under normal conditions (i.e. tcp_receive_window() +// equal to tcp_max_receive_window()). +// Test that such packets are handled as expected for RWIN == 0 and for RWIN > 0. + +--mss=1000 + +`./defaults.sh` + + 0 `nstat -n` + +// Establish a connection. + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 setsockopt(3, SOL_SOCKET, SO_RCVBUF, [1000000], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + + +0 < S 0:0(0) win 32792 <mss 1000,nop,nop,sackOK,nop,wscale 7> + +0 > S. 0:0(0) ack 1 win 65535 <mss 1460,nop,nop,sackOK,nop,wscale 4> + +0 < . 1:1(0) ack 1 win 257 + + +0 accept(3, ..., ...) = 4 + +// Put 1040000 bytes into the receive buffer + +0 < P. 1:65001(65000) ack 1 win 257 + * > . 1:1(0) ack 65001 + +0 < P. 65001:130001(65000) ack 1 win 257 + * > . 1:1(0) ack 130001 + +0 < P. 130001:195001(65000) ack 1 win 257 + * > . 1:1(0) ack 195001 + +0 < P. 195001:260001(65000) ack 1 win 257 + * > . 1:1(0) ack 260001 + +0 < P. 260001:325001(65000) ack 1 win 257 + * > . 1:1(0) ack 325001 + +0 < P. 325001:390001(65000) ack 1 win 257 + * > . 1:1(0) ack 390001 + +0 < P. 390001:455001(65000) ack 1 win 257 + * > . 1:1(0) ack 455001 + +0 < P. 455001:520001(65000) ack 1 win 257 + * > . 1:1(0) ack 520001 + +0 < P. 520001:585001(65000) ack 1 win 257 + * > . 1:1(0) ack 585001 + +0 < P. 585001:650001(65000) ack 1 win 257 + * > . 1:1(0) ack 650001 + +0 < P. 650001:715001(65000) ack 1 win 257 + * > . 1:1(0) ack 715001 + +0 < P. 715001:780001(65000) ack 1 win 257 + * > . 1:1(0) ack 780001 + +0 < P. 780001:845001(65000) ack 1 win 257 + * > . 1:1(0) ack 845001 + +0 < P. 845001:910001(65000) ack 1 win 257 + * > . 1:1(0) ack 910001 + +0 < P. 910001:975001(65000) ack 1 win 257 + * > . 1:1(0) ack 975001 + +0 < P. 975001:1040001(65000) ack 1 win 257 + * > . 1:1(0) ack 1040001 + +// Trigger an extreme memory squeeze by shrinking SO_RCVBUF + +0 setsockopt(4, SOL_SOCKET, SO_RCVBUF, [16000], 4) = 0 + + +0 < P. 1040001:1105001(65000) ack 1 win 257 + * > . 1:1(0) ack 1040001 win 0 +// Check LINUX_MIB_TCPRCVQDROP has been incremented + +0 `nstat -s | grep TcpExtTCPRcvQDrop| grep -q " 1 "` + +// RWIN == 0: rcv_wup = 1040001, rcv_wnd = 0, rcv_mwnd_seq > 1105001 (significantly larger, typically ~1970000) + +// Accept pure ack with seq in max adv. window + +0 write(4, ..., 1000) = 1000 + +0 > P. 1:1001(1000) ack 1040001 win 0 + +0 < . 1105001:1105001(0) ack 1001 win 257 + +// In order segment, in max adv. window -> drop (SKB_DROP_REASON_TCP_ZEROWINDOW) + +0 < P. 1040001:1041001(1000) ack 1001 win 257 + +0 > . 1001:1001(0) ack 1040001 win 0 +// Ooo partial segment, in max adv. window -> drop (SKB_DROP_REASON_TCP_ZEROWINDOW) + +0 < P. 1039001:1041001(2000) ack 1001 win 257 + +0 > . 1001:1001(0) ack 1040001 win 0 <nop,nop,sack 1039001:1040001> +// Check LINUX_MIB_TCPZEROWINDOWDROP has been incremented twice + +0 `nstat -s | grep TcpExtTCPZeroWindowDrop| grep -q " 2 "` + +// Ooo segment, in max adv. window -> drop (SKB_DROP_REASON_TCP_OVERWINDOW) + +0 < P. 1105001:1106001(1000) ack 1001 win 257 + +0 > . 1001:1001(0) ack 1040001 win 0 +// Ooo segment, beyond max adv. window -> drop (SKB_DROP_REASON_TCP_INVALID_SEQUENCE) + +0 < P. 2000001:2001001(1000) ack 1001 win 257 + +0 > . 1001:1001(0) ack 1040001 win 0 +// Check LINUX_MIB_BEYOND_WINDOW has been incremented twice + +0 `nstat -s | grep TcpExtBeyondWindow | grep -q " 2 "` + +// Read all data + +0 read(4, ..., 2000000) = 1040000 + * > . 1001:1001(0) ack 1040001 + +// RWIN > 0: rcv_wup = 1040001, 0 < rcv_wnd < 32000, rcv_mwnd_seq > 1105001 (significantly larger, typically ~1970000) + +// Accept pure ack with seq in max adv. window, beyond adv. window + +0 write(4, ..., 1000) = 1000 + +0 > P. 1001:2001(1000) ack 1040001 + +0 < . 1105001:1105001(0) ack 2001 win 257 + +// In order segment, in max adv. window, in adv. window -> accept +// Note: This also ensures that we cannot hit the empty queue exception in tcp_sequence() in the following tests + +0 < P. 1040001:1041001(1000) ack 2001 win 257 + * > . 2001:2001(0) ack 1041001 + +// Ooo partial segment, in adv. window -> accept + +0 < P. 1040001:1042001(2000) ack 2001 win 257 + +0 > . 2001:2001(0) ack 1042001 <nop,nop,sack 1040001:1041001> + +// Ooo segment, in max adv. window, beyond adv. window -> drop (SKB_DROP_REASON_TCP_OVERWINDOW) + +0 < P. 1105001:1106001(1000) ack 2001 win 257 + +0 > . 2001:2001(0) ack 1042001 +// Ooo segment, beyond max adv. window, beyond adv. window -> drop (SKB_DROP_REASON_TCP_INVALID_SEQUENCE) + +0 < P. 2000001:2001001(1000) ack 2001 win 257 + +0 > . 2001:2001(0) ack 1042001 +// Check LINUX_MIB_BEYOND_WINDOW has been incremented twice + +0 `nstat -s | grep TcpExtBeyondWindow | grep -q " 4 "` + +// We are allowed to go beyond the window and buffer with one packet + +0 < P. 1042001:1062001(20000) ack 2001 win 257 + * > . 2001:2001(0) ack 1062001 + +0 < P. 1062001:1082001(20000) ack 2001 win 257 + * > . 2001:2001(0) ack 1082001 win 0 + +// But not more: In order segment, in max adv. window -> drop (SKB_DROP_REASON_TCP_ZEROWINDOW) + +0 < P. 1082001:1083001(1000) ack 2001 win 257 + * > . 2001:2001(0) ack 1082001 +// Check LINUX_MIB_TCPZEROWINDOWDROP has been incremented again + +0 `nstat -s | grep TcpExtTCPZeroWindowDrop| grep -q " 3 "` diff --git a/tools/testing/selftests/net/packetdrill/tcp_rfc5961_ack-out-of-window.pkt b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_ack-out-of-window.pkt new file mode 100644 index 000000000000..2776b8728085 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_ack-out-of-window.pkt @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// RFC 5961 Section 5.2 / RFC 793 Section 3.9: an incoming segment's +// ACK value must lie in [SND.UNA - MAX.SND.WND, SND.NXT]; otherwise +// the receiver MUST discard the segment and send a challenge ACK +// back. Exercise both edges of that window in a single connection. + +`./defaults.sh +sysctl -q net.ipv4.tcp_invalid_ratelimit=0 +` + + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + +// Three-way handshake. Peer advertises rwnd = 1000 (no wscale), so +// MAX.SND.WND is tracked as 1000. + +0 < S 0:0(0) win 1000 <mss 1000,sackOK,nop,nop,nop,wscale 0> + +0 > S. 0:0(0) ack 1 <...> ++.1 < . 1:1(0) ack 1 win 1000 + +0 accept(3, ..., ...) = 4 + +// ---- Upper edge: SEG.ACK > SND.NXT -------------------------------- +// Server has sent nothing yet, so SND.UNA = SND.NXT = 1. +// Peer sends a pure ACK with SEG.ACK = 2, beyond SND.NXT. + +0 < . 1:1(0) ack 2 win 1000 +// Expect a challenge ACK: <SEQ = SND.NXT = 1, ACK = RCV.NXT = 1>. + +0 > . 1:1(0) ack 1 + +// Advance SND.UNA past MAX.SND.WND so that the lower edge becomes +// reachable. Issue two 1-MSS writes so each skb is exactly one MSS +// and PSH is set by tcp_push() at the end of each sendmsg, keeping +// the setup independent of the TSO / tcp_fragment split path. + +0 write(4, ..., 1000) = 1000 + +0 > P. 1:1001(1000) ack 1 ++.01 < . 1:1(0) ack 1001 win 1000 + +0 write(4, ..., 1000) = 1000 + +0 > P. 1001:2001(1000) ack 1 ++.01 < . 1:1(0) ack 2001 win 1000 +// Now SND.UNA = SND.NXT = 2001, MAX.SND.WND = 1000, bytes_acked = 2000. + +// ---- Lower edge: SEG.ACK < SND.UNA - MAX.SND.WND ------------------ +// SND.UNA - MAX.SND.WND = 2001 - 1000 = 1001, so SEG.ACK = 1000 falls +// below the acceptable range. + +0 < . 1:1(0) ack 1000 win 1000 +// Expect a challenge ACK: <SEQ = SND.NXT = 2001, ACK = RCV.NXT = 1>. + +0 > . 2001:2001(0) ack 1 diff --git a/tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt new file mode 100644 index 000000000000..32dd9de1d366 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_reject-old-ack.pkt @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0 + +`./defaults.sh +sysctl -q net.ipv4.tcp_invalid_ratelimit=0 +` + +// Test rejection of data segments carrying excessively old ACKs + +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 ++0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 ++0 bind(3, ..., ...) = 0 ++0 listen(3, 1024) = 0 + +// ---------------- Handshake ------------------- // ++0 < S 0:0(0) win 65535 ++0 > S. 0:0(0) ack 1 <...> ++0 < . 1:1(0) ack 1 win 65535 ++0 accept(3, ..., ...) = 4 + +// Populate receive memory so the following segment can use +// header prediction. ++0 < P. 1:501(500) ack 1 win 65535 ++0 > . 1:1(0) ack 501 + +// Send an in-sequence data segment carrying an excessively old ACK. ++0 < P. 501:1501(1000) ack 2794967397 win 65535 + +// Challenge ACK; RCV.NXT must remain 501. ++0 > . 1:1(0) ack 501 diff --git a/tools/testing/selftests/net/packetdrill/tcp_rfc5961_rst-syn-recv.pkt b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_rst-syn-recv.pkt new file mode 100644 index 000000000000..3fc2de03658a --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_rfc5961_rst-syn-recv.pkt @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// RFC 9293 Section 3.10.7.4: in SYN-RECEIVED, an exact RST resets +// the connection. A non-exact in-window RST elicits a challenge ACK, +// while an out-of-window RST is silently discarded. + +`./defaults.sh` + +// An exact RST removes the request socket. + 0 socket(..., SOCK_STREAM|SOCK_NONBLOCK, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + +0 < S 0:0(0) win 1000 <mss 1000,sackOK,nop,nop,nop,wscale 0> + +0 > S. 0:0(0) ack 1 <...> + +0 < R 1:1(0) win 1000 + +.1 < . 1:1(0) ack 1 win 1000 + +0 > R 1:1(0) + +0 close(3) = 0 + +// A non-exact in-window RST gets a challenge ACK and the request survives. + +0 socket(..., SOCK_STREAM|SOCK_NONBLOCK, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + +0 < S 0:0(0) win 1000 <mss 1000,sackOK,nop,nop,nop,wscale 0> + +0 > S. 0:0(0) ack 1 <...> + +0 < R 2:2(0) win 1000 + +0 > . 1:1(0) ack 1 + +0 < . 1:1(0) ack 1 win 1000 + +0 accept(3, ..., ...) = 4 + +0 close(4) = 0 + +0 close(3) = 0 + +// RST sequence validation precedes ACK validation. Even an RST|ACK +// with an unacceptable ACK value gets a challenge ACK. + +0 socket(..., SOCK_STREAM|SOCK_NONBLOCK, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + +0 < S 0:0(0) win 1000 <mss 1000,sackOK,nop,nop,nop,wscale 0> + +0 > S. 0:0(0) ack 1 <...> + +0 < R. 2:2(0) ack 100 win 1000 + +0 > . 1:1(0) ack 1 + +0 < . 1:1(0) ack 1 win 1000 + +0 accept(3, ..., ...) = 4 + +0 close(4) = 0 + +0 close(3) = 0 + +// An out-of-window RST is silent and does not remove the request. + +0 socket(..., SOCK_STREAM|SOCK_NONBLOCK, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + +0 < S 0:0(0) win 1000 <mss 1000,sackOK,nop,nop,nop,wscale 0> + +0 > S. 0:0(0) ack 1 <...> + +0 < R 100001:100001(0) win 1000 + +.1 < . 1:1(0) ack 1 win 1000 + +0 accept(3, ..., ...) = 4 + +0 close(4) = 0 + +0 close(3) = 0 diff --git a/tools/testing/selftests/net/packetdrill/tcp_syncookies_ip4_9k.pkt b/tools/testing/selftests/net/packetdrill/tcp_syncookies_ip4_9k.pkt new file mode 100644 index 000000000000..60910069b3d7 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_syncookies_ip4_9k.pkt @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Check syncookies. +// +// Check we are able to rebuild client sack, wscale, ecn and mss options. +// IPv4 msstab[4] = { 536, 1300, 1440, 1460 } + +--ip_version=ipv4 + +`./defaults.sh +sysctl -q net.ipv4.tcp_syncookies=2 +ip link set dev tun0 mtu 9000 +` + + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 10) = 0 + + +0 < S 0:0(0) win 32792 <mss 8960,sackOK,TS val 100 ecr 0,nop,wscale 10> + +0 > S. 0:0(0) ack 1 <mss 8960,sackOK,TS val 4000 ecr 100,nop,wscale 8> + +.01 < . 1:1(0) ack 1 win 1024 <nop,nop,TS val 110 ecr 4000> + + +0 accept(3, ..., ...) = 4 + +// Check we properly infer from the final packet the other peer wanted mss >= 1460, wscale 10, sackOK and no ECN. +// Note that mss is limited to 1460 - 12 because of IPv4 msstab[] +// This is only possible because TCP TS option was used. +// Linux uses the SYNACK TS.val 6 low order bits to encode the options. + + +0 %{ assert tcpi_snd_mss == 1460 - 12, tcpi_snd_mss; \ + assert tcpi_snd_wscale == 10, tcpi_snd_wscale; \ + assert (tcpi_options & TCPI_OPT_SACK) != 0, tcpi_options; \ + assert (tcpi_options & TCPI_OPT_TIMESTAMPS) != 0, tcpi_options; \ + assert (tcpi_options & TCPI_OPT_WSCALE) != 0, tcpi_options; \ + assert (tcpi_options & TCPI_OPT_ECN) == 0, tcpi_options +}% diff --git a/tools/testing/selftests/net/packetdrill/tcp_syncookies_ip6_9k.pkt b/tools/testing/selftests/net/packetdrill/tcp_syncookies_ip6_9k.pkt new file mode 100644 index 000000000000..f333c61044bc --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_syncookies_ip6_9k.pkt @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Check syncookies. +// +// Check we are able to rebuild client sack, wscale, ecn and mss options. +// IPv6 msstab[4] = { 1280 - 60, 1480 - 60, 1500 - 60, 9000 - 60 } + +--ip_version=ipv6 + +`./defaults.sh +sysctl -q net.ipv4.tcp_syncookies=2 +ip link set dev tun0 mtu 9000 +` + + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 10) = 0 + + +0 < S 0:0(0) win 32792 <mss 8940,sackOK,TS val 100 ecr 0,nop,wscale 10> + +0 > S. 0:0(0) ack 1 <mss 8940,sackOK,TS val 4000 ecr 100,nop,wscale 8> + +.01 < . 1:1(0) ack 1 win 1024 <nop,nop,TS val 110 ecr 4000> + + +0 accept(3, ..., ...) = 4 + +// Check we properly infer from the final packet the other peer wanted mss >= 8940, wscale 10, sackOK and no ECN. +// This is only possible because TCP TS option was used. +// Linux uses the SYNACK TS.val 6 low order bits to encode the options. + + +0 %{ assert tcpi_snd_mss == 8940 - 12, tcpi_snd_mss; \ + assert tcpi_snd_wscale == 10, tcpi_snd_wscale; \ + assert (tcpi_options & TCPI_OPT_SACK) != 0, tcpi_options; \ + assert (tcpi_options & TCPI_OPT_TIMESTAMPS) != 0, tcpi_options; \ + assert (tcpi_options & TCPI_OPT_WSCALE) != 0, tcpi_options; \ + assert (tcpi_options & TCPI_OPT_ECN) == 0, tcpi_options +}% diff --git a/tools/testing/selftests/net/packetdrill/tcp_ts_recent_invalid_ack.pkt b/tools/testing/selftests/net/packetdrill/tcp_ts_recent_invalid_ack.pkt index 174ce9a1bfc0..ee6baf7c36cf 100644 --- a/tools/testing/selftests/net/packetdrill/tcp_ts_recent_invalid_ack.pkt +++ b/tools/testing/selftests/net/packetdrill/tcp_ts_recent_invalid_ack.pkt @@ -19,7 +19,9 @@ // bad packet with high tsval (its ACK sequence is above our sndnxt) +0 < F. 1:1(0) ack 9999 win 20000 <nop,nop,TS val 200000 ecr 100> - +// Challenge ACK for SEG.ACK > SND.NXT (RFC 5961 5.2 / RFC 793 3.9). +// ecr=200 (not 200000) proves ts_recent was not updated from the bad packet. + +0 > . 1:1(0) ack 1 <nop,nop,TS val 200 ecr 200> +0 < . 1:1001(1000) ack 1 win 20000 <nop,nop,TS val 201 ecr 100> +0 > . 1:1(0) ack 1001 <nop,nop,TS val 200 ecr 201> diff --git a/tools/testing/selftests/net/packetdrill/tcp_urg_ptr_retransmit.pkt b/tools/testing/selftests/net/packetdrill/tcp_urg_ptr_retransmit.pkt new file mode 100644 index 000000000000..22f750ce09c1 --- /dev/null +++ b/tools/testing/selftests/net/packetdrill/tcp_urg_ptr_retransmit.pkt @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0 +--ip_version=ipv4 +// +// Reproduce urg_ptr being copied across segments on a multi-segment retransmit +// in urgent mode (regression since 10d3be569243). +// +// server (kernel, under test) client (packetdrill) +// | write(5000): 1:1001 .. 4001:5001 | mss 1000 from +// | -------------------------------------------> | the client SYN +// | send(MSG_OOB): 5001:5002 urg 1 | snd_up = 5002 +// | -------------------------------------------> | +// | SACK 2001:5002, leaving hole 1:2001| +// | <------------------------------------------- | +// | retransmit hole 1:2001 as ONE skb: | +// | seq=1, 2 segments, urg_ptr = 5002-1 = 5001| +// | tun tso off -> software GSO splits it: | +// | seg A 1:1001 urg_ptr 5001 (correct) | +// | seg B 1001:2001 urg_ptr ? | +// | want 5002-1001 = 4001 | +// | bug inherits 5001 <- caught here | +// | -------------------------------------------> | +// + +`./defaults.sh` + + 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 + +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 + +0 bind(3, ..., ...) = 0 + +0 listen(3, 1) = 0 + +// 1. client force mss=1000 + +.1 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7> + +0 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 8> + +.1 < . 1:1(0) ack 1 win 320 + +0 accept(3, ..., ...) = 4 + +// 2. server sends 5000 bytes; TSO on, so packetdrill sees whole super-skbs + +0 write(4, ..., 5000) = 5000 + +0 > P. 1:5001(5000) ack 1 + +// 3. server send OOB + +0 send(4, ..., 1, MSG_OOB) = 1 + +0 > PU. 5001:5002(1) ack 1 urg 1 + +// We could disable GSO at the start of the script, but then the PSH flag on +// the 5 initial server segments is not deterministic and hard to match. Keep +// TSO on for the initial send (one super-skb, stable PSH) and disable it only +// here, so software GSO splits the retransmit and each segment's urg_ptr is +// checked on the wire. + +0 `ethtool -K tun0 tso off gso off gro off lro off 2>/dev/null` + +// 4. SACKed blocks reach dupthresh -> fast retransmit of the 1:2001 hole. + +.05 < . 1:1(0) ack 1 win 320 <sack 2001:3001,nop,nop> + +0 < . 1:1(0) ack 1 win 320 <sack 2001:4001,nop,nop> + +0 < . 1:1(0) ack 1 win 320 <sack 2001:5002,nop,nop> + +// Retransmit must keep a per-segment urg_ptr (5002 - seg.seq): seg A 5001, +// seg B 4001. The fix sends the hole as two independent skbs, so seg B has +// no PSH. Unpatched it goes out as one super-skb whose GSO split copies +// urg_ptr onto seg B and also adds PSH there, so on an unpatched kernel the +// mismatch shows up on the PSH bit before the urg_ptr. + +0 > U. 1:1001(1000) ack 1 urg 5001 + +0 > U. 1001:2001(1000) ack 1 urg 4001 + + +.1 < . 1:1(0) ack 5002 win 320 diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh index a3323c21f001..c7cd271714ef 100755 --- a/tools/testing/selftests/net/pmtu.sh +++ b/tools/testing/selftests/net/pmtu.sh @@ -1457,7 +1457,7 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() { mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000)) run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1 - run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1 -s $((${ll_mtu} + 500)) ${dst} || return 1 + run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1 # Check that exceptions were created pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})" diff --git a/tools/testing/selftests/net/ppp/Makefile b/tools/testing/selftests/net/ppp/Makefile new file mode 100644 index 000000000000..6036fa134351 --- /dev/null +++ b/tools/testing/selftests/net/ppp/Makefile @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0 + +top_srcdir = ../../../../.. + +TEST_PROGS := \ + ppp_async.sh \ + pppoe.sh \ + pppol2tp.sh \ +# end of TEST_PROGS + +TEST_FILES := \ + ppp_common.sh \ + pppoe-server-options \ +# end of TEST_FILES + +include ../../lib.mk diff --git a/tools/testing/selftests/net/ppp/config b/tools/testing/selftests/net/ppp/config new file mode 100644 index 000000000000..843545df8f03 --- /dev/null +++ b/tools/testing/selftests/net/ppp/config @@ -0,0 +1,11 @@ +CONFIG_IPV6=y +CONFIG_L2TP=m +CONFIG_PACKET=y +CONFIG_PPP=m +CONFIG_PPP_ASYNC=m +CONFIG_PPP_BSDCOMP=m +CONFIG_PPP_DEFLATE=m +CONFIG_PPPOE=m +CONFIG_PPPOE_HASH_BITS_4=y +CONFIG_PPPOL2TP=m +CONFIG_VETH=y diff --git a/tools/testing/selftests/net/ppp/ppp_async.sh b/tools/testing/selftests/net/ppp/ppp_async.sh new file mode 100755 index 000000000000..10f54c8dd0bc --- /dev/null +++ b/tools/testing/selftests/net/ppp/ppp_async.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +source ppp_common.sh + +# Temporary files for PTY symlinks +TTY_DIR=$(mktemp -d /tmp/ppp.XXXXXX) +TTY_SERVER="$TTY_DIR"/server +TTY_CLIENT="$TTY_DIR"/client + +# shellcheck disable=SC2329 +cleanup() { + cleanup_all_ns + [ -n "$SOCAT_PID" ] && kill_process "$SOCAT_PID" + rm -fr "$TTY_DIR" +} + +trap cleanup EXIT + +ppp_common_init +modprobe -q ppp_async + +# Create the virtual serial device +socat -d PTY,link="$TTY_SERVER",rawer PTY,link="$TTY_CLIENT",rawer & +SOCAT_PID=$! + +# Wait for symlinks to be created +slowwait 5 [ -L "$TTY_SERVER" ] + +# Start the PPP Server +ip netns exec "$NS_SERVER" pppd "$TTY_SERVER" 115200 \ + "$IP_SERVER":"$IP_CLIENT" \ + local noauth nodefaultroute debug + +# Start the PPP Client +ip netns exec "$NS_CLIENT" pppd "$TTY_CLIENT" 115200 \ + local noauth updetach nodefaultroute debug + +ppp_test_connectivity + +log_test "PPP async" + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/ppp/ppp_common.sh b/tools/testing/selftests/net/ppp/ppp_common.sh new file mode 100644 index 000000000000..40bbec317039 --- /dev/null +++ b/tools/testing/selftests/net/ppp/ppp_common.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2153 + +source ../lib.sh + +IP_SERVER="192.168.200.1" +IP_CLIENT="192.168.200.2" + +ppp_common_init() { + # Package requirements + require_command socat + require_command pppd + require_command iperf3 + + # Check for root privileges + if [ "$(id -u)" -ne 0 ];then + echo "SKIP: Need root privileges" + exit "$ksft_skip" + fi + + # Namespaces + setup_ns NS_SERVER NS_CLIENT +} + +ppp_check_addr() { + dev=$1 + addr=$2 + ns=$3 + ip -netns "$ns" -4 addr show dev "$dev" 2>/dev/null | grep -q "$addr" + return $? +} + +ppp_test_connectivity() { + slowwait 10 ppp_check_addr "ppp0" "$IP_CLIENT" "$NS_CLIENT" + + ip netns exec "$NS_CLIENT" ping -c 3 "$IP_SERVER" + check_err $? + + ip netns exec "$NS_SERVER" iperf3 -s -1 -D + wait_local_port_listen "$NS_SERVER" 5201 tcp + + ip netns exec "$NS_CLIENT" iperf3 -c "$IP_SERVER" -Z -t 2 + check_err $? +} diff --git a/tools/testing/selftests/net/ppp/pppoe-server-options b/tools/testing/selftests/net/ppp/pppoe-server-options new file mode 100644 index 000000000000..cd586be7061b --- /dev/null +++ b/tools/testing/selftests/net/ppp/pppoe-server-options @@ -0,0 +1,3 @@ +noauth +noipdefault +nomagic diff --git a/tools/testing/selftests/net/ppp/pppoe.sh b/tools/testing/selftests/net/ppp/pppoe.sh new file mode 100755 index 000000000000..f67b51df7490 --- /dev/null +++ b/tools/testing/selftests/net/ppp/pppoe.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +source ppp_common.sh + +VETH_SERVER="veth-server" +VETH_CLIENT="veth-client" +PPPOE_LOG=$(mktemp /tmp/pppoe.XXXXXX) + +# shellcheck disable=SC2329 +cleanup() { + cleanup_all_ns + [ -n "$SOCAT_PID" ] && kill_process "$SOCAT_PID" + rm -f "$PPPOE_LOG" +} + +trap cleanup EXIT + +require_command pppoe-server +ppp_common_init +modprobe -q pppoe + +# Try to locate pppoe.so plugin +PPPOE_PLUGIN=$(find /usr/{lib,lib64,lib32}/pppd/ -name pppoe.so -type f -print -quit) +if [ -z "$PPPOE_PLUGIN" ]; then + log_test_skip "PPPoE: pppoe.so plugin not found" + exit "$EXIT_STATUS" +fi + +# Create the veth pair +ip link add "$VETH_SERVER" type veth peer name "$VETH_CLIENT" +ip link set "$VETH_SERVER" netns "$NS_SERVER" +ip link set "$VETH_CLIENT" netns "$NS_CLIENT" +ip -netns "$NS_SERVER" link set "$VETH_SERVER" up +ip -netns "$NS_CLIENT" link set "$VETH_CLIENT" up + +# Start socat as syslog listener +socat -v -u UNIX-RECV:/dev/log OPEN:/dev/null > "$PPPOE_LOG" 2>&1 & +SOCAT_PID=$! + +# Start the PPP Server. Note that versions before 4.0 ignore -g option and +# instead use a hardcoded plugin path, so they may fail to find the plugin. +ip netns exec "$NS_SERVER" pppoe-server -I "$VETH_SERVER" \ + -L "$IP_SERVER" -R "$IP_CLIENT" -N 1 -q "$(command -v pppd)" \ + -k -O "$(pwd)/pppoe-server-options" -g "$PPPOE_PLUGIN" + +# Start the PPP Client +ip netns exec "$NS_CLIENT" pppd \ + local debug updetach noipdefault noauth nodefaultroute \ + plugin "$PPPOE_PLUGIN" nic-"$VETH_CLIENT" + +ppp_test_connectivity + +log_test "PPPoE" + +# Dump syslog messages if the test failed +if [ "$RET" -ne 0 ]; then + while read -r _sign _date _time len _from _to + do len=${len##*=} + read -n "$len" -r LINE + echo "$LINE" + done < "$PPPOE_LOG" +fi + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/ppp/pppol2tp.sh b/tools/testing/selftests/net/ppp/pppol2tp.sh new file mode 100755 index 000000000000..37c4d56c5c6e --- /dev/null +++ b/tools/testing/selftests/net/ppp/pppol2tp.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 + +source ppp_common.sh + +VETH_SERVER="veth-server" +VETH_CLIENT="veth-client" +OUTER_IP_SERVER="172.16.1.1" +OUTER_IP_CLIENT="172.16.1.2" + +PPPOL2TP_DIR=$(mktemp -d /tmp/pppol2tp.XXXXXX) +PPPOL2TP_LOG="$PPPOL2TP_DIR/l2tp.log" + +# shellcheck disable=SC2329 +cleanup() { + cleanup_all_ns + [ -n "$SOCAT_PID" ] && kill_process "$SOCAT_PID" + rm -rf "$PPPOL2TP_DIR" +} + +trap cleanup EXIT + +require_command xl2tpd +ppp_common_init +modprobe -q l2tp_ppp + +# Create the veth pair +ip link add "$VETH_SERVER" type veth peer name "$VETH_CLIENT" +ip link set "$VETH_SERVER" netns "$NS_SERVER" +ip link set "$VETH_CLIENT" netns "$NS_CLIENT" +ip -netns "$NS_SERVER" link set "$VETH_SERVER" up +ip -netns "$NS_CLIENT" link set "$VETH_CLIENT" up +ip -netns "$NS_SERVER" address add dev "$VETH_SERVER" "$OUTER_IP_SERVER" peer "$OUTER_IP_CLIENT" +ip -netns "$NS_CLIENT" address add dev "$VETH_CLIENT" "$OUTER_IP_CLIENT" peer "$OUTER_IP_SERVER" + +# Start socat as syslog listener +socat -v -u UNIX-RECV:/dev/log OPEN:/dev/null > "$PPPOL2TP_LOG" 2>&1 & +SOCAT_PID=$! + +# Generate configuration files +cat > "$PPPOL2TP_DIR/l2tp-server.conf" <<EOF +[global] +listen-addr = $OUTER_IP_SERVER +access control = no + +[lns default] +ip range = $IP_CLIENT +local ip = $IP_SERVER +require authentication = no +require chap = no +require pap = no +ppp debug = yes +pppoptfile = $(pwd)/pppoe-server-options +EOF + +cat > "$PPPOL2TP_DIR/l2tp-client.conf" <<EOF +[global] +listen-addr = $OUTER_IP_CLIENT +access control = no + +[lac server] +lns = $OUTER_IP_SERVER +require authentication = no +require chap = no +require pap = no +ppp debug = yes +pppoptfile = $(pwd)/pppoe-server-options +EOF + +# Start the L2TP Server +ip netns exec "$NS_SERVER" xl2tpd -D -c "$PPPOL2TP_DIR/l2tp-server.conf" \ + -p "$PPPOL2TP_DIR/l2tp-server.pid" -C "$PPPOL2TP_DIR/l2tp-server.control" & + +# Start the L2TP Client +ip netns exec "$NS_CLIENT" xl2tpd -D -c "$PPPOL2TP_DIR/l2tp-client.conf" \ + -p "$PPPOL2TP_DIR/l2tp-client.pid" -C "$PPPOL2TP_DIR/l2tp-client.control" & + +# Wait for xl2tpd to start and open their control pipes +slowwait 2 [ -p "$PPPOL2TP_DIR/l2tp-server.control" ] +slowwait 2 [ -p "$PPPOL2TP_DIR/l2tp-client.control" ] + +# Connect LAC to LNS +echo "c server" > "$PPPOL2TP_DIR/l2tp-client.control" + +ppp_test_connectivity + +log_test "PPPoL2TP" + +# Recursion test +RET=0 +# Delete route to LNS IP +ip -netns "$NS_CLIENT" route del "$OUTER_IP_SERVER" +# Add default route through ppp0 +ip -netns "$NS_CLIENT" route add default dev ppp0 +# ping (we expect the ping to fail but not deadlock the system) +ip netns exec "$NS_CLIENT" ping -c 1 "$IP_SERVER" -w 1 +check_fail $? + +log_test "PPPoL2TP Recursion" + +# Dump syslog messages if the test failed +if [ "$EXIT_STATUS" -ne 0 ]; then + while read -r _sign _date _time len _from _to + do len=${len##*=} + read -n "$len" -r LINE + echo "$LINE" + done < "$PPPOL2TP_LOG" +fi + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/protodown.sh b/tools/testing/selftests/net/protodown.sh new file mode 100755 index 000000000000..0a7b78c63c37 --- /dev/null +++ b/tools/testing/selftests/net/protodown.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test the protodown mechanism. Verify basic protodown toggling, protodown +# reasons, operational state when the lower device carrier changes, and correct +# operational state when the lower device has no carrier. + +# shellcheck disable=SC1091,SC2034,SC2154,SC2317 +source lib.sh + +require_command jq + +ALL_TESTS=" + protodown_basic_macvlan + protodown_basic_vxlan + protodown_reasons + protodown_lower_toggle + protodown_lower_down +" + +operstate_get() +{ + local ns=$1; shift + local dev=$1; shift + + ip -n "$ns" -j link show dev "$dev" | jq -r '.[].operstate' +} + +operstate_check() +{ + local ns=$1; shift + local dev=$1; shift + local expected=$1; shift + + local current + current=$(operstate_get "$ns" "$dev") + + [ "$current" = "$expected" ] +} + +setup_prepare() +{ + setup_ns NS + defer cleanup_all_ns + + ip -n "$NS" link add name dummy0 up type dummy + + ip -n "$NS" link add name macvlan0 link dummy0 up type macvlan mode bridge + + ip -n "$NS" link add name vxlan0 up type vxlan id 10010 dstport 4789 +} + +protodown_basic() +{ + local dev=$1; shift + + ip -n "$NS" link set dev "$dev" protodown on + check_err $? "Failed to set protodown on" + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" "$dev" DOWN + check_err $? "Operational state is not DOWN after setting protodown" + + ip -n "$NS" link set dev "$dev" protodown off + check_err $? "Failed to set protodown off" + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" "$dev" UP + check_err $? "Operational state is not UP after clearing protodown" +} + +protodown_basic_macvlan() +{ + RET=0 + + protodown_basic macvlan0 + + log_test "Basic protodown on/off with macvlan" +} + +protodown_basic_vxlan() +{ + RET=0 + + protodown_basic vxlan0 + + log_test "Basic protodown on/off with vxlan" +} + +protodown_reasons() +{ + RET=0 + + ip -n "$NS" link set dev macvlan0 protodown on + + ip -n "$NS" link set dev macvlan0 protodown_reason 0 on + check_err $? "Failed to set protodown reason bit 0" + + # Cannot clear protodown while reasons are active. + ip -n "$NS" link set dev macvlan0 protodown off 2>/dev/null + check_fail $? "Clearing protodown succeeded with active reasons" + + ip -n "$NS" link set dev macvlan0 protodown_reason 0 off + check_err $? "Failed to clear protodown reason bit 0" + + # Can clear protodown when no reasons are active. + ip -n "$NS" link set dev macvlan0 protodown off + check_err $? "Failed to clear protodown with no active reasons" + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 UP + check_err $? "Operational state is not UP after clearing protodown" + + log_test "Protodown reasons" +} + +protodown_lower_toggle() +{ + RET=0 + + ip -n "$NS" link set dev macvlan0 protodown on + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 DOWN + check_err $? "Operational state is not DOWN after setting protodown" + + # Toggle carrier on the lower device. The macvlan should stay DOWN + # because protodown is on. + ip -n "$NS" link set dev dummy0 carrier off + ip -n "$NS" link set dev dummy0 carrier on + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" dummy0 UP + check_err $? "Lower device is not UP after carrier on" + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 DOWN + check_err $? "Macvlan operational state is not DOWN despite protodown" + + # Clear protodown and verify the macvlan comes back up. + ip -n "$NS" link set dev macvlan0 protodown off + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 UP + check_err $? "Operational state is not UP after clearing protodown" + + log_test "Protodown with lower device toggled" +} + +protodown_lower_down() +{ + RET=0 + + # Bring the lower device carrier down first. + ip -n "$NS" link set dev dummy0 carrier off + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 LOWERLAYERDOWN + check_err $? "Macvlan is not LOWERLAYERDOWN with lower carrier off" + + # Toggle protodown on and off while lower has no carrier. The macvlan + # should not transition to UP. + ip -n "$NS" link set dev macvlan0 protodown on + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 LOWERLAYERDOWN + check_err $? "Macvlan is not LOWERLAYERDOWN after setting protodown" + + ip -n "$NS" link set dev macvlan0 protodown off + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 LOWERLAYERDOWN + check_err $? "Macvlan is not LOWERLAYERDOWN after clearing protodown" + + # Bring the lower device carrier up. The macvlan should transition to + # UP. + ip -n "$NS" link set dev dummy0 carrier on + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" dummy0 UP + check_err $? "Lower device is not UP after carrier on" + + busywait "$BUSYWAIT_TIMEOUT" operstate_check "$NS" macvlan0 UP + check_err $? "Macvlan is not UP after lower device is UP" + + log_test "Protodown with lower device down" +} + +trap defer_scopes_cleanup EXIT +setup_prepare +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/psock_snd.c b/tools/testing/selftests/net/psock_snd.c index edf1e6f80d41..0f6a30b26912 100644 --- a/tools/testing/selftests/net/psock_snd.c +++ b/tools/testing/selftests/net/psock_snd.c @@ -39,6 +39,9 @@ static bool cfg_use_gso; static bool cfg_use_qdisc_bypass; static bool cfg_use_vlan; static bool cfg_use_vnet; +static bool cfg_drop; +static bool cfg_aux_data; +static bool cfg_ignore_outgoing; static char *cfg_ifname = "lo"; static int cfg_mtu = 1500; @@ -49,6 +52,8 @@ static uint16_t cfg_port = 8000; /* test sending up to max mtu + 1 */ #define TEST_SZ (sizeof(struct virtio_net_hdr) + ETH_HLEN + ETH_MAX_MTU + 1) +#define BURST_CNT (1000) + static char tbuf[TEST_SZ], rbuf[TEST_SZ]; static unsigned long add_csum_hword(const uint16_t *start, int num_u16) @@ -167,12 +172,12 @@ static int build_packet(int payload_len) return off + payload_len; } -static void do_bind(int fd) +static void do_bind_proto(int fd, uint16_t proto) { struct sockaddr_ll laddr = {0}; laddr.sll_family = AF_PACKET; - laddr.sll_protocol = htons(ETH_P_IP); + laddr.sll_protocol = htons(proto); laddr.sll_ifindex = if_nametoindex(cfg_ifname); if (!laddr.sll_ifindex) error(1, errno, "if_nametoindex"); @@ -181,6 +186,11 @@ static void do_bind(int fd) error(1, errno, "bind"); } +static void do_bind(int fd) +{ + do_bind_proto(fd, ETH_P_IP); +} + static void do_send(int fd, char *buf, int len) { int ret; @@ -212,13 +222,14 @@ static void do_send(int fd, char *buf, int len) if (ret != len) error(1, 0, "write: %u %u", ret, len); - fprintf(stderr, "tx: %u\n", ret); + if (!cfg_drop) + fprintf(stderr, "tx: %u\n", ret); } static int do_tx(void) { const int one = 1; - int fd, len; + int i, fd, len; fd = socket(PF_PACKET, cfg_use_dgram ? SOCK_DGRAM : SOCK_RAW, 0); if (fd == -1) @@ -242,6 +253,10 @@ static int do_tx(void) do_send(fd, tbuf, len); + if (cfg_drop) + for (i = 0; i < BURST_CNT; i++) + do_send(fd, tbuf, len); + if (close(fd)) error(1, errno, "close t"); @@ -271,11 +286,58 @@ static int setup_rx(void) return fd; } -static void do_rx(int fd, int expected_len, char *expected) +static void check_aux_data(struct cmsghdr *cmsg, int expected_len) +{ + struct tpacket_auxdata *adata; + + if (!cmsg) + error(1, 0, "auxdata null"); + + if (cmsg->cmsg_level != SOL_PACKET) + error(1, 0, "cmsg_level != SOL_PACKET"); + + if (cmsg->cmsg_type != PACKET_AUXDATA) + error(1, 0, "cmsg_type != PACKET_AUXDATA"); + + adata = (struct tpacket_auxdata *)CMSG_DATA(cmsg); + + if (adata->tp_net != ETH_HLEN) + error(1, 0, "cmsg tp_net != ETH_HLEN"); + + if (adata->tp_len != expected_len) + error(1, 0, "cmsg tp_len != %u", expected_len); + + if (adata->tp_snaplen != expected_len) + error(1, 0, "cmsg tp_snaplen != %u", expected_len); +} + +/* expected_pkttype < 0 skips the sll_pkttype check. */ +static void do_rx(int fd, int expected_len, char *expected, bool is_psock, + int expected_pkttype) { + char cmsg_buf[1024] __attribute__((aligned(8))) = {}; + bool aux = is_psock && cfg_aux_data; + struct sockaddr_ll saddr = {}; + struct iovec iov = { + .iov_base = rbuf, + .iov_len = sizeof(rbuf), + }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + }; int ret; - ret = recv(fd, rbuf, sizeof(rbuf), 0); + if (aux) { + msg.msg_control = cmsg_buf; + msg.msg_controllen = sizeof(cmsg_buf); + } + if (is_psock) { + msg.msg_name = &saddr; + msg.msg_namelen = sizeof(saddr); + } + + ret = recvmsg(fd, &msg, 0); if (ret == -1) error(1, errno, "recv"); if (ret != expected_len) @@ -284,12 +346,20 @@ static void do_rx(int fd, int expected_len, char *expected) if (memcmp(rbuf, expected, ret)) error(1, 0, "recv: data mismatch"); + if (aux) + check_aux_data(CMSG_FIRSTHDR(&msg), expected_len); + + if (expected_pkttype >= 0 && saddr.sll_pkttype != expected_pkttype) + error(1, 0, "recv: sll_pkttype %d != %d", + saddr.sll_pkttype, expected_pkttype); + fprintf(stderr, "rx: %u\n", ret); } static int setup_sniffer(void) { struct timeval tv = { .tv_usec = 100 * 1000 }; + const int one = 1; int fd; fd = socket(PF_PACKET, SOCK_RAW, 0); @@ -299,8 +369,23 @@ static int setup_sniffer(void) if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) error(1, errno, "setsockopt rcv timeout"); + if (cfg_drop) + if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &one, sizeof(one))) + error(1, errno, "setsockopt SO_RCVBUF"); + + if (cfg_aux_data) + if (setsockopt(fd, SOL_PACKET, PACKET_AUXDATA, &one, sizeof(one))) + error(1, errno, "setsockopt PACKET_AUXDATA"); + pair_udp_setfilter(fd); - do_bind(fd); + + /* binding to ETH_P_ALL adds the sniffer to ptype_all, which will see + * the dev_queue_xmit_nit copy. ignore_outgoing should suppress this. + */ + if (cfg_ignore_outgoing) + do_bind_proto(fd, ETH_P_ALL); + else + do_bind(fd); return fd; } @@ -309,8 +394,11 @@ static void parse_opts(int argc, char **argv) { int c; - while ((c = getopt(argc, argv, "bcCdgl:qt:vV")) != -1) { + while ((c = getopt(argc, argv, "abcCdDgil:qt:vV")) != -1) { switch (c) { + case 'a': + cfg_aux_data = true; + break; case 'b': cfg_use_bind = true; break; @@ -323,9 +411,15 @@ static void parse_opts(int argc, char **argv) case 'd': cfg_use_dgram = true; break; + case 'D': + cfg_drop = true; + break; case 'g': cfg_use_gso = true; break; + case 'i': + cfg_ignore_outgoing = true; + break; case 'l': cfg_payload_len = strtoul(optarg, NULL, 0); break; @@ -357,6 +451,114 @@ static void parse_opts(int argc, char **argv) if (cfg_use_gso && !cfg_use_csum_off) error(1, 0, "option gso (-g) requires csum offload (-c)"); + + if (cfg_aux_data && cfg_drop) + error(1, 0, "option aux data (-a) conflicts with drop (-D)"); + + if (cfg_ignore_outgoing && (cfg_drop || cfg_aux_data)) + error(1, 0, + "option ignore outgoing (-i) conflicts with -D and -a"); +} + +static void check_packet_stats(int fd, unsigned int expected_packets) +{ + struct tpacket_stats st = {}; + socklen_t len = sizeof(st); + + if (getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &st, &len)) + error(1, errno, "getsockopt packet statistics"); + + if (cfg_drop) { + /* PACKET_STATISTICS reports all packets seen (including + * drops) in tp_packets + */ + if (st.tp_packets < st.tp_drops) + error(1, 0, "stats: tp_packets %u < tp_drops %u", + st.tp_packets, st.tp_drops); + + if (st.tp_drops == 0) + error(1, 0, "stats: expected drops but tp_drops == 0"); + } else { + if (st.tp_packets != expected_packets) + error(1, 0, "stats: tp_packets %u != %u", + st.tp_packets, expected_packets); + + if (st.tp_drops != 0) + error(1, 0, "stats: tp_drops %u != 0", st.tp_drops); + } + + /* verify clear on read */ + memset(&st, 0xff, sizeof(st)); + len = sizeof(st); + + if (getsockopt(fd, SOL_PACKET, PACKET_STATISTICS, &st, &len)) + error(1, errno, "getsockopt packet statistics"); + + if (st.tp_packets != 0) + error(1, 0, "stats: tp_packets %u != 0 after clear", st.tp_packets); + + if (st.tp_drops != 0) + error(1, 0, "stats: tp_drops %u != 0 after clear", st.tp_drops); +} + +static void set_ignore_outgoing(int fd, int val) +{ + socklen_t len = sizeof(int); + int got = -1; + + if (setsockopt(fd, SOL_PACKET, PACKET_IGNORE_OUTGOING, + &val, sizeof(val))) + error(1, errno, "setsockopt PACKET_IGNORE_OUTGOING %d", val); + + if (getsockopt(fd, SOL_PACKET, PACKET_IGNORE_OUTGOING, &got, &len)) + error(1, errno, "getsockopt PACKET_IGNORE_OUTGOING"); + if (got != val) + error(1, 0, "getsockopt: expected %d got %d", val, got); +} + +static void check_ignore_outgoing_range(int fd) +{ + int val; + + /* Values outside [0, 1] must be rejected with -EINVAL. */ + val = 2; + if (setsockopt(fd, SOL_PACKET, PACKET_IGNORE_OUTGOING, + &val, sizeof(val)) != -1 || errno != EINVAL) + error(1, errno, + "setsockopt PACKET_IGNORE_OUTGOING val=2: expected EINVAL"); + + val = -1; + if (setsockopt(fd, SOL_PACKET, PACKET_IGNORE_OUTGOING, + &val, sizeof(val)) != -1 || errno != EINVAL) + error(1, errno, + "setsockopt PACKET_IGNORE_OUTGOING val=-1: expected EINVAL"); +} + +static void test_ignore_outgoing(int fds) +{ + char *expected = tbuf + sizeof(struct virtio_net_hdr); + int expected_len; + + /* ptype_all sniffer on loopback should produce two copies per packet + * (RX and TX). + */ + expected_len = do_tx(); + expected_len -= sizeof(struct virtio_net_hdr); + do_rx(fds, expected_len, expected, true, PACKET_OUTGOING); + do_rx(fds, expected_len, expected, true, PACKET_HOST); + check_packet_stats(fds, 2); + + /* 0 and 1 accepted; anything else rejected. */ + set_ignore_outgoing(fds, 0); + set_ignore_outgoing(fds, 1); + check_ignore_outgoing_range(fds); + + /* With PACKET_IGNORE_OUTGOING set, only the rx copy survives. */ + do_tx(); + do_rx(fds, expected_len, expected, true, PACKET_HOST); + if (recv(fds, rbuf, sizeof(rbuf), 0) != -1 || errno != EAGAIN) + error(1, errno, "expected EAGAIN, got extra packet"); + check_packet_stats(fds, 1); } static void run_test(void) @@ -366,15 +568,28 @@ static void run_test(void) fdr = setup_rx(); fds = setup_sniffer(); + if (cfg_ignore_outgoing) { + test_ignore_outgoing(fds); + goto out; + } + total_len = do_tx(); + if (cfg_drop) { + check_packet_stats(fds, 0); + goto out; + } + /* BPF filter accepts only this length, vlan changes MAC */ - if (cfg_payload_len == DATA_LEN && !cfg_use_vlan) + if (cfg_payload_len == DATA_LEN && !cfg_use_vlan) { do_rx(fds, total_len - sizeof(struct virtio_net_hdr), - tbuf + sizeof(struct virtio_net_hdr)); + tbuf + sizeof(struct virtio_net_hdr), true, -1); + check_packet_stats(fds, 1); + } - do_rx(fdr, cfg_payload_len, tbuf + total_len - cfg_payload_len); + do_rx(fdr, cfg_payload_len, tbuf + total_len - cfg_payload_len, false, -1); +out: if (close(fds)) error(1, errno, "close s"); if (close(fdr)) diff --git a/tools/testing/selftests/net/psock_snd.sh b/tools/testing/selftests/net/psock_snd.sh index 1cbfeb5052ec..7fa0a3297988 100755 --- a/tools/testing/selftests/net/psock_snd.sh +++ b/tools/testing/selftests/net/psock_snd.sh @@ -92,4 +92,19 @@ echo "raw gso max size" echo "raw gso max size + 1 (expected to fail)" (! ./in_netns.sh ./psock_snd -v -c -g -l "${max_mss_exceeds}") +# test drops statistics + +echo "test drops statistics" +./in_netns.sh ./psock_snd -D + +# test aux data + +echo "test aux data" +./in_netns.sh ./psock_snd -a + +# test ignore outgoing + +echo "test ignore outgoing" +./in_netns.sh ./psock_snd -i + echo "OK. All tests passed" diff --git a/tools/testing/selftests/net/rds/.gitignore b/tools/testing/selftests/net/rds/.gitignore index 1c6f04e2aa11..7ca4b1440f51 100644 --- a/tools/testing/selftests/net/rds/.gitignore +++ b/tools/testing/selftests/net/rds/.gitignore @@ -1 +1,2 @@ include.sh +getsockopt diff --git a/tools/testing/selftests/net/rds/Makefile b/tools/testing/selftests/net/rds/Makefile index 762845cc973c..ab9e92399a6d 100644 --- a/tools/testing/selftests/net/rds/Makefile +++ b/tools/testing/selftests/net/rds/Makefile @@ -3,10 +3,13 @@ all: @echo mk_build_dir="$(shell pwd)" > include.sh -TEST_PROGS := run.sh +TEST_PROGS := rds_run.sh + +TEST_GEN_PROGS := getsockopt TEST_FILES := \ include.sh \ + settings \ test.py \ # end of TEST_FILES @@ -15,4 +18,6 @@ EXTRA_CLEAN := \ /tmp/rds_logs \ # end of EXTRA_CLEAN +CFLAGS += $(KHDR_INCLUDES) + include ../../lib.mk diff --git a/tools/testing/selftests/net/rds/README.txt b/tools/testing/selftests/net/rds/README.txt index cbde2951ab13..8aa41148b1b5 100644 --- a/tools/testing/selftests/net/rds/README.txt +++ b/tools/testing/selftests/net/rds/README.txt @@ -1,21 +1,27 @@ RDS self-tests ============== -These scripts provide a coverage test for RDS-TCP by creating two -network namespaces and running rds packets between them. A loopback -network is provisioned with optional probability of packet loss or -corruption. A workload of 50000 hashes, each 64 characters in size, -are passed over an RDS socket on this test network. A passing test means -the RDS-TCP stack was able to recover properly. The provided config.sh -can be used to compile the kernel with the necessary gcov options. The -kernel may optionally be configured to omit the coverage report as well. +These scripts provide a coverage test for RDS-TCP and RDS-RDMA (over +RoCE/RXE) by setting up two endpoints and running RDS packets between +them. The TCP path creates two network namespaces; the RDMA path uses +an RXE (soft RoCE) device backed by a veth pair. A workload of 50000 +hashes, each 64 characters in size, is passed over an RDS socket on +this test network with an optional probability of packet loss or +corruption. A passing test means the RDS stack was able to recover +properly. The provided config.sh can be used to compile the kernel +with the necessary gcov options; pass -r to also enable the kernel +configs required for the RDMA transport. The kernel may optionally be +configured to omit the coverage report as well. USAGE: - run.sh [-d logdir] [-l packet_loss] [-c packet_corruption] - [-u packet_duplcate] + rds_run.sh [-d logdir] [-l packet_loss] [-c packet_corruption] + [-u packet_duplicate] [-t timeout] + [-T tcp|rdma|tcp,rdma] OPTIONS: - -d Log directory. Defaults to tools/testing/selftests/net/rds/rds_logs + -d Log directory. If set, logs will be stored in the + given dir, or skipped if unset. Log dir can also be + set through the RDS_LOG_DIR env variable -l Simulates a percentage of packet loss @@ -23,19 +29,49 @@ OPTIONS: -u Simulates a percentage of packet duplication. + -t Test timeout. Defaults to tools/testing/selftests/net/rds/settings + + -T Comma-separated list of transports to test. Accepts + "tcp", "rdma", or "tcp,rdma". Defaults to "tcp". Use + config.sh -r to enable required RDMA configs + +ENV VARIABLES: + RDS_LOG_DIR Log directory. If set, logs will be stored in + the given dir, or skipped if unset. Log dir + can also be set with the -d flag. + + Use with --rwdir on the CI path to retain logs after + test compleation. Log dir end point must be within + the specified --rwdir path for logs to persist on + the host. + + SUDO_USER The user name that should be used for tcpdump + --relinquish-privileges. Set this to a user + belonging to the sudoers group to avoid drop + privilege errors with the vng 9p filesystem + which may result in empty pcaps + EXAMPLE: # Create a suitable gcov enabled .config tools/testing/selftests/net/rds/config.sh -g + # Optionally add RDMA configs (CONFIG_RDS_RDMA, CONFIG_RDMA_RXE) + tools/testing/selftests/net/rds/config.sh -r + # Alternatly create a gcov disabled .config tools/testing/selftests/net/rds/config.sh + # Config paths may also be specified with the -c flag + tools/testing/selftests/net/rds/config.sh -c .config.local + # build the kernel - vng --build --config tools/testing/selftests/net/config + vng --build --config .config # launch the tests in a VM vng -v --rwdir ./ --run . --user root --cpus 4 -- \ - "export PYTHONPATH=tools/testing/selftests/net/; tools/testing/selftests/net/rds/run.sh" + "export PYTHONPATH=tools/testing/selftests/net/; \ + export SUDO_USER=example_user; \ + export RDS_LOG_DIR=tools/testing/selftests/net/rds/rds_logs; \ + tools/testing/selftests/net/rds/rds_run.sh -T tcp,rdma" -An HTML coverage report will be output in tools/testing/selftests/net/rds/rds_logs/coverage/. diff --git a/tools/testing/selftests/net/rds/config b/tools/testing/selftests/net/rds/config new file mode 100644 index 000000000000..97db7ecb892a --- /dev/null +++ b/tools/testing/selftests/net/rds/config @@ -0,0 +1,5 @@ +CONFIG_NET_NS=y +CONFIG_NET_SCH_NETEM=y +CONFIG_RDS=y +CONFIG_RDS_TCP=y +CONFIG_VETH=y diff --git a/tools/testing/selftests/net/rds/config.sh b/tools/testing/selftests/net/rds/config.sh index 791c8dbe1095..2df2226310ef 100755 --- a/tools/testing/selftests/net/rds/config.sh +++ b/tools/testing/selftests/net/rds/config.sh @@ -6,15 +6,24 @@ set -u set -x unset KBUILD_OUTPUT +CONF_FILE="" +FLAGS=() GENERATE_GCOV_REPORT=0 -while getopts "g" opt; do +ENABLE_RDMA=0 +while getopts "gc:r" opt; do case ${opt} in g) GENERATE_GCOV_REPORT=1 ;; + c) + CONF_FILE=$OPTARG + ;; + r) + ENABLE_RDMA=1 + ;; :) - echo "USAGE: config.sh [-g]" + echo "USAGE: config.sh [-g] [-c config] [-r]" exit 1 ;; ?) @@ -24,30 +33,36 @@ while getopts "g" opt; do esac done -CONF_FILE="tools/testing/selftests/net/config" - -# no modules -scripts/config --file "$CONF_FILE" --disable CONFIG_MODULES +if [[ "$CONF_FILE" != "" ]]; then + FLAGS=(--file "$CONF_FILE") +fi # enable RDS -scripts/config --file "$CONF_FILE" --enable CONFIG_RDS -scripts/config --file "$CONF_FILE" --enable CONFIG_RDS_TCP +scripts/config "${FLAGS[@]}" --enable CONFIG_RDS +scripts/config "${FLAGS[@]}" --enable CONFIG_RDS_TCP if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then # instrument RDS and only RDS - scripts/config --file "$CONF_FILE" --enable CONFIG_GCOV_KERNEL - scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_ALL - scripts/config --file "$CONF_FILE" --enable GCOV_PROFILE_RDS + scripts/config "${FLAGS[@]}" --enable CONFIG_GCOV_KERNEL + scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_ALL + scripts/config "${FLAGS[@]}" --enable GCOV_PROFILE_RDS else - scripts/config --file "$CONF_FILE" --disable CONFIG_GCOV_KERNEL - scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_ALL - scripts/config --file "$CONF_FILE" --disable GCOV_PROFILE_RDS + scripts/config "${FLAGS[@]}" --disable CONFIG_GCOV_KERNEL + scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_ALL + scripts/config "${FLAGS[@]}" --disable GCOV_PROFILE_RDS fi # need network namespaces to run tests with veth network interfaces -scripts/config --file "$CONF_FILE" --enable CONFIG_NET_NS -scripts/config --file "$CONF_FILE" --enable CONFIG_VETH +scripts/config "${FLAGS[@]}" --enable CONFIG_NET_NS +scripts/config "${FLAGS[@]}" --enable CONFIG_VETH # simulate packet loss -scripts/config --file "$CONF_FILE" --enable CONFIG_NET_SCH_NETEM +scripts/config "${FLAGS[@]}" --enable CONFIG_NET_SCH_NETEM +if [ "$ENABLE_RDMA" -eq 1 ]; then + # enable RDS over InfiniBand / RDMA (rds_rdma test) + scripts/config "${FLAGS[@]}" --enable CONFIG_INFINIBAND + scripts/config "${FLAGS[@]}" --enable CONFIG_INFINIBAND_ADDR_TRANS + scripts/config "${FLAGS[@]}" --enable CONFIG_RDMA_RXE + scripts/config "${FLAGS[@]}" --enable CONFIG_RDS_RDMA +fi diff --git a/tools/testing/selftests/net/rds/getsockopt.c b/tools/testing/selftests/net/rds/getsockopt.c new file mode 100644 index 000000000000..93ff252c69b8 --- /dev/null +++ b/tools/testing/selftests/net/rds/getsockopt.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Exercise the RDS getsockopt() paths that were converted to the + * getsockopt_iter() / sockopt_t callback. + * + * Three distinct paths are covered: + * + * - RDS_RECVERR and SO_RDS_TRANSPORT, which now return their int value + * through copy_to_iter() and report the written length in opt->optlen. + * + * - RDS_INFO_*, which pins the userspace buffer with + * iov_iter_extract_pages() (including a non-zero starting page offset) + * and lets the info producers memcpy the snapshot in under a spinlock. + * + * The kvec (in-kernel buffer) -> -EOPNOTSUPP path of rds_info_getsockopt() + * is not reachable from a userspace getsockopt() and so is not tested here. + */ +#include <errno.h> +#include <stdint.h> +#include <string.h> +#include <unistd.h> +#include <sys/mman.h> +#include <sys/socket.h> +#include <linux/rds.h> + +#include "../../kselftest_harness.h" + +#ifndef AF_RDS +#define AF_RDS 21 +#endif + +FIXTURE(rds) { + int fd; +}; + +FIXTURE_SETUP(rds) +{ + self->fd = socket(AF_RDS, SOCK_SEQPACKET, 0); + if (self->fd < 0) + SKIP(return, "AF_RDS unavailable (errno %d) - load the rds module", + errno); +} + +FIXTURE_TEARDOWN(rds) +{ + if (self->fd >= 0) + close(self->fd); +} + +/* RDS_RECVERR defaults to 0 and is reported back as a 4-byte int. */ +TEST_F(rds, recverr_default) +{ + socklen_t len = sizeof(int); + int val = 0xdeadbeef; + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, &len)); + EXPECT_EQ(sizeof(int), len); + EXPECT_EQ(0, val); +} + +/* A value set via setsockopt() must be readable back unchanged. */ +TEST_F(rds, recverr_set_get) +{ + socklen_t len = sizeof(int); + int val = 1; + + ASSERT_EQ(0, setsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, len)); + + val = 0; + ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, &val, &len)); + EXPECT_EQ(sizeof(int), len); + EXPECT_EQ(1, val); +} + +/* A buffer smaller than an int is rejected with EINVAL, not silently. */ +TEST_F(rds, recverr_short_buffer) +{ + socklen_t len = sizeof(int) - 1; + char buf[sizeof(int)]; + + EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_RECVERR, buf, &len)); + EXPECT_EQ(EINVAL, errno); +} + +/* An unbound socket reports RDS_TRANS_NONE for SO_RDS_TRANSPORT. */ +TEST_F(rds, transport_unbound) +{ + socklen_t len = sizeof(int); + int val = 0; + + ASSERT_EQ(0, getsockopt(self->fd, SOL_RDS, SO_RDS_TRANSPORT, &val, + &len)); + EXPECT_EQ(sizeof(int), len); + EXPECT_EQ(RDS_TRANS_NONE, (unsigned int)val); +} + +TEST_F(rds, transport_short_buffer) +{ + socklen_t len = sizeof(int) - 1; + char buf[sizeof(int)]; + + EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, SO_RDS_TRANSPORT, buf, + &len)); + EXPECT_EQ(EINVAL, errno); +} + +/* + * RDS_INFO_COUNTERS with a zero-length buffer is the "probe" call: it must + * fail with ENOSPC and report the required snapshot size in optlen. + */ +TEST_F(rds, info_counters_probe) +{ + socklen_t len = 0; + + EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, + &len)); + EXPECT_EQ(ENOSPC, errno); + EXPECT_GT(len, 0); + /* The snapshot is an array of fixed-size counter records. */ + EXPECT_EQ(0, len % (socklen_t)sizeof(struct rds_info_counter)); +} + +/* + * A real snapshot into an unaligned userspace buffer exercises the + * iov_iter_extract_pages() path, including the non-zero offset0 handling + * that the patch reworked. Place the buffer at a non-page-aligned address + * spanning into the next page to make sure multi-page pinning works too. + */ +TEST_F(rds, info_counters_snapshot) +{ + struct rds_info_counter *ctr; + socklen_t need = 0, len; + long pagesz = sysconf(_SC_PAGESIZE); + size_t offset, map_len; + unsigned int i, n; + char *region, *buf; + int ret; + + /* Probe for the required size. */ + getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, &need); + ASSERT_GT(need, 0); + + /* + * Place the buffer at a non-page-aligned offset that runs past the + * first page boundary, and size the mapping from the probed length so + * the test keeps working if the counter set grows. + */ + offset = pagesz - 64; + map_len = ((offset + need + pagesz - 1) / pagesz) * pagesz; + + region = mmap(NULL, map_len, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, region); + + buf = region + offset; + + /* + * On success the RDS_INFO path returns the positive per-element size + * (lens.each) rather than 0, and writes the full snapshot length back + * into optlen. + */ + len = need; + ret = getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, buf, &len); + ASSERT_GE(ret, 0) { + TH_LOG("getsockopt snapshot failed: errno %d", errno); + } + EXPECT_EQ(sizeof(struct rds_info_counter), ret); + EXPECT_EQ(need, len); + + /* The counter names must be NUL-terminated, non-empty strings. */ + ctr = (struct rds_info_counter *)buf; + n = len / sizeof(*ctr); + ASSERT_GT(n, 0); + for (i = 0; i < n; i++) { + size_t namelen = strnlen((char *)ctr[i].name, + sizeof(ctr[i].name)); + + EXPECT_GT(namelen, 0); + EXPECT_LT(namelen, sizeof(ctr[i].name)); + } + + munmap(region, map_len); +} + +/* + * A non-zero but too-small buffer must report ENOSPC and the full required + * length, without corrupting memory past the buffer. + */ +TEST_F(rds, info_counters_short_buffer) +{ + socklen_t need = 0, len; + char small[sizeof(struct rds_info_counter)]; + + getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, NULL, &need); + ASSERT_GT(need, 0); + + /* Ask with a buffer guaranteed smaller than the full snapshot. */ + if (need <= (socklen_t)sizeof(small)) + SKIP(return, "snapshot fits in one record; nothing to test"); + + len = 1; /* < sizeof(struct rds_info_counter) */ + EXPECT_EQ(-1, getsockopt(self->fd, SOL_RDS, RDS_INFO_COUNTERS, small, + &len)); + EXPECT_EQ(ENOSPC, errno); + EXPECT_EQ(need, len); +} + +TEST_HARNESS_MAIN diff --git a/tools/testing/selftests/net/rds/rds_run.sh b/tools/testing/selftests/net/rds/rds_run.sh new file mode 100755 index 000000000000..cdf487ec97dc --- /dev/null +++ b/tools/testing/selftests/net/rds/rds_run.sh @@ -0,0 +1,319 @@ +#! /bin/bash +# SPDX-License-Identifier: GPL-2.0 + +set -e +set -u + +unset KBUILD_OUTPUT + +current_dir="$(realpath "$(dirname "$0")")" +build_dir="$current_dir" + +build_include="$current_dir/include.sh" +if test -f "$build_include"; then + # this include will define "$mk_build_dir" as the location the test was + # built. We will need this if the tests are installed in a location + # other than the kernel source + + source "$build_include" + build_dir="$mk_build_dir" +fi + +# Source settings for timeout value (also used by ksft runner) +source "$current_dir"/settings + +# This test requires kernel source and the *.gcda data therein +# Locate the top level of the kernel source, and the net/rds +# subfolder with the appropriate *.gcno object files +ksrc_dir="$(realpath "$build_dir"/../../../../../)" +kconfig="$ksrc_dir/.config" +obj_dir="$ksrc_dir/net/rds" + +GCOV_CMD=gcov + +#check to see if the host has the required packages to generate a gcov report +check_gcov_env() +{ + if ! which "$GCOV_CMD" > /dev/null 2>&1; then + echo "# Warning: Could not find gcov. " + GENERATE_GCOV_REPORT=0 + return + fi + + # the gcov version must match the gcc version + GCC_VER=$(gcc -dumpfullversion) + GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| awk 'BEGIN {FS="-"}{print $1}') + if [ "$GCOV_VER" != "$GCC_VER" ]; then + #attempt to find a matching gcov version + GCOV_CMD=gcov-$(gcc -dumpversion) + + if ! which "$GCOV_CMD" > /dev/null 2>&1; then + echo "# Warning: Could not find an appropriate gcov installation. \ + gcov version must match gcc version" + GENERATE_GCOV_REPORT=0 + return + fi + + #recheck version number of found gcov executable + GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| \ + awk 'BEGIN {FS="-"}{print $1}') + if [ "$GCOV_VER" != "$GCC_VER" ]; then + echo "# Warning: Could not find an appropriate gcov installation. \ + gcov version must match gcc version" + GENERATE_GCOV_REPORT=0 + else + echo "# Warning: Mismatched gcc and gcov detected. Using $GCOV_CMD" + fi + fi +} + +# Check to see if the kconfig has the required configs to generate a coverage report +check_gcov_conf() +{ + if ! grep -x "CONFIG_GCOV_PROFILE_RDS=y" "$kconfig" > /dev/null 2>&1; then + echo "# INFO: CONFIG_GCOV_PROFILE_RDS should be enabled for coverage reports" + GENERATE_GCOV_REPORT=0 + fi + if ! grep -x "CONFIG_GCOV_KERNEL=y" "$kconfig" > /dev/null 2>&1; then + echo "# INFO: CONFIG_GCOV_KERNEL should be enabled for coverage reports" + GENERATE_GCOV_REPORT=0 + fi + if grep -x "CONFIG_GCOV_PROFILE_ALL=y" "$kconfig" > /dev/null 2>&1; then + echo "# INFO: CONFIG_GCOV_PROFILE_ALL should be disabled for coverage reports" + GENERATE_GCOV_REPORT=0 + fi + + if [ "$GENERATE_GCOV_REPORT" -eq 0 ]; then + echo "# To enable gcov reports, please run "\ + "\"tools/testing/selftests/net/rds/config.sh -g\" and rebuild the kernel" + else + # if we have the required kernel configs, proceed to check the environment to + # ensure we have the required gcov packages + check_gcov_env + fi +} + +# Checks if a kconfig is enabled (set to =y or =m) +# $1: kconfig symbol to check +# $2: (optional) module name backing $1 +# Ex: check_conf_enabled CONFIG_RDS_TCP rds_tcp +# Modules for configs set to =m will be probed +# If omitted, only a built-in (=y) config is accepted. +# Returns on success. exits 4 on failure +# Kselftest framework requirement - SKIP code is 4. +check_conf_enabled() { + if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then + return + fi + if [ -n "${2:-}" ] && grep -x "$1=m" "$kconfig" > /dev/null 2>&1; then + probe_module "$2" + return + fi + echo "selftests: [SKIP] This test requires $1 enabled" + echo "Please run" \ + "tools/testing/selftests/net/rds/config.sh and rebuild the kernel" + exit 4 +} + +check_rdma_conf_enabled() { + if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then + return + fi + if [ -n "${2:-}" ] && grep -x "$1=m" "$kconfig" > /dev/null 2>&1; then + probe_module "$2" + return + fi + echo "selftests: [XFAIL] rdma transport requires $1 enabled" + echo "To enable, run" \ + "tools/testing/selftests/net/rds/config.sh -r and rebuild" + exit 2 +} + +# Load the module backing a config that is built as a loadable module +# (=m). Built-in (=y) configs are already available and don't reach +# here. Exits with the SKIP code if a required module cannot be loaded. +probe_module() { + if ! modprobe -q "$1"; then + echo "selftests: [SKIP] could not load required module $1" + exit 4 + fi +} + +check_conf() { + check_conf_enabled CONFIG_NET_SCH_NETEM sch_netem + check_conf_enabled CONFIG_VETH veth + check_conf_enabled CONFIG_NET_NS + check_conf_enabled CONFIG_RDS_TCP rds_tcp + check_conf_enabled CONFIG_RDS rds +} + +# Check kernel config and host environment for RDS-RDMA support. +# Exits with XFAIL (2) if the user requested rdma but prerequisites +# are not met. +check_rdma_conf() +{ + case "$TRANSPORT" in + *rdma*) ;; + *) return ;; + esac + + # Kconfig will enforce CONFIG_INFINIBAND_* as dependencies + # of CONFIG_RDMA_RXE + check_rdma_conf_enabled CONFIG_RDMA_RXE rdma_rxe + check_rdma_conf_enabled CONFIG_RDS_RDMA rds_rdma + + if ! which rdma > /dev/null 2>&1; then + echo "selftests: [XFAIL] rdma transport requires the 'rdma'" \ + "tool (iproute2)" + exit 2 + fi +} + +check_env() +{ + if ! test -d "$obj_dir"; then + echo "selftests: [SKIP] This test requires a kernel source tree" + exit 4 + fi + if ! test -e "$kconfig"; then + echo "selftests: [SKIP] This test requires a configured kernel source tree" + exit 4 + fi + if ! which strace > /dev/null 2>&1; then + echo "selftests: [SKIP] Could not run test without strace" + exit 4 + fi + if ! which tcpdump > /dev/null 2>&1; then + echo "selftests: [SKIP] Could not run test without tcpdump" + exit 4 + fi + + if ! which python3 > /dev/null 2>&1; then + echo "selftests: [SKIP] Could not run test without python3" + exit 4 + fi + + python_major=$(python3 -c "import sys; print(sys.version_info[0])") + python_minor=$(python3 -c "import sys; print(sys.version_info[1])") + if [[ python_major -lt 3 || ( python_major -eq 3 && python_minor -lt 9 ) ]] ; then + echo "selftests: [SKIP] Could not run test without at least python3.9" + python3 -V + exit 4 + fi +} + +LOG_DIR="${RDS_LOG_DIR:-}" +TIMEOUT=$timeout +GENERATE_GCOV_REPORT=1 +TRANSPORT=tcp +FLAGS=() + +while getopts "d:l:c:u:t:T:" opt; do + case ${opt} in + d) + LOG_DIR=${OPTARG} + ;; + l) + FLAGS+=("-l" "${OPTARG}") + ;; + c) + FLAGS+=("-c" "${OPTARG}") + ;; + t) + TIMEOUT=${OPTARG} + ;; + u) + FLAGS+=("-u" "${OPTARG}") + ;; + T) + TRANSPORT=${OPTARG} + ;; + :) + echo "USAGE: rds_run.sh [-d logdir] [-l packet_loss]" \ + "[-c packet_corruption] [-u packet_duplicate] [-t timeout]" \ + "[-T tcp|rdma|tcp,rdma]" + exit 1 + ;; + ?) + echo "Invalid option: -${OPTARG}." + exit 1 + ;; + esac +done + +# Validate transport tokens +IFS=',' read -ra transports <<< "$TRANSPORT" +for t in "${transports[@]}"; do + if [ "$t" != "tcp" ] && [ "$t" != "rdma" ]; then + echo "rds_run.sh: unknown transport '$t' (expected tcp or rdma)" + exit 1 + fi +done + +FLAGS+=("--transport" "${TRANSPORT}") + +check_env +check_conf +check_gcov_conf +check_rdma_conf + +TRACE_CMD=() +if [[ -n "$LOG_DIR" ]]; then + FLAGS+=("-d" "$LOG_DIR") + + TRACE_FILE="${LOG_DIR}/rds-strace.txt" + COVR_DIR="${LOG_DIR}/coverage/" + DMESG_FILE="${LOG_DIR}/rds-dmesg.out" + + mkdir -p "$LOG_DIR" + mkdir -p "$COVR_DIR" + + rm -f "$TRACE_FILE" + rm -f "$DMESG_FILE" + rm -f "$LOG_DIR"/rds-*.pcap + rm -f "$COVR_DIR"/gcovr* + + echo "# Traces will be logged to ${TRACE_FILE}" + TRACE_CMD=(strace -T -tt -o "${TRACE_FILE}") +fi + +set +e +echo "# running RDS tests..." +"${TRACE_CMD[@]}" python3 "$(dirname "$0")/test.py" "${FLAGS[@]}" -t "$TIMEOUT" + +test_rc=$? + +if [[ -n "$LOG_DIR" ]]; then + dmesg > "${DMESG_FILE}" +fi + +if [[ -n "$LOG_DIR" ]] && [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then + echo "# saving coverage data..." + + # Ensure debugfs is mounted before reading gcov data. + if ! mountpoint -q /sys/kernel/debug 2>/dev/null; then + mount -t debugfs debugfs /sys/kernel/debug 2>/dev/null || true + fi + + (set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \ + while read -r f + do + cat < "/sys/kernel/debug/gcov/$f" > "/$f" + done) + + echo "# running gcovr..." + gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \ + --root "${ksrc_dir}" -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/" \ + > "${LOG_DIR}/gcovr.log" 2>&1 + echo "# gcovr log: ${LOG_DIR}/gcovr.log" +else + echo "# Coverage report will be skipped" +fi + +if [ "$test_rc" -eq 0 ]; then + echo "# PASS: Test completed successfully" +else + echo "# FAIL: Test failed" +fi + +exit "$test_rc" diff --git a/tools/testing/selftests/net/rds/run.sh b/tools/testing/selftests/net/rds/run.sh deleted file mode 100755 index 8aee244f582a..000000000000 --- a/tools/testing/selftests/net/rds/run.sh +++ /dev/null @@ -1,224 +0,0 @@ -#! /bin/bash -# SPDX-License-Identifier: GPL-2.0 - -set -e -set -u - -unset KBUILD_OUTPUT - -current_dir="$(realpath "$(dirname "$0")")" -build_dir="$current_dir" - -build_include="$current_dir/include.sh" -if test -f "$build_include"; then - # this include will define "$mk_build_dir" as the location the test was - # built. We will need this if the tests are installed in a location - # other than the kernel source - - source "$build_include" - build_dir="$mk_build_dir" -fi - -# This test requires kernel source and the *.gcda data therein -# Locate the top level of the kernel source, and the net/rds -# subfolder with the appropriate *.gcno object files -ksrc_dir="$(realpath "$build_dir"/../../../../../)" -kconfig="$ksrc_dir/.config" -obj_dir="$ksrc_dir/net/rds" - -GCOV_CMD=gcov - -#check to see if the host has the required packages to generate a gcov report -check_gcov_env() -{ - if ! which "$GCOV_CMD" > /dev/null 2>&1; then - echo "Warning: Could not find gcov. " - GENERATE_GCOV_REPORT=0 - return - fi - - # the gcov version must match the gcc version - GCC_VER=$(gcc -dumpfullversion) - GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| awk 'BEGIN {FS="-"}{print $1}') - if [ "$GCOV_VER" != "$GCC_VER" ]; then - #attempt to find a matching gcov version - GCOV_CMD=gcov-$(gcc -dumpversion) - - if ! which "$GCOV_CMD" > /dev/null 2>&1; then - echo "Warning: Could not find an appropriate gcov installation. \ - gcov version must match gcc version" - GENERATE_GCOV_REPORT=0 - return - fi - - #recheck version number of found gcov executable - GCOV_VER=$($GCOV_CMD -v | grep gcov | awk '{print $3}'| \ - awk 'BEGIN {FS="-"}{print $1}') - if [ "$GCOV_VER" != "$GCC_VER" ]; then - echo "Warning: Could not find an appropriate gcov installation. \ - gcov version must match gcc version" - GENERATE_GCOV_REPORT=0 - else - echo "Warning: Mismatched gcc and gcov detected. Using $GCOV_CMD" - fi - fi -} - -# Check to see if the kconfig has the required configs to generate a coverage report -check_gcov_conf() -{ - if ! grep -x "CONFIG_GCOV_PROFILE_RDS=y" "$kconfig" > /dev/null 2>&1; then - echo "INFO: CONFIG_GCOV_PROFILE_RDS should be enabled for coverage reports" - GENERATE_GCOV_REPORT=0 - fi - if ! grep -x "CONFIG_GCOV_KERNEL=y" "$kconfig" > /dev/null 2>&1; then - echo "INFO: CONFIG_GCOV_KERNEL should be enabled for coverage reports" - GENERATE_GCOV_REPORT=0 - fi - if grep -x "CONFIG_GCOV_PROFILE_ALL=y" "$kconfig" > /dev/null 2>&1; then - echo "INFO: CONFIG_GCOV_PROFILE_ALL should be disabled for coverage reports" - GENERATE_GCOV_REPORT=0 - fi - - if [ "$GENERATE_GCOV_REPORT" -eq 0 ]; then - echo "To enable gcov reports, please run "\ - "\"tools/testing/selftests/net/rds/config.sh -g\" and rebuild the kernel" - else - # if we have the required kernel configs, proceed to check the environment to - # ensure we have the required gcov packages - check_gcov_env - fi -} - -# Kselftest framework requirement - SKIP code is 4. -check_conf_enabled() { - if ! grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then - echo "selftests: [SKIP] This test requires $1 enabled" - echo "Please run tools/testing/selftests/net/rds/config.sh and rebuild the kernel" - exit 4 - fi -} -check_conf_disabled() { - if grep -x "$1=y" "$kconfig" > /dev/null 2>&1; then - echo "selftests: [SKIP] This test requires $1 disabled" - echo "Please run tools/testing/selftests/net/rds/config.sh and rebuild the kernel" - exit 4 - fi -} -check_conf() { - check_conf_enabled CONFIG_NET_SCH_NETEM - check_conf_enabled CONFIG_VETH - check_conf_enabled CONFIG_NET_NS - check_conf_enabled CONFIG_RDS_TCP - check_conf_enabled CONFIG_RDS - check_conf_disabled CONFIG_MODULES -} - -check_env() -{ - if ! test -d "$obj_dir"; then - echo "selftests: [SKIP] This test requires a kernel source tree" - exit 4 - fi - if ! test -e "$kconfig"; then - echo "selftests: [SKIP] This test requires a configured kernel source tree" - exit 4 - fi - if ! which strace > /dev/null 2>&1; then - echo "selftests: [SKIP] Could not run test without strace" - exit 4 - fi - if ! which tcpdump > /dev/null 2>&1; then - echo "selftests: [SKIP] Could not run test without tcpdump" - exit 4 - fi - - if ! which python3 > /dev/null 2>&1; then - echo "selftests: [SKIP] Could not run test without python3" - exit 4 - fi - - python_major=$(python3 -c "import sys; print(sys.version_info[0])") - python_minor=$(python3 -c "import sys; print(sys.version_info[1])") - if [[ python_major -lt 3 || ( python_major -eq 3 && python_minor -lt 9 ) ]] ; then - echo "selftests: [SKIP] Could not run test without at least python3.9" - python3 -V - exit 4 - fi -} - -LOG_DIR="$current_dir"/rds_logs -PLOSS=0 -PCORRUPT=0 -PDUP=0 -GENERATE_GCOV_REPORT=1 -while getopts "d:l:c:u:" opt; do - case ${opt} in - d) - LOG_DIR=${OPTARG} - ;; - l) - PLOSS=${OPTARG} - ;; - c) - PCORRUPT=${OPTARG} - ;; - u) - PDUP=${OPTARG} - ;; - :) - echo "USAGE: run.sh [-d logdir] [-l packet_loss] [-c packet_corruption]" \ - "[-u packet_duplcate] [-g]" - exit 1 - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done - - -check_env -check_conf -check_gcov_conf - - -rm -fr "$LOG_DIR" -TRACE_FILE="${LOG_DIR}/rds-strace.txt" -COVR_DIR="${LOG_DIR}/coverage/" -mkdir -p "$LOG_DIR" -mkdir -p "$COVR_DIR" - -set +e -echo running RDS tests... -echo Traces will be logged to "$TRACE_FILE" -rm -f "$TRACE_FILE" -strace -T -tt -o "$TRACE_FILE" python3 "$(dirname "$0")/test.py" --timeout 400 -d "$LOG_DIR" \ - -l "$PLOSS" -c "$PCORRUPT" -u "$PDUP" - -test_rc=$? -dmesg > "${LOG_DIR}/dmesg.out" - -if [ "$GENERATE_GCOV_REPORT" -eq 1 ]; then - echo saving coverage data... - (set +x; cd /sys/kernel/debug/gcov; find ./* -name '*.gcda' | \ - while read -r f - do - cat < "/sys/kernel/debug/gcov/$f" > "/$f" - done) - - echo running gcovr... - gcovr -s --html-details --gcov-executable "$GCOV_CMD" --gcov-ignore-parse-errors \ - -o "${COVR_DIR}/gcovr" "${ksrc_dir}/net/rds/" -else - echo "Coverage report will be skipped" -fi - -if [ "$test_rc" -eq 0 ]; then - echo "PASS: Test completed successfully" -else - echo "FAIL: Test failed" -fi - -exit "$test_rc" diff --git a/tools/testing/selftests/net/rds/settings b/tools/testing/selftests/net/rds/settings new file mode 100644 index 000000000000..8cb41e6a83cc --- /dev/null +++ b/tools/testing/selftests/net/rds/settings @@ -0,0 +1 @@ +timeout=800 diff --git a/tools/testing/selftests/net/rds/test.py b/tools/testing/selftests/net/rds/test.py index 4a7178d11193..9e4df01cb0d4 100755 --- a/tools/testing/selftests/net/rds/test.py +++ b/tools/testing/selftests/net/rds/test.py @@ -1,77 +1,486 @@ #! /usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 +""" +This module provides functional testing for the net/rds component. +""" import argparse +import atexit import ctypes import errno import hashlib import os import select +import re import signal import socket import subprocess import sys -import atexit -from pwd import getpwuid -from os import stat +import time # Allow utils module to be imported from different directory this_dir = os.path.dirname(os.path.realpath(__file__)) sys.path.append(os.path.join(this_dir, "../")) -from lib.py.utils import ip +# pylint: disable-next=wrong-import-position,import-error,no-name-in-module +from lib.py.utils import ip, cmd # noqa: E402 +# pylint: disable-next=wrong-import-position,import-error,no-name-in-module +from lib.py.ksft import ksft_pr # noqa: E402 libc = ctypes.cdll.LoadLibrary('libc.so.6') setns = libc.setns -net0 = 'net0' -net1 = 'net1' +NET0 = 'net0' +NET1 = 'net1' + +VETH0 = 'veth0' +VETH1 = 'veth1' + +tcpdump_procs = [] +tcp_addrs = [ + # we technically don't need different port numbers, but this will + # help identify traffic in the network analyzer + ('10.0.0.1', 10000), + ('10.0.0.2', 20000), +] -veth0 = 'veth0' -veth1 = 'veth1' +# RDMA network configs +RXE_DEV0 = 'rxe0' +RXE_DEV1 = 'rxe1' + +VETH_RDMA0 = 'veth_rdma0' +VETH_RDMA1 = 'veth_rdma1' + +rdma_addrs = [ + ('10.0.0.3', 30000), + ('10.0.0.4', 30000), +] + +# send_packets flag space +OP_FLAG_TCP = 0x1 +OP_FLAG_RDMA = 0x2 + +# from include/uapi/linux/rds.h: SO_RDS_TRANSPORT pins a socket to a +# specific RDS transport so connection setup cannot silently fall back +# to another (e.g. loopback) transport. +SOL_RDS = 276 +SO_RDS_TRANSPORT = 8 +RDS_TRANS_TCP = 2 +RDS_TRANS_IB = 0 + +signal_handler_label = "" + +tap_idx = 0 +nr_pass = 0 +nr_fail = 0 # Helper function for creating a socket inside a network namespace. # We need this because otherwise RDS will detect that the two TCP # sockets are on the same interface and use the loop transport instead # of the TCP transport. -def netns_socket(netns, *args): +def netns_socket(netns, *sock_args): + """ + Creates sockets inside of network namespace + + :param netns: the name of the network namespace + :param sock_args: socket family and type + """ u0, u1 = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET) child = os.fork() if child == 0: - # change network namespace - with open(f'/var/run/netns/{netns}') as f: - try: - ret = setns(f.fileno(), 0) - except IOError as e: - print(e.errno) - print(e) - - # create socket in target namespace - s = socket.socket(*args) + try: + # change network namespace + with open(f'/var/run/netns/{netns}', encoding='utf-8') as f: + setns(f.fileno(), 0) + # create socket in target namespace + sock = socket.socket(*sock_args) - # send resulting socket to parent - socket.send_fds(u0, [], [s.fileno()]) + # send resulting socket to parent + socket.send_fds(u0, [], [sock.fileno()]) - sys.exit(0) + os._exit(0) + except BaseException: + os._exit(1) # receive socket from child - _, s, _, _ = socket.recv_fds(u1, 0, 1) - os.waitpid(child, 0) + _, fds, _, _ = socket.recv_fds(u1, 0, 1) + _, status = os.waitpid(child, 0) u0.close() u1.close() - return socket.fromfd(s[0], *args) + if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0: + raise RuntimeError( + f"netns_socket child failed in netns {netns} (status={status})") + return socket.fromfd(fds[0], *sock_args) -def signal_handler(sig, frame): - print('Test timed out') +def send_burst(socks, ip_addrs, snd_hashes, nr_sent, nr_total): + """Send until blocked or nr_total reached. Return updated nr_sent.""" + + while nr_sent < nr_total: + data = hashlib.sha256( + f'packet {nr_sent}'.encode('utf-8')).hexdigest().encode('utf-8') + # pseudo-random send/receive pattern + snd_idx = nr_sent % 2 + rcv_idx = 1 - (nr_sent % 3) % 2 + + snd = socks[snd_idx] + rcv = socks[rcv_idx] + try: + snd.sendto(data, ip_addrs[rcv_idx]) + except BlockingIOError: + return nr_sent + except OSError as e: + if e.errno in (errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE): + return nr_sent + raise + snd_hashes.setdefault((snd.fileno(), rcv.fileno()), + hashlib.sha256()).update(f'<{data}>'.encode('utf-8')) + nr_sent += 1 + return nr_sent + +def recv_burst(epoll, socks, ip_addrs, rcv_hashes, nr_rcv): + """Drain whatever's readable from epoll. Return updated nr_recv.""" + for filen, evntmask in epoll.poll(): + if not evntmask & select.EPOLLRDNORM: + continue + rcv = next(s for s in socks if s.fileno() == filen) + while True: + try: + data, adr = rcv.recvfrom(1024) + except BlockingIOError: + break + snd_idx = ip_addrs.index(adr) + snd = socks[snd_idx] + rcv_hashes.setdefault((snd.fileno(), rcv.fileno()), + hashlib.sha256()).update(f'<{data}>'.encode('utf-8')) + nr_rcv += 1 + return nr_rcv + +def check_info(socks): + """ + Check all rds info pages for errors + + :param socks: list of sockets to check + """ + + # the Python socket module doesn't know these + rds_info_first = 10000 + rds_info_last = 10017 + + nr_success = 0 + nr_error = 0 + + for sock in socks: + for optname in range(rds_info_first, rds_info_last + 1): + # Sigh, the Python socket module doesn't allow us to pass + # buffer lengths greater than 1024 for some reason. RDS + # wants multiple pages. + try: + sock.getsockopt(socket.SOL_RDS, optname, 1024) + nr_success = nr_success + 1 + except OSError as e: + nr_error = nr_error + 1 + if e.errno == errno.ENOSPC: + # ignore + pass + + ksft_pr(f"getsockopt(): {nr_success}/{nr_error}") + +def verify_hashes(snd_hashes, rcv_hashes): + """Compare send/recv hashes per (sender, receiver) pair.""" + for key, snd_hash in snd_hashes.items(): + rcv_hash = rcv_hashes.get(key) + if rcv_hash is None: + ksft_pr("FAIL: No data received") + return 1 + if snd_hash.hexdigest() != rcv_hash.hexdigest(): + ksft_pr("FAIL: Send/recv mismatch") + ksft_pr("hash expected:", snd_hash.hexdigest()) + ksft_pr("hash received:", rcv_hash.hexdigest()) + return 1 + ksft_pr(f"{key[0]}/{key[1]}: ok") + return 0 + +def snd_rcv_packets(env): + """ + Send packets on the given network interfaces + + :param env: transport-environment dict for setup_tcp() / setup_rdma(). + "addrs": list of (ip, port) tuples matching the sockets + "netns": list of netns names for TCP or None for RDMA + "flags": OP_FLAG_TCP or OP_FLAG_RDMA, selects sockets + """ + + addrs = env["addrs"] + netns_list = env["netns"] + flags = env.get("flags", 0) + + if (flags & OP_FLAG_TCP) and (flags & OP_FLAG_RDMA): + raise RuntimeError(f"Invalid transport flag sets multiple transports: {flags}") + + if flags & OP_FLAG_TCP: + sockets = [ + netns_socket(netns_list[0], socket.AF_RDS, socket.SOCK_SEQPACKET), + netns_socket(netns_list[1], socket.AF_RDS, socket.SOCK_SEQPACKET), + ] + + # Pin the sockets to the TCP transport so it doesn't fail over to a + # different transport during this test + for s in sockets: + s.setsockopt(SOL_RDS, SO_RDS_TRANSPORT, RDS_TRANS_TCP) + elif flags & OP_FLAG_RDMA: + sockets = [ + socket.socket(socket.AF_RDS, socket.SOCK_SEQPACKET), + socket.socket(socket.AF_RDS, socket.SOCK_SEQPACKET), + ] + + # Pin the sockets to the RDMA transport so it doesn't fail over to a + # different transport during this test + for s in sockets: + s.setsockopt(SOL_RDS, SO_RDS_TRANSPORT, RDS_TRANS_IB) + else: + raise RuntimeError(f"Invalid transport flag sets no transports: {flags}") + + for s, addr in zip(sockets, addrs): + s.bind(addr) + s.setblocking(0) + + send_hashes = {} + recv_hashes = {} + + ep = select.epoll() + + for s in sockets: + ep.register(s, select.EPOLLRDNORM) + + num_packets = 50000 + nr_send = 0 + nr_recv = 0 + + while nr_send < num_packets: + + # Send as much as we can without blocking + ksft_pr("sending...", nr_send, nr_recv) + nr_send = send_burst(sockets, addrs, send_hashes, nr_send, num_packets) + + # Receive as much as we can without blocking + ksft_pr("receiving...", nr_send, nr_recv) + while nr_recv < nr_send: + nr_recv = recv_burst(ep, sockets, addrs, recv_hashes, nr_recv) + + # exercise net/rds/tcp.c:rds_tcp_sysctl_reset() + if netns_list: + for net in netns_list: + ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000") + ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000") + + ksft_pr("done", nr_send, nr_recv) + + check_info(sockets) + + # We're done sending and receiving stuff, now let's check if what + # we received is what we sent. + rc = verify_hashes(send_hashes, recv_hashes) + + ep.close() + for s in sockets: + s.close() + + return rc + +def stop_pcaps(): + """Stop tcpdump processes. + + We use pop() here to drain the list in the event that the test + completes after the signal handler is fired. List will be empty + if logdir is not set + """ + + if not tcpdump_procs: + return + + ksft_pr("Stopping network packet captures") + while tcpdump_procs: + proc = tcpdump_procs.pop() + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + +def signal_handler(_sig, _frame): + """ + Test timed out signal handler + """ + ksft_pr(f"Test timed out: {signal_handler_label}") + print(f"not ok {tap_idx} rds selftest {signal_handler_label}") sys.exit(1) +def setup_tcp(): + """ + Configure tcp network + """ + + # clean up any leftovers from a previously interrupted run + teardown_tcp() + + ip(f"netns add {NET0}") + ip(f"netns add {NET1}") + ip("link add type veth") + + # Move TCP interfaces into separate namespaces so they can no longer be + # bound directly; this prevents rds from switching over from the tcp + # transport to the loop transport. + ip(f"link set {VETH0} netns {NET0} up") + ip(f"link set {VETH1} netns {NET1} up") + + # add addresses + ip(f"-n {NET0} addr add {tcp_addrs[0][0]}/32 dev {VETH0}") + ip(f"-n {NET1} addr add {tcp_addrs[1][0]}/32 dev {VETH1}") + + # add routes + ip(f"-n {NET0} route add {tcp_addrs[1][0]}/32 dev {VETH0}") + ip(f"-n {NET1} route add {tcp_addrs[0][0]}/32 dev {VETH1}") + + # sanity check that our two interfaces/addresses are correctly set up + # and communicating by doing a single ping + ip(f"netns exec {NET0} ping -c 1 {tcp_addrs[1][0]}") + + # Start a packet capture on each network + if logdir is not None: + for netn in [NET0, NET1]: + pcap = logdir+'/rds-'+netn+'.pcap' + + tcpdump_cmd = ['ip', 'netns', 'exec', netn, '/usr/sbin/tcpdump'] + sudo_user = os.environ.get('SUDO_USER') + if sudo_user: + tcpdump_cmd.extend(['-Z', sudo_user]) + tcpdump_cmd.extend(['-i', 'any', '-w', pcap]) + + # pylint: disable-next=consider-using-with + p = subprocess.Popen(tcpdump_cmd, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + tcpdump_procs.append(p) + + # simulate packet loss, duplication and corruption + for netn, iface in [(NET0, VETH0), (NET1, VETH1)]: + ip(f"netns exec {netn} /usr/sbin/tc qdisc add dev {iface} root netem \ + corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \ + {PACKET_DUPLICATE}") + +def teardown_tcp(): + """ + Tear down the tcp network configured by setup_tcp(). + + Removing the namespaces also removes the veth pair, addresses, + routes, and netem qdisc that live inside them. fail=False so + this is safe to call in error paths after a partial or complete setup. + """ + cmd(f"ip netns del {NET0}", fail=False) + cmd(f"ip netns del {NET1}", fail=False) + +def get_iface_mac(iface): + """Return the MAC address of a local network interface.""" + out = subprocess.check_output(['ip', 'link', 'show', iface], text=True) + mac = re.search(r'link/ether\s+([0-9a-f:]+)', out) + if not mac: + raise RuntimeError(f"Cannot determine MAC address of {iface}") + return mac.group(1) + +def setup_rdma(): + """ + Configure rdma network + """ + + # remove links left over by previously interrupted run. + teardown_rdma() + + # use call here since modprobe may fail if the rdma_rxe + # module is built-in + subprocess.call(['modprobe', 'rdma_rxe'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + ip(f"link add {VETH_RDMA0} type veth peer name {VETH_RDMA1}") + + ip(f"link set {VETH_RDMA0} up") + ip(f"link set {VETH_RDMA1} up") + + # Since both addresses are in the same namespace, the source address + # is always local, so enable accept_local + cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA0}.accept_local=1") + cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA1}.accept_local=1") + + # Reverse path filters must be disabled so that the local routes don't + # cause RPF failures. + cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA0}.rp_filter=0") + cmd(f"/usr/sbin/sysctl -q net.ipv4.conf.{VETH_RDMA1}.rp_filter=0") + + # add addresses + ip(f"addr add {rdma_addrs[0][0]}/32 dev {VETH_RDMA0}") + ip(f"addr add {rdma_addrs[1][0]}/32 dev {VETH_RDMA1}") + + # add routes + ip(f"route add {rdma_addrs[1][0]}/32 dev {VETH_RDMA0}") + ip(f"route add {rdma_addrs[0][0]}/32 dev {VETH_RDMA1}") + + # ARP will not resolve neighbor IPs on /32 routes without a subnet. + # Avoid this by adding neighbors directly so RDMA CM can populate path + # records with correct mac addrs without waiting for the ARP. + mac0 = get_iface_mac(VETH_RDMA0) + mac1 = get_iface_mac(VETH_RDMA1) + ip(f"neigh add {rdma_addrs[1][0]} lladdr {mac1} dev {VETH_RDMA0} nud permanent") + ip(f"neigh add {rdma_addrs[0][0]} lladdr {mac0} dev {VETH_RDMA1} nud permanent") + + cmd(f'rdma link add {RXE_DEV0} type rxe netdev {VETH_RDMA0}') + cmd(f'rdma link add {RXE_DEV1} type rxe netdev {VETH_RDMA1}') + + time.sleep(1) # allow RXE devices to initialise + + # Start a packet capture on each network + if logdir is not None: + for iface in [VETH_RDMA0, VETH_RDMA1]: + pcap = logdir+'/rds-roce-'+iface+'.pcap' + + tcpdump_cmd = ['/usr/sbin/tcpdump'] + sudo_user = os.environ.get('SUDO_USER') + if sudo_user: + tcpdump_cmd.extend(['-Z', sudo_user]) + tcpdump_cmd.extend(['-i', iface, '-w', pcap]) + + # pylint: disable-next=consider-using-with + p = subprocess.Popen(tcpdump_cmd, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + tcpdump_procs.append(p) + + # simulate packet loss, duplication and corruption + for iface in [VETH_RDMA0, VETH_RDMA1]: + cmd(f"/usr/sbin/tc qdisc add dev {iface} root netem \ + corrupt {PACKET_CORRUPTION} loss {PACKET_LOSS} duplicate \ + {PACKET_DUPLICATE}") + +def teardown_rdma(): + """ + Tear down the rdma network configured by setup_rdma(). + """ + + # remove links left over by previously interrupted run. + cmd(f'rdma link del {RXE_DEV0}', fail=False) + cmd(f'rdma link del {RXE_DEV1}', fail=False) + cmd(f'ip link del {VETH_RDMA0}', fail=False) + + #Parse out command line arguments. We take an optional # timeout parameter and an optional log output folder parser = argparse.ArgumentParser(description="init script args", formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("-d", "--logdir", action="store", - help="directory to store logs", default="/tmp") -parser.add_argument('--timeout', help="timeout to terminate hung test", + help="directory to store logs", default=None) +parser.add_argument("-T", "--transport", default="tcp", + help="Comma-separated list of transports to test: " + "tcp, rdma, or tcp,rdma. Each matching test " + "is run once per transport. " + "'rdma' requires CONFIG_RDS_RDMA and rdma_rxe.") +parser.add_argument('-t', '--timeout', help="timeout to terminate hung test", type=int, default=0) parser.add_argument('-l', '--loss', help="Simulate tcp packet loss", type=int, default=0) @@ -81,185 +490,67 @@ parser.add_argument('-u', '--duplicate', help="Simulate tcp packet duplication", type=int, default=0) args = parser.parse_args() logdir=args.logdir -packet_loss=str(args.loss)+'%' -packet_corruption=str(args.corruption)+'%' -packet_duplicate=str(args.duplicate)+'%' - -ip(f"netns add {net0}") -ip(f"netns add {net1}") -ip(f"link add type veth") - -addrs = [ - # we technically don't need different port numbers, but this will - # help identify traffic in the network analyzer - ('10.0.0.1', 10000), - ('10.0.0.2', 20000), -] - -# move interfaces to separate namespaces so they can no longer be -# bound directly; this prevents rds from switching over from the tcp -# transport to the loop transport. -ip(f"link set {veth0} netns {net0} up") -ip(f"link set {veth1} netns {net1} up") - - - -# add addresses -ip(f"-n {net0} addr add {addrs[0][0]}/32 dev {veth0}") -ip(f"-n {net1} addr add {addrs[1][0]}/32 dev {veth1}") - -# add routes -ip(f"-n {net0} route add {addrs[1][0]}/32 dev {veth0}") -ip(f"-n {net1} route add {addrs[0][0]}/32 dev {veth1}") - -# sanity check that our two interfaces/addresses are correctly set up -# and communicating by doing a single ping -ip(f"netns exec {net0} ping -c 1 {addrs[1][0]}") - -# Start a packet capture on each network -for net in [net0, net1]: - tcpdump_pid = os.fork() - if tcpdump_pid == 0: - pcap = logdir+'/'+net+'.pcap' - subprocess.check_call(['touch', pcap]) - user = getpwuid(stat(pcap).st_uid).pw_name - ip(f"netns exec {net} /usr/sbin/tcpdump -Z {user} -i any -w {pcap}") - sys.exit(0) - -# simulate packet loss, duplication and corruption -for net, iface in [(net0, veth0), (net1, veth1)]: - ip(f"netns exec {net} /usr/sbin/tc qdisc add dev {iface} root netem \ - corrupt {packet_corruption} loss {packet_loss} duplicate \ - {packet_duplicate}") - -# add a timeout -if args.timeout > 0: - signal.alarm(args.timeout) - signal.signal(signal.SIGALRM, signal_handler) - -sockets = [ - netns_socket(net0, socket.AF_RDS, socket.SOCK_SEQPACKET), - netns_socket(net1, socket.AF_RDS, socket.SOCK_SEQPACKET), -] - -for s, addr in zip(sockets, addrs): - s.bind(addr) - s.setblocking(0) - -fileno_to_socket = { - s.fileno(): s for s in sockets -} - -addr_to_socket = { - addr: s for addr, s in zip(addrs, sockets) -} - -socket_to_addr = { - s: addr for addr, s in zip(addrs, sockets) -} - -send_hashes = {} -recv_hashes = {} - -ep = select.epoll() - -for s in sockets: - ep.register(s, select.EPOLLRDNORM) - -n = 50000 -nr_send = 0 -nr_recv = 0 - -while nr_send < n: - # Send as much as we can without blocking - print("sending...", nr_send, nr_recv) - while nr_send < n: - send_data = hashlib.sha256( - f'packet {nr_send}'.encode('utf-8')).hexdigest().encode('utf-8') - - # pseudo-random send/receive pattern - sender = sockets[nr_send % 2] - receiver = sockets[1 - (nr_send % 3) % 2] - - try: - sender.sendto(send_data, socket_to_addr[receiver]) - send_hashes.setdefault((sender.fileno(), receiver.fileno()), - hashlib.sha256()).update(f'<{send_data}>'.encode('utf-8')) - nr_send = nr_send + 1 - except BlockingIOError as e: - break - except OSError as e: - if e.errno in [errno.ENOBUFS, errno.ECONNRESET, errno.EPIPE]: - break - raise - - # Receive as much as we can without blocking - print("receiving...", nr_send, nr_recv) - while nr_recv < nr_send: - for fileno, eventmask in ep.poll(): - receiver = fileno_to_socket[fileno] - - if eventmask & select.EPOLLRDNORM: - while True: - try: - recv_data, address = receiver.recvfrom(1024) - sender = addr_to_socket[address] - recv_hashes.setdefault((sender.fileno(), - receiver.fileno()), hashlib.sha256()).update( - f'<{recv_data}>'.encode('utf-8')) - nr_recv = nr_recv + 1 - except BlockingIOError as e: - break - - # exercise net/rds/tcp.c:rds_tcp_sysctl_reset() - for net in [net0, net1]: - ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_rcvbuf=10000") - ip(f"netns exec {net} /usr/sbin/sysctl net.rds.tcp.rds_tcp_sndbuf=10000") - -print("done", nr_send, nr_recv) - -# the Python socket module doesn't know these -RDS_INFO_FIRST = 10000 -RDS_INFO_LAST = 10017 - -nr_success = 0 -nr_error = 0 - -for s in sockets: - for optname in range(RDS_INFO_FIRST, RDS_INFO_LAST + 1): - # Sigh, the Python socket module doesn't allow us to pass - # buffer lengths greater than 1024 for some reason. RDS - # wants multiple pages. - try: - s.getsockopt(socket.SOL_RDS, optname, 1024) - nr_success = nr_success + 1 - except OSError as e: - nr_error = nr_error + 1 - if e.errno == errno.ENOSPC: - # ignore - pass - -print(f"getsockopt(): {nr_success}/{nr_error}") - -print("Stopping network packet captures") -subprocess.check_call(['killall', '-q', 'tcpdump']) - -# We're done sending and receiving stuff, now let's check if what -# we received is what we sent. -for (sender, receiver), send_hash in send_hashes.items(): - recv_hash = recv_hashes.get((sender, receiver)) - - if recv_hash is None: - print("FAIL: No data received") - sys.exit(1) - - if send_hash.hexdigest() != recv_hash.hexdigest(): - print("FAIL: Send/recv mismatch") - print("hash expected:", send_hash.hexdigest()) - print("hash received:", recv_hash.hexdigest()) - sys.exit(1) - - print(f"{sender}/{receiver}: ok") - -print("Success") -sys.exit(0) +PACKET_LOSS=str(args.loss)+'%' +PACKET_CORRUPTION=str(args.corruption)+'%' +PACKET_DUPLICATE=str(args.duplicate)+'%' + +# check transport is either tcp or rdma +transports = [t.strip() for t in args.transport.split(',')] +for t in transports: + if t not in ('tcp', 'rdma'): + raise SystemExit(f"test.py: unknown transport: {t!r}") + +# Register stop_pcaps before any network setups so that any partially setup +# tcpdumps are still cleaned up on error +atexit.register(stop_pcaps) + +# Set up all requested transports upfront so network plumbing is +# ready before any test runs. +transport_envs = {} +FLAGS = 0 +if 'tcp' in transports: + # Register cleanups before setups to handle partial setups that error'd out + atexit.register(teardown_tcp) + setup_tcp() + transport_envs['tcp'] = { + 'addrs': tcp_addrs, + 'netns': [NET0, NET1], + 'flags': FLAGS | OP_FLAG_TCP, + } + +if 'rdma' in transports: + atexit.register(teardown_rdma) + setup_rdma() + transport_envs['rdma'] = { + 'addrs': rdma_addrs, + 'netns': None, + 'flags': FLAGS | OP_FLAG_RDMA, + } + +print("TAP version 13") +print(f"1..{len(transport_envs)}") + +for transport, tenv in transport_envs.items(): + tap_idx += 1 + + # add a timeout + if args.timeout > 0: + signal_handler_label = transport + signal.alarm(args.timeout) + signal.signal(signal.SIGALRM, signal_handler) + + ret = snd_rcv_packets(tenv) + + # cancel timeout + signal.alarm(0) + + if ret == 0: + ksft_pr("Success") + print(f"ok {tap_idx} rds selftest {transport}") + nr_pass += 1 + else: + print(f"not ok {tap_idx} rds selftest {transport}") + nr_fail += 1 + +ksft_pr(f"Totals: pass:{nr_pass} fail:{nr_fail} skip:0") +sys.exit(1 if nr_fail else 0) diff --git a/tools/testing/selftests/net/reuseport_bpf.c b/tools/testing/selftests/net/reuseport_bpf.c index b6634d6da3d6..12e48b97b862 100644 --- a/tools/testing/selftests/net/reuseport_bpf.c +++ b/tools/testing/selftests/net/reuseport_bpf.c @@ -23,6 +23,7 @@ #include <sys/socket.h> #include <sys/resource.h> #include <unistd.h> +#include <sched.h> #include "kselftest.h" @@ -455,8 +456,18 @@ static __attribute__((destructor)) void main_dtor(void) setrlimit(RLIMIT_MEMLOCK, &rlim_old); } +static void setup_netns(void) +{ + if (unshare(CLONE_NEWNET)) + error(1, errno, "failed to unshare netns"); + if (system("ip link set lo up")) + error(1, 0, "failed to bring up lo interface in netns"); +} + int main(void) { + setup_netns(); + fprintf(stderr, "---- IPv4 UDP ----\n"); /* NOTE: UDP socket lookups traverse a different code path when there * are > 10 sockets in a group. Run the bpf test through both paths. diff --git a/tools/testing/selftests/net/reuseport_bpf_cpu.c b/tools/testing/selftests/net/reuseport_bpf_cpu.c index 2d646174729f..ddfe92f6597a 100644 --- a/tools/testing/selftests/net/reuseport_bpf_cpu.c +++ b/tools/testing/selftests/net/reuseport_bpf_cpu.c @@ -228,10 +228,20 @@ static void test(int *rcv_fd, int len, int family, int proto) close(rcv_fd[cpu]); } +static void setup_netns(void) +{ + if (unshare(CLONE_NEWNET)) + error(1, errno, "failed to unshare netns"); + if (system("ip link set lo up")) + error(1, 0, "failed to bring up lo interface in netns"); +} + int main(void) { int *rcv_fd, cpus; + setup_netns(); + cpus = sysconf(_SC_NPROCESSORS_ONLN); if (cpus <= 0) error(1, errno, "failed counting cpus"); diff --git a/tools/testing/selftests/net/reuseport_bpf_numa.c b/tools/testing/selftests/net/reuseport_bpf_numa.c index 2ffd957ffb15..6e4817ef57c5 100644 --- a/tools/testing/selftests/net/reuseport_bpf_numa.c +++ b/tools/testing/selftests/net/reuseport_bpf_numa.c @@ -104,6 +104,26 @@ static void attach_bpf(int fd) close(bpf_fd); } +/* + * Return true if it is a cpuless node. Return false if it isn't or any + * error (very unlikely) happens during the libnuma calls. + */ +static bool is_cpuless_node(int node_id) +{ + struct bitmask *cpumask; + bool ret = false; + + cpumask = numa_allocate_cpumask(); + if (!cpumask) + return ret; + + if (!numa_node_to_cpus(node_id, cpumask) && !numa_bitmask_weight(cpumask)) + ret = true; + + numa_bitmask_free(cpumask); + return ret; +} + static void send_from_node(int node_id, int family, int proto) { struct sockaddr_storage saddr, daddr; @@ -213,6 +233,8 @@ static void test(int *rcv_fd, int len, int family, int proto) for (node = 0; node < len; ++node) { if (!numa_bitmask_isbitset(numa_nodes_ptr, node)) continue; + if (is_cpuless_node(node)) + continue; send_from_node(node, family, proto); receive_on_node(rcv_fd, len, epfd, node, proto); } @@ -221,6 +243,8 @@ static void test(int *rcv_fd, int len, int family, int proto) for (node = len - 1; node >= 0; --node) { if (!numa_bitmask_isbitset(numa_nodes_ptr, node)) continue; + if (is_cpuless_node(node)) + continue; send_from_node(node, family, proto); receive_on_node(rcv_fd, len, epfd, node, proto); } @@ -230,10 +254,20 @@ static void test(int *rcv_fd, int len, int family, int proto) close(rcv_fd[node]); } +static void setup_netns(void) +{ + if (unshare(CLONE_NEWNET)) + error(1, errno, "failed to unshare netns"); + if (system("ip link set lo up")) + error(1, 0, "failed to bring up lo interface in netns"); +} + int main(void) { int *rcv_fd, nodes; + setup_netns(); + if (numa_available() < 0) ksft_exit_skip("no numa api support\n"); diff --git a/tools/testing/selftests/net/reuseport_dualstack.c b/tools/testing/selftests/net/reuseport_dualstack.c index fb7a59ed759e..0eaf739d0c85 100644 --- a/tools/testing/selftests/net/reuseport_dualstack.c +++ b/tools/testing/selftests/net/reuseport_dualstack.c @@ -25,6 +25,7 @@ #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> +#include <sched.h> static const int PORT = 8888; @@ -156,10 +157,20 @@ static void test(int *rcv_fds, int count, int proto) close(epfd); } +static void setup_netns(void) +{ + if (unshare(CLONE_NEWNET)) + error(1, errno, "failed to unshare netns"); + if (system("ip link set lo up")) + error(1, 0, "failed to bring up lo interface in netns"); +} + int main(void) { int rcv_fds[32], i; + setup_netns(); + fprintf(stderr, "---- UDP IPv4 created before IPv6 ----\n"); build_rcv_fd(AF_INET, SOCK_DGRAM, rcv_fds, 5); build_rcv_fd(AF_INET6, SOCK_DGRAM, &(rcv_fds[5]), 5); diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py index e9ad5e88da97..5cc3ebdcf08d 100755 --- a/tools/testing/selftests/net/rtnetlink.py +++ b/tools/testing/selftests/net/rtnetlink.py @@ -1,29 +1,324 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0 -from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily import socket +import struct +import time +from lib.py import bkg, ip, ksft_exit, ksft_run, ksft_eq, ksft_ge, ksft_true, KsftSkipEx +from lib.py import ksft_not_in, ksft_not_none +from lib.py import CmdExitFailure, NetNS, NetNSEnter, RtnlAddrFamily, RtnlRouteFamily +from lib.py import defer IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01' +IPV4_TEST_MULTICAST = b'\xef\x01\x01\x01' +IPV6_TEST_MULTICAST = bytes.fromhex('ff020000000000000000000000000123') -def dump_mcaddr_check(rtnl: RtnlAddrFamily) -> None: + +def _users_for(rtnl: RtnlAddrFamily, family: int, grp: bytes, ifindex: int): + """Return mc-users for grp on ifindex, or 0 if absent.""" + + addrs = rtnl.getmulticast({"ifa-family": family}, dump=True) + matches = [addr for addr in addrs + if addr['multicast'] == grp and addr['ifa-index'] == ifindex] + if not matches: + return 0 + if 'mc-users' not in matches[0]: + return None + + return matches[0]['mc-users'] + + +def dump_mcaddr_check() -> None: + """ + Verify IPv4 multicast addresses and their user counts in RTM_GETMULTICAST. + """ + + with NetNS() as ns: + with NetNSEnter(str(ns)): + ip("link set lo up") + rtnl = RtnlAddrFamily() + lo_idx = socket.if_nametoindex('lo') + addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True) + + all_host_multicasts = [ + addr for addr in addresses + if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST + ] + + ksft_ge(len(all_host_multicasts), 1, + "No interface found with the IPv4 all-hosts multicast address") + + mreq = IPV4_TEST_MULTICAST + socket.inet_aton('127.0.0.1') + before = _users_for(rtnl, socket.AF_INET, IPV4_TEST_MULTICAST, lo_idx) + if before is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS") + + s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s1.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + s2.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + + after_join = _users_for(rtnl, socket.AF_INET, + IPV4_TEST_MULTICAST, lo_idx) + if after_join is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS") + ksft_eq(after_join - before, 2, + f"users delta != 2 after two joins " + f"(before={before}, after={after_join})") + finally: + s1.close() + s2.close() + + +def dump_mcaddr6_check() -> None: + """ + Verify IPv6 multicast addresses and their user counts in RTM_GETMULTICAST. + """ + + with NetNS() as ns: + with NetNSEnter(str(ns)): + ip("link set lo up") + rtnl = RtnlAddrFamily() + lo_idx = socket.if_nametoindex('lo') + before = _users_for(rtnl, socket.AF_INET6, + IPV6_TEST_MULTICAST, lo_idx) + if before is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS for IPv6") + + mreq = IPV6_TEST_MULTICAST + struct.pack('=I', lo_idx) + s1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + try: + s1.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + s2.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq) + + after_join = _users_for(rtnl, socket.AF_INET6, + IPV6_TEST_MULTICAST, lo_idx) + if after_join is None: + raise KsftSkipEx("kernel does not expose IFA_MC_USERS for IPv6") + ksft_eq(after_join - before, 2, + f"IPv6 users delta != 2 after two joins " + f"(before={before}, after={after_join})") + finally: + s1.close() + s2.close() + + +def ipv4_devconf_notify() -> None: + """ + Configure an interface and set ipv4-devconf values through netlink + to verify that the appropriate netlink notifications are being sent. + """ + + with NetNS() as ns: + with NetNSEnter(str(ns)): + ifname = "dummy1" + ip(f"link add name {ifname} type dummy", ns=str(ns)) + + with bkg("ip monitor", ns=str(ns)) as cmd_obj: + time.sleep(1) + try: + ip(f"link set dev {ifname} inet forwarding on") + ip(f"link set dev {ifname} inet proxy_arp on") + ip(f"link set dev {ifname} inet rp_filter 1") + ip(f"link set dev {ifname} inet ignore_routes_with_linkdown on") + except CmdExitFailure: + raise KsftSkipEx("iproute2 does not support IPv4 devconf attributes") + time.sleep(1) + + ksft_true(f"inet {ifname} ignore_routes_with_linkdown on" in cmd_obj.stdout, + f"No 'ignore_routes_with_linkdown on' notificiation found for interface {ifname}") + ksft_true(f"inet {ifname} rp_filter strict" in cmd_obj.stdout, + f"No 'rp_filter strict' notificiation found for interface {ifname}") + ksft_true(f"inet {ifname} proxy_neigh on" in cmd_obj.stdout, + f"No 'proxy_neigh on' notificiation found for interface {ifname}") + ksft_true(f"inet {ifname} forwarding on" in cmd_obj.stdout, + f"No 'forwarding on' notificiation found for interface {ifname}") + +def _rtnl_route_subscribe(ns): + with NetNSEnter(str(ns)): + rtnl = RtnlRouteFamily() + defer(rtnl.close) + rtnl.ntf_subscribe("rtnlgrp-ipv6-route") + return rtnl + + +def _wait_route_ntf(rtnl, name, dst_len, dst=None, deadline=10): + """Return the attrs of the first matching notification, None on timeout.""" + + for msg in rtnl.poll_ntf(duration=deadline): + if msg['name'] != name: + continue + attrs = msg['msg'] + if attrs['rtm-dst-len'] != dst_len: + continue + if dst is not None and attrs.get('dst') != dst: + continue + return attrs + return None + + +def _collect_route_ntfs(rtnl, name, want, deadline=10): + """Gather attrs of matching notifications, keyed by (dst_len, dst).""" + + seen = {} + for msg in rtnl.poll_ntf(duration=deadline): + if msg['name'] != name: + continue + attrs = msg['msg'] + key = (attrs['rtm-dst-len'], attrs.get('dst')) + if key in want: + seen[key] = attrs + if len(seen) == len(want): + break + return seen + + +def _write_ipv6_sysctl(name, value): + with open(f"/proc/sys/net/ipv6/{name}", "w") as f: + f.write(f"{value}\n") + + +def ipv6_route_del_reason_expired() -> None: + """An expired route reports RTA_DEL_REASON == expired.""" + + with NetNS() as ns: + rtnl = _rtnl_route_subscribe(ns) + with NetNSEnter(str(ns)): + _write_ipv6_sysctl("route/gc_interval", 2) + ip("link add name dummy1 type dummy", ns=str(ns)) + ip("link set dev dummy1 up", ns=str(ns)) + ip("-6 route add 2001:db8:2::/64 dev dummy1 expires 2", ns=str(ns)) + + attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:2::', + deadline=15) + ksft_not_none(attrs, "no RTM_DELROUTE for the expired route") + if attrs is not None: + ksft_eq(attrs.get('del-reason'), 'expired') + + +def _send_ra(sock, ifindex, lifetime, rio=None, pio=None): + """The kernel fills in the ICMPv6 checksum on raw ICMPv6 sockets.""" + + # type, code, cksum, hop limit, flags, router lifetime, + # reachable time, retrans timer + ra = struct.pack('!BBHBBHII', 134, 0, 0, 64, 0, lifetime, 0, 0) + if rio is not None: + prefix, plen, rio_lifetime = rio + # RFC 4191 route information option, /64 prefix (8 bytes) + ra += struct.pack('!BBBBI', 24, 2, plen, 0, rio_lifetime) + ra += socket.inet_pton(socket.AF_INET6, prefix)[:8] + if pio is not None: + prefix, plen, valid_lft = pio + # RFC 4861 prefix information option, on-link only (L set, A clear) + ra += struct.pack('!BBBBIII', 3, 4, plen, 0x80, valid_lft, 0, 0) + ra += socket.inet_pton(socket.AF_INET6, prefix) + sock.sendto(ra, ('ff02::1', 0, 0, ifindex)) + + +def _ra_router_sock(ns_r, ifname): + with NetNSEnter(str(ns_r)): + sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW, + socket.IPPROTO_ICMPV6) + sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255) + defer(sock.close) + return sock, socket.if_nametoindex(ifname) + + +def _ra_advertise_routes(rtnl, sock, ifindex, want, **ra_opts): + """ + Sending fails with EADDRNOTAVAIL while the router's link-local + address is still tentative. addrconf_dad_start() only queues + addrconf_dad_work(), and IFA_F_TENTATIVE is cleared when that work + item runs, so retry until it does. + """ + + seen = {} + for _ in range(10): + try: + _send_ra(sock, ifindex, **ra_opts) + except OSError: + time.sleep(0.2) + continue + seen.update(_collect_route_ntfs(rtnl, 'newroute-ntf', + want - set(seen.keys()), deadline=2)) + if len(seen) == len(want): + break + return seen + + +def ipv6_route_del_reason_ra_withdrawn() -> None: + """ + Routes withdrawn by a zero-lifetime RA (router lifetime, RFC 4861 + PIO, RFC 4191 RIO) report RTA_DEL_REASON == ra-withdrawn. + """ + + # (rtm-dst-len, dst); the default route carries no RTA_DST + routes = {(0, None), (64, '2001:db8:6::'), (64, '2001:db8:5::')} + + with NetNS() as ns_h, NetNS() as ns_r: + ip(f"link add veth0 netns {ns_h} type veth peer name veth1 netns {ns_r}") + with NetNSEnter(str(ns_h)): + _write_ipv6_sysctl("conf/veth0/accept_ra", 2) + _write_ipv6_sysctl("conf/veth0/forwarding", 0) + try: + _write_ipv6_sysctl("conf/veth0/accept_ra_rt_info_max_plen", 64) + except FileNotFoundError: + raise KsftSkipEx("no CONFIG_IPV6_ROUTE_INFO") + with NetNSEnter(str(ns_r)): + # skip the DAD probe so the router's link-local source only + # has to wait for addrconf_dad_work() to clear IFA_F_TENTATIVE + _write_ipv6_sysctl("conf/veth1/accept_dad", 0) + ip("link set dev veth0 up", ns=str(ns_h)) + ip("link set dev veth1 up", ns=str(ns_r)) + + rtnl = _rtnl_route_subscribe(ns_h) + sock, ifindex = _ra_router_sock(ns_r, "veth1") + + seen = _ra_advertise_routes(rtnl, sock, ifindex, routes, + lifetime=1800, + rio=('2001:db8:5::', 64, 600), + pio=('2001:db8:6::', 64, 600)) + ksft_eq(set(seen), routes, "not all RA routes were installed") + if set(seen) != routes: + return + + _send_ra(sock, ifindex, 0, rio=('2001:db8:5::', 64, 0), + pio=('2001:db8:6::', 64, 0)) + seen = _collect_route_ntfs(rtnl, 'delroute-ntf', routes) + for key in routes: + attrs = seen.get(key) + ksft_not_none(attrs, f"no RTM_DELROUTE for {key}") + if attrs is not None: + ksft_eq(attrs.get('del-reason'), 'ra-withdrawn') + + +def ipv6_route_del_reason_absent() -> None: """ - Verify that at least one interface has the IPv4 all-hosts multicast address. - At least the loopback interface should have this address. + A deletion path that records no cause (here a userspace request) + must not carry RTA_DEL_REASON at all. """ - addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True) + with NetNS() as ns: + rtnl = _rtnl_route_subscribe(ns) + ip("link add name dummy1 type dummy", ns=str(ns)) + ip("link set dev dummy1 up", ns=str(ns)) + ip("-6 route add 2001:db8:1::/64 dev dummy1", ns=str(ns)) + ip("-6 route del 2001:db8:1::/64 dev dummy1", ns=str(ns)) - all_host_multicasts = [ - addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST - ] + attrs = _wait_route_ntf(rtnl, 'delroute-ntf', 64, '2001:db8:1::') + ksft_not_none(attrs, "no RTM_DELROUTE for 2001:db8:1::/64") + if attrs is not None: + ksft_not_in('del-reason', attrs, + "user deletion must not carry del-reason") - ksft_ge(len(all_host_multicasts), 1, - "No interface found with the IPv4 all-hosts multicast address") def main() -> None: - rtnl = RtnlAddrFamily() - ksft_run([dump_mcaddr_check], args=(rtnl, )) + ksft_run([dump_mcaddr_check, dump_mcaddr6_check, ipv4_devconf_notify, + ipv6_route_del_reason_expired, + ipv6_route_del_reason_ra_withdrawn, + ipv6_route_del_reason_absent]) ksft_exit() if __name__ == "__main__": diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh index 248c2b91fe42..ace3a99023ed 100755 --- a/tools/testing/selftests/net/rtnetlink.sh +++ b/tools/testing/selftests/net/rtnetlink.sh @@ -23,11 +23,15 @@ ALL_TESTS=" kci_test_encap kci_test_macsec kci_test_macsec_vlan + kci_test_team_bridge_macvlan + kci_test_bridge_promisc_netlink + kci_test_bridge_promisc_sysfs kci_test_ipsec kci_test_ipsec_offload kci_test_fdb_get kci_test_fdb_del kci_test_neigh_get + kci_test_neigh_update kci_test_bridge_parent_id kci_test_address_proto kci_test_enslave_bonding @@ -59,6 +63,14 @@ check_fail() fi } +sysfs_write() +{ + local val="$1" + local path="$2" + + echo "$val" > "$path" +} + run_cmd_common() { local cmd="$*" @@ -635,6 +647,102 @@ kci_test_macsec_vlan() end_test "PASS: macsec_vlan" } +# Test ndo_change_rx_flags call from dev_uc_add under addr_list_lock spinlock. +# When we are flipping the promisc, make sure it runs on the work queue. +# +# https://lore.kernel.org/netdev/20260214033859.43857-1-jiayuan.chen@linux.dev/ +# With (more conventional) macvlan instead of macsec. +# macvlan -> bridge -> team -> dummy +kci_test_team_bridge_macvlan() +{ + local vlan="test_macv1" + local bridge="test_br1" + local team="test_team1" + local dummy="test_dummy1" + local ret=0 + + run_cmd ip link add $team type team + if [ $ret -ne 0 ]; then + end_test "SKIP: team_bridge_macvlan: can't add team interface" + return $ksft_skip + fi + + run_cmd ip link add $dummy type dummy + run_cmd ip link set $dummy master $team + run_cmd ip link set $team up + run_cmd ip link add $bridge type bridge vlan_filtering 1 + run_cmd ip link set $bridge up + run_cmd ip link set $team master $bridge + run_cmd ip link add link $bridge name $vlan \ + address 00:aa:bb:cc:dd:ee type macvlan mode bridge + run_cmd ip link set $vlan up + + run_cmd ip link del $vlan + run_cmd ip link del $bridge + run_cmd ip link del $team + run_cmd ip link del $dummy + + if [ $ret -ne 0 ]; then + end_test "FAIL: team_bridge_macvlan" + return 1 + fi + + end_test "PASS: team_bridge_macvlan" +} + +# Test that changing bridge port flags via the netlink path does not sleep with +# the bridge spin lock held. +kci_test_bridge_promisc_netlink() +{ + local dummy="test_dummy1" + local bridge="test_br1" + local team="test_team1" + local ret=0 + + run_cmd ip link add $team up type team + run_cmd ip link add $bridge up type bridge vlan_filtering 1 + run_cmd ip link add $dummy up type dummy + run_cmd ip link set $dummy master $bridge + run_cmd ip link set $team master $bridge + + # This causes the bridge driver to sync all the static FDB entries to + # the team device (which supports unicast filtering) and remove it from + # promiscuous mode. The call to dev_set_promiscuity() can sleep due to + # Rx mode inlining, which is a problem if the bridge spin lock is held. + run_cmd bridge link set dev $dummy flood off learning off + + run_cmd ip link del $dummy + run_cmd ip link del $bridge + run_cmd ip link del $team + + end_test "PASS: bridge_promisc_netlink" +} + +# Same as kci_test_bridge_promisc_netlink(), but the flags are changed via the +# sysfs path. +kci_test_bridge_promisc_sysfs() +{ + local dummy="test_dummy1" + local bridge="test_br1" + local team="test_team1" + local ret=0 + + run_cmd ip link add $team up type team + run_cmd ip link add $bridge up type bridge vlan_filtering 1 + run_cmd ip link add $dummy up type dummy + run_cmd ip link set $dummy master $bridge + run_cmd ip link set $team master $bridge + + run_cmd sysfs_write 0 /sys/class/net/$dummy/brport/unicast_flood + run_cmd sysfs_write 0 /sys/class/net/$dummy/brport/learning + + run_cmd ip link del $dummy + run_cmd ip link del $bridge + run_cmd ip link del $team + + end_test "PASS: bridge_promisc_sysfs" +} + #------------------------------------------------------------------- # Example commands # ip x s add proto esp src 14.0.0.52 dst 14.0.0.70 \ @@ -1160,6 +1268,60 @@ kci_test_neigh_get() end_test "PASS: neigh get" } +kci_test_neigh_update() +{ + dstip=10.0.2.4 + dstmac=de:ad:be:ef:13:37 + local ret=0 + + for proxy in "" "proxy" ; do + # add a neighbour entry without any flags + run_cmd ip neigh add $proxy $dstip dev "$devdummy" lladdr $dstmac nud permanent + run_cmd_grep $dstip ip neigh show $proxy + run_cmd_grep_fail "$dstip dev $devdummy .*\(managed\|use\|router\|extern\)" ip neigh show $proxy + + # set the extern_learn flag, but no other + run_cmd ip neigh change $proxy $dstip dev "$devdummy" extern_learn + run_cmd_grep "$dstip dev $devdummy .* extern_learn" ip neigh show $proxy + run_cmd_grep_fail "$dstip dev $devdummy .* \(managed\|use\|router\)" ip neigh show $proxy + + # flags are reset when not provided + run_cmd ip neigh change $proxy $dstip dev "$devdummy" + run_cmd_grep $dstip ip neigh show $proxy + run_cmd_grep_fail "$dstip dev $devdummy .* extern_learn" ip neigh show $proxy + + # add a protocol + run_cmd ip neigh change $proxy $dstip dev "$devdummy" protocol boot + run_cmd_grep "$dstip dev $devdummy .* proto boot" ip neigh show $proxy + + # protocol is retained when not provided + run_cmd ip neigh change $proxy $dstip dev "$devdummy" + run_cmd_grep "$dstip dev $devdummy .* proto boot" ip neigh show $proxy + + # change protocol + run_cmd ip neigh change $proxy $dstip dev "$devdummy" protocol static + run_cmd_grep "$dstip dev $devdummy .* proto static" ip neigh show $proxy + + # also check an extended flag for non-proxy neighs + if [ "$proxy" = "" ]; then + run_cmd ip neigh change $proxy $dstip dev "$devdummy" managed + run_cmd_grep "$dstip dev $devdummy managed" ip neigh show $proxy + + run_cmd ip neigh change $proxy $dstip dev "$devdummy" lladdr $dstmac + run_cmd_grep_fail "$dstip dev $devdummy managed" ip neigh show $proxy + fi + + run_cmd ip neigh del $proxy $dstip dev "$devdummy" + done + + if [ $ret -ne 0 ];then + end_test "FAIL: neigh update" + return 1 + fi + + end_test "PASS: neigh update" +} + kci_test_bridge_parent_id() { local ret=0 diff --git a/tools/testing/selftests/net/so_txtime.c b/tools/testing/selftests/net/so_txtime.c deleted file mode 100644 index b76df1efc2ef..000000000000 --- a/tools/testing/selftests/net/so_txtime.c +++ /dev/null @@ -1,517 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0 -/* - * Test the SO_TXTIME API - * - * Takes a stream of { payload, delivery time }[], to be sent across two - * processes. Start this program on two separate network namespaces or - * connected hosts, one instance in transmit mode and the other in receive - * mode using the '-r' option. Receiver will compare arrival timestamps to - * the expected stream. Sender will read transmit timestamps from the error - * queue. The streams can differ due to out-of-order delivery and drops. - */ - -#define _GNU_SOURCE - -#include <arpa/inet.h> -#include <error.h> -#include <errno.h> -#include <inttypes.h> -#include <linux/net_tstamp.h> -#include <linux/errqueue.h> -#include <linux/if_ether.h> -#include <linux/ipv6.h> -#include <linux/udp.h> -#include <stdbool.h> -#include <stdlib.h> -#include <stdio.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/stat.h> -#include <sys/time.h> -#include <sys/types.h> -#include <time.h> -#include <unistd.h> -#include <poll.h> - -static int cfg_clockid = CLOCK_TAI; -static uint16_t cfg_port = 8000; -static int cfg_variance_us = 4000; -static uint64_t cfg_start_time_ns; -static int cfg_mark; -static bool cfg_rx; - -static uint64_t glob_tstart; -static uint64_t tdeliver_max; - -/* encode one timed transmission (of a 1B payload) */ -struct timed_send { - char data; - int64_t delay_us; -}; - -#define MAX_NUM_PKT 8 -static struct timed_send cfg_buf[MAX_NUM_PKT]; -static int cfg_num_pkt; - -static int cfg_errq_level; -static int cfg_errq_type; - -static struct sockaddr_storage cfg_dst_addr; -static struct sockaddr_storage cfg_src_addr; -static socklen_t cfg_alen; - -static uint64_t gettime_ns(clockid_t clock) -{ - struct timespec ts; - - if (clock_gettime(clock, &ts)) - error(1, errno, "gettime"); - - return ts.tv_sec * (1000ULL * 1000 * 1000) + ts.tv_nsec; -} - -static void do_send_one(int fdt, struct timed_send *ts) -{ - char control[CMSG_SPACE(sizeof(uint64_t))]; - struct msghdr msg = {0}; - struct iovec iov = {0}; - struct cmsghdr *cm; - uint64_t tdeliver; - int ret; - - iov.iov_base = &ts->data; - iov.iov_len = 1; - - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_name = (struct sockaddr *)&cfg_dst_addr; - msg.msg_namelen = cfg_alen; - - if (ts->delay_us >= 0) { - memset(control, 0, sizeof(control)); - msg.msg_control = &control; - msg.msg_controllen = sizeof(control); - - tdeliver = glob_tstart + ts->delay_us * 1000; - tdeliver_max = tdeliver_max > tdeliver ? - tdeliver_max : tdeliver; - - cm = CMSG_FIRSTHDR(&msg); - cm->cmsg_level = SOL_SOCKET; - cm->cmsg_type = SCM_TXTIME; - cm->cmsg_len = CMSG_LEN(sizeof(tdeliver)); - memcpy(CMSG_DATA(cm), &tdeliver, sizeof(tdeliver)); - } - - ret = sendmsg(fdt, &msg, 0); - if (ret == -1) - error(1, errno, "write"); - if (ret == 0) - error(1, 0, "write: 0B"); - -} - -static void do_recv_one(int fdr, struct timed_send *ts) -{ - int64_t tstop, texpect; - char rbuf[2]; - int ret; - - ret = recv(fdr, rbuf, sizeof(rbuf), 0); - if (ret == -1 && errno == EAGAIN) - error(1, EAGAIN, "recv: timeout"); - if (ret == -1) - error(1, errno, "read"); - if (ret != 1) - error(1, 0, "read: %dB", ret); - - tstop = (gettime_ns(cfg_clockid) - glob_tstart) / 1000; - texpect = ts->delay_us >= 0 ? ts->delay_us : 0; - - fprintf(stderr, "payload:%c delay:%lld expected:%lld (us)\n", - rbuf[0], (long long)tstop, (long long)texpect); - - if (rbuf[0] != ts->data) - error(1, 0, "payload mismatch. expected %c", ts->data); - - if (llabs(tstop - texpect) > cfg_variance_us) { - fprintf(stderr, "exceeds variance (%d us)\n", cfg_variance_us); - if (!getenv("KSFT_MACHINE_SLOW")) - exit(1); - } -} - -static void do_recv_verify_empty(int fdr) -{ - char rbuf[1]; - int ret; - - ret = recv(fdr, rbuf, sizeof(rbuf), 0); - if (ret != -1 || errno != EAGAIN) - error(1, 0, "recv: not empty as expected (%d, %d)", ret, errno); -} - -static int do_recv_errqueue_timeout(int fdt) -{ - char control[CMSG_SPACE(sizeof(struct sock_extended_err)) + - CMSG_SPACE(sizeof(struct sockaddr_in6))] = {0}; - char data[sizeof(struct ethhdr) + sizeof(struct ipv6hdr) + - sizeof(struct udphdr) + 1]; - struct sock_extended_err *err; - int ret, num_tstamp = 0; - struct msghdr msg = {0}; - struct iovec iov = {0}; - struct cmsghdr *cm; - int64_t tstamp = 0; - - iov.iov_base = data; - iov.iov_len = sizeof(data); - - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - - msg.msg_control = control; - msg.msg_controllen = sizeof(control); - - while (1) { - const char *reason = NULL; - - ret = recvmsg(fdt, &msg, MSG_ERRQUEUE); - if (ret == -1 && errno == EAGAIN) - break; - if (ret == -1) - error(1, errno, "errqueue"); - if (msg.msg_flags != MSG_ERRQUEUE) - error(1, 0, "errqueue: flags 0x%x\n", msg.msg_flags); - - cm = CMSG_FIRSTHDR(&msg); - if (cm->cmsg_level != cfg_errq_level || - cm->cmsg_type != cfg_errq_type) - error(1, 0, "errqueue: type 0x%x.0x%x\n", - cm->cmsg_level, cm->cmsg_type); - - err = (struct sock_extended_err *)CMSG_DATA(cm); - if (err->ee_origin != SO_EE_ORIGIN_TXTIME) - error(1, 0, "errqueue: origin 0x%x\n", err->ee_origin); - - switch (err->ee_errno) { - case ECANCELED: - if (err->ee_code != SO_EE_CODE_TXTIME_MISSED) - error(1, 0, "errqueue: unknown ECANCELED %u\n", - err->ee_code); - reason = "missed txtime"; - break; - case EINVAL: - if (err->ee_code != SO_EE_CODE_TXTIME_INVALID_PARAM) - error(1, 0, "errqueue: unknown EINVAL %u\n", - err->ee_code); - reason = "invalid txtime"; - break; - default: - error(1, 0, "errqueue: errno %u code %u\n", - err->ee_errno, err->ee_code); - } - - tstamp = ((int64_t) err->ee_data) << 32 | err->ee_info; - tstamp -= (int64_t) glob_tstart; - tstamp /= 1000 * 1000; - fprintf(stderr, "send: pkt %c at %" PRId64 "ms dropped: %s\n", - data[ret - 1], tstamp, reason); - - msg.msg_flags = 0; - msg.msg_controllen = sizeof(control); - num_tstamp++; - } - - return num_tstamp; -} - -static void recv_errqueue_msgs(int fdt) -{ - struct pollfd pfd = { .fd = fdt, .events = POLLERR }; - const int timeout_ms = 10; - int ret, num_tstamp = 0; - - do { - ret = poll(&pfd, 1, timeout_ms); - if (ret == -1) - error(1, errno, "poll"); - - if (ret && (pfd.revents & POLLERR)) - num_tstamp += do_recv_errqueue_timeout(fdt); - - if (num_tstamp == cfg_num_pkt) - break; - - } while (gettime_ns(cfg_clockid) < tdeliver_max); -} - -static void start_time_wait(void) -{ - uint64_t now; - int err; - - if (!cfg_start_time_ns) - return; - - now = gettime_ns(CLOCK_REALTIME); - if (cfg_start_time_ns < now) - return; - - err = usleep((cfg_start_time_ns - now) / 1000); - if (err) - error(1, errno, "usleep"); -} - -static void setsockopt_txtime(int fd) -{ - struct sock_txtime so_txtime_val = { .clockid = cfg_clockid }; - struct sock_txtime so_txtime_val_read = { 0 }; - socklen_t vallen = sizeof(so_txtime_val); - - so_txtime_val.flags = SOF_TXTIME_REPORT_ERRORS; - - if (setsockopt(fd, SOL_SOCKET, SO_TXTIME, - &so_txtime_val, sizeof(so_txtime_val))) - error(1, errno, "setsockopt txtime"); - - if (getsockopt(fd, SOL_SOCKET, SO_TXTIME, - &so_txtime_val_read, &vallen)) - error(1, errno, "getsockopt txtime"); - - if (vallen != sizeof(so_txtime_val) || - memcmp(&so_txtime_val, &so_txtime_val_read, vallen)) - error(1, 0, "getsockopt txtime: mismatch"); -} - -static int setup_tx(struct sockaddr *addr, socklen_t alen) -{ - int fd; - - fd = socket(addr->sa_family, SOCK_DGRAM, 0); - if (fd == -1) - error(1, errno, "socket t"); - - if (connect(fd, addr, alen)) - error(1, errno, "connect"); - - setsockopt_txtime(fd); - - if (cfg_mark && - setsockopt(fd, SOL_SOCKET, SO_MARK, &cfg_mark, sizeof(cfg_mark))) - error(1, errno, "setsockopt mark"); - - return fd; -} - -static int setup_rx(struct sockaddr *addr, socklen_t alen) -{ - struct timeval tv = { .tv_usec = 100 * 1000 }; - int fd; - - fd = socket(addr->sa_family, SOCK_DGRAM, 0); - if (fd == -1) - error(1, errno, "socket r"); - - if (bind(fd, addr, alen)) - error(1, errno, "bind"); - - if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv))) - error(1, errno, "setsockopt rcv timeout"); - - return fd; -} - -static void do_test_tx(struct sockaddr *addr, socklen_t alen) -{ - int fdt, i; - - fprintf(stderr, "\nSO_TXTIME ipv%c clock %s\n", - addr->sa_family == PF_INET ? '4' : '6', - cfg_clockid == CLOCK_TAI ? "tai" : "monotonic"); - - fdt = setup_tx(addr, alen); - - start_time_wait(); - glob_tstart = gettime_ns(cfg_clockid); - - for (i = 0; i < cfg_num_pkt; i++) - do_send_one(fdt, &cfg_buf[i]); - - recv_errqueue_msgs(fdt); - - if (close(fdt)) - error(1, errno, "close t"); -} - -static void do_test_rx(struct sockaddr *addr, socklen_t alen) -{ - int fdr, i; - - fdr = setup_rx(addr, alen); - - start_time_wait(); - glob_tstart = gettime_ns(cfg_clockid); - - for (i = 0; i < cfg_num_pkt; i++) - do_recv_one(fdr, &cfg_buf[i]); - - do_recv_verify_empty(fdr); - - if (close(fdr)) - error(1, errno, "close r"); -} - -static void setup_sockaddr(int domain, const char *str_addr, - struct sockaddr_storage *sockaddr) -{ - struct sockaddr_in6 *addr6 = (void *) sockaddr; - struct sockaddr_in *addr4 = (void *) sockaddr; - - switch (domain) { - case PF_INET: - memset(addr4, 0, sizeof(*addr4)); - addr4->sin_family = AF_INET; - addr4->sin_port = htons(cfg_port); - if (str_addr && - inet_pton(AF_INET, str_addr, &(addr4->sin_addr)) != 1) - error(1, 0, "ipv4 parse error: %s", str_addr); - break; - case PF_INET6: - memset(addr6, 0, sizeof(*addr6)); - addr6->sin6_family = AF_INET6; - addr6->sin6_port = htons(cfg_port); - if (str_addr && - inet_pton(AF_INET6, str_addr, &(addr6->sin6_addr)) != 1) - error(1, 0, "ipv6 parse error: %s", str_addr); - break; - } -} - -static int parse_io(const char *optarg, struct timed_send *array) -{ - char *arg, *tok; - int aoff = 0; - - arg = strdup(optarg); - if (!arg) - error(1, errno, "strdup"); - - while ((tok = strtok(arg, ","))) { - arg = NULL; /* only pass non-zero on first call */ - - if (aoff / 2 == MAX_NUM_PKT) - error(1, 0, "exceeds max pkt count (%d)", MAX_NUM_PKT); - - if (aoff & 1) { /* parse delay */ - array->delay_us = strtol(tok, NULL, 0) * 1000; - array++; - } else { /* parse character */ - array->data = tok[0]; - } - - aoff++; - } - - free(arg); - - return aoff / 2; -} - -static void usage(const char *progname) -{ - fprintf(stderr, "\nUsage: %s [options] <payload>\n" - "Options:\n" - " -4 only IPv4\n" - " -6 only IPv6\n" - " -c <clock> monotonic or tai (default)\n" - " -D <addr> destination IP address (server)\n" - " -S <addr> source IP address (client)\n" - " -r run rx mode\n" - " -t <nsec> start time (UTC nanoseconds)\n" - " -m <mark> socket mark\n" - "\n", - progname); - exit(1); -} - -static void parse_opts(int argc, char **argv) -{ - char *daddr = NULL, *saddr = NULL; - int domain = PF_UNSPEC; - int c; - - while ((c = getopt(argc, argv, "46c:S:D:rt:m:")) != -1) { - switch (c) { - case '4': - if (domain != PF_UNSPEC) - error(1, 0, "Pass one of -4 or -6"); - domain = PF_INET; - cfg_alen = sizeof(struct sockaddr_in); - cfg_errq_level = SOL_IP; - cfg_errq_type = IP_RECVERR; - break; - case '6': - if (domain != PF_UNSPEC) - error(1, 0, "Pass one of -4 or -6"); - domain = PF_INET6; - cfg_alen = sizeof(struct sockaddr_in6); - cfg_errq_level = SOL_IPV6; - cfg_errq_type = IPV6_RECVERR; - break; - case 'c': - if (!strcmp(optarg, "tai")) - cfg_clockid = CLOCK_TAI; - else if (!strcmp(optarg, "monotonic") || - !strcmp(optarg, "mono")) - cfg_clockid = CLOCK_MONOTONIC; - else - error(1, 0, "unknown clock id %s", optarg); - break; - case 'S': - saddr = optarg; - break; - case 'D': - daddr = optarg; - break; - case 'r': - cfg_rx = true; - break; - case 't': - cfg_start_time_ns = strtoll(optarg, NULL, 0); - break; - case 'm': - cfg_mark = strtol(optarg, NULL, 0); - break; - default: - usage(argv[0]); - } - } - - if (argc - optind != 1) - usage(argv[0]); - - if (domain == PF_UNSPEC) - error(1, 0, "Pass one of -4 or -6"); - if (!daddr) - error(1, 0, "-D <server addr> required\n"); - if (!cfg_rx && !saddr) - error(1, 0, "-S <client addr> required\n"); - - setup_sockaddr(domain, daddr, &cfg_dst_addr); - setup_sockaddr(domain, saddr, &cfg_src_addr); - - cfg_num_pkt = parse_io(argv[optind], cfg_buf); -} - -int main(int argc, char **argv) -{ - parse_opts(argc, argv); - - if (cfg_rx) - do_test_rx((void *)&cfg_dst_addr, cfg_alen); - else - do_test_tx((void *)&cfg_src_addr, cfg_alen); - - return 0; -} diff --git a/tools/testing/selftests/net/so_txtime.sh b/tools/testing/selftests/net/so_txtime.sh deleted file mode 100755 index 5e861ad32a42..000000000000 --- a/tools/testing/selftests/net/so_txtime.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: GPL-2.0 -# -# Regression tests for the SO_TXTIME interface - -set -e - -readonly ksft_skip=4 -readonly DEV="veth0" -readonly BIN="./so_txtime" - -readonly RAND="$(mktemp -u XXXXXX)" -readonly NSPREFIX="ns-${RAND}" -readonly NS1="${NSPREFIX}1" -readonly NS2="${NSPREFIX}2" - -readonly SADDR4='192.168.1.1' -readonly DADDR4='192.168.1.2' -readonly SADDR6='fd::1' -readonly DADDR6='fd::2' - -cleanup() { - ip netns del "${NS2}" - ip netns del "${NS1}" -} - -trap cleanup EXIT - -# Create virtual ethernet pair between network namespaces -ip netns add "${NS1}" -ip netns add "${NS2}" - -ip link add "${DEV}" netns "${NS1}" type veth \ - peer name "${DEV}" netns "${NS2}" - -# Bring the devices up -ip -netns "${NS1}" link set "${DEV}" up -ip -netns "${NS2}" link set "${DEV}" up - -# Set fixed MAC addresses on the devices -ip -netns "${NS1}" link set dev "${DEV}" address 02:02:02:02:02:02 -ip -netns "${NS2}" link set dev "${DEV}" address 06:06:06:06:06:06 - -# Add fixed IP addresses to the devices -ip -netns "${NS1}" addr add 192.168.1.1/24 dev "${DEV}" -ip -netns "${NS2}" addr add 192.168.1.2/24 dev "${DEV}" -ip -netns "${NS1}" addr add fd::1/64 dev "${DEV}" nodad -ip -netns "${NS2}" addr add fd::2/64 dev "${DEV}" nodad - -run_test() { - local readonly IP="$1" - local readonly CLOCK="$2" - local readonly TXARGS="$3" - local readonly RXARGS="$4" - - if [[ "${IP}" == "4" ]]; then - local readonly SADDR="${SADDR4}" - local readonly DADDR="${DADDR4}" - elif [[ "${IP}" == "6" ]]; then - local readonly SADDR="${SADDR6}" - local readonly DADDR="${DADDR6}" - else - echo "Invalid IP version ${IP}" - exit 1 - fi - - local readonly START="$(date +%s%N --date="+ 0.1 seconds")" - - ip netns exec "${NS2}" "${BIN}" -"${IP}" -c "${CLOCK}" -t "${START}" -S "${SADDR}" -D "${DADDR}" "${RXARGS}" -r & - ip netns exec "${NS1}" "${BIN}" -"${IP}" -c "${CLOCK}" -t "${START}" -S "${SADDR}" -D "${DADDR}" "${TXARGS}" - wait "$!" -} - -do_test() { - run_test $@ - [ $? -ne 0 ] && ret=1 -} - -do_fail_test() { - run_test $@ - [ $? -eq 0 ] && ret=1 -} - -ip netns exec "${NS1}" tc qdisc add dev "${DEV}" root fq -set +e -ret=0 -do_test 4 mono a,-1 a,-1 -do_test 6 mono a,0 a,0 -do_test 6 mono a,10 a,10 -do_test 4 mono a,10,b,20 a,10,b,20 -do_test 6 mono a,20,b,10 b,20,a,20 - -if ip netns exec "${NS1}" tc qdisc replace dev "${DEV}" root etf clockid CLOCK_TAI delta 400000; then - do_fail_test 4 tai a,-1 a,-1 - do_fail_test 6 tai a,0 a,0 - do_test 6 tai a,10 a,10 - do_test 4 tai a,10,b,20 a,10,b,20 - do_test 6 tai a,20,b,10 b,10,a,20 -else - echo "tc ($(tc -V)) does not support qdisc etf. skipping" - [ $ret -eq 0 ] && ret=$ksft_skip -fi - -if [ $ret -eq 0 ]; then - echo OK. All tests passed -elif [[ $ret -ne $ksft_skip && -n "$KSFT_MACHINE_SLOW" ]]; then - echo "Ignoring errors due to slow environment" 1>&2 - ret=0 -fi -exit $ret diff --git a/tools/testing/selftests/net/srv6_encap_lookup_l3vpn_test.sh b/tools/testing/selftests/net/srv6_encap_lookup_l3vpn_test.sh new file mode 100755 index 000000000000..d6249303b7ea --- /dev/null +++ b/tools/testing/selftests/net/srv6_encap_lookup_l3vpn_test.sh @@ -0,0 +1,1027 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# author: Andrea Mayer <andrea.mayer@uniroma2.it> + +# This test evaluates the SRv6 encap "lookup" attribute. After encapsulation +# the router looks up the route for the first SID, that is the outer IPv6 +# destination of the encapsulated packet. The attribute selects the FIB table +# used for this post-encap SID route lookup. +# +# Two routers (rt-1, rt-2) provide L3 VPN services over an IPv6 underlay +# (fd00::/64). Each router uses a separate VRF per tenant, with default +# blackhole routes (IPv4 and IPv6) to prevent traffic from leaking out of +# the VRF. Tenant traffic is encapsulated, then decapsulated with an +# End.DT46. Each router proxies both NDP and ARP. +# +# The routes that match the first SIDs are installed in a dedicated underlay +# table (500) rather than the main table (254). The encap routes use +# "lookup 500" to select this table for the post-encap SID route lookup. +# +# Without the "lookup" attribute, the route for the first SID cannot be found: +# - on the input path (forwarded traffic), the lookup stays in the VRF +# and hits the blackhole; +# - on the output path (locally originated traffic), the lookup falls +# through to the main table, with no route to the first SID. +# +# +# Legend (specific per-tenant addresses are in the instantiation tables below): +# X = tenant id and VRF table id; two tenants: 100 and 200 +# ("tX" means tenant X, e.g. t100, t200) +# a, b = the two host ids of the tenant +# HA, HB = addresses of host a, host b +# RLO1, RLO2 = rlo-X router loopback address on rt-1, rt-2 (tenant gateway +# for the output path, dual-stack) +# vrf-X = per-tenant VRF on each router (table X) +# +# Constants (same for every tenant): +# underlay = table 500; post-encap SID route lookup (via "lookup 500") +# localsid = table 90; holds the decap SIDs (End.DT46) +# fd00::/64 = underlay link between rt-1 and rt-2 +# veth-tX = cafe::254/10.0.0.254 (tenant gateway on veth, both routers) +# +# +# +-------------------+ +-------------------+ +# | | | | +# | hs-tX-a netns | | hs-tX-b netns | +# | | | | +# | +-------------+ | | +-------------+ | +# | | veth0 | | | | veth0 | | +# | | HA | | | | HB | | +# | +-------------+ | | +-------------+ | +# | . | | . | +# +-------------------+ +-------------------+ +# . . +# . . +# +-----------------------------------+ +-----------------------------------+ +# | . | | . | +# | +---------------+ | | +---------------+ | +# | | veth-tX | +----------+ | | +----------+ | veth-tX | | +# | | ::254/.254 | | localsid | | | | localsid | | ::254/.254 | | +# | +-------+-------+ +----------+ | | +----------+ +-------+-------+ | +# | | +----------+ | | +----------+ | | +# | +----+----+ | underlay | | | | underlay | +----+----+ | +# | | vrf-X | +----------+ | | +----------+ | vrf-X | | +# | +----+----+ | | +----+----+ | +# | | | | | | +# | +-----+----+ +------------+ | | +------------+ +----+-----+ | +# | | rlo-X | | veth0 | | | | veth0 | | rlo-X | | +# | | RLO1 | | fd00::1/64 |..|...|..| fd00::2/64 | | RLO2 | | +# | +----------+ +------------+ | | +------------+ +----------+ | +# | rt-1 netns | | rt-2 netns | +# +-----------------------------------+ +-----------------------------------+ +# +# +# Per-tenant instantiation: +# +-----+------+-------------------+-------------------+ +# | X | a, b | HA | HB | +# +-----+------+-------------------+-------------------+ +# | 100 | 1, 2 | cafe::1, 10.0.0.1 | cafe::2, 10.0.0.2 | +# | 200 | 3, 4 | cafe::3, 10.0.0.3 | cafe::4, 10.0.0.4 | +# +-----+------+-------------------+-------------------+ +# +# Router loopback (rlo-X) addresses, per tenant: +# +-----+-----------------------+-----------------------+ +# | X | RLO1 (rt-1) | RLO2 (rt-2) | +# +-----+-----------------------+-----------------------+ +# | 100 | cafe::101, 10.0.0.101 | cafe::102, 10.0.0.102 | +# | 200 | cafe::201, 10.0.0.201 | cafe::202, 10.0.0.202 | +# +-----+-----------------------+-----------------------+ +# +# +# Network configuration +# ===================== +# +# rt-1: localsid table (table 90) +# +--------+--------------------+----------------------------------+ +# | tenant | SID | Action | +# +--------+--------------------+----------------------------------+ +# | 100 | fc00:2:1:100::0d46 | apply SRv6 End.DT46 vrftable 100 | +# | 200 | fc00:2:1:200::0d46 | apply SRv6 End.DT46 vrftable 200 | +# +--------+--------------------+----------------------------------+ +# +# rt-1: underlay table (table 500) - post-encap SID route lookup +# +--------+--------------------+-------------------------------+ +# | tenant | SID | Action | +# +--------+--------------------+-------------------------------+ +# | 100 | fc00:1:2:100::0d46 | forward via fd00::2 dev veth0 | +# | 200 | fc00:1:2:200::0d46 | forward via fd00::2 dev veth0 | +# +--------+--------------------+-------------------------------+ +# +# rt-1: VRF tables (per tenant: vrf-X = table X) +# +--------+------------+------------------------------------------+ +# | tenant | dst | encap action | +# +--------+------------+------------------------------------------+ +# | 100 | cafe::2 | encap segs fc00:1:2:100::0d46 lookup 500 | +# | | 10.0.0.2 | | +# | | cafe::102 | | +# | | 10.0.0.102 | | +# | 200 | cafe::4 | encap segs fc00:1:2:200::0d46 lookup 500 | +# | | 10.0.0.4 | | +# | | cafe::202 | | +# | | 10.0.0.202 | | +# +--------+------------+------------------------------------------+ +# +# +# rt-2: localsid table (table 90) +# +--------+--------------------+----------------------------------+ +# | tenant | SID | Action | +# +--------+--------------------+----------------------------------+ +# | 100 | fc00:1:2:100::0d46 | apply SRv6 End.DT46 vrftable 100 | +# | 200 | fc00:1:2:200::0d46 | apply SRv6 End.DT46 vrftable 200 | +# +--------+--------------------+----------------------------------+ +# +# rt-2: underlay table (table 500) - post-encap SID route lookup +# +--------+--------------------+-------------------------------+ +# | tenant | SID | Action | +# +--------+--------------------+-------------------------------+ +# | 100 | fc00:2:1:100::0d46 | forward via fd00::1 dev veth0 | +# | 200 | fc00:2:1:200::0d46 | forward via fd00::1 dev veth0 | +# +--------+--------------------+-------------------------------+ +# +# rt-2: VRF tables (per tenant: vrf-X = table X) +# +--------+------------+------------------------------------------+ +# | tenant | dst | encap action | +# +--------+------------+------------------------------------------+ +# | 100 | cafe::1 | encap segs fc00:2:1:100::0d46 lookup 500 | +# | | 10.0.0.1 | | +# | | cafe::101 | | +# | | 10.0.0.101 | | +# | 200 | cafe::3 | encap segs fc00:2:1:200::0d46 lookup 500 | +# | | 10.0.0.3 | | +# | | cafe::201 | | +# | | 10.0.0.201 | | +# +--------+------------+------------------------------------------+ +# Within a tenant, a single SID reaches the adjacent router (its loopback) +# and the remote host connected to it, in both IPv4 and IPv6. +# +# For both rt-1 and rt-2, each VRF also has the connected host prefix (cafe::/64 +# or 10.0.0.0/24) and a default blackhole (IPv4 and IPv6). +# +# +# Locally originated traffic (output path) +# ======================================== +# +# The configuration above covers forwarded traffic, where packets arrive from +# a host and are encapsulated by the router. To also test router-originated +# traffic, each router pings the other router's loopback address through +# the VPN. +# +# Example (tenant 100), rt-1 pings cafe::102 (rt-2's loopback): +# 1. rt-1 looks up cafe::102 in vrf-100 and encapsulates it (SID +# fc00:1:2:100::0d46), then "lookup 500" finds the route for the SID in the +# underlay table (next hop fd00::2) and forwards it; +# 2. rt-2 decapsulates it (localsid, End.DT46) and delivers it locally +# (cafe::102 is on the rlo-100 interface); +# 3. rt-2 replies with destination cafe::101 (rt-1's loopback). rt-2 looks up +# cafe::101 in vrf-100 and encapsulates it back to rt-1 (again via "lookup +# 500"). rt-1 decapsulates it and delivers it. + +# shellcheck source=lib.sh +source lib.sh + +readonly LOCALSID_TABLE_ID=90 +readonly UNDERLAY_TABLE_ID=500 +readonly IPv6_RT_NETWORK=fd00 +readonly IPv6_HS_NETWORK=cafe +readonly IPv4_HS_NETWORK=10.0.0 +readonly VPN_LOCATOR_SERVICE=fc00 +readonly DT46_FUNC=0d46 +readonly DUMMY_DEVNAME=dum0 +readonly IPv6_TESTS_ADDR=2001:db8::1 +readonly TESTS_TABLE_ID=54321 +PING_TIMEOUT_SEC=4 + +SETUP_ERR=1 + +ret=${ksft_skip} +nsuccess=0 +nfail=0 + +PAUSE_ON_FAIL=${PAUSE_ON_FAIL:=no} + +log_test() +{ + local rc="$1" + local expected="$2" + local msg="$3" + + if [ "${rc}" -eq "${expected}" ]; then + nsuccess=$((nsuccess+1)) + printf "\n TEST: %-60s [ OK ]\n" "${msg}" + else + ret=1 + nfail=$((nfail+1)) + printf "\n TEST: %-60s [FAIL]\n" "${msg}" + if [ "${PAUSE_ON_FAIL}" = "yes" ]; then + echo + echo "hit enter to continue, 'q' to quit" + read -r a + [ "$a" = "q" ] && exit 1 + fi + fi +} + +print_log_test_results() +{ + printf "\nTests passed: %3d\n" "${nsuccess}" + printf "Tests failed: %3d\n" "${nfail}" + + # when a test fails, the value of 'ret' is set to 1 (error code). + # Conversely, when all tests are passed successfully, the 'ret' value + # is set to 0 (success code). + if [ "${ret}" -ne 1 ]; then + ret=0 + fi +} + +log_section() +{ + echo + echo "################################################################################" + echo "TEST SECTION: $*" + echo "################################################################################" +} + +get_rtname() +{ + local rtid="$1" + + echo "rt_${rtid}" +} + +get_rt_nsname() +{ + local rtid="$1" + local varname + + varname="$(get_rtname "${rtid}")" + echo "${!varname}" +} + +get_hsname() +{ + local tid="$1" + local hsid="$2" + + echo "hs_t${tid}_${hsid}" +} + +get_hs_nsname() +{ + local tid="$1" + local hsid="$2" + local varname + + varname="$(get_hsname "${tid}" "${hsid}")" + echo "${!varname}" +} + +cleanup() +{ + ip link del veth-rt-1 2>/dev/null || true + ip link del veth-rt-2 2>/dev/null || true + + cleanup_all_ns + + # check whether the setup phase was completed successfully or not. In + # case of an error during the setup phase of the testing environment, + # the selftest is considered as "skipped". + if [ "${SETUP_ERR}" -ne 0 ]; then + echo "SKIP: Setting up the testing environment failed" + exit "${ksft_skip}" + fi + + exit "${ret}" +} + +# Host id of the router loopback (rlo) for a (router, tenant) pair. +# E.g. rt-1/tenant 100 -> 101, rt-2/tenant 200 -> 202. +get_rlo_hostid() +{ + local rtid="$1" + local tid="$2" + + echo "$((tid + rtid))" +} + +build_vpn_sid() +{ + local rtsrc="$1" + local rtdst="$2" + local tid="$3" + + echo "${VPN_LOCATOR_SERVICE}:${rtsrc}:${rtdst}:${tid}::${DT46_FUNC}" +} + +# Install a dual-stack (IPv6 and IPv4) encap route in a VRF on the given +# router. +# args: +# $1 - router id +# $2 - host part of the IPv6 destination +# $3 - host part of the IPv4 destination +# $4 - SRv6 SID used as the encap destination +# $5 - tenant id +# $6 - if "true", add the "lookup" attribute to the encap route +__set_encap_route() +{ + local rt="$1" + local dst6="$2" + local dst4="$3" + local sid="$4" + local tid="$5" + local use_lookup="$6" + local lookup='' + local rtname + + rtname="$(get_rt_nsname "${rt}")" + + if [ "${use_lookup}" = "true" ]; then + lookup="lookup ${UNDERLAY_TABLE_ID}" + fi + + # shellcheck disable=SC2086 + ip -netns "${rtname}" -6 route replace \ + "${IPv6_HS_NETWORK}::${dst6}/128" vrf "vrf-${tid}" \ + encap seg6 mode encap segs "${sid}" ${lookup} dev veth0 + + # shellcheck disable=SC2086 + ip -netns "${rtname}" -4 route replace \ + "${IPv4_HS_NETWORK}.${dst4}/32" vrf "vrf-${tid}" \ + encap seg6 mode encap segs "${sid}" ${lookup} dev veth0 +} + +# Install the dual-stack encap route for a tenant host on rt, with the +# "lookup" attribute so the first SID is looked up in the underlay table. +# args: +# $1 - router id where the encap route is installed +# $2 - host destination id (host part of cafe::<id>/128 and 10.0.0.<id>/32) +# $3 - SRv6 SID used as the encap destination +# $4 - tenant id +set_host_encap_route() +{ + local rt="$1" + local hsdst="$2" + local sid="$3" + local tid="$4" + + __set_encap_route "${rt}" "${hsdst}" "${hsdst}" "${sid}" "${tid}" true +} + +set_host_encap_route_nolookup() +{ + local rt="$1" + local hsdst="$2" + local sid="$3" + local tid="$4" + + __set_encap_route "${rt}" "${hsdst}" "${hsdst}" "${sid}" "${tid}" false +} + +# Install the dual-stack encap route on rtsrc toward rtdst's rlo loopback +# (RLO1 or RLO2, see header), with the "lookup" attribute so the first +# SID is looked up in the underlay table. +# args: +# $1 - router id where the encap route is installed +# $2 - router id whose loopback address is the route destination +# $3 - SRv6 SID used as the encap destination +# $4 - tenant id +set_gw_encap_route() +{ + local rtsrc="$1" + local rtdst="$2" + local sid="$3" + local tid="$4" + local dst + + dst="$(get_rlo_hostid "${rtdst}" "${tid}")" + + __set_encap_route "${rtsrc}" "${dst}" "${dst}" "${sid}" "${tid}" true +} + +set_gw_encap_route_nolookup() +{ + local rtsrc="$1" + local rtdst="$2" + local sid="$3" + local tid="$4" + local dst + + dst="$(get_rlo_hostid "${rtdst}" "${tid}")" + + __set_encap_route "${rtsrc}" "${dst}" "${dst}" "${sid}" "${tid}" false +} + +# Setup the basic networking for a router +setup_rt_networking() +{ + local id="$1" + local nsname + + nsname="$(get_rt_nsname "${id}")" + + ip link set "veth-rt-${id}" netns "${nsname}" + ip -netns "${nsname}" link set "veth-rt-${id}" name veth0 + + ip netns exec "${nsname}" sysctl -wq net.ipv6.conf.all.accept_dad=0 + ip netns exec "${nsname}" sysctl -wq net.ipv6.conf.default.accept_dad=0 + + ip -netns "${nsname}" addr add "${IPv6_RT_NETWORK}::${id}/64" dev veth0 nodad + ip -netns "${nsname}" link set veth0 up + + ip netns exec "${nsname}" sysctl -wq net.ipv4.ip_forward=1 + ip netns exec "${nsname}" sysctl -wq net.ipv6.conf.all.forwarding=1 +} + +# Setup a host namespace and attach it to its gateway +setup_hs() +{ + local hid="$1" + local rid="$2" + local tid="$3" + local rtveth="veth-t${tid}" + local hsname + local rtname + + hsname="$(get_hs_nsname "${tid}" "${hid}")" + rtname="$(get_rt_nsname "${rid}")" + + ip netns exec "${hsname}" sysctl -wq net.ipv6.conf.all.accept_dad=0 + ip netns exec "${hsname}" sysctl -wq net.ipv6.conf.default.accept_dad=0 + + ip -netns "${hsname}" link add veth0 type veth peer name "${rtveth}" + ip -netns "${hsname}" link set "${rtveth}" netns "${rtname}" + + ip -netns "${hsname}" addr add \ + "${IPv6_HS_NETWORK}::${hid}/64" dev veth0 nodad + ip -netns "${hsname}" addr add \ + "${IPv4_HS_NETWORK}.${hid}/24" dev veth0 + + ip -netns "${hsname}" link set veth0 up +} + +# Setup the per-tenant VRF on a router (gateway, loopback, blackhole) +setup_rt() +{ + local rid="$1" + local tid="$2" + local rtveth="veth-t${tid}" + local rlo_dev="rlo-${tid}" + local rtname + local gw_addr_v6 + local gw_addr_v4 + + rtname="$(get_rt_nsname "${rid}")" + + gw_addr_v6="${IPv6_HS_NETWORK}::$(get_rlo_hostid "${rid}" "${tid}")" + gw_addr_v4="${IPv4_HS_NETWORK}.$(get_rlo_hostid "${rid}" "${tid}")" + + ip -netns "${rtname}" link add "vrf-${tid}" type vrf table "${tid}" + ip -netns "${rtname}" link set "vrf-${tid}" up + + ip -netns "${rtname}" link set "${rtveth}" master "vrf-${tid}" + + ip -netns "${rtname}" addr add \ + "${IPv6_HS_NETWORK}::254/64" dev "${rtveth}" nodad + ip -netns "${rtname}" addr add \ + "${IPv4_HS_NETWORK}.254/24" dev "${rtveth}" + + ip -netns "${rtname}" link set "${rtveth}" up + + ip netns exec "${rtname}" \ + sysctl -wq "net.ipv6.conf.${rtveth}.proxy_ndp=1" + ip netns exec "${rtname}" \ + sysctl -wq "net.ipv4.conf.${rtveth}.proxy_arp=1" + + ip netns exec "${rtname}" sh -c "echo 1 > /proc/sys/net/vrf/strict_mode" + + # router loopback interface for locally originated traffic + ip -netns "${rtname}" link add "${rlo_dev}" type dummy + ip -netns "${rtname}" link set "${rlo_dev}" master "vrf-${tid}" + + ip -netns "${rtname}" addr add "${gw_addr_v6}/128" \ + dev "${rlo_dev}" nodad + ip -netns "${rtname}" addr add "${gw_addr_v4}/32" \ + dev "${rlo_dev}" + + ip -netns "${rtname}" link set "${rlo_dev}" up + + # default blackhole routes in the VRF: any traffic that does not match + # a specific route is dropped. Without the "lookup" attribute on the + # encap route, the route for the first SID cannot be found from within + # the VRF. + ip -netns "${rtname}" -6 route add blackhole default metric 4278198272 \ + vrf "vrf-${tid}" + ip -netns "${rtname}" -4 route add blackhole default metric 4278198272 \ + vrf "vrf-${tid}" +} + +# Configure a one-way VPN path towards hsdst (on rtdst) for tenant tid. +# The encap side is set up on rtsrc and the decap side on rtdst. +# args: +# $1 - router id where the encap side is set up +# $2 - host id of the destination host +# $3 - router id of the destination router (connected to the destination host) +# $4 - tenant id +setup_vpn_config() +{ + local rtsrc="$1" + local hsdst="$2" + local rtdst="$3" + local tid="$4" + local rtveth="veth-t${tid}" + local rtsrc_name + local rtdst_name + local vpn_sid + + rtsrc_name="$(get_rt_nsname "${rtsrc}")" + rtdst_name="$(get_rt_nsname "${rtdst}")" + vpn_sid="$(build_vpn_sid "${rtsrc}" "${rtdst}" "${tid}")" + + ip -netns "${rtsrc_name}" -6 neigh add proxy \ + "${IPv6_HS_NETWORK}::${hsdst}" dev "${rtveth}" + set_host_encap_route "${rtsrc}" "${hsdst}" "${vpn_sid}" "${tid}" + + ip -netns "${rtsrc_name}" -6 route add "${vpn_sid}/128" \ + table "${UNDERLAY_TABLE_ID}" \ + via "fd00::${rtdst}" dev veth0 + + # set the decap route for decapsulating packets arriving from rtsrc + # and destined to hsdst + ip -netns "${rtdst_name}" -6 route add "${vpn_sid}/128" \ + table "${LOCALSID_TABLE_ID}" \ + encap seg6local action End.DT46 \ + vrftable "${tid}" dev "vrf-${tid}" + + # all SIDs for VPNs start with a common locator which is fc00::/16. + # Routes for handling the SRv6 End.DT* behavior instances are grouped + # together in the 'localsid' table. + # + # NOTE: added only once + if ! ip -netns "${rtdst_name}" -6 rule show | \ + grep -q "to ${VPN_LOCATOR_SERVICE}::/16 lookup ${LOCALSID_TABLE_ID}"; then + ip -netns "${rtdst_name}" -6 rule add \ + to "${VPN_LOCATOR_SERVICE}::/16" \ + lookup "${LOCALSID_TABLE_ID}" prio 999 + fi +} + +# Configure rtsrc to reach rtdst's loopback address through the VPN. +# args: +# $1 - router id where the encap route is installed +# $2 - router id whose loopback is the destination +# $3 - tenant id +setup_vpn_gw_encap() +{ + local rtsrc="$1" + local rtdst="$2" + local tid="$3" + local sid + + sid="$(build_vpn_sid "${rtsrc}" "${rtdst}" "${tid}")" + + set_gw_encap_route "${rtsrc}" "${rtdst}" "${sid}" "${tid}" +} + +setup() +{ + ip link add veth-rt-1 type veth peer name veth-rt-2 + setup_ns rt_1 rt_2 + setup_rt_networking 1 + setup_rt_networking 2 + + # setup two hosts for the tenant 100. + # - host hs-t100-1 is directly connected to the router rt-1; + # - host hs-t100-2 is directly connected to the router rt-2. + setup_ns hs_t100_1 hs_t100_2 + setup_hs 1 1 100 + setup_hs 2 2 100 + + # setup two hosts for the tenant 200. + # - host hs-t200-3 is directly connected to the router rt-1; + # - host hs-t200-4 is directly connected to the router rt-2. + setup_ns hs_t200_3 hs_t200_4 + setup_hs 3 1 200 + setup_hs 4 2 200 + + # configure each router for each tenant: VRF, blackhole routes, + # router loopback interface + setup_rt 1 100 + setup_rt 2 100 + setup_rt 1 200 + setup_rt 2 200 + + # setup the L3 VPN which connects the host hs-t100-1 and host hs-t100-2 + # within the same tenant 100. + setup_vpn_config 1 2 2 100 + setup_vpn_config 2 1 1 100 + + # setup the L3 VPN which connects the host hs-t200-3 and host hs-t200-4 + # within the same tenant 200. + setup_vpn_config 1 4 2 200 + setup_vpn_config 2 3 1 200 + + # allow each router to reach the other's loopback through the VPN + setup_vpn_gw_encap 2 1 100 + setup_vpn_gw_encap 1 2 100 + setup_vpn_gw_encap 2 1 200 + setup_vpn_gw_encap 1 2 200 + + # testing environment was set up successfully + SETUP_ERR=0 +} + +check_rt_connectivity() +{ + local rtsrc="$1" + local rtdst="$2" + local nsname + + nsname="$(get_rt_nsname "${rtsrc}")" + + ip netns exec "${nsname}" ping -c 1 -W 1 "${IPv6_RT_NETWORK}::${rtdst}" \ + >/dev/null 2>&1 +} + +check_and_log_rt_connectivity() +{ + local rtsrc="$1" + local rtdst="$2" + + check_rt_connectivity "${rtsrc}" "${rtdst}" + log_test $? 0 "Routers connectivity: rt-${rtsrc} -> rt-${rtdst}" +} + +check_hs_ipv6_connectivity() +{ + local hssrc="$1" + local hsdst="$2" + local tid="$3" + local nsname + + nsname="$(get_hs_nsname "${tid}" "${hssrc}")" + + ip netns exec "${nsname}" ping -c 1 -W "${PING_TIMEOUT_SEC}" \ + "${IPv6_HS_NETWORK}::${hsdst}" >/dev/null 2>&1 +} + +check_hs_ipv4_connectivity() +{ + local hssrc="$1" + local hsdst="$2" + local tid="$3" + local nsname + + nsname="$(get_hs_nsname "${tid}" "${hssrc}")" + + ip netns exec "${nsname}" ping -c 1 -W "${PING_TIMEOUT_SEC}" \ + "${IPv4_HS_NETWORK}.${hsdst}" >/dev/null 2>&1 +} + +check_and_log_hs_connectivity() +{ + local hssrc="$1" + local hsdst="$2" + local tid="$3" + + check_hs_ipv6_connectivity "${hssrc}" "${hsdst}" "${tid}" + log_test $? 0 "IPv6 connectivity: hs-t${tid}-${hssrc} -> hs-t${tid}-${hsdst} (tenant ${tid})" + + check_hs_ipv4_connectivity "${hssrc}" "${hsdst}" "${tid}" + log_test $? 0 "IPv4 connectivity: hs-t${tid}-${hssrc} -> hs-t${tid}-${hsdst} (tenant ${tid})" +} + +check_and_log_hs_isolation() +{ + local hssrc="$1" + local tidsrc="$2" + local hsdst="$3" + local tiddst="$4" + + check_hs_ipv6_connectivity "${hssrc}" "${hsdst}" "${tidsrc}" + log_test $? 1 "IPv6 isolation: hs-t${tidsrc}-${hssrc} -X-> hs-t${tiddst}-${hsdst}" + + check_hs_ipv4_connectivity "${hssrc}" "${hsdst}" "${tidsrc}" + log_test $? 1 "IPv4 isolation: hs-t${tidsrc}-${hssrc} -X-> hs-t${tiddst}-${hsdst}" +} + +check_and_log_hs2gw_connectivity() +{ + local hssrc="$1" + local tid="$2" + + check_hs_ipv6_connectivity "${hssrc}" 254 "${tid}" + log_test $? 0 "IPv6 connectivity: hs-t${tid}-${hssrc} -> gw (tenant ${tid})" + + check_hs_ipv4_connectivity "${hssrc}" 254 "${tid}" + log_test $? 0 "IPv4 connectivity: hs-t${tid}-${hssrc} -> gw (tenant ${tid})" +} + +router_tests() +{ + log_section "IPv6 routers connectivity test" + + check_and_log_rt_connectivity 1 2 + check_and_log_rt_connectivity 2 1 +} + +host2gateway_tests() +{ + log_section "Connectivity test among hosts and gateway" + + check_and_log_hs2gw_connectivity 1 100 + check_and_log_hs2gw_connectivity 2 100 + + check_and_log_hs2gw_connectivity 3 200 + check_and_log_hs2gw_connectivity 4 200 +} + +host_vpn_tests() +{ + log_section "SRv6 VPN connectivity test among hosts in the same tenant" + + check_and_log_hs_connectivity 1 2 100 + check_and_log_hs_connectivity 2 1 100 + + check_and_log_hs_connectivity 3 4 200 + check_and_log_hs_connectivity 4 3 200 +} + +host_vpn_isolation_tests() +{ + local l1="1 2" + local l2="3 4" + local t1=100 + local t2=200 + local i + local j + local tmp + + log_section "SRv6 VPN isolation test among hosts in different tenants" + + for _ in 0 1; do + for i in ${l1}; do + for j in ${l2}; do + check_and_log_hs_isolation "${i}" "${t1}" "${j}" "${t2}" + done + done + + # let us test the reverse path + tmp="${l1}"; l1="${l2}"; l2="${tmp}" + tmp=${t1}; t1=${t2}; t2=${tmp} + done +} + +__test_nolookup() +{ + local hssrc="$1" + local hsdst="$2" + local rtsrc="$3" + local rtdst="$4" + local tid="$5" + local vpn_sid + + vpn_sid="$(build_vpn_sid "${rtsrc}" "${rtdst}" "${tid}")" + + # replace encap route(s) without "lookup" attribute + set_host_encap_route_nolookup "${rtsrc}" "${hsdst}" "${vpn_sid}" "${tid}" + + check_hs_ipv6_connectivity "${hssrc}" "${hsdst}" "${tid}" + log_test $? 1 "IPv6 w/o lookup: hs-t${tid}-${hssrc} -X-> hs-t${tid}-${hsdst} (tenant ${tid})" + + check_hs_ipv4_connectivity "${hssrc}" "${hsdst}" "${tid}" + log_test $? 1 "IPv4 w/o lookup: hs-t${tid}-${hssrc} -X-> hs-t${tid}-${hsdst} (tenant ${tid})" + + # restore encap route(s) with "lookup" for subsequent tests + set_host_encap_route "${rtsrc}" "${hsdst}" "${vpn_sid}" "${tid}" +} + +host_vpn_nolookup_tests() +{ + log_section "SRv6 VPN connectivity test among hosts w/o lookup" + + __test_nolookup 1 2 1 2 100 + __test_nolookup 2 1 2 1 100 + + __test_nolookup 3 4 1 2 200 + __test_nolookup 4 3 2 1 200 +} + +check_gw_ipv6_connectivity() +{ + local rtsrc="$1" + local rtdst="$2" + local tidsrc="$3" + local tiddst="$4" + local rtname + local src_v6 + local dst_v6 + + rtname="$(get_rt_nsname "${rtsrc}")" + src_v6="${IPv6_HS_NETWORK}::$(get_rlo_hostid "${rtsrc}" "${tidsrc}")" + dst_v6="${IPv6_HS_NETWORK}::$(get_rlo_hostid "${rtdst}" "${tiddst}")" + + ip netns exec "${rtname}" ip vrf exec "vrf-${tidsrc}" \ + ping -c 1 -W "${PING_TIMEOUT_SEC}" \ + -I "${src_v6}" "${dst_v6}" >/dev/null 2>&1 +} + +check_gw_ipv4_connectivity() +{ + local rtsrc="$1" + local rtdst="$2" + local tidsrc="$3" + local tiddst="$4" + local rtname + local src_v4 + local dst_v4 + + rtname="$(get_rt_nsname "${rtsrc}")" + src_v4="${IPv4_HS_NETWORK}.$(get_rlo_hostid "${rtsrc}" "${tidsrc}")" + dst_v4="${IPv4_HS_NETWORK}.$(get_rlo_hostid "${rtdst}" "${tiddst}")" + + ip netns exec "${rtname}" ip vrf exec "vrf-${tidsrc}" \ + ping -c 1 -W "${PING_TIMEOUT_SEC}" \ + -I "${src_v4}" "${dst_v4}" >/dev/null 2>&1 +} + +check_and_log_gw_connectivity() +{ + local rtsrc="$1" + local rtdst="$2" + local tid="$3" + + check_gw_ipv6_connectivity "${rtsrc}" "${rtdst}" "${tid}" "${tid}" + log_test $? 0 "IPv6 connectivity: rt-${rtsrc} -> rt-${rtdst} (tenant ${tid})" + + check_gw_ipv4_connectivity "${rtsrc}" "${rtdst}" "${tid}" "${tid}" + log_test $? 0 "IPv4 connectivity: rt-${rtsrc} -> rt-${rtdst} (tenant ${tid})" +} + +check_and_log_gw_isolation() +{ + local rtsrc="$1" + local rtdst="$2" + local tidsrc="$3" + local tiddst="$4" + + check_gw_ipv6_connectivity "${rtsrc}" "${rtdst}" "${tidsrc}" "${tiddst}" + log_test $? 1 "IPv6 isolation: rt-${rtsrc} -X-> rt-${rtdst} (tenants ${tidsrc}/${tiddst})" + + check_gw_ipv4_connectivity "${rtsrc}" "${rtdst}" "${tidsrc}" "${tiddst}" + log_test $? 1 "IPv4 isolation: rt-${rtsrc} -X-> rt-${rtdst} (tenants ${tidsrc}/${tiddst})" +} + +gw_vpn_isolation_tests() +{ + log_section "SRv6 VPN isolation test among routers in different tenants" + + check_and_log_gw_isolation 1 2 100 200 + check_and_log_gw_isolation 2 1 100 200 + + check_and_log_gw_isolation 1 2 200 100 + check_and_log_gw_isolation 2 1 200 100 +} + +gw_vpn_tests() +{ + log_section "SRv6 VPN connectivity test among routers in the same tenant" + + check_and_log_gw_connectivity 1 2 100 + check_and_log_gw_connectivity 2 1 100 + + check_and_log_gw_connectivity 1 2 200 + check_and_log_gw_connectivity 2 1 200 +} + +__test_gw_nolookup() +{ + local rtsrc="$1" + local rtdst="$2" + local tid="$3" + local sid + + sid="$(build_vpn_sid "${rtsrc}" "${rtdst}" "${tid}")" + + # replace gw encap route without "lookup" attribute + set_gw_encap_route_nolookup "${rtsrc}" "${rtdst}" "${sid}" "${tid}" + + check_gw_ipv6_connectivity "${rtsrc}" "${rtdst}" "${tid}" "${tid}" + log_test $? 1 "IPv6 w/o lookup: rt-${rtsrc} -X-> rt-${rtdst} (tenant ${tid})" + + check_gw_ipv4_connectivity "${rtsrc}" "${rtdst}" "${tid}" "${tid}" + log_test $? 1 "IPv4 w/o lookup: rt-${rtsrc} -X-> rt-${rtdst} (tenant ${tid})" + + # restore gw encap route with "lookup" for subsequent tests + set_gw_encap_route "${rtsrc}" "${rtdst}" "${sid}" "${tid}" +} + +gw_vpn_nolookup_tests() +{ + log_section "SRv6 VPN connectivity test among routers w/o lookup" + + __test_gw_nolookup 1 2 100 + __test_gw_nolookup 2 1 100 + + __test_gw_nolookup 1 2 200 + __test_gw_nolookup 2 1 200 +} + +test_command_or_ksft_skip() +{ + local cmd="$1" + + if [ ! -x "$(command -v "${cmd}")" ]; then + echo "SKIP: Could not run test without \"${cmd}\" tool" + exit "${ksft_skip}" + fi +} + +test_vrf_or_ksft_skip() +{ + modprobe vrf &>/dev/null || true + if [ ! -e /proc/sys/net/vrf/strict_mode ]; then + echo "SKIP: vrf sysctl does not exist" + exit "${ksft_skip}" + fi +} + +test_dummy_dev_or_ksft_skip() +{ + local test_netns + + setup_ns test_netns + + modprobe dummy &>/dev/null || true + if ! ip -netns "${test_netns}" link add "${DUMMY_DEVNAME}" \ + type dummy; then + cleanup_ns "${test_netns}" + echo "SKIP: dummy dev not supported" + exit "${ksft_skip}" + fi + + cleanup_ns "${test_netns}" +} + +test_encap_lookup_supp_or_ksft_skip() +{ + local nsname + + setup_ns nsname + + ip -netns "${nsname}" link add "${DUMMY_DEVNAME}" type dummy + ip -netns "${nsname}" link set "${DUMMY_DEVNAME}" up + + if ! ip -netns "${nsname}" -6 route add "${IPv6_TESTS_ADDR}/128" \ + encap seg6 mode encap segs fc00::1 \ + lookup "${TESTS_TABLE_ID}" \ + dev "${DUMMY_DEVNAME}" 2>/dev/null; then + cleanup_ns "${nsname}" + echo "SKIP: seg6 encap lookup attribute not supported" + exit "${ksft_skip}" + fi + + # An old kernel with a recent iproute2 accepts the route but + # silently ignores the lookup attribute. Dump the route and check + # the attribute is really there, otherwise the test falsely passes. + if ! ip -netns "${nsname}" -6 route show "${IPv6_TESTS_ADDR}/128" | \ + grep -q "lookup ${TESTS_TABLE_ID}"; then + cleanup_ns "${nsname}" + echo "SKIP: seg6 encap lookup attribute not supported" + exit "${ksft_skip}" + fi + + cleanup_ns "${nsname}" +} + +if [ "$(id -u)" -ne 0 ]; then + echo "SKIP: Need root privileges" + exit "${ksft_skip}" +fi + +# required programs to carry out this selftest +test_command_or_ksft_skip ip +test_command_or_ksft_skip ping +test_command_or_ksft_skip sysctl +test_command_or_ksft_skip grep + +test_dummy_dev_or_ksft_skip +test_vrf_or_ksft_skip +test_encap_lookup_supp_or_ksft_skip + +set -e +trap cleanup EXIT + +setup +set +e + +router_tests +host2gateway_tests +host_vpn_tests +host_vpn_isolation_tests +host_vpn_nolookup_tests +gw_vpn_tests +gw_vpn_isolation_tests +gw_vpn_nolookup_tests + +print_log_test_results diff --git a/tools/testing/selftests/net/srv6_end_dt46_l3vpn_test.sh b/tools/testing/selftests/net/srv6_end_dt46_l3vpn_test.sh index a5e959a080bb..50e37d3217ea 100755 --- a/tools/testing/selftests/net/srv6_end_dt46_l3vpn_test.sh +++ b/tools/testing/selftests/net/srv6_end_dt46_l3vpn_test.sh @@ -536,6 +536,14 @@ host_vpn_isolation_tests() done } +test_iproute2_supp_or_ksft_skip() +{ + if ! ip route add help 2>&1 | grep -qo "End.DT46"; then + echo "SKIP: Missing SRv6 End.DT46 support in iproute2" + exit "${ksft_skip}" + fi +} + if [ "$(id -u)" -ne 0 ];then echo "SKIP: Need root privileges" exit $ksft_skip @@ -546,6 +554,8 @@ if [ ! -x "$(command -v ip)" ]; then exit $ksft_skip fi +test_iproute2_supp_or_ksft_skip + modprobe vrf &>/dev/null if [ ! -e /proc/sys/net/vrf/strict_mode ]; then echo "SKIP: vrf sysctl does not exist" diff --git a/tools/testing/selftests/net/srv6_hencap_red_l3vpn_test.sh b/tools/testing/selftests/net/srv6_hencap_red_l3vpn_test.sh index 6a68c7eff1dc..cd7d061e21f8 100755 --- a/tools/testing/selftests/net/srv6_hencap_red_l3vpn_test.sh +++ b/tools/testing/selftests/net/srv6_hencap_red_l3vpn_test.sh @@ -193,6 +193,8 @@ ret=${ksft_skip} nsuccess=0 nfail=0 +HAS_TUNSRC=false + log_test() { local rc="$1" @@ -345,6 +347,17 @@ setup_rt_networking() ip -netns "${nsname}" addr \ add "${net_prefix}::${rt}/64" dev "${devname}" nodad + # A dedicated ::dead:<rt> address (with preferred_lft 0, i.e., + # deprecated) is added when there is support for tunsrc. Because + # it is deprecated, the kernel should never auto-select it as + # source with current config. Only an explicit tunsrc can place + # it in the outer header. + if $HAS_TUNSRC; then + ip -netns "${nsname}" addr \ + add "${net_prefix}::dead:${rt}/64" \ + dev "${devname}" nodad preferred_lft 0 + fi + ip -netns "${nsname}" link set "${devname}" up done @@ -420,6 +433,7 @@ setup_rt_local_sids() # to the destination host) # $5 - encap mode (full or red) # $6 - traffic type (IPv6 or IPv4) +# $7 - force tunsrc (true or false) __setup_rt_policy() { local dst="$1" @@ -428,10 +442,46 @@ __setup_rt_policy() local dec_rt="$4" local mode="$5" local traffic="$6" + local with_tunsrc="$7" local nsname local policy='' + local tunsrc='' local n + # Verify the per-route tunnel source address ("tunsrc") feature. + # If it is not supported, fallback on encap config without tunsrc. + if $with_tunsrc && $HAS_TUNSRC; then + local net_prefix + local drule + local nxt + + eval nsname=\${$(get_rtname "${dec_rt}")} + + # Next SRv6 hop: first End router if any, or the decap router + [ -z "${end_rts}" ] && nxt="${dec_rt}" || nxt="${end_rts%% *}" + + # Use the right prefix for tunsrc depending on the next SRv6 hop + net_prefix="$(get_network_prefix "${encap_rt}" "${nxt}")" + tunsrc="tunsrc ${net_prefix}::dead:${encap_rt}" + + # To verify that the outer source address matches the one + # configured with tunsrc, the decap router discards packets + # with any other source address. + ip netns exec "${nsname}" ip6tables -t raw -I PREROUTING 1 \ + -s "${net_prefix}::dead:${encap_rt}" \ + -d "${VPN_LOCATOR_SERVICE}:${dec_rt}::${DT46_FUNC}" \ + -j ACCEPT + + drule="PREROUTING \ + -d ${VPN_LOCATOR_SERVICE}:${dec_rt}::${DT46_FUNC} \ + -j DROP" + + if ! ip netns exec "${nsname}" \ + ip6tables -t raw -C ${drule} &>/dev/null; then + ip netns exec "${nsname}" ip6tables -t raw -A ${drule} + fi + fi + eval nsname=\${$(get_rtname "${encap_rt}")} for n in ${end_rts}; do @@ -444,7 +494,7 @@ __setup_rt_policy() if [ "${traffic}" -eq 6 ]; then ip -netns "${nsname}" -6 route \ add "${IPv6_HS_NETWORK}::${dst}" vrf "${VRF_DEVNAME}" \ - encap seg6 mode "${mode}" segs "${policy}" \ + encap seg6 mode "${mode}" ${tunsrc} segs "${policy}" \ dev "${VRF_DEVNAME}" ip -netns "${nsname}" -6 neigh \ @@ -455,7 +505,7 @@ __setup_rt_policy() # received, otherwise the proxy arp does not work. ip -netns "${nsname}" -4 route \ add "${IPv4_HS_NETWORK}.${dst}" vrf "${VRF_DEVNAME}" \ - encap seg6 mode "${mode}" segs "${policy}" \ + encap seg6 mode "${mode}" ${tunsrc} segs "${policy}" \ dev "${VRF_DEVNAME}" fi } @@ -463,13 +513,13 @@ __setup_rt_policy() # see __setup_rt_policy setup_rt_policy_ipv6() { - __setup_rt_policy "$1" "$2" "$3" "$4" "$5" 6 + __setup_rt_policy "$1" "$2" "$3" "$4" "$5" 6 "$6" } #see __setup_rt_policy setup_rt_policy_ipv4() { - __setup_rt_policy "$1" "$2" "$3" "$4" "$5" 4 + __setup_rt_policy "$1" "$2" "$3" "$4" "$5" 4 "$6" } setup_hs() @@ -567,41 +617,41 @@ setup() # the network path between hs-1 and hs-2 traverses several routers # depending on the direction of traffic. # - # Direction hs-1 -> hs-2 (H.Encaps.Red) + # Direction hs-1 -> hs-2 (H.Encaps.Red + tunsrc) # - rt-3,rt-4 (SRv6 End behaviors) # - rt-2 (SRv6 End.DT46 behavior) # # Direction hs-2 -> hs-1 (H.Encaps.Red) # - rt-1 (SRv6 End.DT46 behavior) - setup_rt_policy_ipv6 2 1 "3 4" 2 encap.red - setup_rt_policy_ipv6 1 2 "" 1 encap.red + setup_rt_policy_ipv6 2 1 "3 4" 2 encap.red true + setup_rt_policy_ipv6 1 2 "" 1 encap.red false # create an IPv4 VPN between hosts hs-1 and hs-2 # the network path between hs-1 and hs-2 traverses several routers # depending on the direction of traffic. # - # Direction hs-1 -> hs-2 (H.Encaps.Red) + # Direction hs-1 -> hs-2 (H.Encaps.Red + tunsrc) # - rt-2 (SRv6 End.DT46 behavior) # # Direction hs-2 -> hs-1 (H.Encaps.Red) # - rt-4,rt-3 (SRv6 End behaviors) # - rt-1 (SRv6 End.DT46 behavior) - setup_rt_policy_ipv4 2 1 "" 2 encap.red - setup_rt_policy_ipv4 1 2 "4 3" 1 encap.red + setup_rt_policy_ipv4 2 1 "" 2 encap.red true + setup_rt_policy_ipv4 1 2 "4 3" 1 encap.red false # create an IPv6 VPN between hosts hs-3 and hs-4 # the network path between hs-3 and hs-4 traverses several routers # depending on the direction of traffic. # - # Direction hs-3 -> hs-4 (H.Encaps.Red) + # Direction hs-3 -> hs-4 (H.Encaps.Red + tunsrc) # - rt-2 (SRv6 End Behavior) # - rt-4 (SRv6 End.DT46 behavior) # # Direction hs-4 -> hs-3 (H.Encaps.Red) # - rt-1 (SRv6 End behavior) # - rt-3 (SRv6 End.DT46 behavior) - setup_rt_policy_ipv6 4 3 "2" 4 encap.red - setup_rt_policy_ipv6 3 4 "1" 3 encap.red + setup_rt_policy_ipv6 4 3 "2" 4 encap.red true + setup_rt_policy_ipv6 3 4 "1" 3 encap.red false # testing environment was set up successfully SETUP_ERR=0 @@ -809,6 +859,38 @@ test_vrf_or_ksft_skip() fi } +# Before enabling tunsrc tests, make sure tunsrc and ip6tables are supported. +check_tunsrc_support() +{ + setup_ns tunsrc_ns + + ip -netns "${tunsrc_ns}" link add veth0 type veth \ + peer name veth1 netns "${tunsrc_ns}" + + ip -netns "${tunsrc_ns}" link set veth0 up + + if ! ip -netns "${tunsrc_ns}" -6 route add fc00::dead:beef/128 \ + encap seg6 mode encap.red tunsrc fc00::1 segs fc00::2 \ + dev veth0 &>/dev/null; then + cleanup_ns "${tunsrc_ns}" + return + fi + + if ! ip -netns "${tunsrc_ns}" -6 route show | grep -q "tunsrc"; then + cleanup_ns "${tunsrc_ns}" + return + fi + + if ! ip netns exec "${tunsrc_ns}" ip6tables -t raw -A PREROUTING \ + -d fc00::dead:beef -j DROP &>/dev/null; then + cleanup_ns "${tunsrc_ns}" + return + fi + + cleanup_ns "${tunsrc_ns}" + HAS_TUNSRC=true +} + if [ "$(id -u)" -ne 0 ]; then echo "SKIP: Need root privileges" exit "${ksft_skip}" @@ -826,6 +908,7 @@ test_vrf_or_ksft_skip set -e trap cleanup EXIT +check_tunsrc_support setup set +e diff --git a/tools/testing/selftests/net/srv6_iptunnel_cache.sh b/tools/testing/selftests/net/srv6_iptunnel_cache.sh new file mode 100755 index 000000000000..62638ab679d9 --- /dev/null +++ b/tools/testing/selftests/net/srv6_iptunnel_cache.sh @@ -0,0 +1,197 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# author: Andrea Mayer <andrea.mayer@uniroma2.it> + +# This test verifies that the seg6 lwtunnel does not share the dst_cache +# between the input (forwarding) and output (locally generated) paths. +# +# A shared dst_cache allows a forwarded packet to populate the cache and a +# subsequent locally generated packet to silently reuse that entry, bypassing +# its own route lookup. To expose this, the SID is made reachable only for +# forwarded traffic (via an ip rule matching iif) and blackholed for everything +# else. A local ping on ns_router must always hit the blackhole; +# if it succeeds after a forwarded packet has populated the +# cache, the bug is confirmed. +# +# Both forwarded and local packets are pinned to the same CPU with taskset, +# since dst_cache is per-cpu. +# +# +# +--------------------+ +--------------------+ +# | ns_src | | ns_dst | +# | | | | +# | veth-s0 | | veth-d0 | +# | fd00::1/64 | | fd01::2/64 | +# +-------+------------+ +----------+---------+ +# | | +# | +--------------------+ | +# | | ns_router | | +# | | | | +# +------------+ veth-r0 veth-r1 +--------------+ +# | fd00::2 fd01::1 | +# +--------------------+ +# +# +# ns_router: encap (main table) +# +---------+---------------------------------------+ +# | dst | action | +# +---------+---------------------------------------+ +# | cafe::1 | encap seg6 mode encap segs fc00::100 | +# +---------+---------------------------------------+ +# +# ns_router: post-encap SID resolution +# +-------+------------+----------------------------+ +# | table | dst | action | +# +-------+------------+----------------------------+ +# | 100 | fc00::100 | via fd01::2 dev veth-r1 | +# +-------+------------+----------------------------+ +# | main | fc00::100 | blackhole | +# +-------+------------+----------------------------+ +# +# ns_router: ip rule +# +------------------+------------------------------+ +# | match | action | +# +------------------+------------------------------+ +# | iif veth-r0 | lookup 100 | +# +------------------+------------------------------+ +# +# ns_dst: SRv6 decap (main table) +# +--------------+----------------------------------+ +# | SID | action | +# +--------------+----------------------------------+ +# | fc00::100 | End.DT6 table 255 (local) | +# +--------------+----------------------------------+ + +source lib.sh + +readonly SID="fc00::100" +readonly DEST="cafe::1" + +readonly SRC_MAC="02:00:00:00:00:01" +readonly RTR_R0_MAC="02:00:00:00:00:02" +readonly RTR_R1_MAC="02:00:00:00:00:03" +readonly DST_MAC="02:00:00:00:00:04" + +cleanup() +{ + cleanup_ns "${NS_SRC}" "${NS_RTR}" "${NS_DST}" +} + +check_prerequisites() +{ + if ! command -v ip &>/dev/null; then + echo "SKIP: ip tool not found" + exit "${ksft_skip}" + fi + + if ! command -v ping &>/dev/null; then + echo "SKIP: ping not found" + exit "${ksft_skip}" + fi + + if ! command -v sysctl &>/dev/null; then + echo "SKIP: sysctl not found" + exit "${ksft_skip}" + fi + + if ! command -v taskset &>/dev/null; then + echo "SKIP: taskset not found" + exit "${ksft_skip}" + fi +} + +setup() +{ + setup_ns NS_SRC NS_RTR NS_DST + + ip link add veth-s0 netns "${NS_SRC}" type veth \ + peer name veth-r0 netns "${NS_RTR}" + ip link add veth-r1 netns "${NS_RTR}" type veth \ + peer name veth-d0 netns "${NS_DST}" + + ip -n "${NS_SRC}" link set veth-s0 address "${SRC_MAC}" + ip -n "${NS_RTR}" link set veth-r0 address "${RTR_R0_MAC}" + ip -n "${NS_RTR}" link set veth-r1 address "${RTR_R1_MAC}" + ip -n "${NS_DST}" link set veth-d0 address "${DST_MAC}" + + # ns_src + ip -n "${NS_SRC}" link set veth-s0 up + ip -n "${NS_SRC}" addr add fd00::1/64 dev veth-s0 nodad + ip -n "${NS_SRC}" -6 route add "${DEST}"/128 via fd00::2 + + # ns_router + ip -n "${NS_RTR}" link set veth-r0 up + ip -n "${NS_RTR}" addr add fd00::2/64 dev veth-r0 nodad + ip -n "${NS_RTR}" link set veth-r1 up + ip -n "${NS_RTR}" addr add fd01::1/64 dev veth-r1 nodad + ip netns exec "${NS_RTR}" sysctl -qw net.ipv6.conf.all.forwarding=1 + + ip -n "${NS_RTR}" -6 route add "${DEST}"/128 \ + encap seg6 mode encap segs "${SID}" dev veth-r0 + ip -n "${NS_RTR}" -6 route add "${SID}"/128 table 100 \ + via fd01::2 dev veth-r1 + ip -n "${NS_RTR}" -6 route add blackhole "${SID}"/128 + ip -n "${NS_RTR}" -6 rule add iif veth-r0 lookup 100 + + # ns_dst + ip -n "${NS_DST}" link set veth-d0 up + ip -n "${NS_DST}" addr add fd01::2/64 dev veth-d0 nodad + ip -n "${NS_DST}" addr add "${DEST}"/128 dev lo nodad + ip -n "${NS_DST}" -6 route add "${SID}"/128 \ + encap seg6local action End.DT6 table 255 dev veth-d0 + ip -n "${NS_DST}" -6 route add fd00::/64 via fd01::1 + + # static neighbors + ip -n "${NS_SRC}" -6 neigh add fd00::2 dev veth-s0 \ + lladdr "${RTR_R0_MAC}" nud permanent + ip -n "${NS_RTR}" -6 neigh add fd00::1 dev veth-r0 \ + lladdr "${SRC_MAC}" nud permanent + ip -n "${NS_RTR}" -6 neigh add fd01::2 dev veth-r1 \ + lladdr "${DST_MAC}" nud permanent + ip -n "${NS_DST}" -6 neigh add fd01::1 dev veth-d0 \ + lladdr "${RTR_R1_MAC}" nud permanent +} + +test_cache_isolation() +{ + RET=0 + + # local ping with empty cache: must fail (SID is blackholed) + if ip netns exec "${NS_RTR}" taskset -c 0 \ + ping -c 1 -W 2 "${DEST}" &>/dev/null; then + echo "SKIP: local ping succeeded, topology broken" + exit "${ksft_skip}" + fi + + # forward from ns_src to populate the input cache + if ! ip netns exec "${NS_SRC}" taskset -c 0 \ + ping -c 1 -W 2 "${DEST}" &>/dev/null; then + echo "SKIP: forwarded ping failed, topology broken" + exit "${ksft_skip}" + fi + + # local ping again: must still fail; if the output path reuses + # the input cache, it bypasses the blackhole and the ping succeeds + if ip netns exec "${NS_RTR}" taskset -c 0 \ + ping -c 1 -W 2 "${DEST}" &>/dev/null; then + echo "FAIL: output path used dst cached by input path" + RET="${ksft_fail}" + else + echo "PASS: output path dst_cache is independent" + fi + + return "${RET}" +} + +if [ "$(id -u)" -ne 0 ]; then + echo "SKIP: Need root privileges" + exit "${ksft_skip}" +fi + +trap cleanup EXIT + +check_prerequisites +setup +test_cache_isolation +exit "${RET}" diff --git a/tools/testing/selftests/net/tcp_ao/config b/tools/testing/selftests/net/tcp_ao/config index 971cb6fa2d63..1b120bfd89c4 100644 --- a/tools/testing/selftests/net/tcp_ao/config +++ b/tools/testing/selftests/net/tcp_ao/config @@ -1,6 +1,3 @@ -CONFIG_CRYPTO_HMAC=y -CONFIG_CRYPTO_RMD160=y -CONFIG_CRYPTO_SHA1=y CONFIG_IPV6=y CONFIG_IPV6_MULTIPLE_TABLES=y CONFIG_NET_L3_MASTER_DEV=y diff --git a/tools/testing/selftests/net/tcp_ao/key-management.c b/tools/testing/selftests/net/tcp_ao/key-management.c index 69d9a7a05d5c..0451f92f4645 100644 --- a/tools/testing/selftests/net/tcp_ao/key-management.c +++ b/tools/testing/selftests/net/tcp_ao/key-management.c @@ -63,8 +63,8 @@ static int prepare_lsk(union tcp_addr *addr, uint8_t sndid, uint8_t rcvid) return sk; } -static int test_del_key(int sk, uint8_t sndid, uint8_t rcvid, bool async, - int current_key, int rnext_key) +static int test_del_key(int sk, uint8_t sndid, uint8_t rcvid, int ifindex, + bool async, int current_key, int rnext_key) { struct tcp_ao_info_opt ao_info = {}; struct tcp_ao_getsockopt key = {}; @@ -76,6 +76,10 @@ static int test_del_key(int sk, uint8_t sndid, uint8_t rcvid, bool async, del.prefix = DEFAULT_TEST_PREFIX; del.sndid = sndid; del.rcvid = rcvid; + if (ifindex) { + del.keyflags = TCP_AO_KEYF_IFINDEX; + del.ifindex = ifindex; + } if (current_key >= 0) { del.set_current = 1; @@ -95,7 +99,8 @@ static int test_del_key(int sk, uint8_t sndid, uint8_t rcvid, bool async, tcp_addr_to_sockaddr_in(&sockaddr, &this_ip_dest, 0); err = test_get_one_ao(sk, &key, &sockaddr, sizeof(sockaddr), - DEFAULT_TEST_PREFIX, sndid, rcvid); + DEFAULT_TEST_PREFIX, sndid, rcvid, + del.keyflags, del.ifindex); if (!err) return -EEXIST; if (err != -E2BIG) @@ -112,12 +117,12 @@ static int test_del_key(int sk, uint8_t sndid, uint8_t rcvid, bool async, } static void try_delete_key(char *tst_name, int sk, uint8_t sndid, uint8_t rcvid, - bool async, int current_key, int rnext_key, + int ifindex, bool async, int current_key, int rnext_key, fault_t inj) { int err; - err = test_del_key(sk, sndid, rcvid, async, current_key, rnext_key); + err = test_del_key(sk, sndid, rcvid, ifindex, async, current_key, rnext_key); if ((err == -EBUSY && fault(BUSY)) || (err == -EINVAL && fault(CURRNEXT))) { test_ok("%s: key deletion was prevented", tst_name); return; @@ -236,15 +241,15 @@ static void check_closed_socket(void) int sk; sk = prepare_sk(&this_ip_dest, 200, 200); - try_delete_key("closed socket, delete a key", sk, 200, 200, 0, -1, -1, 0); - try_delete_key("closed socket, delete all keys", sk, 100, 100, 0, -1, -1, 0); + try_delete_key("closed socket, delete a key", sk, 200, 200, 0, 0, -1, -1, 0); + try_delete_key("closed socket, delete all keys", sk, 100, 100, 0, 0, -1, -1, 0); close(sk); sk = prepare_sk(&this_ip_dest, 200, 200); if (test_set_key(sk, 100, 200)) test_error("failed to set current/rnext keys"); - try_delete_key("closed socket, delete current key", sk, 100, 100, 0, -1, -1, FAULT_BUSY); - try_delete_key("closed socket, delete rnext key", sk, 200, 200, 0, -1, -1, FAULT_BUSY); + try_delete_key("closed socket, delete current key", sk, 100, 100, 0, 0, -1, -1, FAULT_BUSY); + try_delete_key("closed socket, delete rnext key", sk, 200, 200, 0, 0, -1, -1, FAULT_BUSY); close(sk); sk = prepare_sk(&this_ip_dest, 200, 200); @@ -254,10 +259,12 @@ static void check_closed_socket(void) if (test_add_key(sk, "Glory to Ukraine!", this_ip_dest, DEFAULT_TEST_PREFIX, 12, 13)) test_error("test_add_key()"); - try_delete_key("closed socket, delete a key + set current/rnext", sk, 100, 100, 0, 10, 13, 0); - try_delete_key("closed socket, force-delete current key", sk, 10, 11, 0, 200, -1, 0); - try_delete_key("closed socket, force-delete rnext key", sk, 12, 13, 0, -1, 200, 0); - try_delete_key("closed socket, delete current+rnext key", sk, 200, 200, 0, -1, -1, FAULT_BUSY); + try_delete_key("closed socket, delete a key + set current/rnext", sk, + 100, 100, 0, 0, 10, 13, 0); + try_delete_key("closed socket, force-delete current key", sk, 10, 11, 0, 0, 200, -1, 0); + try_delete_key("closed socket, force-delete rnext key", sk, 12, 13, 0, 0, -1, 200, 0); + try_delete_key("closed socket, delete current+rnext key", sk, + 200, 200, 0, 0, -1, -1, FAULT_BUSY); close(sk); sk = prepare_sk(&this_ip_dest, 200, 200); @@ -272,6 +279,18 @@ static void check_closed_socket(void) this_ip_dest, DEFAULT_TEST_PREFIX, false, true, 20, 10, 0); close(sk); + + if (!should_skip_test("closed socket, add + delete VRF-scoped key", + KCONFIG_NET_VRF)) { + sk = prepare_sk(&this_ip_dest, 200, 200); + if (test_add_key_vrf(sk, SECOND_PASSWORD, TCP_AO_KEYF_IFINDEX, + this_ip_dest, DEFAULT_TEST_PREFIX, + test_vrf_ifindex, 201, 201)) + test_error("test_add_key_vrf()"); + try_delete_key("closed socket, add + delete VRF-scoped key", sk, 201, 201, + test_vrf_ifindex, 0, -1, -1, 0); + close(sk); + } } static void assert_no_current_rnext(const char *tst_msg, int sk) @@ -322,8 +341,8 @@ static void check_listen_socket(void) int sk, err; sk = prepare_lsk(&this_ip_dest, 200, 200); - try_delete_key("listen socket, delete a key", sk, 200, 200, 0, -1, -1, 0); - try_delete_key("listen socket, delete all keys", sk, 100, 100, 0, -1, -1, 0); + try_delete_key("listen socket, delete a key", sk, 200, 200, 0, 0, -1, -1, 0); + try_delete_key("listen socket, delete all keys", sk, 100, 100, 0, 0, -1, -1, 0); close(sk); sk = prepare_lsk(&this_ip_dest, 200, 200); @@ -345,8 +364,10 @@ static void check_listen_socket(void) if (listen(sk, 10)) test_error("listen()"); assert_no_current_rnext("listen() after current/rnext keys set", sk); - try_delete_key("listen socket, delete current key from before listen()", sk, 100, 100, 0, -1, -1, FAULT_FIXME); - try_delete_key("listen socket, delete rnext key from before listen()", sk, 200, 200, 0, -1, -1, FAULT_FIXME); + try_delete_key("listen socket, delete current key from before listen()", sk, + 100, 100, 0, 0, -1, -1, FAULT_FIXME); + try_delete_key("listen socket, delete rnext key from before listen()", sk, + 200, 200, 0, 0, -1, -1, FAULT_FIXME); close(sk); assert_no_tcp_repair(); @@ -359,13 +380,13 @@ static void check_listen_socket(void) DEFAULT_TEST_PREFIX, 12, 13)) test_error("test_add_key()"); try_delete_key("listen socket, delete a key + set current/rnext", sk, - 100, 100, 0, 10, 13, FAULT_CURRNEXT); + 100, 100, 0, 0, 10, 13, FAULT_CURRNEXT); try_delete_key("listen socket, force-delete current key", sk, - 10, 11, 0, 200, -1, FAULT_CURRNEXT); + 10, 11, 0, 0, 200, -1, FAULT_CURRNEXT); try_delete_key("listen socket, force-delete rnext key", sk, - 12, 13, 0, -1, 200, FAULT_CURRNEXT); + 12, 13, 0, 0, -1, 200, FAULT_CURRNEXT); try_delete_key("listen socket, delete a key", sk, - 200, 200, 0, -1, -1, 0); + 200, 200, 0, 0, -1, -1, 0); close(sk); sk = prepare_lsk(&this_ip_dest, 200, 200); @@ -380,31 +401,6 @@ static void check_listen_socket(void) close(sk); } -static const char *fips_fpath = "/proc/sys/crypto/fips_enabled"; -static bool is_fips_enabled(void) -{ - static int fips_checked = -1; - FILE *fenabled; - int enabled; - - if (fips_checked >= 0) - return !!fips_checked; - if (access(fips_fpath, R_OK)) { - if (errno != ENOENT) - test_error("Can't open %s", fips_fpath); - fips_checked = 0; - return false; - } - fenabled = fopen(fips_fpath, "r"); - if (!fenabled) - test_error("Can't open %s", fips_fpath); - if (fscanf(fenabled, "%d", &enabled) != 1) - test_error("Can't read from %s", fips_fpath); - fclose(fenabled); - fips_checked = !!enabled; - return !!fips_checked; -} - struct test_key { char password[TCP_AO_MAXKEYLEN]; const char *alg; @@ -430,14 +426,7 @@ struct key_collection { static struct key_collection collection; #define TEST_MAX_MACLEN 16 -const char *test_algos[] = { - "cmac(aes128)", - "hmac(sha1)", "hmac(sha512)", "hmac(sha384)", "hmac(sha256)", - "hmac(sha224)", "hmac(sha3-512)", - /* only if !CONFIG_FIPS */ -#define TEST_NON_FIPS_ALGOS 2 - "hmac(rmd160)", "hmac(md5)" -}; +const char *test_algos[] = { "cmac(aes128)", "hmac(sha1)", "hmac(sha256)" }; const unsigned int test_maclens[] = { 1, 4, 12, 16 }; #define MACLEN_SHIFT 2 #define ALGOS_SHIFT 4 @@ -452,7 +441,7 @@ static unsigned int make_mask(unsigned int shift, unsigned int prev_shift) static void init_key_in_collection(unsigned int index, bool randomized) { struct test_key *key = &collection.keys[index]; - unsigned int algos_nr, algos_index; + unsigned int algos_index; /* Same for randomized and non-randomized test flows */ key->client_keyid = index; @@ -474,10 +463,7 @@ static void init_key_in_collection(unsigned int index, bool randomized) key->maclen = test_maclens[index & make_mask(shift, 0)]; algos_index = index & make_mask(ALGOS_SHIFT, shift); } - algos_nr = ARRAY_SIZE(test_algos); - if (is_fips_enabled()) - algos_nr -= TEST_NON_FIPS_ALGOS; - key->alg = test_algos[algos_index % algos_nr]; + key->alg = test_algos[algos_index % ARRAY_SIZE(test_algos)]; } static int init_default_key_collection(unsigned int nr_keys, bool randomized) @@ -1166,7 +1152,6 @@ static void check_established_socket(void) { unsigned int port = test_server_port; - setup_vrfs(); try_client_run("client: Check current/rnext keys unset before connect()", port++, 20, -1, -1); try_client_run("client: Check current/rnext keys set before connect()", @@ -1185,6 +1170,7 @@ static void *client_fn(void *arg) { if (inet_pton(TEST_FAMILY, TEST_WRONG_IP, &wrong_addr) != 1) test_error("Can't convert ip address %s", TEST_WRONG_IP); + setup_vrfs(); check_closed_socket(); check_listen_socket(); check_established_socket(); @@ -1193,6 +1179,6 @@ static void *client_fn(void *arg) int main(int argc, char *argv[]) { - test_init(121, server_fn, client_fn); + test_init(122, server_fn, client_fn); return 0; } diff --git a/tools/testing/selftests/net/tcp_ao/lib/aolib.h b/tools/testing/selftests/net/tcp_ao/lib/aolib.h index ebb2899c12fe..53be1744237e 100644 --- a/tools/testing/selftests/net/tcp_ao/lib/aolib.h +++ b/tools/testing/selftests/net/tcp_ao/lib/aolib.h @@ -404,7 +404,8 @@ static inline int test_prepare_def_key(struct tcp_ao_add *ao, extern int test_get_one_ao(int sk, struct tcp_ao_getsockopt *out, void *addr, size_t addr_sz, - uint8_t prefix, uint8_t sndid, uint8_t rcvid); + uint8_t prefix, uint8_t sndid, uint8_t rcvid, + uint8_t keyflags, int ifindex); extern int test_get_ao_info(int sk, struct tcp_ao_info_opt *out); extern int test_set_ao_info(int sk, struct tcp_ao_info_opt *in); extern int test_cmp_getsockopt_setsockopt(const struct tcp_ao_add *a, @@ -418,7 +419,8 @@ static inline int test_verify_socket_key(int sk, struct tcp_ao_add *key) int err; err = test_get_one_ao(sk, &key2, &key->addr, sizeof(key->addr), - key->prefix, key->sndid, key->rcvid); + key->prefix, key->sndid, key->rcvid, + key->keyflags, key->ifindex); if (err) return err; diff --git a/tools/testing/selftests/net/tcp_ao/lib/sock.c b/tools/testing/selftests/net/tcp_ao/lib/sock.c index ef8e9031d47a..2e7b06a1a156 100644 --- a/tools/testing/selftests/net/tcp_ao/lib/sock.c +++ b/tools/testing/selftests/net/tcp_ao/lib/sock.c @@ -252,7 +252,7 @@ static int test_get_ao_keys_nr(int sk) int test_get_one_ao(int sk, struct tcp_ao_getsockopt *out, void *addr, size_t addr_sz, uint8_t prefix, - uint8_t sndid, uint8_t rcvid) + uint8_t sndid, uint8_t rcvid, uint8_t keyflags, int ifindex) { struct tcp_ao_getsockopt tmp = {}; socklen_t tmp_sz = sizeof(tmp); @@ -262,6 +262,8 @@ int test_get_one_ao(int sk, struct tcp_ao_getsockopt *out, tmp.prefix = prefix; tmp.sndid = sndid; tmp.rcvid = rcvid; + tmp.keyflags = keyflags; + tmp.ifindex = ifindex; tmp.nkeys = 1; ret = getsockopt(sk, IPPROTO_TCP, TCP_AO_GET_KEYS, &tmp, &tmp_sz); diff --git a/tools/testing/selftests/net/tcp_ecmp_failover.sh b/tools/testing/selftests/net/tcp_ecmp_failover.sh new file mode 100755 index 000000000000..5768aa8bff6a --- /dev/null +++ b/tools/testing/selftests/net/tcp_ecmp_failover.sh @@ -0,0 +1,216 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright 2026 Google LLC. +# +# This test verifies TCP flow failover between ECMP routes +# upon carrier loss on the active device. +# +# socat -----------------------------> socat +# | +# .-- veth-c1 -|- veth-s1 --. +# dummy0 -| | |-- dummy0 +# '-- veth-c2 -|- veth-s2 --' +# | +# + +REQUIRE_JQ=no +REQUIRE_MZ=no +NUM_NETIFS=0 + +source forwarding/lib.sh + +CLIENT_IP="10.0.59.1" +SERVER_IP="10.0.92.1" +CLIENT_IP6="2001:db8:5a9a::1" +SERVER_IP6="2001:db8:9292::1" + +setup_server() +{ + IP="ip -n $server" + NS_EXEC="ip netns exec $server" + + $IP link add dummy0 type dummy + $IP link set dummy0 up + + $IP -4 addr add $SERVER_IP/32 dev dummy0 + $IP -6 addr add $SERVER_IP6/128 dev dummy0 nodad + + $IP link set veth-s1 up + $IP link set veth-s2 up + + $IP -4 addr add 192.168.1.2/24 dev veth-s1 + $IP -4 addr add 192.168.2.2/24 dev veth-s2 + + $IP -4 route add $CLIENT_IP/32 \ + nexthop via 192.168.1.1 dev veth-s1 weight 1 \ + nexthop via 192.168.2.1 dev veth-s2 weight 1 + + $IP -6 addr add 2001:db8:1::2/64 dev veth-s1 nodad + $IP -6 addr add 2001:db8:2::2/64 dev veth-s2 nodad + + $IP -6 route add $CLIENT_IP6/128 \ + nexthop via 2001:db8:1::1 dev veth-s1 weight 1 \ + nexthop via 2001:db8:2::1 dev veth-s2 weight 1 +} + +setup_client() +{ + IP="ip -n $client" + NS_EXEC="ip netns exec $client" + + $IP link add dummy0 type dummy + $IP link set dummy0 up + + $IP -4 addr add $CLIENT_IP/32 dev dummy0 + $IP -6 addr add $CLIENT_IP6/128 dev dummy0 nodad + + $IP link set veth-c1 up + $IP link set veth-c2 up + + $IP -4 addr add 192.168.1.1/24 dev veth-c1 + $IP -4 addr add 192.168.2.1/24 dev veth-c2 + + $IP -4 route add $SERVER_IP/32 \ + nexthop via 192.168.1.2 dev veth-c1 weight 1 \ + nexthop via 192.168.2.2 dev veth-c2 weight 1 + + $IP -6 addr add 2001:db8:1::1/64 dev veth-c1 nodad + $IP -6 addr add 2001:db8:2::1/64 dev veth-c2 nodad + + $IP -6 route add $SERVER_IP6/128 \ + nexthop via 2001:db8:1::2 dev veth-c1 weight 1 \ + nexthop via 2001:db8:2::2 dev veth-c2 weight 1 + + # By default, tcp_retries1=3 triggers a route refresh + # after 3 retransmits (~5s). Ensure this never occurs + # for test stability. + $NS_EXEC sysctl -qw net.ipv4.tcp_retries1=100 + + # When NETDEV_CHANGE is issued for a dev tied to an ECMP + # route, RTNH_F_LINKDOWN is flagged and the sernum is + # bumped to invalidate the route via sk_dst_check(). + # + # Without ignore_routes_with_linkdown=1, subsequent + # lookups may still select the same RTNH_F_LINKDOWN route. + $NS_EXEC sysctl -qw net.ipv4.conf.veth-c1.ignore_routes_with_linkdown=1 + $NS_EXEC sysctl -qw net.ipv4.conf.veth-c2.ignore_routes_with_linkdown=1 + + $NS_EXEC sysctl -qw net.ipv6.conf.veth-c1.ignore_routes_with_linkdown=1 + $NS_EXEC sysctl -qw net.ipv6.conf.veth-c2.ignore_routes_with_linkdown=1 +} + +setup() +{ + setup_ns client server + + ip -n "$client" link add veth-c1 type veth peer veth-s1 netns "$server" + ip -n "$client" link add veth-c2 type veth peer veth-s2 netns "$server" + + setup_server + setup_client +} + +cleanup() +{ + cleanup_all_ns > /dev/null 2>&1 +} + +tcp_ecmp_failover() +{ + local pf=$1; shift + local server_ip=$1; shift + local client_ip=$1; shift + + RET=0 + + tcpdump_start veth-s1 "$server" + tcpdump_start veth-s2 "$server" + + ip netns exec "$server" \ + socat -u TCP-LISTEN:8080,pf="$pf",bind="$server_ip",reuseaddr /dev/null & + server_pid=$! + + # Wait for server to start listening. + # Sometimes client fails without this sleep. + sleep 1 + + ip netns exec "$client" \ + socat -u /dev/zero TCP:"$server_ip":8080,pf="$pf",bind="$client_ip" & + client_pid=$! + + # To capture enough packets. + sleep 3 + + tcpdump_stop veth-s1 + tcpdump_stop veth-s2 + + pkts_s1=$(tcpdump_show veth-s1 | wc -l) + pkts_s2=$(tcpdump_show veth-s2 | wc -l) + + tcpdump_cleanup veth-s1 + tcpdump_cleanup veth-s2 + + # Detect the device chosen by the client + if [ "$pkts_s1" -gt "$pkts_s2" ]; then + veth_down=veth-s1 + veth_up=veth-s2 + else + veth_down=veth-s2 + veth_up=veth-s1 + fi + + # Taking down $veth_down causes its peer to lose carrier, + # triggering NETDEV_CHANGE. This flags RTNH_F_LINKDOWN + # and bumps the sernum for the route associated with that + # peer, invalidating the cached dst in the TCP socket. + # + # Consequently, sk_dst_check() fails, forcing the subsequent + # lookup to select the remaining healthy route via $veth_up. + ip -n "$server" link set "$veth_down" down + + tcpdump_start "$veth_up" "$server" + + # To capture enough packets. + sleep 3 + + tcpdump_stop "$veth_up" + + kill -9 "$client_pid" > /dev/null 2>&1 + kill -9 "$server_pid" > /dev/null 2>&1 + wait 2> /dev/null + + pkts=$(tcpdump_show $veth_up | wc -l) + + tcpdump_cleanup "$veth_up" + + if [ "$pkts" -lt 1000 ]; then + RET=$ksft_fail + fi +} + +test_ipv4() +{ + setup + tcp_ecmp_failover IPv4 $SERVER_IP $CLIENT_IP + log_test "TCP IPv4 failover" + cleanup +} + +test_ipv6() +{ + setup + tcp_ecmp_failover IPv6 "[$SERVER_IP6]" "[$CLIENT_IP6]" + log_test "TCP IPv6 failover" + cleanup +} + +require_command socat +require_command tcpdump + +trap cleanup EXIT + +test_ipv4 +test_ipv6 + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/tcp_mmap.c b/tools/testing/selftests/net/tcp_mmap.c index 4fcce5150850..487ae659a1f1 100644 --- a/tools/testing/selftests/net/tcp_mmap.c +++ b/tools/testing/selftests/net/tcp_mmap.c @@ -141,12 +141,12 @@ static void *mmap_large_buffer(size_t need, size_t *allocated) buffer = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0); - if (buffer == (void *)-1) { + if (buffer == MAP_FAILED) { sz = need; buffer = mmap(NULL, sz, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0); - if (buffer != (void *)-1) + if (buffer != MAP_FAILED) fprintf(stderr, "MAP_HUGETLB attempt failed, look at /sys/kernel/mm/hugepages for optimal performance\n"); } *allocated = sz; @@ -189,13 +189,13 @@ void *child_thread(void *arg) fcntl(fd, F_SETFL, O_NDELAY); buffer = mmap_large_buffer(chunk_size, &buffer_sz); - if (buffer == (void *)-1) { + if (buffer == MAP_FAILED) { perror("mmap"); goto error; } if (zflg) { raddr = mmap(NULL, chunk_size + map_align, PROT_READ, flags, fd, 0); - if (raddr == (void *)-1) { + if (raddr == MAP_FAILED) { perror("mmap"); zflg = 0; } else { @@ -313,6 +313,8 @@ end: tcp_info_get_rcv_mss(fd)); } error: + if (ctx) + EVP_MD_CTX_free(ctx); munmap(buffer, buffer_sz); close(fd); if (zflg) @@ -545,7 +547,7 @@ int main(int argc, char *argv[]) } buffer = mmap_large_buffer(chunk_size, &buffer_sz); - if (buffer == (unsigned char *)-1) { + if (buffer == MAP_FAILED) { perror("mmap"); exit(1); } @@ -606,6 +608,8 @@ int main(int argc, char *argv[]) EVP_DigestFinal_ex(ctx, digest, &digest_len); send(fd, digest, (size_t)SHA256_DIGEST_LENGTH, 0); } + if (ctx) + EVP_MD_CTX_free(ctx); close(fd); munmap(buffer, buffer_sz); return 0; diff --git a/tools/testing/selftests/net/test_bridge_neigh_suppress.sh b/tools/testing/selftests/net/test_bridge_neigh_suppress.sh index 9067197c9055..e9ed0d750996 100755 --- a/tools/testing/selftests/net/test_bridge_neigh_suppress.sh +++ b/tools/testing/selftests/net/test_bridge_neigh_suppress.sh @@ -56,6 +56,12 @@ TESTS=" neigh_suppress_uc_ns neigh_vlan_suppress_arp neigh_vlan_suppress_ns + neigh_suppress_arp_probe + neigh_suppress_dad_ns + neigh_forward_grat_arp + neigh_forward_grat_na + neigh_vlan_forward_grat_arp + neigh_vlan_forward_grat_na " VERBOSE=0 PAUSE_ON_FAIL=no @@ -74,7 +80,8 @@ log_test() printf "TEST: %-60s [ OK ]\n" "${msg}" nsuccess=$((nsuccess+1)) else - ret=1 + # shellcheck disable=SC2154 + ret=$(ksft_exit_status_merge "$ret" "$ksft_fail") nfail=$((nfail+1)) printf "TEST: %-60s [FAIL]\n" "${msg}" if [ "$VERBOSE" = "1" ]; then @@ -97,6 +104,7 @@ log_test() fi [ "$VERBOSE" = "1" ] && echo + return 0 } run_cmd() @@ -134,6 +142,15 @@ tc_check_packets() [[ $pkts == $count ]] } +neigh_forward_grat_check() +{ + if ! bridge link help 2>&1 | grep -q "neigh_forward_grat"; then + echo "SKIP: iproute2 bridge too old, missing gratuitous ARP/unsolicited NA forwarding control support" + # shellcheck disable=SC2154 + return "$ksft_skip" + fi +} + ################################################################################ # Setup @@ -561,6 +578,17 @@ icmpv6_header_get() echo $p } +icmpv6_na_header_get() +{ + local csum=$1; shift + local tip=$1; shift + + # Type 136 (Neighbor Advertisement), hex format, Override flag set, + # Solicited flag clear (unsolicited NA). + # ICMPv6.type : ICMPv6.code : ICMPv6.checksum : Flags : Target Address + echo "88:00:$csum:20:00:00:00:$tip:" +} + neigh_suppress_uc_ns_common() { local vid=$1; shift @@ -875,6 +903,439 @@ neigh_vlan_suppress_ns() log_test $? 0 "NS suppression (VLAN $vid2)" } +neigh_suppress_arp_probe() +{ + local vid=10 + local tip=192.0.2.2 + local h2_mac + + echo + echo "Per-port ARP probe suppression" + echo "------------------------------" + + run_cmd "tc -n $sw1 qdisc replace dev vx0 clsact" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 101 proto 0x0806 flower indev swp1 arp_tip $tip arp_sip 0.0.0.0 arp_op request action pass" + + # Initial state - check that ARP probes are not suppressed. + run_cmd "ip netns exec $h1 arping -D -q -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "ARP probe suppression" + + # Enable neighbor suppression and check that nothing changes. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on" + + run_cmd "ip netns exec $h1 arping -D -q -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 2 + log_test $? 0 "ARP probe suppression" + + # Install FDB and a neighbor and check that ARP probes are suppressed. + h2_mac=$(ip -n "$h2" -j -p link show eth0."$vid" | jq -r '.[]["address"]') + run_cmd "bridge -n $sw1 fdb replace $h2_mac dev vx0 master static vlan $vid" + run_cmd "ip -n $sw1 neigh replace $tip lladdr $h2_mac nud permanent dev br0.$vid" + log_test $? 0 "FDB and neighbor entry installation" + + run_cmd "ip netns exec $h1 arping -D -q -c 1 -w 5 -I eth0.$vid $tip" + log_test $? 1 "arping" + tc_check_packets "$sw1" "dev vx0 egress" 101 2 + log_test $? 0 "ARP probe suppression" + + # Remove the neighbor entry and check that ARP probes are not suppressed. + run_cmd "ip -n $sw1 neigh del $tip dev br0.$vid" + log_test $? 0 "neighbor removal" + + run_cmd "ip netns exec $h1 arping -D -q -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 3 + log_test $? 0 "ARP probe suppression" + + # Disable neighbor suppression. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_suppress off" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_suppress off\"" + log_test $? 0 "\"neigh_suppress\" is off" + + run_cmd "ip netns exec $h1 arping -D -q -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 4 + log_test $? 0 "ARP probe suppression" +} + +neigh_suppress_dad_ns() +{ + local vid=10 + local tip=2001:db8:1::99 + local mcast=ff02::1:ff00:99 + local dmac=33:33:ff:00:00:99 + local full_tip=20:01:0d:b8:00:01:00:00:00:00:00:00:00:00:00:99 + local csum="4b:bc" + local smac + local tmac + + echo + echo "Per-port DAD NS suppression" + echo "---------------------------" + + smac=$(ip -n "$h1" -j -p link show eth0."$vid" | jq -r '.[]["address"]') + + run_cmd "tc -n $sw1 qdisc replace dev vx0 clsact" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 101 proto ipv6 flower indev swp1 ip_proto icmpv6 dst_ip $mcast src_ip :: type 135 code 0 action pass" + + # Initial state - check that DAD NS are not suppressed. + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A :: -B $mcast -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum" "$full_tip") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "DAD NS suppression" + + # Enable neighbor suppression and check that nothing changes. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A :: -B $mcast -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum" "$full_tip") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 2 + log_test $? 0 "DAD NS suppression" + + # Install FDB and a neighbor and check that DAD NS are suppressed + # and that a proxy NA is sent back to h1. + tmac=$(ip -n "$h2" -j -p link show eth0."$vid" | jq -r '.[]["address"]') + run_cmd "bridge -n $sw1 fdb replace $tmac dev vx0 master static vlan $vid" + run_cmd "ip -n $sw1 -6 neigh replace $tip lladdr $tmac nud permanent dev br0.$vid" + log_test $? 0 "FDB and neighbor entry installation" + + run_cmd "tc -n $h1 qdisc replace dev eth0.$vid clsact" + run_cmd "tc -n $h1 filter replace dev eth0.$vid ingress pref 1 handle 101 proto ipv6 flower ip_proto icmpv6 dst_ip ff02::1 src_ip $tip type 136 code 0 action pass" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A :: -B $mcast -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum" "$full_tip") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 2 + log_test $? 0 "DAD NS suppression" + tc_check_packets "$h1" "dev eth0.$vid ingress" 101 1 + log_test $? 0 "DAD NS proxy NA reply" + + # Remove the neighbor entry and check that DAD NS are not suppressed. + run_cmd "ip -n $sw1 -6 neigh del $tip dev br0.$vid" + log_test $? 0 "neighbor removal" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A :: -B $mcast -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum" "$full_tip") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 3 + log_test $? 0 "DAD NS suppression" + + # Disable neighbor suppression. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_suppress off" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_suppress off\"" + log_test $? 0 "\"neigh_suppress\" is off" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A :: -B $mcast -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum" "$full_tip") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 4 + log_test $? 0 "DAD NS suppression" +} + +neigh_forward_grat_arp() +{ + local vid=10 + local sip=192.0.2.1 + local tip=$sip + local h2_ip=192.0.2.2 + local h2_mac + + neigh_forward_grat_check || return $? + + echo + echo "Gratuitous ARP forwarding" + echo "-------------------------" + + run_cmd "tc -n $sw1 qdisc replace dev vx0 clsact" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 101 proto 0x0806 flower indev swp1 arp_tip $tip arp_sip $sip arp_op request action pass" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 102 proto 0x0806 flower indev swp1 arp_tip $h2_ip arp_sip $sip arp_op request action pass" + + h2_mac=$(ip -n "$h2" -j -p link show eth0."$vid" | jq -r '.[]["address"]') + run_cmd "bridge -n $sw1 fdb replace $h2_mac dev vx0 master static vlan $vid" + run_cmd "ip -n $sw1 neigh replace $tip lladdr $h2_mac nud permanent dev br0.$vid" + run_cmd "ip -n $sw1 neigh replace $h2_ip lladdr $h2_mac nud permanent dev br0.$vid" + + # Enable neighbor suppression. Gratuitous ARP should be suppressed by + # default (neigh_forward_grat defaults to off). + run_cmd "ip -n $sw1 link set dev vx0 type bridge_slave neigh_suppress on" + run_cmd "ip -n $sw1 -d link show dev vx0 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on" + + # Send gratuitous ARP (sip == tip) and check it's suppressed. + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 0 + log_test $? 0 "Gratuitous ARP suppression" + + # Explicitly enable neigh_forward_grat and verify gratuitous ARP is + # now forwarded. + run_cmd "ip -n $sw1 link set dev vx0 type bridge_slave neigh_forward_grat on" + run_cmd "ip -n $sw1 -d link show dev vx0 | grep \"neigh_forward_grat on\"" + log_test $? 0 "\"neigh_forward_grat\" is on" + + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "Gratuitous ARP forwarding" + + # Verify that regular (non-gratuitous) ARP requests are still + # suppressed when neigh_forward_grat is enabled. + run_cmd "ip netns exec $h1 arping -c 1 -w 5 -I eth0.$vid $h2_ip" + tc_check_packets "$sw1" "dev vx0 egress" 102 0 + log_test $? 0 "Regular ARP suppression with \"neigh_forward_grat\" on" + + # Disable neigh_forward_grat and verify suppression resumes. + run_cmd "ip -n $sw1 link set dev vx0 type bridge_slave neigh_forward_grat off" + run_cmd "ip -n $sw1 -d link show dev vx0 | grep \"neigh_forward_grat off\"" + log_test $? 0 "\"neigh_forward_grat\" is off" + + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid $tip" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "Gratuitous ARP suppression" +} + +# neigh_forward_grat_arp() uses 'ip link' interface, and neigh_forward_grat_na() +# uses 'bridge link' interface to exercise both paths. +neigh_forward_grat_na() +{ + local vid=10 + local saddr=2001:db8:1::1 + local daddr=ff02::1 + local h2_addr=2001:db8:1::2 + local h2_maddr=ff02::1:ff00:2 + local full_addr=20:01:0d:b8:00:01:00:00:00:00:00:00:00:00:00:01 + local h2_full_addr=20:01:0d:b8:00:01:00:00:00:00:00:00:00:00:00:02 + local csum="fd:32" + local csum_ns="1f:2f" + local dmac=33:33:00:00:00:01 + local h2_dmac=33:33:ff:00:00:02 + local h2_mac + local smac + + neigh_forward_grat_check || return $? + + echo + echo "Unsolicited NA forwarding" + echo "-------------------------" + + smac=$(ip -n "$h1" -j -p link show eth0."$vid" | jq -r '.[]["address"]') + + run_cmd "tc -n $sw1 qdisc replace dev vx0 clsact" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 101 proto ipv6 flower indev swp1 ip_proto icmpv6 dst_ip $daddr src_ip $saddr type 136 code 0 action pass" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 102 proto ipv6 flower indev swp1 ip_proto icmpv6 dst_ip $h2_maddr src_ip $saddr type 135 code 0 action pass" + + h2_mac=$(ip -n "$h2" -j -p link show eth0."$vid" | jq -r '.[]["address"]') + run_cmd "bridge -n $sw1 fdb replace $h2_mac dev vx0 master static vlan $vid" + run_cmd "ip -n $sw1 neigh replace $saddr lladdr $h2_mac nud permanent dev br0.$vid" + run_cmd "ip -n $sw1 neigh replace $h2_addr lladdr $h2_mac nud permanent dev br0.$vid" + + # Enable neighbor suppression. Unsolicited NA should be suppressed by + # default (neigh_forward_grat defaults to off). + run_cmd "bridge -n $sw1 link set dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on" + + # Send unsolicited NA and check it's suppressed. + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A $saddr -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum" "$full_addr") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 0 + log_test $? 0 "Unsolicited NA suppression" + + # Explicitly enable neigh_forward_grat and verify unsolicited NA is + # now forwarded. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_forward_grat on" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_forward_grat on\"" + log_test $? 0 "\"neigh_forward_grat\" is on" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A $saddr -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum" "$full_addr") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "Unsolicited NA forwarding" + + # Verify that solicited NS messages are still suppressed when + # neigh_forward_grat is enabled. + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $h2_dmac -A $saddr -B $h2_maddr -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum_ns" "$h2_full_addr") -q" + tc_check_packets "$sw1" "dev vx0 egress" 102 0 + log_test $? 0 "Solicited NS suppression with \"neigh_forward_grat\" on" + + # Disable neigh_forward_grat and verify suppression resumes. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_forward_grat off" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_forward_grat off\"" + log_test $? 0 "\"neigh_forward_grat\" is off" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid -c 1 -a $smac -b $dmac -A $saddr -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum" "$full_addr") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "Unsolicited NA suppression" +} + +neigh_vlan_forward_grat_arp() +{ + local vid1=10 + local vid2=20 + local sip1=192.0.2.1 + local sip2=192.0.2.17 + local h2_ip1=192.0.2.2 + local h2_mac1 + local h2_mac2 + + neigh_forward_grat_check || return $? + + echo + echo "Per-VLAN gratuitous ARP forwarding" + echo "----------------------------------" + + run_cmd "tc -n $sw1 qdisc replace dev vx0 clsact" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 101 proto 0x0806 flower indev swp1 arp_tip $sip1 arp_sip $sip1 arp_op request action pass" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 102 proto 0x0806 flower indev swp1 arp_tip $sip2 arp_sip $sip2 arp_op request action pass" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 103 proto 0x0806 flower indev swp1 arp_tip $h2_ip1 arp_sip $sip1 arp_op request action pass" + + h2_mac1=$(ip -n "$h2" -j -p link show eth0."$vid1" | jq -r '.[]["address"]') + h2_mac2=$(ip -n "$h2" -j -p link show eth0."$vid2" | jq -r '.[]["address"]') + run_cmd "bridge -n $sw1 fdb replace $h2_mac1 dev vx0 master static vlan $vid1" + run_cmd "bridge -n $sw1 fdb replace $h2_mac2 dev vx0 master static vlan $vid2" + run_cmd "ip -n $sw1 neigh replace $sip1 lladdr $h2_mac1 nud permanent dev br0.$vid1" + run_cmd "ip -n $sw1 neigh replace $sip2 lladdr $h2_mac2 nud permanent dev br0.$vid2" + run_cmd "ip -n $sw1 neigh replace $h2_ip1 lladdr $h2_mac1 nud permanent dev br0.$vid1" + + # Enable per-{Port, VLAN} neighbor suppression. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_vlan_suppress on" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_vlan_suppress on\"" + log_test $? 0 "\"neigh_vlan_suppress\" is on" + + # Enable neighbor suppression on VLAN 10. Gratuitous ARP should be + # suppressed by default on VLAN 10 (neigh_forward_grat defaults to off) + # but not on VLAN 20. + run_cmd "bridge -n $sw1 vlan set vid $vid1 dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d vlan show dev vx0 vid $vid1 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on (VLAN $vid1)" + + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid1 $sip1" + tc_check_packets "$sw1" "dev vx0 egress" 101 0 + log_test $? 0 "Gratuitous ARP suppression (VLAN $vid1)" + + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid2 $sip2" + tc_check_packets "$sw1" "dev vx0 egress" 102 1 + log_test $? 0 "Gratuitous ARP forwarding (VLAN $vid2)" + + # Enable neigh_forward_grat on VLAN 10 and verify gratuitous ARP is + # now forwarded. + run_cmd "bridge -n $sw1 vlan set vid $vid1 dev vx0 neigh_forward_grat on" + run_cmd "bridge -n $sw1 -d vlan show dev vx0 vid $vid1 | grep \"neigh_forward_grat on\"" + log_test $? 0 "\"neigh_forward_grat\" is on (VLAN $vid1)" + + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid1 $sip1" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "Gratuitous ARP forwarding (VLAN $vid1)" + + # Verify that regular (non-gratuitous) ARP requests on VLAN $vid1 are + # still suppressed when neigh_forward_grat is enabled. + run_cmd "ip netns exec $h1 arping -c 1 -w 5 -I eth0.$vid1 $h2_ip1" + tc_check_packets "$sw1" "dev vx0 egress" 103 0 + log_test $? 0 "Regular ARP suppression with \"neigh_forward_grat\" on (VLAN $vid1)" + + # Enable neighbor suppression on VLAN 20 (neigh_forward_grat defaults to + # off), and verify gratuitous ARP is suppressed on VLAN 20. + run_cmd "bridge -n $sw1 vlan set vid $vid2 dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d vlan show dev vx0 vid $vid2 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on (VLAN $vid2)" + + # VLAN 10 should still forward (neigh_forward_grat is on). + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid1 $sip1" + tc_check_packets "$sw1" "dev vx0 egress" 101 2 + log_test $? 0 "Gratuitous ARP forwarding (VLAN $vid1)" + + # VLAN 20 should suppress (neigh_forward_grat defaults to off). + run_cmd "ip netns exec $h1 arping -U -c 1 -w 5 -I eth0.$vid2 $sip2" + tc_check_packets "$sw1" "dev vx0 egress" 102 1 + log_test $? 0 "Gratuitous ARP suppression (VLAN $vid2)" +} + +neigh_vlan_forward_grat_na() +{ + local vid1=10 + local vid2=20 + local saddr1=2001:db8:1::1 + local daddr=ff02::1 + local h2_addr1=2001:db8:1::2 + local h2_maddr1=ff02::1:ff00:2 + local full_addr1=20:01:0d:b8:00:01:00:00:00:00:00:00:00:00:00:01 + local h2_full_addr1=20:01:0d:b8:00:01:00:00:00:00:00:00:00:00:00:02 + local csum1="fd:32" + local csum_ns1="1f:2f" + local saddr2=2001:db8:2::1 + local full_addr2=20:01:0d:b8:00:02:00:00:00:00:00:00:00:00:00:01 + local csum2="fd:30" + local dmac=33:33:00:00:00:01 + local h2_dmac1=33:33:ff:00:00:02 + local h2_mac1 + local h2_mac2 + local smac + + neigh_forward_grat_check || return $? + + echo + echo "Per-VLAN unsolicited NA forwarding" + echo "----------------------------------" + + smac=$(ip -n "$h1" -j -p link show eth0."$vid1" | jq -r '.[]["address"]') + + run_cmd "tc -n $sw1 qdisc replace dev vx0 clsact" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 101 proto ipv6 flower indev swp1 ip_proto icmpv6 dst_ip $daddr src_ip $saddr1 type 136 code 0 action pass" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 102 proto ipv6 flower indev swp1 ip_proto icmpv6 dst_ip $daddr src_ip $saddr2 type 136 code 0 action pass" + run_cmd "tc -n $sw1 filter replace dev vx0 egress pref 1 handle 103 proto ipv6 flower indev swp1 ip_proto icmpv6 dst_ip $h2_maddr1 src_ip $saddr1 type 135 code 0 action pass" + + h2_mac1=$(ip -n "$h2" -j -p link show eth0."$vid1" | jq -r '.[]["address"]') + h2_mac2=$(ip -n "$h2" -j -p link show eth0."$vid2" | jq -r '.[]["address"]') + run_cmd "bridge -n $sw1 fdb replace $h2_mac1 dev vx0 master static vlan $vid1" + run_cmd "bridge -n $sw1 fdb replace $h2_mac2 dev vx0 master static vlan $vid2" + run_cmd "ip -n $sw1 neigh replace $saddr1 lladdr $h2_mac1 nud permanent dev br0.$vid1" + run_cmd "ip -n $sw1 neigh replace $saddr2 lladdr $h2_mac2 nud permanent dev br0.$vid2" + run_cmd "ip -n $sw1 neigh replace $h2_addr1 lladdr $h2_mac1 nud permanent dev br0.$vid1" + + # Enable per-{Port, VLAN} neighbor suppression. + run_cmd "bridge -n $sw1 link set dev vx0 neigh_vlan_suppress on" + run_cmd "bridge -n $sw1 -d link show dev vx0 | grep \"neigh_vlan_suppress on\"" + log_test $? 0 "\"neigh_vlan_suppress\" is on" + + # Enable neighbor suppression on VLAN 10. Unsolicited NA should be + # suppressed by default on VLAN 10 (neigh_forward_grat defaults to off) + # but not on VLAN 20. + run_cmd "bridge -n $sw1 vlan set vid $vid1 dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d vlan show dev vx0 vid $vid1 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on (VLAN $vid1)" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid1 -c 1 -a $smac -b $dmac -A $saddr1 -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum1" "$full_addr1") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 0 + log_test $? 0 "Unsolicited NA suppression (VLAN $vid1)" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid2 -c 1 -a $smac -b $dmac -A $saddr2 -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum2" "$full_addr2") -q" + tc_check_packets "$sw1" "dev vx0 egress" 102 1 + log_test $? 0 "Unsolicited NA forwarding (VLAN $vid2)" + + # Enable neigh_forward_grat on VLAN 10 and verify unsolicited NA is + # now forwarded. + run_cmd "bridge -n $sw1 vlan set vid $vid1 dev vx0 neigh_forward_grat on" + run_cmd "bridge -n $sw1 -d vlan show dev vx0 vid $vid1 | grep \"neigh_forward_grat on\"" + log_test $? 0 "\"neigh_forward_grat\" is on (VLAN $vid1)" + + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid1 -c 1 -a $smac -b $dmac -A $saddr1 -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum1" "$full_addr1") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 1 + log_test $? 0 "Unsolicited NA forwarding (VLAN $vid1)" + + # Verify that solicited NS messages on VLAN $vid1 are still suppressed + # when neigh_forward_grat is enabled. + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid1 -c 1 -a $smac -b $h2_dmac1 -A $saddr1 -B $h2_maddr1 -t ip hop=255,next=58,payload=$(icmpv6_header_get "$csum_ns1" "$h2_full_addr1") -q" + tc_check_packets "$sw1" "dev vx0 egress" 103 0 + log_test $? 0 "Solicited NS suppression with \"neigh_forward_grat\" on (VLAN $vid1)" + + # Enable neighbor suppression on VLAN 20 (neigh_forward_grat defaults to + # off), and verify unsolicited NA is suppressed on VLAN 20. + run_cmd "bridge -n $sw1 vlan set vid $vid2 dev vx0 neigh_suppress on" + run_cmd "bridge -n $sw1 -d vlan show dev vx0 vid $vid2 | grep \"neigh_suppress on\"" + log_test $? 0 "\"neigh_suppress\" is on (VLAN $vid2)" + + # VLAN 10 should still forward (neigh_forward_grat is on). + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid1 -c 1 -a $smac -b $dmac -A $saddr1 -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum1" "$full_addr1") -q" + tc_check_packets "$sw1" "dev vx0 egress" 101 2 + log_test $? 0 "Unsolicited NA forwarding (VLAN $vid1)" + + # VLAN 20 should suppress (neigh_forward_grat defaults to off). + run_cmd "ip netns exec $h1 mausezahn -6 eth0.$vid2 -c 1 -a $smac -b $dmac -A $saddr2 -B $daddr -t ip hop=255,next=58,payload=$(icmpv6_na_header_get "$csum2" "$full_addr2") -q" + tc_check_packets "$sw1" "dev vx0 egress" 102 1 + log_test $? 0 "Unsolicited NA suppression (VLAN $vid2)" +} + ################################################################################ # Usage @@ -961,7 +1422,10 @@ cleanup for t in $TESTS do - setup; $t; cleanup; + setup + $t + ret=$(ksft_exit_status_merge "$ret" $?) + cleanup done if [ "$TESTS" != "none" ]; then diff --git a/tools/testing/selftests/net/test_vxlan_mdb.sh b/tools/testing/selftests/net/test_vxlan_mdb.sh index 58da5de99ac4..f9600aabd4a2 100755 --- a/tools/testing/selftests/net/test_vxlan_mdb.sh +++ b/tools/testing/selftests/net/test_vxlan_mdb.sh @@ -685,6 +685,9 @@ star_g_common() run_cmd "bridge -n $ns1 mdb add dev vx0 port vx0 grp $grp permanent filter_mode exclude source_list $grp dst $vtep_ip src_vni 10010" log_test $? 255 "Invalid source in source list" + run_cmd "bridge -n $ns1 mdb add dev vx0 port vx0 grp $grp permanent filter_mode exclude source_list $all_zeros_grp dst $vtep_ip src_vni 10010" + log_test $? 255 "All-zeros source in source list" + run_cmd "bridge -n $ns1 mdb add dev vx0 port vx0 grp $grp permanent source_list $src1 dst $vtep_ip src_vni 10010" log_test $? 255 "Source list without filter mode" } @@ -784,6 +787,9 @@ sg_common() run_cmd "bridge -n $ns1 mdb add dev vx0 port vx0 grp $grp src $grp permanent dst $vtep_ip src_vni 10010" log_test $? 255 "(S, G) with an invalid source list" + run_cmd "bridge -n $ns1 mdb add dev vx0 port vx0 grp $grp src $all_zeros_grp permanent dst $vtep_ip src_vni 10010" + log_test $? 255 "(S, G) with an all-zeros source" + run_cmd "bridge -n $ns1 mdb add dev vx0 port vx0 grp $all_zeros_grp src $src permanent dst $vtep_ip src_vni 10010" log_test $? 255 "All-zeros group with source" } diff --git a/tools/testing/selftests/net/test_vxlan_nh.sh b/tools/testing/selftests/net/test_vxlan_nh.sh index 20f3369f776b..5ce6f27f6cf4 100755 --- a/tools/testing/selftests/net/test_vxlan_nh.sh +++ b/tools/testing/selftests/net/test_vxlan_nh.sh @@ -56,6 +56,17 @@ tc_stats_get() tc_rule_handle_stats_get "dev dummy1 egress" 101 ".packets" "-n $ns1" } +nh_stats_get_port() +{ + ip -n "$ns1" -s -j nexthop show id 20 | \ + jq ".[][\"group_stats\"][][\"packets\"]" +} + +tc_stats_get_port() +{ + tc_rule_handle_stats_get "dev dummy1 egress" 102 ".packets" "-n $ns1" +} + basic_tx_common() { local af_str=$1; shift @@ -90,6 +101,31 @@ basic_tx_common() busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" tc_stats_get > /dev/null check_err $? "tc filter stats did not increase" + # Add a second FDB nexthop group whose nexthop carries a per-nexthop + # destination port (NHA_DST_PORT) that differs from the VXLAN device + # default. Matching outer traffic must egress with that port, so a + # separate flower filter keyed on the new port catches it. + run_cmd "tc -n $ns1 filter add dev dummy1 egress proto $proto \ + pref 1 handle 102 flower ip_proto udp dst_ip $remote_addr \ + dst_port 4790 action pass" + + run_cmd "ip -n $ns1 nexthop add id 2 via $remote_addr fdb dst_port 4790" + run_cmd "ip -n $ns1 nexthop add id 20 group 2 fdb" + + run_cmd "bridge -n $ns1 fdb add 00:11:22:33:44:66 dev vx0 \ + self static nhid 20" + + run_cmd "ip netns exec $ns1 mausezahn vx0 -a own \ + -b 00:11:22:33:44:66 -c 1 -q" + + busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" \ + nh_stats_get_port > /dev/null + check_err $? "FDB nexthop group stats did not increase (with port)" + + busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" \ + tc_stats_get_port > /dev/null + check_err $? "tc filter stats did not increase (with port)" + log_test "VXLAN FDB nexthop: $af_str basic Tx" } @@ -210,8 +246,8 @@ require_command arping require_command ndisc6 require_command jq -if ! ip nexthop help 2>&1 | grep -q "stats"; then - echo "SKIP: iproute2 ip too old, missing nexthop stats support" +if ! ip nexthop help 2>&1 | grep -q "dst_port"; then + echo "SKIP: iproute2 ip too old, missing nexthop dst_port support" exit "$ksft_skip" fi diff --git a/tools/testing/selftests/net/test_vxlan_vnifilter_notify.sh b/tools/testing/selftests/net/test_vxlan_vnifilter_notify.sh new file mode 100755 index 000000000000..9d51a9e02ae0 --- /dev/null +++ b/tools/testing/selftests/net/test_vxlan_vnifilter_notify.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# shellcheck disable=SC2034,SC2154,SC2317,SC2329 +# +# Test for VXLAN vnifilter netlink notifications (RTM_NEWTUNNEL / +# RTM_DELTUNNEL). +# +# Verifies that: +# - Adding a new VNI sends a notification +# - Adding a new VNI with a remote sends a notification +# - Deleting a VNI sends a notification +# - Re-adding an existing VNI with the same attributes does not send +# a spurious notification +# - Updating an existing VNI's remote sends a notification +# - Deleting a non-existent VNI does not send a notification + +source lib.sh + +require_command bridge + +VXLAN_DEV=vxlan100 + +ALL_TESTS=" + test_vni_add_notify + test_vni_add_remote_notify + test_vni_del_notify + test_vni_readd_no_notify + test_vni_update_remote_notify + test_vni_del_nonexistent_no_notify +" + +setup_prepare() +{ + setup_ns NS1 + defer cleanup_all_ns + + ip -n "$NS1" link add $VXLAN_DEV type vxlan dstport 4789 \ + local 10.0.0.1 nolearning external vnifilter + ip -n "$NS1" link set $VXLAN_DEV up +} + +# Run bridge monitor in the background, execute a command, then count +# the notification lines. +# Usage: vni_notify_check <command> [args...] +# Sets: NOTIFY_COUNT with the number of notifications observed. +vni_notify_check() +{ + local tmpf cmd_ret monitor_pid + + tmpf=$(mktemp) + defer rm "$tmpf" + + defer_scope_push + ip netns exec "$NS1" bridge monitor vni > "$tmpf" 2>/dev/null & + monitor_pid=$! + defer kill_process "$monitor_pid" + + sleep 0.5 + if [ ! -e "/proc/$monitor_pid" ]; then + RET=$ksft_skip + log_test "iproute2 'bridge monitor vni' not supported" + return "$RET" + fi + + "$@" + cmd_ret=$? + sleep 0.2 + defer_scope_pop + + NOTIFY_COUNT=$(grep -c "$VXLAN_DEV" "$tmpf") + NOTIFY_COUNT=${NOTIFY_COUNT:-0} + return "$cmd_ret" +} + +# Adding a brand new VNI should produce a notification. +test_vni_add_notify() +{ + RET=0 + + vni_notify_check \ + bridge -n "$NS1" vni add vni 1000 dev "$VXLAN_DEV" + check_err $? "Failed to add VNI" + + [ "$NOTIFY_COUNT" -eq 1 ] + check_err $? "Expected 1 notification for VNI add, got $NOTIFY_COUNT" + + bridge -n "$NS1" vni delete vni 1000 dev "$VXLAN_DEV" 2>/dev/null + + log_test "VNI add sends notification" +} + +# Adding a VNI with a remote should produce a notification. +test_vni_add_remote_notify() +{ + RET=0 + + vni_notify_check \ + bridge -n "$NS1" vni add vni 4000 remote 10.0.0.2 dev "$VXLAN_DEV" + check_err $? "Failed to add VNI with remote" + + [ "$NOTIFY_COUNT" -eq 1 ] + check_err $? "Expected 1 notification for VNI add with remote, got $NOTIFY_COUNT" + + bridge -n "$NS1" vni delete vni 4000 dev "$VXLAN_DEV" + + log_test "VNI add with remote sends notification" +} + +# Deleting a VNI should produce a notification. +test_vni_del_notify() +{ + RET=0 + + bridge -n "$NS1" vni add vni 2000 dev "$VXLAN_DEV" + + vni_notify_check \ + bridge -n "$NS1" vni delete vni 2000 dev "$VXLAN_DEV" + check_err $? "Failed to delete VNI" + + [ "$NOTIFY_COUNT" -eq 1 ] + check_err $? "Expected 1 notification for VNI del, got $NOTIFY_COUNT" + + log_test "VNI delete sends notification" +} + +# Re-adding an existing VNI with the same attributes should not produce +# a notification. +test_vni_readd_no_notify() +{ + RET=0 + + bridge -n "$NS1" vni add vni 3000 dev "$VXLAN_DEV" + + vni_notify_check \ + bridge -n "$NS1" vni add vni 3000 dev "$VXLAN_DEV" + check_err $? "Failed to re-add VNI" + + [ "$NOTIFY_COUNT" -eq 0 ] + check_err $? "Expected 0 notifications for VNI re-add, got $NOTIFY_COUNT" + + bridge -n "$NS1" vni delete vni 3000 dev "$VXLAN_DEV" + + log_test "VNI re-add does not send spurious notification" +} + +# Updating an existing VNI's remote should produce a notification. +test_vni_update_remote_notify() +{ + RET=0 + + bridge -n "$NS1" vni add vni 5000 remote 10.0.0.2 dev "$VXLAN_DEV" + + vni_notify_check \ + bridge -n "$NS1" vni add vni 5000 remote 10.0.0.3 dev "$VXLAN_DEV" + check_err $? "Failed to update VNI remote" + + [ "$NOTIFY_COUNT" -eq 1 ] + check_err $? "Expected 1 notification for VNI remote update, got $NOTIFY_COUNT" + + bridge -n "$NS1" vni delete vni 5000 dev "$VXLAN_DEV" + + log_test "VNI remote update sends notification" +} + +# Deleting a non-existent VNI should not produce a notification. +test_vni_del_nonexistent_no_notify() +{ + RET=0 + + vni_notify_check \ + bridge -n "$NS1" vni delete vni 9999 dev "$VXLAN_DEV" 2>/dev/null + + [ "$NOTIFY_COUNT" -eq 0 ] + check_err $? "Expected 0 notifications for non-existent VNI del, got $NOTIFY_COUNT" + + log_test "Non-existent VNI delete does not send notification" +} + +trap defer_scopes_cleanup EXIT + +setup_prepare +tests_run + +exit "$EXIT_STATUS" diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c index 9e2ccea13d70..9d3cd4fff062 100644 --- a/tools/testing/selftests/net/tls.c +++ b/tools/testing/selftests/net/tls.c @@ -24,6 +24,7 @@ #include "kselftest_harness.h" #define TLS_PAYLOAD_MAX_LEN 16384 +#define TLS_HDR_LEN 5 #define SOL_TLS 282 static int fips_enabled; @@ -835,6 +836,43 @@ TEST_F(tls, send_and_splice) EXPECT_EQ(memcmp(mem_send, mem_recv, send_len), 0); } +TEST_F(tls, splice_onto_full_record) +{ + char mem_send[4608]; + char mem_recv[4608]; + int frag_len = 100; + int nfrags, i, off; + int p[2]; + + memrnd(mem_send, sizeof(mem_send)); + ASSERT_GE(pipe(p), 0); + + for (nfrags = 16; nfrags <= 44; nfrags++) { + for (i = 0, off = 0; i < nfrags; i++, off += frag_len) { + EXPECT_EQ(write(p[1], mem_send + off, frag_len), frag_len); + EXPECT_EQ(splice(p[0], NULL, self->fd, NULL, frag_len, + SPLICE_F_MORE), frag_len); + } + + EXPECT_EQ(send(self->fd, mem_send + off, 1, MSG_MORE), 1); + off++; + + EXPECT_EQ(write(p[1], mem_send + off, frag_len), frag_len); + EXPECT_EQ(splice(p[0], NULL, self->fd, NULL, frag_len, + SPLICE_F_MORE), frag_len); + off += frag_len; + + EXPECT_EQ(send(self->fd, mem_send + off, 1, 0), 1); + off++; + + EXPECT_EQ(recv(self->cfd, mem_recv, off, MSG_WAITALL), off); + EXPECT_EQ(memcmp(mem_send, mem_recv, off), 0); + } + + close(p[0]); + close(p[1]); +} + TEST_F(tls, splice_to_pipe) { int send_len = TLS_PAYLOAD_MAX_LEN; @@ -946,6 +984,49 @@ TEST_F(tls, peek_and_splice) EXPECT_EQ(memcmp(mem_send, mem_recv, send_len), 0); } +TEST_F(tls, splice_to_pipe_small) +{ + int send_len = TLS_PAYLOAD_MAX_LEN; + char mem_send[TLS_PAYLOAD_MAX_LEN]; + char mem_recv[TLS_PAYLOAD_MAX_LEN]; + size_t total = 0; + int p[2]; + + memrnd(mem_send, sizeof(mem_send)); + + ASSERT_GE(pipe(p), 0); + + /* Shrink pipe to 1 page (typically 4096 bytes) to force multiple + * splice iterations for a 16384-byte TLS record. + */ + EXPECT_GE(fcntl(p[1], F_SETPIPE_SZ, 4096), 4096); + + EXPECT_EQ(send(self->fd, mem_send, send_len, 0), send_len); + + while (total < (size_t)send_len) { + ssize_t spliced, drained; + + spliced = splice(self->cfd, NULL, p[1], NULL, + send_len - total, 0); + EXPECT_GT(spliced, 0); + if (spliced <= 0) + break; + + drained = read(p[0], mem_recv + total, spliced); + EXPECT_EQ(drained, spliced); + if (drained <= 0) + break; + + total += drained; + } + + EXPECT_EQ(total, (size_t)send_len); + EXPECT_EQ(memcmp(mem_send, mem_recv, send_len), 0); + + close(p[0]); + close(p[1]); +} + #define MAX_FRAGS 48 TEST_F(tls, splice_short) { @@ -954,6 +1035,8 @@ TEST_F(tls, splice_short) char sendbuf[0x100]; char sendchar = 'S'; int pipefds[2]; + int pipe_sz; + int ret; int i; sendchar_iov.iov_base = &sendchar; @@ -962,7 +1045,11 @@ TEST_F(tls, splice_short) memset(sendbuf, 's', sizeof(sendbuf)); ASSERT_GE(pipe2(pipefds, O_NONBLOCK), 0); - ASSERT_GE(fcntl(pipefds[0], F_SETPIPE_SZ, (MAX_FRAGS + 1) * 0x1000), 0); + pipe_sz = (MAX_FRAGS + 1) * getpagesize(); + ret = fcntl(pipefds[0], F_SETPIPE_SZ, pipe_sz); + if (ret < 0 && errno == EPERM) + SKIP(return, "insufficient pipe capacity"); + ASSERT_GE(ret, pipe_sz); for (i = 0; i < MAX_FRAGS; i++) ASSERT_GE(vmsplice(pipefds[1], &sendchar_iov, 1, 0), 0); @@ -1506,7 +1593,7 @@ test_mutliproc(struct __test_metadata *_metadata, struct _test_data_tls *self, res = recv(self->cfd, rb, left > sizeof(rb) ? sizeof(rb) : left, 0); - EXPECT_GE(res, 0); + ASSERT_GE(res, 0); left -= res; } } else { @@ -1523,7 +1610,7 @@ test_mutliproc(struct __test_metadata *_metadata, struct _test_data_tls *self, res = send(self->fd, buf, left > file_sz ? file_sz : left, 0); - EXPECT_GE(res, 0); + ASSERT_GE(res, 0); left -= res; } } @@ -1753,6 +1840,63 @@ TEST_F(tls, recv_efault) } #define TLS_RECORD_TYPE_HANDSHAKE 0x16 + +TEST_F(tls_basic, recvmsg_nopad_retry_iov) +{ + char payload[32]; + char first_iov[sizeof(payload)]; + char later_iov[sizeof(payload) * 2]; + char expected_later_iov[sizeof(later_iov)]; + char cbuf[CMSG_SPACE(sizeof(char))]; + struct tls_crypto_info_keys tls13; + struct iovec iov[] = { + { .iov_base = first_iov, .iov_len = sizeof(first_iov) }, + { .iov_base = later_iov, .iov_len = sizeof(later_iov) }, + }; + struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = ARRAY_SIZE(iov), + .msg_control = cbuf, + .msg_controllen = sizeof(cbuf), + }; + int one = 1; + int ret; + int i; + + if (self->notls) + SKIP(return, "no TLS support"); + + tls_crypto_info_init(TLS_1_3_VERSION, TLS_CIPHER_AES_GCM_128, + &tls13, 0); + + ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls13, tls13.len); + ASSERT_EQ(ret, 0); + + ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls13, tls13.len); + ASSERT_EQ(ret, 0); + + ret = setsockopt(self->cfd, SOL_TLS, TLS_RX_EXPECT_NO_PAD, + &one, sizeof(one)); + ASSERT_EQ(ret, 0); + + for (i = 0; i < sizeof(payload); i++) + payload[i] = 0x40 + i; + memset(first_iov, 0xa5, sizeof(first_iov)); + memset(later_iov, 0x5a, sizeof(later_iov)); + memset(expected_later_iov, 0x5a, sizeof(expected_later_iov)); + + /* A control record forces optimistic TLS 1.3 RX to retry. */ + ret = tls_send_cmsg(self->fd, TLS_RECORD_TYPE_HANDSHAKE, + payload, sizeof(payload), 0); + ASSERT_EQ(ret, sizeof(payload)); + + ret = recvmsg(self->cfd, &msg, 0); + ASSERT_EQ(ret, sizeof(payload)); + EXPECT_EQ(memcmp(first_iov, payload, sizeof(payload)), 0); + EXPECT_EQ(memcmp(later_iov, expected_later_iov, + sizeof(later_iov)), 0); +} + /* key_update, length 1, update_not_requested */ static const char key_update_msg[] = "\x18\x00\x00\x01\x00"; static void tls_send_keyupdate(struct __test_metadata *_metadata, int fd) @@ -2589,28 +2733,83 @@ TEST_F(tls_err, bad_rec) EXPECT_EQ(errno, EAGAIN); } +/* cfd carries a byte stream, so one recv() can return part of a + * record. Take the fragment length from the record header and wait + * for the remainder. + */ +static void tls_send_bad_auth(struct __test_metadata *_metadata, + int fd, int cfd, int fd2) +{ + char buf[128]; + int len; + + memrnd(buf, sizeof(buf) / 2); + ASSERT_EQ(send(fd, buf, sizeof(buf) / 2, 0), sizeof(buf) / 2); + + ASSERT_EQ(recv(cfd, buf, TLS_HDR_LEN, MSG_WAITALL), TLS_HDR_LEN); + + len = ((unsigned char)buf[3] << 8) | (unsigned char)buf[4]; + ASSERT_GT(len, 0); + ASSERT_LE(len, (int)sizeof(buf) - TLS_HDR_LEN); + + ASSERT_EQ(recv(cfd, buf + TLS_HDR_LEN, len, MSG_WAITALL), len); + + buf[TLS_HDR_LEN + len - 1]++; + + ASSERT_EQ(send(fd2, buf, TLS_HDR_LEN + len, 0), TLS_HDR_LEN + len); +} + TEST_F(tls_err, bad_auth) { char buf[128]; - int n; if (self->notls) SKIP(return, "no TLS support"); - memrnd(buf, sizeof(buf) / 2); - EXPECT_EQ(send(self->fd, buf, sizeof(buf) / 2, 0), sizeof(buf) / 2); - n = recv(self->cfd, buf, sizeof(buf), 0); - EXPECT_GT(n, sizeof(buf) / 2); - - buf[n - 1]++; + tls_send_bad_auth(_metadata, self->fd, self->cfd, self->fd2); - EXPECT_EQ(send(self->fd2, buf, n, 0), n); EXPECT_EQ(recv(self->cfd2, buf, sizeof(buf), 0), -1); EXPECT_EQ(errno, EBADMSG); EXPECT_EQ(recv(self->cfd2, buf, sizeof(buf), 0), -1); EXPECT_EQ(errno, EBADMSG); } +/* A record that did not authenticate breaks the connection for every + * reader, splice included. + * + * The two decrypt paths reach that result differently. A synchronous + * decrypt leaves the record parsed, so the splice re-runs the decrypt + * and fails on the record itself; the ctx->async_wait.err check in + * tls_sw_splice_read() is not what stops it. Only an asynchronous + * decrypt, which needs a TLS 1.2 socket and an AEAD advertising + * CRYPTO_ALG_ASYNC, consumes the record before the failure is + * recorded, leaving that check the sole reason the splice fails. + */ +TEST_F(tls_err, bad_auth_splice) +{ + char buf[128]; + ssize_t ret; + int p[2]; + + if (self->notls) + SKIP(return, "no TLS support"); + + tls_send_bad_auth(_metadata, self->fd, self->cfd, self->fd2); + + EXPECT_EQ(recv(self->cfd2, buf, sizeof(buf), 0), -1); + EXPECT_EQ(errno, EBADMSG); + + ASSERT_GE(pipe(p), 0); + + ret = splice(self->cfd2, NULL, p[1], NULL, sizeof(buf), + SPLICE_F_NONBLOCK); + EXPECT_EQ(ret, -1); + EXPECT_EQ(errno, EBADMSG); + + close(p[0]); + close(p[1]); +} + TEST_F(tls_err, bad_in_large_read) { char txt[3][64]; @@ -2866,7 +3065,6 @@ static size_t parse_tls_records(struct __test_metadata *_metadata, { const __u8 *rec = rx_buf; size_t total_plaintext_rx = 0; - const __u8 rec_header_len = 5; while (rec < rx_buf + rx_len) { __u16 record_payload_len; @@ -2886,7 +3084,7 @@ static size_t parse_tls_records(struct __test_metadata *_metadata, /* Plaintext must not exceed the specified limit */ ASSERT_LE(plaintext_len, max_payload_len); - rec += rec_header_len + record_payload_len; + rec += TLS_HDR_LEN + record_payload_len; } return total_plaintext_rx; diff --git a/tools/testing/selftests/net/tun.c b/tools/testing/selftests/net/tun.c index 8a5cd5cb5472..abe488bac50b 100644 --- a/tools/testing/selftests/net/tun.c +++ b/tools/testing/selftests/net/tun.c @@ -42,19 +42,19 @@ static struct in_addr param_ipaddr4_inner_src = { }; static struct in6_addr param_ipaddr6_outer_dst = { - { { 0x20, 0x02, 0x0d, 0xb8, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 } }, + { { 0xfd, 0x00, 0x0d, 0xb8, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 } }, }; static struct in6_addr param_ipaddr6_outer_src = { - { { 0x20, 0x02, 0x0d, 0xb8, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 } }, + { { 0xfd, 0x00, 0x0d, 0xb8, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 } }, }; static struct in6_addr param_ipaddr6_inner_dst = { - { { 0x20, 0x02, 0x0d, 0xb8, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 } }, + { { 0xfd, 0x00, 0x0d, 0xb8, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 } }, }; static struct in6_addr param_ipaddr6_inner_src = { - { { 0x20, 0x02, 0x0d, 0xb8, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 } }, + { { 0xfd, 0x00, 0x0d, 0xb8, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 } }, }; #ifndef BIT @@ -944,8 +944,8 @@ TEST_F(tun_vnet_udptnl, send_gso_packet) ASSERT_EQ(ret, off); ret = receive_gso_packet_from_tunnel(self, variant, &r_num_mss); - ASSERT_EQ(ret, variant->data_size); - ASSERT_EQ(r_num_mss, variant->r_num_mss); + EXPECT_EQ(ret, variant->data_size); + EXPECT_EQ(r_num_mss, variant->r_num_mss); } TEST_F(tun_vnet_udptnl, recv_gso_packet) @@ -955,18 +955,18 @@ TEST_F(tun_vnet_udptnl, recv_gso_packet) int ret, gso_type = VIRTIO_NET_HDR_GSO_UDP_L4; ret = send_gso_packet_into_tunnel(self, variant); - ASSERT_EQ(ret, variant->data_size); + EXPECT_EQ(ret, variant->data_size); memset(&vnet_hdr, 0, sizeof(vnet_hdr)); ret = receive_gso_packet_from_tun(self, variant, &vnet_hdr); - ASSERT_EQ(ret, variant->data_size); + EXPECT_EQ(ret, variant->data_size); if (!variant->no_gso) { - ASSERT_EQ(vh->gso_size, variant->gso_size); + EXPECT_EQ(vh->gso_size, variant->gso_size); gso_type |= (variant->tunnel_type & UDP_TUNNEL_OUTER_IPV4) ? (VIRTIO_NET_HDR_GSO_UDP_TUNNEL_IPV4) : (VIRTIO_NET_HDR_GSO_UDP_TUNNEL_IPV6); - ASSERT_EQ(vh->gso_type, gso_type); + EXPECT_EQ(vh->gso_type, gso_type); } } diff --git a/tools/testing/selftests/net/vlan_bridge_binding.sh b/tools/testing/selftests/net/vlan_bridge_binding.sh index e8c02c64e03a..d04caa14202d 100755 --- a/tools/testing/selftests/net/vlan_bridge_binding.sh +++ b/tools/testing/selftests/net/vlan_bridge_binding.sh @@ -64,7 +64,7 @@ check_operstate() local expect=$1; shift local operstate - operstate=$(busywait 1000 \ + operstate=$(busywait 2000 \ operstate_is "$dev" "$expect") check_err $? "Got operstate of $operstate, expected $expect" } diff --git a/tools/testing/selftests/net/xfrm_state.sh b/tools/testing/selftests/net/xfrm_state.sh new file mode 100755 index 000000000000..f6c54a6496d7 --- /dev/null +++ b/tools/testing/selftests/net/xfrm_state.sh @@ -0,0 +1,613 @@ +#!/bin/bash -e +# SPDX-License-Identifier: GPL-2.0 +# +# xfrm/IPsec tests. +# Currently implemented: +# - ICMP error source address verification (IETF RFC 4301 section 6) +# - ICMP MTU exceeded handling over IPsec tunnels. +# +# Addresses and topology: +# IPv4 prefix 10.1.c.d IPv6 prefix fc00:c::d/64 where c is the segment number +# and d is the interface identifier. +# IPv6 uses the same c:d as IPv4, and start with IPv6 prefix instead ipv4 prefix +# +# Network topology default: ns_set_v4 or ns_set_v6 +# 1.1 1.2 2.1 2.2 3.1 3.2 4.1 4.2 5.1 5.2 6.1 6.2 +# eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 +# a -------- r1 -------- s1 -------- r2 -------- s2 -------- r3 -------- b +# a, b = Alice and Bob hosts without IPsec. +# r1, r2, r3 routers, without IPsec +# s1, s2, IPsec gateways/routers that setup tunnel(s). + +# Network topology x: IPsec gateway that generates ICMP response - ns_set_v4x or ns_set_v6x +# 1.1 1.2 2.1 2.2 3.1 3.2 4.1 4.2 5.1 5.2 +# eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 eth0 eth1 +# a -------- r1 -------- s1 -------- r2 -------- s2 -------- b + +. lib.sh + +EXIT_ON_TEST_FAIL=no +PAUSE=no +VERBOSE=${VERBOSE:-0} +DEBUG=0 + +# Name Description +tests=" + unreachable_ipv4 IPv4 unreachable from router r3 + unreachable_ipv6 IPv6 unreachable from router r3 + unreachable_gw_ipv4 IPv4 unreachable from IPsec gateway s2 + unreachable_gw_ipv6 IPv6 unreachable from IPsec gateway s2 + mtu_ipv4_s2 IPv4 MTU exceeded from IPsec gateway s2 + mtu_ipv6_s2 IPv6 MTU exceeded from IPsec gateway s2 + mtu_ipv4_r2 IPv4 MTU exceeded from ESP router r2 + mtu_ipv6_r2 IPv6 MTU exceeded from ESP router r2 + mtu_ipv4_r3 IPv4 MTU exceeded from router r3 + mtu_ipv6_r3 IPv6 MTU exceeded from router r3" + +prefix4="10.1" +prefix6="fc00" + +run_cmd_err() { + cmd="$*" + + if [ "$VERBOSE" -gt 0 ]; then + printf " COMMAND: %s\n" "$cmd" + fi + + out="$($cmd 2>&1)" && rc=0 || rc=$? + if [ "$VERBOSE" -gt 1 ] && [ -n "$out" ]; then + echo " $out" + echo + fi + return 0 +} + +run_cmd() { + run_cmd_err "$@" || exit 1 +} + +run_test() { + # If errexit is set, unset it for sub-shell and restore after test + errexit=0 + if [[ $- =~ "e" ]]; then + errexit=1 + set +e + fi + + ( + unset IFS + + # shellcheck disable=SC2030 # fail is read by trap/cleanup within this subshell + fail="yes" + + # Since cleanup() relies on variables modified by this sub shell, + # it has to run in this context. + trap 'log_test_error $?; cleanup' EXIT INT TERM + + if [ "$VERBOSE" -gt 0 ]; then + printf "\n#############################################################\n\n" + fi + + ret=0 + case "${name}" in + # can't use eval and test names shell check will complain about unused code + unreachable_ipv4) test_unreachable_ipv4 ;; + unreachable_ipv6) test_unreachable_ipv6 ;; + unreachable_gw_ipv4) test_unreachable_gw_ipv4 ;; + unreachable_gw_ipv6) test_unreachable_gw_ipv6 ;; + mtu_ipv4_s2) test_mtu_ipv4_s2 ;; + mtu_ipv6_s2) test_mtu_ipv6_s2 ;; + mtu_ipv4_r2) test_mtu_ipv4_r2 ;; + mtu_ipv6_r2) test_mtu_ipv6_r2 ;; + mtu_ipv4_r3) test_mtu_ipv4_r3 ;; + mtu_ipv6_r3) test_mtu_ipv6_r3 ;; + esac + ret=$? + + if [ $ret -eq 0 ]; then + fail="no" + + if [ "$VERBOSE" -gt 1 ]; then + show_icmp_filter + fi + + printf "TEST: %-60s [ PASS ]\n" "${desc}" + elif [ $ret -eq "$ksft_skip" ]; then + fail="no" + printf "TEST: %-60s [SKIP]\n" "${desc}" + fi + + return $ret + ) + ret=$? + + [ $errexit -eq 1 ] && set -e + + case $ret in + 0) + all_skipped=false + [ "$exitcode" -eq "$ksft_skip" ] && exitcode=0 + ;; + "$ksft_skip") + [ $all_skipped = true ] && exitcode=$ksft_skip + ;; + *) + all_skipped=false + exitcode=1 + ;; + esac + + return 0 # don't trigger errexit (-e); actual status in exitcode +} + +setup_namespaces() { + local namespaces="" + + NS_A="" + NS_B="" + NS_R1="" + NS_R2="" + NS_R3="" + NS_S1="" + NS_S2="" + + for ns in ${ns_set}; do + namespaces="$namespaces NS_${ns^^}" + done + + # shellcheck disable=SC2086 # setup_ns expects unquoted list + setup_ns $namespaces + + ns_active= #ordered list of namespaces for this test. + + [ -n "${NS_A}" ] && ns_a=(ip netns exec "${NS_A}") && ns_active="${ns_active} $NS_A" + [ -n "${NS_R1}" ] && ns_active="${ns_active} $NS_R1" + [ -n "${NS_S1}" ] && ns_s1=(ip netns exec "${NS_S1}") && ns_active="${ns_active} $NS_S1" + [ -n "${NS_R2}" ] && ns_r2=(ip netns exec "${NS_R2}") && ns_active="${ns_active} $NS_R2" + [ -n "${NS_S2}" ] && ns_s2=(ip netns exec "${NS_S2}") && ns_active="${ns_active} $NS_S2" + [ -n "${NS_R3}" ] && ns_r3=(ip netns exec "${NS_R3}") && ns_active="${ns_active} $NS_R3" + [ -n "${NS_B}" ] && ns_active="${ns_active} $NS_B" +} + +addr_add() { + local -a ns_cmd=(ip netns exec "$1") + local addr="$2" + local dev="$3" + + run_cmd "${ns_cmd[@]}" ip addr add "${addr}" dev "${dev}" + run_cmd "${ns_cmd[@]}" ip link set up "${dev}" +} + +veth_add() { + local ns=$2 + local pns=$1 + local -a ns_cmd=(ip netns exec "${pns}") + local ln="eth0" + local rn="eth1" + + run_cmd "${ns_cmd[@]}" ip link add "${ln}" type veth peer name "${rn}" netns "${ns}" +} + +show_icmp_filter() { + run_cmd "${ns_r2[@]}" nft list ruleset + echo "$out" +} + +setup_icmp_filter() { + run_cmd "${ns_r2[@]}" nft add table inet filter + run_cmd "${ns_r2[@]}" nft add chain inet filter FORWARD \ + '{ type filter hook forward priority filter; policy drop ; }' + run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD counter ip protocol esp \ + counter log accept + run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD counter ip protocol \ + icmp counter log drop + + if [ "$VERBOSE" -gt 0 ]; then + run_cmd "${ns_r2[@]}" nft list ruleset + echo "$out" + fi +} + +setup_icmpv6_filter() { + run_cmd "${ns_r2[@]}" nft add table inet filter + run_cmd "${ns_r2[@]}" nft add chain inet filter FORWARD \ + '{ type filter hook forward priority filter; policy drop ; }' + run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD ip6 nexthdr \ + ipv6-icmp icmpv6 type echo-request counter log drop + run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD ip6 nexthdr esp \ + counter log accept + run_cmd "${ns_r2[@]}" nft add rule inet filter FORWARD ip6 nexthdr \ + ipv6-icmp icmpv6 type \ + '{nd-neighbor-solicit,nd-neighbor-advert,nd-router-solicit,nd-router-advert}' \ + counter log drop + if [ "$VERBOSE" -gt 0 ]; then + run_cmd "${ns_r2[@]}" nft list ruleset + echo "$out" + fi +} + +set_xfrm_params() { + s1_src=${src} + s1_dst=${dst} + s1_src_net=${src_net} + s1_dst_net=${dst_net} +} + +setup_ns_set_v4() { + ns_set="a r1 s1 r2 s2 r3 b" # Network topology default + imax=$(echo "$ns_set" | wc -w) # number of namespaces in this topology + + src="10.1.3.1" + dst="10.1.4.2" + src_net="10.1.1.0/24" + dst_net="10.1.6.0/24" + + prefix=${prefix4} + prefix_len=24 + s="." + S="." + + set_xfrm_params +} + +setup_ns_set_v4x() { + ns_set="a r1 s1 r2 s2 b" # Network topology: x + imax=$(echo "$ns_set" | wc -w) # number of namespaces in this topology + prefix=${prefix4} + s="." + S="." + src="10.1.3.1" + dst="10.1.4.2" + src_net="10.1.1.0/24" + dst_net="10.1.5.0/24" + prefix_len=24 + + set_xfrm_params +} + +setup_ns_set_v6() { + ns_set="a r1 s1 r2 s2 r3 b" # Network topology default + imax=$(echo "$ns_set" | wc -w) # number of namespaces in this topology + prefix=${prefix6} + s=":" + S="::" + src="fc00:3::1" + dst="fc00:4::2" + src_net="fc00:1::0/64" + dst_net="fc00:6::0/64" + prefix_len=64 + + set_xfrm_params +} + +setup_ns_set_v6x() { + ns_set="a r1 s1 r2 s2 b" # Network topology: x + imax=$(echo "$ns_set" | wc -w) + prefix=${prefix6} + s=":" + S="::" + src="fc00:3::1" + dst="fc00:4::2" + src_net="fc00:1::0/64" + dst_net="fc00:5::0/64" + prefix_len=64 + + set_xfrm_params +} + +setup_network() { + # Create veths and add addresses + local -a ns_cmd + i=1 + p="" + for ns in ${ns_active}; do + ns_cmd=(ip netns exec "${ns}") + + if [ "${i}" -ne 1 ]; then + # Create veth between previous and current namespace + veth_add "${p}" "${ns}" + # Add addresses: previous gets .1 on eth0, current gets .2 on eth1 + addr_add "${p}" "${prefix}${s}$((i-1))${S}1/${prefix_len}" eth0 + addr_add "${ns}" "${prefix}${s}$((i-1))${S}2/${prefix_len}" eth1 + fi + + # Enable forwarding + run_cmd "${ns_cmd[@]}" sysctl -q net/ipv4/ip_forward=1 + run_cmd "${ns_cmd[@]}" sysctl -q net/ipv6/conf/all/forwarding=1 + run_cmd "${ns_cmd[@]}" sysctl -q net/ipv6/conf/default/accept_dad=0 + + p=${ns} + i=$((i + 1)) + done + + # Add routes (needs all addresses to exist first) + i=1 + for ns in ${ns_active}; do + ns_cmd=(ip netns exec "${ns}") + + # Forward routes to networks beyond this node + if [ "${i}" -ne "${imax}" ]; then + nhf="${prefix}${s}${i}${S}2" # nexthop forward + for j in $(seq $((i + 1)) "${imax}"); do + run_cmd "${ns_cmd[@]}" ip route replace \ + "${prefix}${s}${j}${S}0/${prefix_len}" via "${nhf}" + done + fi + + # Reverse routes to networks before this node + if [ "${i}" -gt 1 ]; then + nhr="${prefix}${s}$((i-1))${S}1" # nexthop reverse + for j in $(seq 1 $((i - 2))); do + run_cmd "${ns_cmd[@]}" ip route replace \ + "${prefix}${s}${j}${S}0/${prefix_len}" via "${nhr}" + done + fi + + i=$((i + 1)) + done +} + +setup_xfrm_mode() { + local MODE=${1:-tunnel} + if [ "${MODE}" != "tunnel" ] && [ "${MODE}" != "beet" ]; then + echo "xfrm mode ${MODE} not supported" + log_test_error + return 1 + fi + + run_cmd "${ns_s1[@]}" ip xfrm policy add src "${s1_src_net}" dst "${s1_dst_net}" dir out \ + tmpl src "${s1_src}" dst "${s1_dst}" proto esp reqid 1 mode "${MODE}" + + # no "input" policies. we are only doing forwarding so far + + run_cmd "${ns_s1[@]}" ip xfrm policy add src "${s1_dst_net}" dst "${s1_src_net}" dir fwd \ + flag icmp tmpl src "${s1_dst}" dst "${s1_src}" proto esp reqid 2 mode "${MODE}" + + run_cmd "${ns_s1[@]}" ip xfrm state add src "${s1_src}" dst "${s1_dst}" proto esp spi 1 \ + reqid 1 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ + 0x1111111111111111111111111111111111111111 96 \ + sel src "${s1_src_net}" dst "${s1_dst_net}" dir out + + run_cmd "${ns_s1[@]}" ip xfrm state add src "${s1_dst}" dst "${s1_src}" proto esp spi 2 \ + reqid 2 flag icmp replay-window 8 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ + 0x2222222222222222222222222222222222222222 96 \ + sel src "${s1_dst_net}" dst "${s1_src_net}" dir in + + run_cmd "${ns_s2[@]}" ip xfrm policy add src "${s1_dst_net}" dst "${s1_src_net}" dir out \ + flag icmp tmpl src "${s1_dst}" dst "${s1_src}" proto esp reqid 2 mode "${MODE}" + + run_cmd "${ns_s2[@]}" ip xfrm policy add src "${s1_src_net}" dst "${s1_dst_net}" dir fwd \ + tmpl src "${s1_src}" dst "${s1_dst}" proto esp reqid 1 mode "${MODE}" + + run_cmd "${ns_s2[@]}" ip xfrm state add src "${s1_dst}" dst "${s1_src}" proto esp spi 2 \ + reqid 2 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ + 0x2222222222222222222222222222222222222222 96 \ + sel src "${s1_dst_net}" dst "${s1_src_net}" dir out + + run_cmd "${ns_s2[@]}" ip xfrm state add src "${s1_src}" dst "${s1_dst}" proto esp spi 1 \ + reqid 1 flag icmp replay-window 8 mode "${MODE}" aead 'rfc4106(gcm(aes))' \ + 0x1111111111111111111111111111111111111111 96 \ + sel src "${s1_src_net}" dst "${s1_dst_net}" dir in +} + +setup_xfrm() { + setup_xfrm_mode tunnel +} + +setup() { + [ "$(id -u)" -ne 0 ] && echo " need to run as root" && return "$ksft_skip" + + for arg; do + case "${arg}" in + ns_set_v4) setup_ns_set_v4 ;; + ns_set_v4x) setup_ns_set_v4x ;; + ns_set_v6) setup_ns_set_v6 ;; + ns_set_v6x) setup_ns_set_v6x ;; + namespaces) setup_namespaces ;; + network) setup_network ;; + xfrm) setup_xfrm ;; + icmp_filter) setup_icmp_filter ;; + icmpv6_filter) setup_icmpv6_filter ;; + *) echo " ${arg} not supported"; return 1 ;; + esac || return 1 + done +} + +# shellcheck disable=SC2317 # called via trap +pause() { + echo + echo "Pausing. Hit enter to continue" + read -r _ +} + +# shellcheck disable=SC2317 # called via trap +log_test_error() { + # shellcheck disable=SC2031 # fail is set in subshell, read via trap + if [ "${fail}" = "yes" ] && [ -n "${desc}" ]; then + if [ "$VERBOSE" -gt 0 ]; then + show_icmp_filter + fi + printf "TEST: %-60s [ FAIL ] %s\n" "${desc}" "${name}" + [ -n "${cmd}" ] && printf '%s\n\n' "${cmd}" + [ -n "${out}" ] && printf '%s\n\n' "${out}" + fi +} + +# shellcheck disable=SC2317 # called via trap +cleanup() { + # shellcheck disable=SC2031 # fail is set in subshell, read via trap + [[ "$PAUSE" = "always" || ( "$PAUSE" = "fail" && "$fail" = "yes" ) ]] && pause + cleanup_all_ns + # shellcheck disable=SC2031 # fail is set in subshell, read via trap + [ "${EXIT_ON_TEST_FAIL}" = "yes" ] && [ "${fail}" = "yes" ] && exit 1 +} + +test_unreachable_ipv6() { + setup ns_set_v6 namespaces network xfrm icmpv6_filter || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::2 + run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::3 + rc=0 + echo -e "$out" | grep -q -E 'From fc00:5::2 icmp_seq.* Destination' || rc=1 + return "${rc}" +} + +test_unreachable_gw_ipv6() { + setup ns_set_v6x namespaces network xfrm icmpv6_filter || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:5::2 + run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:5::3 + rc=0 + echo -e "$out" | grep -q -E 'From fc00:4::2 icmp_seq.* Destination' || rc=1 + return "${rc}" +} + +test_unreachable_ipv4() { + setup ns_set_v4 namespaces network icmp_filter xfrm || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.2 + run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.3 + rc=0 + echo -e "$out" | grep -q -E 'From 10.1.5.2 icmp_seq.* Destination' || rc=1 + return "${rc}" +} + +test_unreachable_gw_ipv4() { + setup ns_set_v4x namespaces network icmp_filter xfrm || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.5.2 + run_cmd_err "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.5.3 + rc=0 + echo -e "$out" | grep -q -E 'From 10.1.4.2 icmp_seq.* Destination' || rc=1 + return "${rc}" +} + +test_mtu_ipv4_r2() { + setup ns_set_v4 namespaces network icmp_filter xfrm || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.2 + run_cmd "${ns_r2[@]}" ip route replace 10.1.3.0/24 dev eth1 src 10.1.3.2 mtu 1300 + run_cmd "${ns_r2[@]}" ip route replace 10.1.4.0/24 dev eth0 src 10.1.4.1 mtu 1300 + # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword + run_cmd "${ns_a[@]}" ping -M do -s 1300 -W 5 -w 4 -c 1 10.1.6.2 || true + rc=0 + echo -e "$out" | grep -q -E "From 10.1.2.2 icmp_seq=.* Frag needed and DF set" || rc=1 + return "${rc}" +} + +test_mtu_ipv6_r2() { + setup ns_set_v6 namespaces network xfrm icmpv6_filter || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::2 + run_cmd "${ns_r2[@]}" ip -6 route replace fc00:3::/64 \ + dev eth1 metric 256 src fc00:3::2 mtu 1300 + run_cmd "${ns_r2[@]}" ip -6 route replace fc00:4::/64 \ + dev eth0 metric 256 src fc00:4::1 mtu 1300 + # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword + run_cmd "${ns_a[@]}" ping -M do -s 1300 -W 5 -w 4 -c 1 fc00:6::2 || true + rc=0 + echo -e "$out" | grep -q -E "From fc00:2::2 icmp_seq=.* Packet too big: mtu=1230" || rc=1 + return "${rc}" +} + +test_mtu_ipv4_r3() { + setup ns_set_v4 namespaces network icmp_filter xfrm || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.6.2 + run_cmd "${ns_r3[@]}" ip route replace 10.1.6.0/24 dev eth0 mtu 1300 + # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword + run_cmd "${ns_a[@]}" ping -M do -s 1350 -W 5 -w 4 -c 1 10.1.6.2 || true + rc=0 + echo -e "$out" | grep -q -E "From 10.1.5.2 .* Frag needed and DF set \(mtu = 1300\)" || rc=1 + return "${rc}" +} + +test_mtu_ipv4_s2() { + setup ns_set_v4x namespaces network icmp_filter xfrm || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 10.1.5.2 + run_cmd "${ns_s2[@]}" ip route replace 10.1.5.0/24 dev eth0 src 10.1.5.1 mtu 1300 + # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword + run_cmd "${ns_a[@]}" ping -M do -s 1350 -W 5 -w 4 -c 1 10.1.5.2 || true + rc=0 + echo -e "$out" | grep -q -E "From 10.1.4.2.*Frag needed and DF set \(mtu = 1300\)" || rc=1 + return "${rc}" +} + +test_mtu_ipv6_s2() { + setup ns_set_v6x namespaces network xfrm icmpv6_filter || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:5::2 + run_cmd "${ns_s2[@]}" ip -6 route replace fc00:5::/64 dev eth0 metric 256 mtu 1300 + # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword + run_cmd "${ns_a[@]}" ping -M do -s 1350 -W 5 -w 4 -c 1 fc00:5::2 || true + rc=0 + echo -e "$out" | grep -q -E "From fc00:4::2.*Packet too big: mtu=1300" || rc=1 + return "${rc}" +} + +test_mtu_ipv6_r3() { + setup ns_set_v6 namespaces network xfrm icmpv6_filter || return "$ksft_skip" + run_cmd "${ns_a[@]}" ping -W 5 -w 4 -c 1 fc00:6::2 + run_cmd "${ns_r3[@]}" ip -6 route replace fc00:6::/64 dev eth1 metric 256 mtu 1300 + # shellcheck disable=SC1010 # -M do: do = dont-fragment, not shell keyword + run_cmd "${ns_a[@]}" ping -M do -s 1300 -W 5 -w 4 -c 1 fc00:6::2 || true + rc=0 + echo -e "$out" | grep -q -E "From fc00:5::2 icmp_seq=.* Packet too big: mtu=1300" || rc=1 + return "${rc}" +} + +################################################################################ +# +usage() { + echo + echo "$0 [OPTIONS] [TEST]..." + echo "If no TEST argument is given, all tests will be run." + echo + echo -e "\t-p Pause on fail. Namespaces are kept for diagnostics" + echo -e "\t-P Pause after the test. Namespaces are kept for diagnostics" + echo -e "\t-v Verbose output. Show commands; -vv Show output and nft rules also" + echo "Available tests${tests}" + exit 1 +} + +################################################################################ +# +exitcode=0 +all_skipped=true +out= +cmd= + +while getopts :epPv o; do + case $o in + e) EXIT_ON_TEST_FAIL=yes ;; + P) PAUSE=always ;; + p) PAUSE=fail ;; + v) VERBOSE=$((VERBOSE + 1)) ;; + *) usage ;; + esac +done +shift $((OPTIND - 1)) + +IFS=$'\t\n' + +for arg; do + # Check first that all requested tests are available before running any + command -v "test_${arg}" >/dev/null || { + echo "=== Test ${arg} not found" + usage + } +done + +name="" +desc="" +fail="no" + +for t in ${tests}; do + [ "${name}" = "" ] && name="${t}" && continue + [ "${desc}" = "" ] && desc="${t}" + + run_this=1 + for arg; do + [ "${arg}" = "${name}" ] && run_this=1 && break + run_this=0 + done + if [ $run_this -eq 1 ]; then + run_test + fi + name="" + desc="" +done + +exit ${exitcode} |
