root/tools/testing/selftests/bpf/prog_tests/sk_assign.c
// 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"
#include "network_helpers.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 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))) {
                pclose(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 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_addr(type, (struct sockaddr_storage *)addr, len, NULL);
        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->type,
                                           (const struct sockaddr_storage *)addr,
                                           test->len, NULL);
                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);
}