| // SPDX-License-Identifier: GPL-2.0 |
| // Copyright (c) 2018 Facebook |
| // Copyright (c) 2019 Cloudflare |
| // Copyright (c) 2020 Isovalent, Inc. |
| /* |
| * Test that the socket assign program is able to redirect traffic towards a |
| * socket, regardless of whether the port or address destination of the traffic |
| * matches the port. |
| */ |
| |
| #define _GNU_SOURCE |
| #include <fcntl.h> |
| #include <signal.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| |
| #include "test_progs.h" |
| |
| #define BIND_PORT 1234 |
| #define CONNECT_PORT 4321 |
| #define TEST_DADDR (0xC0A80203) |
| #define NS_SELF "/proc/self/ns/net" |
| #define SERVER_MAP_PATH "/sys/fs/bpf/tc/globals/server_map" |
| |
| static const struct timeval timeo_sec = { .tv_sec = 3 }; |
| static const size_t timeo_optlen = sizeof(timeo_sec); |
| static int stop, duration; |
| |
| static bool |
| configure_stack(void) |
| { |
| char tc_version[128]; |
| char tc_cmd[BUFSIZ]; |
| char *prog; |
| FILE *tc; |
| |
| /* Check whether tc is built with libbpf. */ |
| tc = popen("tc -V", "r"); |
| if (CHECK_FAIL(!tc)) |
| return false; |
| if (CHECK_FAIL(!fgets(tc_version, sizeof(tc_version), tc))) |
| return false; |
| if (strstr(tc_version, ", libbpf ")) |
| prog = "test_sk_assign_libbpf.bpf.o"; |
| else |
| prog = "test_sk_assign.bpf.o"; |
| if (CHECK_FAIL(pclose(tc))) |
| return false; |
| |
| /* Move to a new networking namespace */ |
| if (CHECK_FAIL(unshare(CLONE_NEWNET))) |
| return false; |
| |
| /* Configure necessary links, routes */ |
| if (CHECK_FAIL(system("ip link set dev lo up"))) |
| return false; |
| if (CHECK_FAIL(system("ip route add local default dev lo"))) |
| return false; |
| if (CHECK_FAIL(system("ip -6 route add local default dev lo"))) |
| return false; |
| |
| /* Load qdisc, BPF program */ |
| if (CHECK_FAIL(system("tc qdisc add dev lo clsact"))) |
| return false; |
| sprintf(tc_cmd, "%s %s %s %s %s", "tc filter add dev lo ingress bpf", |
| "direct-action object-file", prog, |
| "section tc", |
| (env.verbosity < VERBOSE_VERY) ? " 2>/dev/null" : "verbose"); |
| if (CHECK(system(tc_cmd), "BPF load failed;", |
| "run with -vv for more info\n")) |
| return false; |
| |
| return true; |
| } |
| |
| static int |
| start_server(const struct sockaddr *addr, socklen_t len, int type) |
| { |
| int fd; |
| |
| fd = socket(addr->sa_family, type, 0); |
| if (CHECK_FAIL(fd == -1)) |
| goto out; |
| if (CHECK_FAIL(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeo_sec, |
| timeo_optlen))) |
| goto close_out; |
| if (CHECK_FAIL(bind(fd, addr, len) == -1)) |
| goto close_out; |
| if (type == SOCK_STREAM && CHECK_FAIL(listen(fd, 128) == -1)) |
| goto close_out; |
| |
| goto out; |
| close_out: |
| close(fd); |
| fd = -1; |
| out: |
| return fd; |
| } |
| |
| static int |
| connect_to_server(const struct sockaddr *addr, socklen_t len, int type) |
| { |
| int fd = -1; |
| |
| fd = socket(addr->sa_family, type, 0); |
| if (CHECK_FAIL(fd == -1)) |
| goto out; |
| if (CHECK_FAIL(setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeo_sec, |
| timeo_optlen))) |
| goto close_out; |
| if (CHECK_FAIL(connect(fd, addr, len))) |
| goto close_out; |
| |
| goto out; |
| close_out: |
| close(fd); |
| fd = -1; |
| out: |
| return fd; |
| } |
| |
| static in_port_t |
| get_port(int fd) |
| { |
| struct sockaddr_storage ss; |
| socklen_t slen = sizeof(ss); |
| in_port_t port = 0; |
| |
| if (CHECK_FAIL(getsockname(fd, (struct sockaddr *)&ss, &slen))) |
| return port; |
| |
| switch (ss.ss_family) { |
| case AF_INET: |
| port = ((struct sockaddr_in *)&ss)->sin_port; |
| break; |
| case AF_INET6: |
| port = ((struct sockaddr_in6 *)&ss)->sin6_port; |
| break; |
| default: |
| CHECK(1, "Invalid address family", "%d\n", ss.ss_family); |
| } |
| return port; |
| } |
| |
| static ssize_t |
| rcv_msg(int srv_client, int type) |
| { |
| char buf[BUFSIZ]; |
| |
| if (type == SOCK_STREAM) |
| return read(srv_client, &buf, sizeof(buf)); |
| else |
| return recvfrom(srv_client, &buf, sizeof(buf), 0, NULL, NULL); |
| } |
| |
| static int |
| run_test(int server_fd, const struct sockaddr *addr, socklen_t len, int type) |
| { |
| int client = -1, srv_client = -1; |
| char buf[] = "testing"; |
| in_port_t port; |
| int ret = 1; |
| |
| client = connect_to_server(addr, len, type); |
| if (client == -1) { |
| perror("Cannot connect to server"); |
| goto out; |
| } |
| |
| if (type == SOCK_STREAM) { |
| srv_client = accept(server_fd, NULL, NULL); |
| if (CHECK_FAIL(srv_client == -1)) { |
| perror("Can't accept connection"); |
| goto out; |
| } |
| } else { |
| srv_client = server_fd; |
| } |
| if (CHECK_FAIL(write(client, buf, sizeof(buf)) != sizeof(buf))) { |
| perror("Can't write on client"); |
| goto out; |
| } |
| if (CHECK_FAIL(rcv_msg(srv_client, type) != sizeof(buf))) { |
| perror("Can't read on server"); |
| goto out; |
| } |
| |
| port = get_port(srv_client); |
| if (CHECK_FAIL(!port)) |
| goto out; |
| /* SOCK_STREAM is connected via accept(), so the server's local address |
| * will be the CONNECT_PORT rather than the BIND port that corresponds |
| * to the listen socket. SOCK_DGRAM on the other hand is connectionless |
| * so we can't really do the same check there; the server doesn't ever |
| * create a socket with CONNECT_PORT. |
| */ |
| if (type == SOCK_STREAM && |
| CHECK(port != htons(CONNECT_PORT), "Expected", "port %u but got %u", |
| CONNECT_PORT, ntohs(port))) |
| goto out; |
| else if (type == SOCK_DGRAM && |
| CHECK(port != htons(BIND_PORT), "Expected", |
| "port %u but got %u", BIND_PORT, ntohs(port))) |
| goto out; |
| |
| ret = 0; |
| out: |
| close(client); |
| if (srv_client != server_fd) |
| close(srv_client); |
| if (ret) |
| WRITE_ONCE(stop, 1); |
| return ret; |
| } |
| |
| static void |
| prepare_addr(struct sockaddr *addr, int family, __u16 port, bool rewrite_addr) |
| { |
| struct sockaddr_in *addr4; |
| struct sockaddr_in6 *addr6; |
| |
| switch (family) { |
| case AF_INET: |
| addr4 = (struct sockaddr_in *)addr; |
| memset(addr4, 0, sizeof(*addr4)); |
| addr4->sin_family = family; |
| addr4->sin_port = htons(port); |
| if (rewrite_addr) |
| addr4->sin_addr.s_addr = htonl(TEST_DADDR); |
| else |
| addr4->sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| break; |
| case AF_INET6: |
| addr6 = (struct sockaddr_in6 *)addr; |
| memset(addr6, 0, sizeof(*addr6)); |
| addr6->sin6_family = family; |
| addr6->sin6_port = htons(port); |
| addr6->sin6_addr = in6addr_loopback; |
| if (rewrite_addr) |
| addr6->sin6_addr.s6_addr32[3] = htonl(TEST_DADDR); |
| break; |
| default: |
| fprintf(stderr, "Invalid family %d", family); |
| } |
| } |
| |
| struct test_sk_cfg { |
| const char *name; |
| int family; |
| struct sockaddr *addr; |
| socklen_t len; |
| int type; |
| bool rewrite_addr; |
| }; |
| |
| #define TEST(NAME, FAMILY, TYPE, REWRITE) \ |
| { \ |
| .name = NAME, \ |
| .family = FAMILY, \ |
| .addr = (FAMILY == AF_INET) ? (struct sockaddr *)&addr4 \ |
| : (struct sockaddr *)&addr6, \ |
| .len = (FAMILY == AF_INET) ? sizeof(addr4) : sizeof(addr6), \ |
| .type = TYPE, \ |
| .rewrite_addr = REWRITE, \ |
| } |
| |
| void test_sk_assign(void) |
| { |
| struct sockaddr_in addr4; |
| struct sockaddr_in6 addr6; |
| struct test_sk_cfg tests[] = { |
| TEST("ipv4 tcp port redir", AF_INET, SOCK_STREAM, false), |
| TEST("ipv4 tcp addr redir", AF_INET, SOCK_STREAM, true), |
| TEST("ipv6 tcp port redir", AF_INET6, SOCK_STREAM, false), |
| TEST("ipv6 tcp addr redir", AF_INET6, SOCK_STREAM, true), |
| TEST("ipv4 udp port redir", AF_INET, SOCK_DGRAM, false), |
| TEST("ipv4 udp addr redir", AF_INET, SOCK_DGRAM, true), |
| TEST("ipv6 udp port redir", AF_INET6, SOCK_DGRAM, false), |
| TEST("ipv6 udp addr redir", AF_INET6, SOCK_DGRAM, true), |
| }; |
| __s64 server = -1; |
| int server_map; |
| int self_net; |
| int i; |
| |
| self_net = open(NS_SELF, O_RDONLY); |
| if (CHECK_FAIL(self_net < 0)) { |
| perror("Unable to open "NS_SELF); |
| return; |
| } |
| |
| if (!configure_stack()) { |
| perror("configure_stack"); |
| goto cleanup; |
| } |
| |
| server_map = bpf_obj_get(SERVER_MAP_PATH); |
| if (CHECK_FAIL(server_map < 0)) { |
| perror("Unable to open " SERVER_MAP_PATH); |
| goto cleanup; |
| } |
| |
| for (i = 0; i < ARRAY_SIZE(tests) && !READ_ONCE(stop); i++) { |
| struct test_sk_cfg *test = &tests[i]; |
| const struct sockaddr *addr; |
| const int zero = 0; |
| int err; |
| |
| if (!test__start_subtest(test->name)) |
| continue; |
| prepare_addr(test->addr, test->family, BIND_PORT, false); |
| addr = (const struct sockaddr *)test->addr; |
| server = start_server(addr, test->len, test->type); |
| if (server == -1) |
| goto close; |
| |
| err = bpf_map_update_elem(server_map, &zero, &server, BPF_ANY); |
| if (CHECK_FAIL(err)) { |
| perror("Unable to update server_map"); |
| goto close; |
| } |
| |
| /* connect to unbound ports */ |
| prepare_addr(test->addr, test->family, CONNECT_PORT, |
| test->rewrite_addr); |
| if (run_test(server, addr, test->len, test->type)) |
| goto close; |
| |
| close(server); |
| server = -1; |
| } |
| |
| close: |
| close(server); |
| close(server_map); |
| cleanup: |
| if (CHECK_FAIL(unlink(SERVER_MAP_PATH))) |
| perror("Unable to unlink " SERVER_MAP_PATH); |
| if (CHECK_FAIL(setns(self_net, CLONE_NEWNET))) |
| perror("Failed to setns("NS_SELF")"); |
| close(self_net); |
| } |