Skip to content

Commit 78a5883

Browse files
Florian Westphalummakynes
authored andcommitted
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: Florian Westphal <[email protected]> Signed-off-by: Pablo Neira Ayuso <[email protected]>
1 parent b085905 commit 78a5883

File tree

4 files changed

+337
-0
lines changed

4 files changed

+337
-0
lines changed

tools/testing/selftests/net/netfilter/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ conntrack_dump_flush
55
conntrack_reverse_clash
66
sctp_collision
77
nf_queue
8+
udpclash

tools/testing/selftests/net/netfilter/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ TEST_PROGS += conntrack_tcp_unreplied.sh
1515
TEST_PROGS += conntrack_resize.sh
1616
TEST_PROGS += conntrack_sctp_collision.sh
1717
TEST_PROGS += conntrack_vrf.sh
18+
TEST_PROGS += conntrack_clash.sh
1819
TEST_PROGS += conntrack_reverse_clash.sh
1920
TEST_PROGS += ipvs.sh
2021
TEST_PROGS += nf_conntrack_packetdrill.sh
@@ -44,6 +45,7 @@ TEST_GEN_FILES += connect_close nf_queue
4445
TEST_GEN_FILES += conntrack_dump_flush
4546
TEST_GEN_FILES += conntrack_reverse_clash
4647
TEST_GEN_FILES += sctp_collision
48+
TEST_GEN_FILES += udpclash
4749

4850
include ../../lib.mk
4951

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

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

