Commit 78a58836 authored by Florian Westphal's avatar Florian Westphal Committed by Pablo Neira Ayuso
Browse files

selftests: netfilter: add conntrack clash resolution test case



Add a dedicated test to exercise conntrack clash resolution path.
Test program emits 128 identical udp packets in parallel, then reads
back replies from socat echo server.

Also check (via conntrack -S) that the clash path was hit at least once.
Due to the racy nature of the test its possible that despite the
threaded program all packets were processed in-order or on same cpu,
emit a SKIP warning in this case.

Two tests are added:
 - one to test the simpler, non-nat case
 - one to exercise clash resolution where packets
   might have different nat transformations attached to them.

Signed-off-by: default avatarFlorian Westphal <fw@strlen.de>
Signed-off-by: default avatarPablo Neira Ayuso <pablo@netfilter.org>
parent b0859055
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -5,3 +5,4 @@ conntrack_dump_flush
conntrack_reverse_clash
sctp_collision
nf_queue
udpclash
+3 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ TEST_PROGS += conntrack_tcp_unreplied.sh
TEST_PROGS += conntrack_resize.sh
TEST_PROGS += conntrack_sctp_collision.sh
TEST_PROGS += conntrack_vrf.sh
TEST_PROGS += conntrack_clash.sh
TEST_PROGS += conntrack_reverse_clash.sh
TEST_PROGS += ipvs.sh
TEST_PROGS += nf_conntrack_packetdrill.sh
@@ -44,6 +45,7 @@ TEST_GEN_FILES += connect_close nf_queue
TEST_GEN_FILES += conntrack_dump_flush
TEST_GEN_FILES += conntrack_reverse_clash
TEST_GEN_FILES += sctp_collision
TEST_GEN_FILES += udpclash

include ../../lib.mk

@@ -52,6 +54,7 @@ $(OUTPUT)/nf_queue: LDLIBS += $(MNL_LDLIBS)

$(OUTPUT)/conntrack_dump_flush: CFLAGS += $(MNL_CFLAGS)
$(OUTPUT)/conntrack_dump_flush: LDLIBS += $(MNL_LDLIBS)
$(OUTPUT)/udpclash: LDLIBS += -lpthread

TEST_FILES := lib.sh
TEST_FILES += packetdrill
+175 −0
Original line number Diff line number Diff line
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0

source lib.sh

clash_resolution_active=0
dport=22111
ret=0

cleanup()
{
	# netns cleanup also zaps any remaining socat echo server.
	cleanup_all_ns
}

checktool "nft --version" "run test without nft"
checktool "conntrack --version" "run test without conntrack"
checktool "socat -h" "run test without socat"

trap cleanup EXIT

setup_ns nsclient1 nsclient2 nsrouter

ip netns exec "$nsrouter" nft -f -<<EOF
table ip t {
	chain lb {
		meta l4proto udp dnat to numgen random mod 3 map { 0 : 10.0.2.1 . 9000, 1 : 10.0.2.1 . 9001, 2 : 10.0.2.1 . 9002 }
	}

	chain prerouting {
		type nat hook prerouting priority dstnat

		udp dport $dport counter jump lb
	}

	chain output {
		type nat hook output priority dstnat

		udp dport $dport counter jump lb
	}
}
EOF

load_simple_ruleset()
{
ip netns exec "$1" nft -f -<<EOF
table ip t {
	chain forward {
		type filter hook forward priority 0

		ct state new counter
	}
}
EOF
}

spawn_servers()
{
	local ns="$1"
	local ports="9000 9001 9002"

	for port in $ports; do
		ip netns exec "$ns" socat UDP-RECVFROM:$port,fork PIPE 2>/dev/null &
	done

	for port in $ports; do
		wait_local_port_listen "$ns" $port udp
	done
}

add_addr()
{
	local ns="$1"
	local dev="$2"
	local i="$3"
	local j="$4"

	ip -net "$ns" link set "$dev" up
	ip -net "$ns" addr add "10.0.$i.$j/24" dev "$dev"
}

ping_test()
{
	local ns="$1"
	local daddr="$2"

	if ! ip netns exec "$ns" ping -q -c 1 $daddr > /dev/null;then
		echo "FAIL: ping from $ns to $daddr"
		exit 1
	fi
}

run_one_clash_test()
{
	local ns="$1"
	local daddr="$2"
	local dport="$3"
	local entries
	local cre

	if ! ip netns exec "$ns" ./udpclash $daddr $dport;then
		echo "FAIL: did not receive expected number of replies for $daddr:$dport"
		ret=1
		return 1
	fi

	entries=$(conntrack -S | wc -l)
	cre=$(conntrack -S | grep -v "clash_resolve=0" | wc -l)

	if [ "$cre" -ne "$entries" ] ;then
		clash_resolution_active=1
		return 0
	fi

	# 1 cpu -> parallel insertion impossible
	if [ "$entries" -eq 1 ]; then
		return 0
	fi

	# not a failure: clash resolution logic did not trigger, but all replies
	# were received.  With right timing, xmit completed sequentially and
	# no parallel insertion occurs.
	return $ksft_skip
}

run_clash_test()
{
	local ns="$1"
	local daddr="$2"
	local dport="$3"

	for i in $(seq 1 10);do
		run_one_clash_test "$ns" "$daddr" "$dport"
		local rv=$?
		if [ $rv -eq 0 ];then
			echo "PASS: clash resolution test for $daddr:$dport on attempt $i"
			return 0
		elif [ $rv -eq 1 ];then
			echo "FAIL: clash resolution test for $daddr:$dport on attempt $i"
			return 1
		fi
	done
}

