// SPDX-License-Identifier: GPL-2.0 /** * Test XDP bonding support * * Sets up two bonded veth pairs between two fresh namespaces * and verifies that XDP_TX program loaded on a bond device * are correctly loaded onto the slave devices and XDP_TX'd * packets are balanced using bonding. */ #define _GNU_SOURCE #include <sched.h> #include <net/if.h> #include <linux/if_link.h> #include "test_progs.h" #include "network_helpers.h" #include <linux/if_bonding.h> #include <linux/limits.h> #include <linux/udp.h> #include <uapi/linux/netdev.h> #include "xdp_dummy.skel.h" #include "xdp_redirect_multi_kern.skel.h" #include "xdp_tx.skel.h" #define BOND1_MAC {0x00, 0x11, 0x22, 0x33, 0x44, 0x55} #define BOND1_MAC_STR "00:11:22:33:44:55" #define BOND2_MAC {0x00, 0x22, 0x33, 0x44, 0x55, 0x66} #define BOND2_MAC_STR "00:22:33:44:55:66" #define NPACKETS 100 static int root_netns_fd = -1; static void restore_root_netns(void) { ASSERT_OK(setns(root_netns_fd, CLONE_NEWNET), "restore_root_netns"); } static int setns_by_name(char *name) { int nsfd, err; char nspath[PATH_MAX]; snprintf(nspath, sizeof(nspath), "%s/%s", "/var/run/netns", name); nsfd = open(nspath, O_RDONLY | O_CLOEXEC); if (nsfd < 0) return -1; err = setns(nsfd, CLONE_NEWNET); close(nsfd); return err; } static int get_rx_packets(const char *iface) { FILE *f; char line[512]; int iface_len = strlen(iface); f = fopen("/proc/net/dev", "r"); if (!f) return -1; while (fgets(line, sizeof(line), f)) { char *p = line; while (*p == ' ') p++; /* skip whitespace */ if (!strncmp(p, iface, iface_len)) { p += iface_len; if (*p++ != ':') continue; while (*p == ' ') p++; /* skip whitespace */ while (*p && *p != ' ') p++; /* skip rx bytes */ while (*p == ' ') p++; /* skip whitespace */ fclose(f); return atoi(p); } } fclose(f); return -1; } #define MAX_BPF_LINKS 8 struct skeletons { struct xdp_dummy *xdp_dummy; struct xdp_tx *xdp_tx; struct xdp_redirect_multi_kern *xdp_redirect_multi_kern; int nlinks; struct bpf_link *links[MAX_BPF_LINKS]; }; static int xdp_attach(struct skeletons *skeletons, struct bpf_program *prog, char *iface) { struct bpf_link *link; int ifindex; ifindex = if_nametoindex(iface); if (!ASSERT_GT(ifindex, 0, "get ifindex")) return -1; if (!ASSERT_LE(skeletons->nlinks+1, MAX_BPF_LINKS, "too many XDP programs attached")) return -1; link = bpf_program__attach_xdp(prog, ifindex); if (!ASSERT_OK_PTR(link, "attach xdp program")) return -1; skeletons->links[skeletons->nlinks++] = link; return 0; } enum { BOND_ONE_NO_ATTACH = 0, BOND_BOTH_AND_ATTACH, }; static const char * const mode_names[] = { [BOND_MODE_ROUNDROBIN] = "balance-rr", [BOND_MODE_ACTIVEBACKUP] = "active-backup", [BOND_MODE_XOR] = "balance-xor", [BOND_MODE_BROADCAST] = "broadcast", [BOND_MODE_8023AD] = "802.3ad", [BOND_MODE_TLB] = "balance-tlb", [BOND_MODE_ALB] = "balance-alb", }; static const char * const xmit_policy_names[] = { [BOND_XMIT_POLICY_LAYER2] = "layer2", [BOND_XMIT_POLICY_LAYER34] = "layer3+4", [BOND_XMIT_POLICY_LAYER23] = "layer2+3", [BOND_XMIT_POLICY_ENCAP23] = "encap2+3", [BOND_XMIT_POLICY_ENCAP34] = "encap3+4", }; static int bonding_setup(struct skeletons *skeletons, int mode, int xmit_policy, int bond_both_attach) { SYS(fail, "ip netns add ns_dst"); SYS(fail, "ip link add veth1_1 type veth peer name veth2_1 netns ns_dst"); SYS(fail, "ip link add veth1_2 type veth peer name veth2_2 netns ns_dst"); SYS(fail, "ip link add bond1 type bond mode %s xmit_hash_policy %s", mode_names[mode], xmit_policy_names[xmit_policy]); SYS(fail, "ip link set bond1 up address " BOND1_MAC_STR " addrgenmode none"); SYS(fail, "ip -netns ns_dst link add bond2 type bond mode %s xmit_hash_policy %s", mode_names[mode], xmit_policy_names[xmit_policy]); SYS(fail, "ip -netns ns_dst link set bond2 up address " BOND2_MAC_STR " addrgenmode none"); SYS(fail, "ip link set veth1_1 master bond1"); if (bond_both_attach == BOND_BOTH_AND_ATTACH) { SYS(fail, "ip link set veth1_2 master bond1"); } else { SYS(fail, "ip link set veth1_2 up addrgenmode none"); if (xdp_attach(skeletons, skeletons->xdp_dummy->progs.xdp_dummy_prog, "veth1_2")) return -1; } SYS(fail, "ip -netns ns_dst link set veth2_1 master bond2"); if (bond_both_attach == BOND_BOTH_AND_ATTACH) SYS(fail, "ip -netns ns_dst link set veth2_2 master bond2"); else SYS(fail, "ip -netns ns_dst link set veth2_2 up addrgenmode none"); /* Load a dummy program on sending side as with veth peer needs to have a * XDP program loaded as well. */ if (xdp_attach(skeletons, skeletons->xdp_dummy->progs.xdp_dummy_prog, "bond1")) return -1; if (bond_both_attach == BOND_BOTH_AND_ATTACH) { if (!ASSERT_OK(setns_by_name("ns_dst"), "set netns to ns_dst")) return -1; if (xdp_attach(skeletons, skeletons->xdp_tx->progs.xdp_tx, "bond2")) return -1; restore_root_netns(); } return 0; fail: return -1; } static void bonding_cleanup(struct skeletons *skeletons) { restore_root_netns(); while (skeletons->nlinks) { skeletons->nlinks--; bpf_link__destroy(skeletons->links[skeletons->nlinks]); } ASSERT_OK(system("ip link delete bond1"), "delete bond1"); ASSERT_OK(system("ip link delete veth1_1"), "delete veth1_1"); ASSERT_OK(system("ip link delete veth1_2"), "delete veth1_2"); ASSERT_OK(system("ip netns delete ns_dst"), "delete ns_dst"); } static int send_udp_packets(int vary_dst_ip) { struct ethhdr eh = { .h_source = BOND1_MAC, .h_dest = BOND2_MAC, .h_proto = htons(ETH_P_IP), }; struct iphdr iph = {}; struct udphdr uh = {}; uint8_t buf[128]; int i, s = -1; int ifindex; s = socket(AF_PACKET, SOCK_RAW, IPPROTO_RAW); if (!ASSERT_GE(s, 0, "socket")) goto err; ifindex = if_nametoindex("bond1"); if (!ASSERT_GT(ifindex, 0, "get bond1 ifindex")) goto err; iph.ihl = 5; iph.version = 4; iph.tos = 16; iph.id = 1; iph.ttl = 64; iph.protocol = IPPROTO_UDP; iph.saddr = 1; iph.daddr = 2; iph.tot_len = htons(sizeof(buf) - ETH_HLEN); iph.check = 0; for (i = 1; i <= NPACKETS; i++) { int n; struct sockaddr_ll saddr_ll = { .sll_ifindex = ifindex, .sll_halen = ETH_ALEN, .sll_addr = BOND2_MAC, }; /* vary the UDP destination port for even distribution with roundrobin/xor modes */ uh.dest++; if (vary_dst_ip) iph.daddr++; /* construct a packet */ memcpy(buf, &eh, sizeof(eh)); memcpy(buf + sizeof(eh), &iph, sizeof(iph)); memcpy(buf + sizeof(eh) + sizeof(iph), &uh, sizeof(uh)); n = sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)&saddr_ll, sizeof(saddr_ll)); if (!ASSERT_EQ(n, sizeof(buf), "sendto")) goto err; } return 0; err: if (s >= 0) close(s); return -1; } static void test_xdp_bonding_with_mode(struct skeletons *skeletons, int mode, int xmit_policy) { int bond1_rx; if (bonding_setup(skeletons, mode, xmit_policy, BOND_BOTH_AND_ATTACH)) goto out; if (send_udp_packets(xmit_policy != BOND_XMIT_POLICY_LAYER34)) goto out; bond1_rx = get_rx_packets("bond1"); ASSERT_EQ(bond1_rx, NPACKETS, "expected more received packets"); switch (mode) { case BOND_MODE_ROUNDROBIN: case BOND_MODE_XOR: { int veth1_rx = get_rx_packets("veth1_1"); int veth2_rx = get_rx_packets("veth1_2"); int diff = abs(veth1_rx - veth2_rx); ASSERT_GE(veth1_rx + veth2_rx, NPACKETS, "expected more packets"); switch (xmit_policy) { case BOND_XMIT_POLICY_LAYER2: ASSERT_GE(diff, NPACKETS, "expected packets on only one of the interfaces"); break; case BOND_XMIT_POLICY_LAYER23: case BOND_XMIT_POLICY_LAYER34: ASSERT_LT(diff, NPACKETS/2, "expected even distribution of packets"); break; default: PRINT_FAIL("Unimplemented xmit_policy=%d\n", xmit_policy); break; } break; } case BOND_MODE_ACTIVEBACKUP: { int veth1_rx = get_rx_packets("veth1_1"); int veth2_rx = get_rx_packets("veth1_2"); int diff = abs(veth1_rx - veth2_rx); ASSERT_GE(diff, NPACKETS, "expected packets on only one of the interfaces"); break; } default: PRINT_FAIL("Unimplemented xmit_policy=%d\n", xmit_policy); break; } out: bonding_cleanup(skeletons); } /* Test the broadcast redirection using xdp_redirect_map_multi_prog and adding * all the interfaces to it and checking that broadcasting won't send the packet * to neither the ingress bond device (bond2) or its slave (veth2_1). */ static void test_xdp_bonding_redirect_multi(struct skeletons *skeletons) { static const char * const ifaces[] = {"bond2", "veth2_1", "veth2_2"}; int veth1_1_rx, veth1_2_rx; int err; if (bonding_setup(skeletons, BOND_MODE_ROUNDROBIN, BOND_XMIT_POLICY_LAYER23, BOND_ONE_NO_ATTACH)) goto out; if (!ASSERT_OK(setns_by_name("ns_dst"), "could not set netns to ns_dst")) goto out; /* populate the devmap with the relevant interfaces */ for (int i = 0; i < ARRAY_SIZE(ifaces); i++) { int ifindex = if_nametoindex(ifaces[i]); int map_fd = bpf_map__fd(skeletons->xdp_redirect_multi_kern->maps.map_all); if (!ASSERT_GT(ifindex, 0, "could not get interface index")) goto out; err = bpf_map_update_elem(map_fd, &ifindex, &ifindex, 0); if (!ASSERT_OK(err, "add interface to map_all")) goto out; } if (xdp_attach(skeletons, skeletons->xdp_redirect_multi_kern->progs.xdp_redirect_map_multi_prog, "bond2")) goto out; restore_root_netns(); if (send_udp_packets(BOND_MODE_ROUNDROBIN)) goto out; veth1_1_rx = get_rx_packets("veth1_1"); veth1_2_rx = get_rx_packets("veth1_2"); ASSERT_EQ(veth1_1_rx, 0, "expected no packets on veth1_1"); ASSERT_GE(veth1_2_rx, NPACKETS, "expected packets on veth1_2"); out: restore_root_netns(); bonding_cleanup(skeletons); } /* Test that XDP programs cannot be attached to both the bond master and slaves simultaneously */ static void test_xdp_bonding_attach(struct skeletons *skeletons) { struct bpf_link *link = NULL; struct bpf_link *link2 = NULL; int veth, bond, err; if (!ASSERT_OK(system("ip link add veth type veth"), "add veth")) goto out; if (!ASSERT_OK(system("ip link add bond type bond"), "add bond")) goto out; veth = if_nametoindex("veth"); if (!ASSERT_GE(veth, 0, "if_nametoindex veth")) goto out; bond = if_nametoindex("bond"); if (!ASSERT_GE(bond, 0, "if_nametoindex bond")) goto out; /* enslaving with a XDP program loaded is allowed */ link = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, veth); if (!ASSERT_OK_PTR(link, "attach program to veth")) goto out; err = system("ip link set veth master bond"); if (!ASSERT_OK(err, "set veth master")) goto out; bpf_link__destroy(link); link = NULL; /* attaching to slave when master has no program is allowed */ link = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, veth); if (!ASSERT_OK_PTR(link, "attach program to slave when enslaved")) goto out; /* attaching to master not allowed when slave has program loaded */ link2 = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, bond); if (!ASSERT_ERR_PTR(link2, "attach program to master when slave has program")) goto out; bpf_link__destroy(link); link = NULL; /* attaching XDP program to master allowed when slave has no program */ link = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, bond); if (!ASSERT_OK_PTR(link, "attach program to master")) goto out; /* attaching to slave not allowed when master has program loaded */ link2 = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, veth); if (!ASSERT_ERR_PTR(link2, "attach program to slave when master has program")) goto out; bpf_link__destroy(link); link = NULL; /* test program unwinding with a non-XDP slave */ if (!ASSERT_OK(system("ip link add vxlan type vxlan id 1 remote 1.2.3.4 dstport 0 dev lo"), "add vxlan")) goto out; err = system("ip link set vxlan master bond"); if (!ASSERT_OK(err, "set vxlan master")) goto out; /* attaching not allowed when one slave does not support XDP */ link = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, bond); if (!ASSERT_ERR_PTR(link, "attach program to master when slave does not support XDP")) goto out; out: bpf_link__destroy(link); bpf_link__destroy(link2); system("ip link del veth"); system("ip link del bond"); system("ip link del vxlan"); } /* Test with nested bonding devices to catch issue with negative jump label count */ static void test_xdp_bonding_nested(struct skeletons *skeletons) { struct bpf_link *link = NULL; int bond, err; if (!ASSERT_OK(system("ip link add bond type bond"), "add bond")) goto out; bond = if_nametoindex("bond"); if (!ASSERT_GE(bond, 0, "if_nametoindex bond")) goto out; if (!ASSERT_OK(system("ip link add bond_nest1 type bond"), "add bond_nest1")) goto out; err = system("ip link set bond_nest1 master bond"); if (!ASSERT_OK(err, "set bond_nest1 master")) goto out; if (!ASSERT_OK(system("ip link add bond_nest2 type bond"), "add bond_nest1")) goto out; err = system("ip link set bond_nest2 master bond_nest1"); if (!ASSERT_OK(err, "set bond_nest2 master")) goto out; link = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, bond); ASSERT_OK_PTR(link, "attach program to master"); out: bpf_link__destroy(link); system("ip link del bond"); system("ip link del bond_nest1"); system("ip link del bond_nest2"); } static void test_xdp_bonding_features(struct skeletons *skeletons) { LIBBPF_OPTS(bpf_xdp_query_opts, query_opts); int bond_idx, veth1_idx, err; struct bpf_link *link = NULL; if (!ASSERT_OK(system("ip link add bond type bond"), "add bond")) goto out; bond_idx = if_nametoindex("bond"); if (!ASSERT_GE(bond_idx, 0, "if_nametoindex bond")) goto out; /* query default xdp-feature for bond device */ err = bpf_xdp_query(bond_idx, XDP_FLAGS_DRV_MODE, &query_opts); if (!ASSERT_OK(err, "bond bpf_xdp_query")) goto out; if (!ASSERT_EQ(query_opts.feature_flags, NETDEV_XDP_ACT_MASK, "bond query_opts.feature_flags")) goto out; if (!ASSERT_OK(system("ip link add veth0 type veth peer name veth1"), "add veth{0,1} pair")) goto out; if (!ASSERT_OK(system("ip link add veth2 type veth peer name veth3"), "add veth{2,3} pair")) goto out; if (!ASSERT_OK(system("ip link set veth0 master bond"), "add veth0 to master bond")) goto out; /* xdp-feature for bond device should be obtained from the single slave * device (veth0) */ err = bpf_xdp_query(bond_idx, XDP_FLAGS_DRV_MODE, &query_opts); if (!ASSERT_OK(err, "bond bpf_xdp_query")) goto out; if (!ASSERT_EQ(query_opts.feature_flags, NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT | NETDEV_XDP_ACT_RX_SG, "bond query_opts.feature_flags")) goto out; veth1_idx = if_nametoindex("veth1"); if (!ASSERT_GE(veth1_idx, 0, "if_nametoindex veth1")) goto out; link = bpf_program__attach_xdp(skeletons->xdp_dummy->progs.xdp_dummy_prog, veth1_idx); if (!ASSERT_OK_PTR(link, "attach program to veth1")) goto out; /* xdp-feature for veth0 are changed */ err = bpf_xdp_query(bond_idx, XDP_FLAGS_DRV_MODE, &query_opts); if (!ASSERT_OK(err, "bond bpf_xdp_query")) goto out; if (!ASSERT_EQ(query_opts.feature_flags, NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT | NETDEV_XDP_ACT_RX_SG | NETDEV_XDP_ACT_NDO_XMIT | NETDEV_XDP_ACT_NDO_XMIT_SG, "bond query_opts.feature_flags")) goto out; if (!ASSERT_OK(system("ip link set veth2 master bond"), "add veth2 to master bond")) goto out; err = bpf_xdp_query(bond_idx, XDP_FLAGS_DRV_MODE, &query_opts); if (!ASSERT_OK(err, "bond bpf_xdp_query")) goto out; /* xdp-feature for bond device should be set to the most restrict * value obtained from attached slave devices (veth0 and veth2) */ if (!ASSERT_EQ(query_opts.feature_flags, NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT | NETDEV_XDP_ACT_RX_SG, "bond query_opts.feature_flags")) goto out; if (!ASSERT_OK(system("ip link set veth2 nomaster"), "del veth2 to master bond")) goto out; err = bpf_xdp_query(bond_idx, XDP_FLAGS_DRV_MODE, &query_opts); if (!ASSERT_OK(err, "bond bpf_xdp_query")) goto out; if (!ASSERT_EQ(query_opts.feature_flags, NETDEV_XDP_ACT_BASIC | NETDEV_XDP_ACT_REDIRECT | NETDEV_XDP_ACT_RX_SG | NETDEV_XDP_ACT_NDO_XMIT | NETDEV_XDP_ACT_NDO_XMIT_SG, "bond query_opts.feature_flags")) goto out; if (!ASSERT_OK(system("ip link set veth0 nomaster"), "del veth0 to master bond")) goto out; err = bpf_xdp_query(bond_idx, XDP_FLAGS_DRV_MODE, &query_opts); if (!ASSERT_OK(err, "bond bpf_xdp_query")) goto out; ASSERT_EQ(query_opts.feature_flags, NETDEV_XDP_ACT_MASK, "bond query_opts.feature_flags"); out: bpf_link__destroy(link); system("ip link del veth0"); system("ip link del veth2"); system("ip link del bond"); } static int libbpf_debug_print(enum libbpf_print_level level, const char *format, va_list args) { if (level != LIBBPF_WARN) vprintf(format, args); return 0; } struct bond_test_case { char *name; int mode; int xmit_policy; }; static struct bond_test_case bond_test_cases[] = { { "xdp_bonding_roundrobin", BOND_MODE_ROUNDROBIN, BOND_XMIT_POLICY_LAYER23, }, { "xdp_bonding_activebackup", BOND_MODE_ACTIVEBACKUP, BOND_XMIT_POLICY_LAYER23 }, { "xdp_bonding_xor_layer2", BOND_MODE_XOR, BOND_XMIT_POLICY_LAYER2, }, { "xdp_bonding_xor_layer23", BOND_MODE_XOR, BOND_XMIT_POLICY_LAYER23, }, { "xdp_bonding_xor_layer34", BOND_MODE_XOR, BOND_XMIT_POLICY_LAYER34, }, }; void serial_test_xdp_bonding(void) { libbpf_print_fn_t old_print_fn; struct skeletons skeletons = {}; int i; old_print_fn = libbpf_set_print(libbpf_debug_print); root_netns_fd = open("/proc/self/ns/net", O_RDONLY); if (!ASSERT_GE(root_netns_fd, 0, "open /proc/self/ns/net")) goto out; skeletons.xdp_dummy = xdp_dummy__open_and_load(); if (!ASSERT_OK_PTR(skeletons.xdp_dummy, "xdp_dummy__open_and_load")) goto out; skeletons.xdp_tx = xdp_tx__open_and_load(); if (!ASSERT_OK_PTR(skeletons.xdp_tx, "xdp_tx__open_and_load")) goto out; skeletons.xdp_redirect_multi_kern = xdp_redirect_multi_kern__open_and_load(); if (!ASSERT_OK_PTR(skeletons.xdp_redirect_multi_kern, "xdp_redirect_multi_kern__open_and_load")) goto out; if (test__start_subtest("xdp_bonding_attach")) test_xdp_bonding_attach(&skeletons); if (test__start_subtest("xdp_bonding_nested")) test_xdp_bonding_nested(&skeletons); if (test__start_subtest("xdp_bonding_features")) test_xdp_bonding_features(&skeletons); for (i = 0; i < ARRAY_SIZE(bond_test_cases); i++) { struct bond_test_case *test_case = &bond_test_cases[i]; if (test__start_subtest(test_case->name)) test_xdp_bonding_with_mode( &skeletons, test_case->mode, test_case->xmit_policy); } if (test__start_subtest("xdp_bonding_redirect_multi")) test_xdp_bonding_redirect_multi(&skeletons); out: xdp_dummy__destroy(skeletons.xdp_dummy); xdp_tx__destroy(skeletons.xdp_tx); xdp_redirect_multi_kern__destroy(skeletons.xdp_redirect_multi_kern); libbpf_set_print(old_print_fn); if (root_netns_fd >= 0) close(root_netns_fd); }