5659
TEST_FILES := lib.sh
5760
TEST_FILES += packetdrill
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/bin/bash
2+
# SPDX-License-Identifier: GPL-2.0
3+
4+
source lib.sh
5+
6+
clash_resolution_active=0
7+
dport=22111
8+
ret=0
9+
10+
cleanup()
11+
{
12+
# netns cleanup also zaps any remaining socat echo server.
13+
cleanup_all_ns
14+
}
15+
16+
checktool "nft --version" "run test without nft"
17+
checktool "conntrack --version" "run test without conntrack"
18+
checktool "socat -h" "run test without socat"
19+
20+
trap cleanup EXIT
21+
22+
setup_ns nsclient1 nsclient2 nsrouter
23+
24+
ip netns exec "$nsrouter" nft -f -<<EOF
25+
table ip t {
26+
chain lb {
27+
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 }
28+
}
29+
30+
chain prerouting {
31+
type nat hook prerouting priority dstnat
32+
33+
udp dport $dport counter jump lb
34+
}
35+
36+
chain output {
37+
type nat hook output priority dstnat
38+
39+
udp dport $dport counter jump lb
40+
}
41+
}
42+
EOF
43+
44+
load_simple_ruleset()
45+
{
46+
ip netns exec "$1" nft -f -<<EOF
47+
table ip t {
48+
chain forward {
49+
type filter hook forward priority 0
50+
51+
ct state new counter
52+
}
53+
}
54+
EOF
55+
}
56+
57+
spawn_servers()
58+
{
59+
local ns="$1"
60+
local ports="9000 9001 9002"
61+
62+
for port in $ports; do
63+
ip netns exec "$ns" socat UDP-RECVFROM:$port,fork PIPE 2>/dev/null &
64+
done
65+
66+
for port in $ports; do
67+
wait_local_port_listen "$ns" $port udp
68+
done
69+
}
70+
71+
add_addr()
72+
{
73+
local ns="$1"
74+
local dev="$2"
75+
local i="$3"
76+
local j="$4"
77+
78+
ip -net "$ns" link set "$dev" up
79+
ip -net "$ns" addr add "10.0.$i.$j/24" dev "$dev"
80+
}
81+
82+
ping_test()
83+
{
84+
local ns="$1"
85+
local daddr="$2"
86+
87+
if ! ip netns exec "$ns" ping -q -c 1 $daddr > /dev/null;then
88+
echo "FAIL: ping from $ns to $daddr"
89+
exit 1
90+
fi
91+
}
92+
93+
run_one_clash_test()
94+
{
95+
local ns="$1"
96+
local daddr="$2"
97+
local dport="$3"
98+
local entries
99+
local cre
100+
101+
if ! ip netns exec "$ns" ./udpclash $daddr $dport;then
102+
echo "FAIL: did not receive expected number of replies for $daddr:$dport"
103+
ret=1
104+
return 1
105+
fi
106+
107+
entries=$(conntrack -S | wc -l)
108+
cre=$(conntrack -S | grep -v "clash_resolve=0" | wc -l)
109+
110+
if [ "$cre" -ne "$entries" ] ;then
111+
clash_resolution_active=1
112+
return 0
113+
fi
114+
115+
# 1 cpu -> parallel insertion impossible
116+
if [ "$entries" -eq 1 ]; then
117+
return 0
118+
fi
119+
120+
# not a failure: clash resolution logic did not trigger, but all replies
121+
# were received. With right timing, xmit completed sequentially and
122+
# no parallel insertion occurs.
123+
return $ksft_skip
124+
}
125+
126+
run_clash_test()
127+
{
128+
local ns="$1"
129+
local daddr="$2"
130+
local dport="$3"
131+
132+
for i in $(seq 1 10);do
133+
run_one_clash_test "$ns" "$daddr" "$dport"
134+
local rv=$?
135+
if [ $rv -eq 0 ];then
136+
echo "PASS: clash resolution test for $daddr:$dport on attempt $i"
137+
return 0
138+
elif [ $rv -eq 1 ];then
139+
echo "FAIL: clash resolution test for $daddr:$dport on attempt $i"
140+
return 1
141+
fi
142+
done
143+
}
144+
145+
ip link add veth0 netns "$nsclient1" type veth peer name veth0 netns "$nsrouter"
146+
ip link add veth0 netns "$nsclient2" type veth peer name veth1 netns "$nsrouter"
147+
add_addr "$nsclient1" veth0 1 1
148+
add_addr "$nsclient2" veth0 2 1
149+
add_addr "$nsrouter" veth0 1 99
150+
add_addr "$nsrouter" veth1 2 99
151+
152+
ip -net "$nsclient1" route add default via 10.0.1.99
153+
ip -net "$nsclient2" route add default via 10.0.2.99
154+
ip netns exec "$nsrouter" sysctl -q net.ipv4.ip_forward=1
155+
156+
ping_test "$nsclient1" 10.0.1.99
157+
ping_test "$nsclient1" 10.0.2.1
158+
ping_test "$nsclient2" 10.0.1.1
159+
160+
spawn_servers "$nsclient2"
161+
162+
# exercise clash resolution with nat:
163+
# nsrouter is supposed to dnat to 10.0.2.1:900{0,1,2,3}.
164+
run_clash_test "$nsclient1" 10.0.1.99 "$dport"
165+
166+
# exercise clash resolution without nat.
167+
load_simple_ruleset "$nsclient2"
168+
run_clash_test "$nsclient2" 127.0.0.1 9001
169+
170+
if [ $clash_resolution_active -eq 0 ];then
171+
[ "$ret" -eq 0 ] && ret=$ksft_skip
172+
echo "SKIP: Clash resolution did not trigger"
173+
fi
174+
175+
exit $ret
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// SPDX-License-Identifier: GPL-2.0
2+
3+
/* Usage: ./udpclash <IP> <PORT>
4+
*
5+
* Emit THREAD_COUNT UDP packets sharing the same saddr:daddr pair.
6+
*
7+
* This mimics DNS resolver libraries that emit A and AAAA requests
8+
* in parallel.
9+
*
10+
* This exercises conntrack clash resolution logic added and later
11+
* refined in
12+
*
13+
* 71d8c47fc653 ("netfilter: conntrack: introduce clash resolution on insertion race")
14+
* ed07d9a021df ("netfilter: nf_conntrack: resolve clash for matching conntracks")
15+
* 6a757c07e51f ("netfilter: conntrack: allow insertion of clashing entries")
16+
*/
17+
#include <stdio.h>
18+
#include <string.h>
19+
#include <stdlib.h>
20+
#include <unistd.h>
21+
#include <arpa/inet.h>
22+
#include <sys/socket.h>
23+
#include <pthread.h>
24+
25+
#define THREAD_COUNT 128
26+
27+
struct thread_args {
28+
const struct sockaddr_in *si_remote;
29+
int sockfd;
30+
};
31+
32+
static int wait = 1;
33+
34+
static void *thread_main(void *varg)
35+
{
36+
const struct sockaddr_in *si_remote;
37+
const struct thread_args *args = varg;
38+
static const char msg[] = "foo";
39+
40+
si_remote = args->si_remote;
41+
42+
while (wait == 1)
43+
;
44+
45+
if (sendto(args->sockfd, msg, strlen(msg), MSG_NOSIGNAL,
46+
(struct sockaddr *)si_remote, sizeof(*si_remote)) < 0)
47+
exit(111);
48+
49+
return varg;
50+
}
51+
52+
static int run_test(int fd, const struct sockaddr_in *si_remote)
53+
{
54+
struct thread_args thread_args = {
55+
.si_remote = si_remote,
56+
.sockfd = fd,
57+
};
58+
pthread_t *tid = calloc(THREAD_COUNT, sizeof(pthread_t));
59+
unsigned int repl_count = 0, timeout = 0;
60+
int i;
61+
62+
if (!tid) {
63+
perror("calloc");
64+
return 1;
65+
}
66+
67+
for (i = 0; i < THREAD_COUNT; i++) {
68+
int err = pthread_create(&tid[i], NULL, &thread_main, &thread_args);
69+
70+
if (err != 0) {
71+
perror("pthread_create");
72+
exit(1);
73+
}
74+
}
75+
76+
wait = 0;
77+
78+
for (i = 0; i < THREAD_COUNT; i++)
79+
pthread_join(tid[i], NULL);
80+
81+
while (repl_count < THREAD_COUNT) {
82+
struct sockaddr_in si_repl;
83+
socklen_t si_repl_len = sizeof(si_repl);
84+
char repl[512];
85+
ssize_t ret;
86+
87+
ret = recvfrom(fd, repl, sizeof(repl), MSG_NOSIGNAL,
88+
(struct sockaddr *) &si_repl, &si_repl_len);
89+
if (ret < 0) {
90+
if (timeout++ > 5000) {
91+
fputs("timed out while waiting for reply from thread\n", stderr);
92+
break;
93+
}
94+
95+
/* give reply time to pass though the stack */
96+
usleep(1000);
97+
continue;
98+
}
99+
100+
if (si_repl_len != sizeof(*si_remote)) {
101+
fprintf(stderr, "warning: reply has unexpected repl_len %d vs %d\n",
102+
(int)si_repl_len, (int)sizeof(si_repl));
103+
} else if (si_remote->sin_addr.s_addr != si_repl.sin_addr.s_addr ||
104+
si_remote->sin_port != si_repl.sin_port) {
105+
char a[64], b[64];
106+
107+
inet_ntop(AF_INET, &si_remote->sin_addr, a, sizeof(a));
108+
inet_ntop(AF_INET, &si_repl.sin_addr, b, sizeof(b));
109+
110+
fprintf(stderr, "reply from wrong source: want %s:%d got %s:%d\n",
111+
a, ntohs(si_remote->sin_port), b, ntohs(si_repl.sin_port));
112+
}
113+
114+
repl_count++;
115+
}
116+
117+
printf("got %d of %d replies\n", repl_count, THREAD_COUNT);
118+
119+
free(tid);
120+
121+
return repl_count == THREAD_COUNT ? 0 : 1;
122+
}
123+
124+
int main(int argc, char *argv[])
125+
{
126+
struct sockaddr_in si_local = {
127+
.sin_family = AF_INET,
128+
};
129+
struct sockaddr_in si_remote = {
130+
.sin_family = AF_INET,
131+
};
132+
int fd, ret;
133+
134+
if (argc < 3) {
135+
fputs("Usage: send_udp <daddr> <dport>\n", stderr);
136+
return 1;
137+
}
138+
139+
si_remote.sin_port = htons(atoi(argv[2]));
140+
si_remote.sin_addr.s_addr = inet_addr(argv[1]);
141+
142+
fd = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_UDP);
143+
if (fd < 0) {
144+
perror("socket");
145+
return 1;
146+
}
147+
148+
if (bind(fd, (struct sockaddr *)&si_local, sizeof(si_local)) < 0) {
149+
perror("bind");
150+
return 1;
151+
}
152+
153+
ret = run_test(fd, &si_remote);
154+
155+
close(fd);
156+
157+
return ret;
158+
}

0 commit comments

Comments
 (0)