ip link add veth0 netns "$nsclient1" type veth peer name veth0 netns "$nsrouter"
ip link add veth0 netns "$nsclient2" type veth peer name veth1 netns "$nsrouter"
add_addr "$nsclient1" veth0 1 1
add_addr "$nsclient2" veth0 2 1
add_addr "$nsrouter" veth0 1 99
add_addr "$nsrouter" veth1 2 99

ip -net "$nsclient1" route add default via 10.0.1.99
ip -net "$nsclient2" route add default via 10.0.2.99
ip netns exec "$nsrouter" sysctl -q net.ipv4.ip_forward=1

ping_test "$nsclient1" 10.0.1.99
ping_test "$nsclient1" 10.0.2.1
ping_test "$nsclient2" 10.0.1.1

spawn_servers "$nsclient2"

# exercise clash resolution with nat:
# nsrouter is supposed to dnat to 10.0.2.1:900{0,1,2,3}.
run_clash_test "$nsclient1" 10.0.1.99 "$dport"

# exercise clash resolution without nat.
load_simple_ruleset "$nsclient2"
run_clash_test "$nsclient2" 127.0.0.1 9001

if [ $clash_resolution_active -eq 0 ];then
	[ "$ret" -eq 0 ] && ret=$ksft_skip
	echo "SKIP: Clash resolution did not trigger"
fi

exit $ret
+158 −0
Original line number Diff line number Diff line
// SPDX-License-Identifier: GPL-2.0

/* Usage: ./udpclash <IP> <PORT>
 *
 * Emit THREAD_COUNT UDP packets sharing the same saddr:daddr pair.
 *
 * This mimics DNS resolver libraries that emit A and AAAA requests
 * in parallel.
 *
 * This exercises conntrack clash resolution logic added and later
 * refined in
 *
 *  71d8c47fc653 ("netfilter: conntrack: introduce clash resolution on insertion race")
 *  ed07d9a021df ("netfilter: nf_conntrack: resolve clash for matching conntracks")
 *  6a757c07e51f ("netfilter: conntrack: allow insertion of clashing entries")
 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define THREAD_COUNT 128

struct thread_args {
	const struct sockaddr_in *si_remote;
	int sockfd;
};

static int wait = 1;

static void *thread_main(void *varg)
{
	const struct sockaddr_in *si_remote;
	const struct thread_args *args = varg;
	static const char msg[] = "foo";

	si_remote = args->si_remote;

	while (wait == 1)
		;

	if (sendto(args->sockfd, msg, strlen(msg), MSG_NOSIGNAL,
		   (struct sockaddr *)si_remote, sizeof(*si_remote)) < 0)
		exit(111);

	return varg;
}

static int run_test(int fd, const struct sockaddr_in *si_remote)
{
	struct thread_args thread_args = {
		.si_remote = si_remote,
		.sockfd = fd,
	};
	pthread_t *tid = calloc(THREAD_COUNT, sizeof(pthread_t));
	unsigned int repl_count = 0, timeout = 0;
	int i;

	if (!tid) {
		perror("calloc");
		return 1;
	}

	for (i = 0; i < THREAD_COUNT; i++) {
		int err = pthread_create(&tid[i], NULL, &thread_main, &thread_args);

		if (err != 0) {
			perror("pthread_create");
			exit(1);
		}
	}

	wait = 0;

	for (i = 0; i < THREAD_COUNT; i++)
		pthread_join(tid[i], NULL);

	while (repl_count < THREAD_COUNT) {
		struct sockaddr_in si_repl;
		socklen_t si_repl_len = sizeof(si_repl);
		char repl[512];
		ssize_t ret;

		ret = recvfrom(fd, repl, sizeof(repl), MSG_NOSIGNAL,
			       (struct sockaddr *) &si_repl, &si_repl_len);
		if (ret < 0) {
			if (timeout++ > 5000) {
				fputs("timed out while waiting for reply from thread\n", stderr);
				break;
			}

			/* give reply time to pass though the stack */
			usleep(1000);
			continue;
		}

		if (si_repl_len != sizeof(*si_remote)) {
			fprintf(stderr, "warning: reply has unexpected repl_len %d vs %d\n",
				(int)si_repl_len, (int)sizeof(si_repl));
		} else if (si_remote->sin_addr.s_addr != si_repl.sin_addr.s_addr ||
			si_remote->sin_port != si_repl.sin_port) {
			char a[64], b[64];

			inet_ntop(AF_INET, &si_remote->sin_addr, a, sizeof(a));
			inet_ntop(AF_INET, &si_repl.sin_addr, b, sizeof(b));

			fprintf(stderr, "reply from wrong source: want %s:%d got %s:%d\n",
				a, ntohs(si_remote->sin_port), b, ntohs(si_repl.sin_port));
		}

		repl_count++;
	}

	printf("got %d of %d replies\n", repl_count, THREAD_COUNT);

	free(tid);

	return repl_count == THREAD_COUNT ? 0 : 1;
}

int main(int argc, char *argv[])
{
	struct sockaddr_in si_local = {
		.sin_family = AF_INET,
	};
	struct sockaddr_in si_remote = {
		.sin_family = AF_INET,
	};
	int fd, ret;

	if (argc < 3) {
		fputs("Usage: send_udp <daddr> <dport>\n", stderr);
		return 1;
	}

	si_remote.sin_port = htons(atoi(argv[2]));
	si_remote.sin_addr.s_addr = inet_addr(argv[1]);

	fd = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_UDP);
	if (fd < 0) {
		perror("socket");
		return 1;
	}

	if (bind(fd, (struct sockaddr *)&si_local, sizeof(si_local)) < 0) {
		perror("bind");
		return 1;
	}

	ret = run_test(fd, &si_remote);

	close(fd);

	return ret;
}