// SPDX-License-Identifier: GPL-2.0 /* * Check if we can migrate child sockets. * * 1. call listen() for 4 server sockets. * 2. call connect() for 25 client sockets. * 3. call listen() for 1 server socket. (migration target) * 4. update a map to migrate all child sockets * to the last server socket (migrate_map[cookie] = 4) * 5. call shutdown() for first 4 server sockets * and migrate the requests in the accept queue * to the last server socket. * 6. call listen() for the second server socket. * 7. call shutdown() for the last server * and migrate the requests in the accept queue * to the second server socket. * 8. call listen() for the last server. * 9. call shutdown() for the second server * and migrate the requests in the accept queue * to the last server socket. * 10. call accept() for the last server socket. * * Author: Kuniyuki Iwashima */ #include #include #include "test_progs.h" #include "test_migrate_reuseport.skel.h" #include "network_helpers.h" #ifndef TCP_FASTOPEN_CONNECT #define TCP_FASTOPEN_CONNECT 30 #endif #define IFINDEX_LO 1 #define NR_SERVERS 5 #define NR_CLIENTS (NR_SERVERS * 5) #define MIGRATED_TO (NR_SERVERS - 1) /* fastopenq->max_qlen and sk->sk_max_ack_backlog */ #define QLEN (NR_CLIENTS * 5) #define MSG "Hello World\0" #define MSGLEN 12 static struct migrate_reuseport_test_case { const char *name; __s64 servers[NR_SERVERS]; __s64 clients[NR_CLIENTS]; struct sockaddr_storage addr; socklen_t addrlen; int family; int state; bool drop_ack; bool expire_synack_timer; bool fastopen; struct bpf_link *link; } test_cases[] = { { .name = "IPv4 TCP_ESTABLISHED inet_csk_listen_stop", .family = AF_INET, .state = BPF_TCP_ESTABLISHED, .drop_ack = false, .expire_synack_timer = false, .fastopen = false, }, { .name = "IPv4 TCP_SYN_RECV inet_csk_listen_stop", .family = AF_INET, .state = BPF_TCP_SYN_RECV, .drop_ack = true, .expire_synack_timer = false, .fastopen = true, }, { .name = "IPv4 TCP_NEW_SYN_RECV reqsk_timer_handler", .family = AF_INET, .state = BPF_TCP_NEW_SYN_RECV, .drop_ack = true, .expire_synack_timer = true, .fastopen = false, }, { .name = "IPv4 TCP_NEW_SYN_RECV inet_csk_complete_hashdance", .family = AF_INET, .state = BPF_TCP_NEW_SYN_RECV, .drop_ack = true, .expire_synack_timer = false, .fastopen = false, }, { .name = "IPv6 TCP_ESTABLISHED inet_csk_listen_stop", .family = AF_INET6, .state = BPF_TCP_ESTABLISHED, .drop_ack = false, .expire_synack_timer = false, .fastopen = false, }, { .name = "IPv6 TCP_SYN_RECV inet_csk_listen_stop", .family = AF_INET6, .state = BPF_TCP_SYN_RECV, .drop_ack = true, .expire_synack_timer = false, .fastopen = true, }, { .name = "IPv6 TCP_NEW_SYN_RECV reqsk_timer_handler", .family = AF_INET6, .state = BPF_TCP_NEW_SYN_RECV, .drop_ack = true, .expire_synack_timer = true, .fastopen = false, }, { .name = "IPv6 TCP_NEW_SYN_RECV inet_csk_complete_hashdance", .family = AF_INET6, .state = BPF_TCP_NEW_SYN_RECV, .drop_ack = true, .expire_synack_timer = false, .fastopen = false, } }; static void init_fds(__s64 fds[], int len) { int i; for (i = 0; i < len; i++) fds[i] = -1; } static void close_fds(__s64 fds[], int len) { int i; for (i = 0; i < len; i++) { if (fds[i] != -1) { close(fds[i]); fds[i] = -1; } } } static int setup_fastopen(char *buf, int size, int *saved_len, bool restore) { int err = 0, fd, len; fd = open("/proc/sys/net/ipv4/tcp_fastopen", O_RDWR); if (!ASSERT_NEQ(fd, -1, "open")) return -1; if (restore) { len = write(fd, buf, *saved_len); if (!ASSERT_EQ(len, *saved_len, "write - restore")) err = -1; } else { *saved_len = read(fd, buf, size); if (!ASSERT_GE(*saved_len, 1, "read")) { err = -1; goto close; } err = lseek(fd, 0, SEEK_SET); if (!ASSERT_OK(err, "lseek")) goto close; /* (TFO_CLIENT_ENABLE | TFO_SERVER_ENABLE | * TFO_CLIENT_NO_COOKIE | TFO_SERVER_COOKIE_NOT_REQD) */ len = write(fd, "519", 3); if (!ASSERT_EQ(len, 3, "write - setup")) err = -1; } close: close(fd); return err; } static int drop_ack(struct migrate_reuseport_test_case *test_case, struct test_migrate_reuseport *skel) { if (test_case->family == AF_INET) skel->bss->server_port = ((struct sockaddr_in *) &test_case->addr)->sin_port; else skel->bss->server_port = ((struct sockaddr_in6 *) &test_case->addr)->sin6_port; test_case->link = bpf_program__attach_xdp(skel->progs.drop_ack, IFINDEX_LO); if (!ASSERT_OK_PTR(test_case->link, "bpf_program__attach_xdp")) return -1; return 0; } static int pass_ack(struct migrate_reuseport_test_case *test_case) { int err; err = bpf_link__destroy(test_case->link); if (!ASSERT_OK(err, "bpf_link__destroy")) return -1; test_case->link = NULL; return 0; } static int start_servers(struct migrate_reuseport_test_case *test_case, struct test_migrate_reuseport *skel) { int i, err, prog_fd, reuseport = 1, qlen = QLEN; prog_fd = bpf_program__fd(skel->progs.migrate_reuseport); make_sockaddr(test_case->family, test_case->family == AF_INET ? "127.0.0.1" : "::1", 0, &test_case->addr, &test_case->addrlen); for (i = 0; i < NR_SERVERS; i++) { test_case->servers[i] = socket(test_case->family, SOCK_STREAM, IPPROTO_TCP); if (!ASSERT_NEQ(test_case->servers[i], -1, "socket")) return -1; err = setsockopt(test_case->servers[i], SOL_SOCKET, SO_REUSEPORT, &reuseport, sizeof(reuseport)); if (!ASSERT_OK(err, "setsockopt - SO_REUSEPORT")) return -1; err = bind(test_case->servers[i], (struct sockaddr *)&test_case->addr, test_case->addrlen); if (!ASSERT_OK(err, "bind")) return -1; if (i == 0) { err = setsockopt(test_case->servers[i], SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF, &prog_fd, sizeof(prog_fd)); if (!ASSERT_OK(err, "setsockopt - SO_ATTACH_REUSEPORT_EBPF")) return -1; err = getsockname(test_case->servers[i], (struct sockaddr *)&test_case->addr, &test_case->addrlen); if (!ASSERT_OK(err, "getsockname")) return -1; } if (test_case->fastopen) { err = setsockopt(test_case->servers[i], SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen)); if (!ASSERT_OK(err, "setsockopt - TCP_FASTOPEN")) return -1; } /* All requests will be tied to the first four listeners */ if (i != MIGRATED_TO) { err = listen(test_case->servers[i], qlen); if (!ASSERT_OK(err, "listen")) return -1; } } return 0; } static int start_clients(struct migrate_reuseport_test_case *test_case) { char buf[MSGLEN] = MSG; int i, err; for (i = 0; i < NR_CLIENTS; i++) { test_case->clients[i] = socket(test_case->family, SOCK_STREAM, IPPROTO_TCP); if (!ASSERT_NEQ(test_case->clients[i], -1, "socket")) return -1; /* The attached XDP program drops only the final ACK, so * clients will transition to TCP_ESTABLISHED immediately. */ err = settimeo(test_case->clients[i], 100); if (!ASSERT_OK(err, "settimeo")) return -1; if (test_case->fastopen) { int fastopen = 1; err = setsockopt(test_case->clients[i], IPPROTO_TCP, TCP_FASTOPEN_CONNECT, &fastopen, sizeof(fastopen)); if (!ASSERT_OK(err, "setsockopt - TCP_FASTOPEN_CONNECT")) return -1; } err = connect(test_case->clients[i], (struct sockaddr *)&test_case->addr, test_case->addrlen); if (!ASSERT_OK(err, "connect")) return -1; err = write(test_case->clients[i], buf, MSGLEN); if (!ASSERT_EQ(err, MSGLEN, "write")) return -1; } return 0; } static int update_maps(struct migrate_reuseport_test_case *test_case, struct test_migrate_reuseport *skel) { int i, err, migrated_to = MIGRATED_TO; int reuseport_map_fd, migrate_map_fd; __u64 value; reuseport_map_fd = bpf_map__fd(skel->maps.reuseport_map); migrate_map_fd = bpf_map__fd(skel->maps.migrate_map); for (i = 0; i < NR_SERVERS; i++) { value = (__u64)test_case->servers[i]; err = bpf_map_update_elem(reuseport_map_fd, &i, &value, BPF_NOEXIST); if (!ASSERT_OK(err, "bpf_map_update_elem - reuseport_map")) return -1; err = bpf_map_lookup_elem(reuseport_map_fd, &i, &value); if (!ASSERT_OK(err, "bpf_map_lookup_elem - reuseport_map")) return -1; err = bpf_map_update_elem(migrate_map_fd, &value, &migrated_to, BPF_NOEXIST); if (!ASSERT_OK(err, "bpf_map_update_elem - migrate_map")) return -1; } return 0; } static int migrate_dance(struct migrate_reuseport_test_case *test_case) { int i, err; /* Migrate TCP_ESTABLISHED and TCP_SYN_RECV requests * to the last listener based on eBPF. */ for (i = 0; i < MIGRATED_TO; i++) { err = shutdown(test_case->servers[i], SHUT_RDWR); if (!ASSERT_OK(err, "shutdown")) return -1; } /* No dance for TCP_NEW_SYN_RECV to migrate based on eBPF */ if (test_case->state == BPF_TCP_NEW_SYN_RECV) return 0; /* Note that we use the second listener instead of the * first one here. * * The fist listener is bind()ed with port 0 and, * SOCK_BINDPORT_LOCK is not set to sk_userlocks, so * calling listen() again will bind() the first listener * on a new ephemeral port and detach it from the existing * reuseport group. (See: __inet_bind(), tcp_set_state()) * * OTOH, the second one is bind()ed with a specific port, * and SOCK_BINDPORT_LOCK is set. Thus, re-listen() will * resurrect the listener on the existing reuseport group. */ err = listen(test_case->servers[1], QLEN); if (!ASSERT_OK(err, "listen")) return -1; /* Migrate from the last listener to the second one. * * All listeners were detached out of the reuseport_map, * so migration will be done by kernel random pick from here. */ err = shutdown(test_case->servers[MIGRATED_TO], SHUT_RDWR); if (!ASSERT_OK(err, "shutdown")) return -1; /* Back to the existing reuseport group */ err = listen(test_case->servers[MIGRATED_TO], QLEN); if (!ASSERT_OK(err, "listen")) return -1; /* Migrate back to the last one from the second one */ err = shutdown(test_case->servers[1], SHUT_RDWR); if (!ASSERT_OK(err, "shutdown")) return -1; return 0; } static void count_requests(struct migrate_reuseport_test_case *test_case, struct test_migrate_reuseport *skel) { struct sockaddr_storage addr; socklen_t len = sizeof(addr); int err, cnt = 0, client; char buf[MSGLEN]; err = settimeo(test_case->servers[MIGRATED_TO], 4000); if (!ASSERT_OK(err, "settimeo")) goto out; for (; cnt < NR_CLIENTS; cnt++) { client = accept(test_case->servers[MIGRATED_TO], (struct sockaddr *)&addr, &len); if (!ASSERT_NEQ(client, -1, "accept")) goto out; memset(buf, 0, MSGLEN); read(client, &buf, MSGLEN); close(client); if (!ASSERT_STREQ(buf, MSG, "read")) goto out; } out: ASSERT_EQ(cnt, NR_CLIENTS, "count in userspace"); switch (test_case->state) { case BPF_TCP_ESTABLISHED: cnt = skel->bss->migrated_at_close; break; case BPF_TCP_SYN_RECV: cnt = skel->bss->migrated_at_close_fastopen; break; case BPF_TCP_NEW_SYN_RECV: if (test_case->expire_synack_timer) cnt = skel->bss->migrated_at_send_synack; else cnt = skel->bss->migrated_at_recv_ack; break; default: cnt = 0; } ASSERT_EQ(cnt, NR_CLIENTS, "count in BPF prog"); } static void run_test(struct migrate_reuseport_test_case *test_case, struct test_migrate_reuseport *skel) { int err, saved_len; char buf[16]; skel->bss->migrated_at_close = 0; skel->bss->migrated_at_close_fastopen = 0; skel->bss->migrated_at_send_synack = 0; skel->bss->migrated_at_recv_ack = 0; init_fds(test_case->servers, NR_SERVERS); init_fds(test_case->clients, NR_CLIENTS); if (test_case->fastopen) { memset(buf, 0, sizeof(buf)); err = setup_fastopen(buf, sizeof(buf), &saved_len, false); if (!ASSERT_OK(err, "setup_fastopen - setup")) return; } err = start_servers(test_case, skel); if (!ASSERT_OK(err, "start_servers")) goto close_servers; if (test_case->drop_ack) { /* Drop the final ACK of the 3-way handshake and stick the * in-flight requests on TCP_SYN_RECV or TCP_NEW_SYN_RECV. */ err = drop_ack(test_case, skel); if (!ASSERT_OK(err, "drop_ack")) goto close_servers; } /* Tie requests to the first four listeners */ err = start_clients(test_case); if (!ASSERT_OK(err, "start_clients")) goto close_clients; err = listen(test_case->servers[MIGRATED_TO], QLEN); if (!ASSERT_OK(err, "listen")) goto close_clients; err = update_maps(test_case, skel); if (!ASSERT_OK(err, "fill_maps")) goto close_clients; /* Migrate the requests in the accept queue only. * TCP_NEW_SYN_RECV requests are not migrated at this point. */ err = migrate_dance(test_case); if (!ASSERT_OK(err, "migrate_dance")) goto close_clients; if (test_case->expire_synack_timer) { /* Wait for SYN+ACK timers to expire so that * reqsk_timer_handler() migrates TCP_NEW_SYN_RECV requests. */ sleep(1); } if (test_case->link) { /* Resume 3WHS and migrate TCP_NEW_SYN_RECV requests */ err = pass_ack(test_case); if (!ASSERT_OK(err, "pass_ack")) goto close_clients; } count_requests(test_case, skel); close_clients: close_fds(test_case->clients, NR_CLIENTS); if (test_case->link) { err = pass_ack(test_case); ASSERT_OK(err, "pass_ack - clean up"); } close_servers: close_fds(test_case->servers, NR_SERVERS); if (test_case->fastopen) { err = setup_fastopen(buf, sizeof(buf), &saved_len, true); ASSERT_OK(err, "setup_fastopen - restore"); } } void serial_test_migrate_reuseport(void) { struct test_migrate_reuseport *skel; int i; skel = test_migrate_reuseport__open_and_load(); if (!ASSERT_OK_PTR(skel, "open_and_load")) return; for (i = 0; i < ARRAY_SIZE(test_cases); i++) { test__start_subtest(test_cases[i].name); run_test(&test_cases[i], skel); } test_migrate_reuseport__destroy(skel); }