1#!/bin/bash
2#
3# This tests tproxy on the following scenario:
4#
5#                         +------------+
6# +-------+               |  nsrouter  |                  +-------+
7# |ns1    |.99          .1|            |.1             .99|    ns2|
8# |   eth0|---------------|veth0  veth1|------------------|eth0   |
9# |       |  10.0.1.0/24  |            |   10.0.2.0/24    |       |
10# +-------+  dead:1::/64  |    veth2   |   dead:2::/64    +-------+
11#                         +------------+
12#                                |.1
13#                                |
14#                                |
15#                                |                        +-------+
16#                                |                     .99|    ns3|
17#                                +------------------------|eth0   |
18#                                       10.0.3.0/24       |       |
19#                                       dead:3::/64       +-------+
20#
21# The tproxy implementation acts as an echo server so the client
22# must receive the same message it sent if it has been proxied.
23# If is not proxied the servers return PONG_NS# with the number
24# of the namespace the server is running.
25#
26# shellcheck disable=SC2162,SC2317
27
28source lib.sh
29ret=0
30timeout=5
31
32cleanup()
33{
34	ip netns pids "$ns1" | xargs kill 2>/dev/null
35	ip netns pids "$ns2" | xargs kill 2>/dev/null
36	ip netns pids "$ns3" | xargs kill 2>/dev/null
37	ip netns pids "$nsrouter" | xargs kill 2>/dev/null
38
39	cleanup_all_ns
40}
41
42checktool "nft --version" "test without nft tool"
43checktool "socat -h" "run test without socat"
44
45trap cleanup EXIT
46setup_ns ns1 ns2 ns3 nsrouter
47
48if ! ip link add veth0 netns "$nsrouter" type veth peer name eth0 netns "$ns1" > /dev/null 2>&1; then
49    echo "SKIP: No virtual ethernet pair device support in kernel"
50    exit $ksft_skip
51fi
52ip link add veth1 netns "$nsrouter" type veth peer name eth0 netns "$ns2"
53ip link add veth2 netns "$nsrouter" type veth peer name eth0 netns "$ns3"
54
55ip -net "$nsrouter" link set veth0 up
56ip -net "$nsrouter" addr add 10.0.1.1/24 dev veth0
57ip -net "$nsrouter" addr add dead:1::1/64 dev veth0 nodad
58
59ip -net "$nsrouter" link set veth1 up
60ip -net "$nsrouter" addr add 10.0.2.1/24 dev veth1
61ip -net "$nsrouter" addr add dead:2::1/64 dev veth1 nodad
62
63ip -net "$nsrouter" link set veth2 up
64ip -net "$nsrouter" addr add 10.0.3.1/24 dev veth2
65ip -net "$nsrouter" addr add dead:3::1/64 dev veth2 nodad
66
67ip -net "$ns1" link set eth0 up
68ip -net "$ns2" link set eth0 up
69ip -net "$ns3" link set eth0 up
70
71ip -net "$ns1" addr add 10.0.1.99/24 dev eth0
72ip -net "$ns1" addr add dead:1::99/64 dev eth0 nodad
73ip -net "$ns1" route add default via 10.0.1.1
74ip -net "$ns1" route add default via dead:1::1
75
76ip -net "$ns2" addr add 10.0.2.99/24 dev eth0
77ip -net "$ns2" addr add dead:2::99/64 dev eth0 nodad
78ip -net "$ns2" route add default via 10.0.2.1
79ip -net "$ns2" route add default via dead:2::1
80
81ip -net "$ns3" addr add 10.0.3.99/24 dev eth0
82ip -net "$ns3" addr add dead:3::99/64 dev eth0 nodad
83ip -net "$ns3" route add default via 10.0.3.1
84ip -net "$ns3" route add default via dead:3::1
85
86ip netns exec "$nsrouter" sysctl net.ipv6.conf.all.forwarding=1 > /dev/null
87ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth0.forwarding=1 > /dev/null
88ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth1.forwarding=1 > /dev/null
89ip netns exec "$nsrouter" sysctl net.ipv4.conf.veth2.forwarding=1 > /dev/null
90
91test_ping() {
92  if ! ip netns exec "$ns1" ping -c 1 -q 10.0.2.99 > /dev/null; then
93	return 1
94  fi
95
96  if ! ip netns exec "$ns1" ping -c 1 -q dead:2::99 > /dev/null; then
97	return 2
98  fi
99
100  if ! ip netns exec "$ns1" ping -c 1 -q 10.0.3.99 > /dev/null; then
101	return 1
102  fi
103
104  if ! ip netns exec "$ns1" ping -c 1 -q dead:3::99 > /dev/null; then
105	return 2
106  fi
107
108  return 0
109}
110
111test_ping_router() {
112  if ! ip netns exec "$ns1" ping -c 1 -q 10.0.2.1 > /dev/null; then
113	return 3
114  fi
115
116  if ! ip netns exec "$ns1" ping -c 1 -q dead:2::1 > /dev/null; then
117	return 4
118  fi
119
120  return 0
121}
122
123
124listener_ready()
125{
126	local ns="$1"
127	local port="$2"
128	local proto="$3"
129	ss -N "$ns" -ln "$proto" -o "sport = :$port" | grep -q "$port"
130}
131
132test_tproxy()
133{
134	local traffic_origin="$1"
135	local ip_proto="$2"
136	local expect_ns1_ns2="$3"
137	local expect_ns1_ns3="$4"
138	local expect_nsrouter_ns2="$5"
139	local expect_nsrouter_ns3="$6"
140
141	# derived variables
142	local testname="test_${ip_proto}_tcp_${traffic_origin}"
143	local socat_ipproto
144	local ns1_ip
145	local ns2_ip
146	local ns3_ip
147	local ns2_target
148	local ns3_target
149	local nftables_subject
150	local ip_command
151
152	# socat 1.8.0 has a bug that requires to specify the IP family to bind (fixed in 1.8.0.1)
153	case $ip_proto in
154	"ip")
155		socat_ipproto="-4"
156		ns1_ip=10.0.1.99
157		ns2_ip=10.0.2.99
158		ns3_ip=10.0.3.99
159		ns2_target="tcp:$ns2_ip:8080"
160		ns3_target="tcp:$ns3_ip:8080"
161		nftables_subject="ip daddr $ns2_ip tcp dport 8080"
162		ip_command="ip"
163	;;
164	"ip6")
165		socat_ipproto="-6"
166		ns1_ip=dead:1::99
167		ns2_ip=dead:2::99
168		ns3_ip=dead:3::99
169		ns2_target="tcp:[$ns2_ip]:8080"
170		ns3_target="tcp:[$ns3_ip]:8080"
171		nftables_subject="ip6 daddr $ns2_ip tcp dport 8080"
172		ip_command="ip -6"
173	;;
174	*)
175	echo "FAIL: unsupported protocol"
176	exit 255
177	;;
178	esac
179
180	case $traffic_origin in
181	# to capture the local originated traffic we need to mark the outgoing
182	# traffic so the policy based routing rule redirects it and can be processed
183	# in the prerouting chain.
184	"local")
185		nftables_rules="
186flush ruleset
187table inet filter {
188	chain divert {
189		type filter hook prerouting priority 0; policy accept;
190		$nftables_subject tproxy $ip_proto to :12345 meta mark set 1 accept
191	}
192	chain output {
193		type route hook output priority 0; policy accept;
194		$nftables_subject meta mark set 1 accept
195	}
196}"
197	;;
198	"forward")
199		nftables_rules="
200flush ruleset
201table inet filter {
202	chain divert {
203		type filter hook prerouting priority 0; policy accept;
204		$nftables_subject tproxy $ip_proto to :12345 meta mark set 1 accept
205	}
206}"
207	;;
208	*)
209	echo "FAIL: unsupported parameter for traffic origin"
210	exit 255
211	;;
212	esac
213
214	# shellcheck disable=SC2046 # Intended splitting of ip_command
215	ip netns exec "$nsrouter" $ip_command rule add fwmark 1 table 100
216	ip netns exec "$nsrouter" $ip_command route add local "${ns2_ip}" dev lo table 100
217	echo "$nftables_rules" | ip netns exec "$nsrouter" nft -f /dev/stdin
218
219	timeout "$timeout" ip netns exec "$nsrouter" socat "$socat_ipproto" tcp-listen:12345,fork,ip-transparent SYSTEM:"cat" 2>/dev/null &
220	local tproxy_pid=$!
221
222	timeout "$timeout" ip netns exec "$ns2" socat "$socat_ipproto" tcp-listen:8080,fork SYSTEM:"echo PONG_NS2" 2>/dev/null &
223	local server2_pid=$!
224
225	timeout "$timeout" ip netns exec "$ns3" socat "$socat_ipproto" tcp-listen:8080,fork SYSTEM:"echo PONG_NS3" 2>/dev/null &
226	local server3_pid=$!
227
228	busywait "$BUSYWAIT_TIMEOUT" listener_ready "$nsrouter" 12345 "-t"
229	busywait "$BUSYWAIT_TIMEOUT" listener_ready "$ns2" 8080 "-t"
230	busywait "$BUSYWAIT_TIMEOUT" listener_ready "$ns3" 8080 "-t"
231
232	local result
233	# request from ns1 to ns2 (forwarded traffic)
234	result=$(echo I_M_PROXIED | ip netns exec "$ns1" socat -t 2 -T 2 STDIO "$ns2_target")
235	if [ "$result" == "$expect_ns1_ns2" ] ;then
236		echo "PASS: tproxy test $testname: ns1 got reply \"$result\" connecting to ns2"
237	else
238		echo "ERROR: tproxy test $testname: ns1 got reply \"$result\" connecting to ns2, not \"${expect_ns1_ns2}\" as intended"
239		ret=1
240	fi
241
242	# request from ns1 to ns3(forwarded traffic)
243	result=$(echo I_M_PROXIED | ip netns exec "$ns1" socat -t 2 -T 2 STDIO "$ns3_target")
244	if [ "$result" = "$expect_ns1_ns3" ] ;then
245		echo "PASS: tproxy test $testname: ns1 got reply \"$result\" connecting to ns3"
246	else
247		echo "ERROR: tproxy test $testname: ns1 got reply \"$result\" connecting to ns3, not \"$expect_ns1_ns3\" as intended"
248		ret=1
249	fi
250
251	# request from nsrouter to ns2 (localy originated traffic)
252	result=$(echo I_M_PROXIED | ip netns exec "$nsrouter" socat -t 2 -T 2 STDIO "$ns2_target")
253	if [ "$result" == "$expect_nsrouter_ns2" ] ;then
254		echo "PASS: tproxy test $testname: nsrouter got reply \"$result\" connecting to ns2"
255	else
256		echo "ERROR: tproxy test $testname: nsrouter got reply \"$result\" connecting to ns2, not \"$expect_nsrouter_ns2\" as intended"
257		ret=1
258	fi
259
260	# request from nsrouter to ns3 (localy originated traffic)
261	result=$(echo I_M_PROXIED | ip netns exec "$nsrouter" socat -t 2 -T 2 STDIO "$ns3_target")
262	if [ "$result" = "$expect_nsrouter_ns3" ] ;then
263		echo "PASS: tproxy test $testname: nsrouter got reply \"$result\" connecting to ns3"
264	else
265		echo "ERROR: tproxy test $testname: nsrouter got reply \"$result\" connecting to ns3, not \"$expect_nsrouter_ns3\"  as intended"
266		ret=1
267	fi
268
269	# cleanup
270	kill "$tproxy_pid" "$server2_pid" "$server3_pid" 2>/dev/null
271	# shellcheck disable=SC2046 # Intended splitting of ip_command
272	ip netns exec "$nsrouter" $ip_command rule del fwmark 1 table 100
273	ip netns exec "$nsrouter" $ip_command route flush table 100
274}
275
276
277test_ipv4_tcp_forward()
278{
279	local traffic_origin="forward"
280	local ip_proto="ip"
281	local expect_ns1_ns2="I_M_PROXIED"
282	local expect_ns1_ns3="PONG_NS3"
283	local expect_nsrouter_ns2="PONG_NS2"
284	local expect_nsrouter_ns3="PONG_NS3"
285
286	test_tproxy     "$traffic_origin" \
287			"$ip_proto" \
288			"$expect_ns1_ns2" \
289			"$expect_ns1_ns3" \
290			"$expect_nsrouter_ns2" \
291			"$expect_nsrouter_ns3"
292}
293
294test_ipv4_tcp_local()
295{
296	local traffic_origin="local"
297	local ip_proto="ip"
298	local expect_ns1_ns2="I_M_PROXIED"
299	local expect_ns1_ns3="PONG_NS3"
300	local expect_nsrouter_ns2="I_M_PROXIED"
301	local expect_nsrouter_ns3="PONG_NS3"
302
303	test_tproxy     "$traffic_origin" \
304			"$ip_proto" \
305			"$expect_ns1_ns2" \
306			"$expect_ns1_ns3" \
307			"$expect_nsrouter_ns2" \
308			"$expect_nsrouter_ns3"
309}
310
311test_ipv6_tcp_forward()
312{
313	local traffic_origin="forward"
314	local ip_proto="ip6"
315	local expect_ns1_ns2="I_M_PROXIED"
316	local expect_ns1_ns3="PONG_NS3"
317	local expect_nsrouter_ns2="PONG_NS2"
318	local expect_nsrouter_ns3="PONG_NS3"
319
320	test_tproxy     "$traffic_origin" \
321			"$ip_proto" \
322			"$expect_ns1_ns2" \
323			"$expect_ns1_ns3" \
324			"$expect_nsrouter_ns2" \
325			"$expect_nsrouter_ns3"
326}
327
328test_ipv6_tcp_local()
329{
330	local traffic_origin="local"
331	local ip_proto="ip6"
332	local expect_ns1_ns2="I_M_PROXIED"
333	local expect_ns1_ns3="PONG_NS3"
334	local expect_nsrouter_ns2="I_M_PROXIED"
335	local expect_nsrouter_ns3="PONG_NS3"
336
337	test_tproxy     "$traffic_origin" \
338			"$ip_proto" \
339			"$expect_ns1_ns2" \
340			"$expect_ns1_ns3" \
341			"$expect_nsrouter_ns2" \
342			"$expect_nsrouter_ns3"
343}
344
345if test_ping; then
346	# queue bypass works (rules were skipped, no listener)
347	echo "PASS: ${ns1} can reach ${ns2}"
348else
349	echo "FAIL: ${ns1} cannot reach ${ns2}: $ret" 1>&2
350	exit $ret
351fi
352
353test_ipv4_tcp_forward
354test_ipv4_tcp_local
355test_ipv6_tcp_forward
356test_ipv6_tcp_local
357
358exit $ret
359