343 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			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"
 | 
						|
 | 
						|
#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);
 | 
						|
}
 |