| // 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 <kuniyu@amazon.co.jp> |
| */ |
| |
| #include <bpf/bpf.h> |
| #include <bpf/libbpf.h> |
| |
| #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); |
| } |