xref: /linux/tools/testing/selftests/net/ovpn/common.sh (revision 7e170da2edec9fb0717543571cc14b0aabee2b46)
1#!/bin/bash
2# SPDX-License-Identifier: GPL-2.0
3# Copyright (C) 2020-2025 OpenVPN, Inc.
4#
5#  Author:	Antonio Quartulli <antonio@openvpn.net>
6
7OVPN_COMMON_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
8source "$OVPN_COMMON_DIR/../../kselftest/ktap_helpers.sh"
9
10OVPN_UDP_PEERS_FILE=${OVPN_UDP_PEERS_FILE:-udp_peers.txt}
11OVPN_TCP_PEERS_FILE=${OVPN_TCP_PEERS_FILE:-tcp_peers.txt}
12OVPN_CLI=${OVPN_CLI:-${OVPN_COMMON_DIR}/ovpn-cli}
13OVPN_YNL=${OVPN_YNL:-${OVPN_COMMON_DIR}/../../../../net/ynl/pyynl/cli.py}
14OVPN_ALG=${OVPN_ALG:-aes}
15OVPN_PROTO=${OVPN_PROTO:-UDP}
16OVPN_FLOAT=${OVPN_FLOAT:-0}
17OVPN_SYMMETRIC_ID=${OVPN_SYMMETRIC_ID:-0}
18OVPN_VERBOSE=${OVPN_VERBOSE:-0}
19
20export OVPN_ID_OFFSET=$(( 9 * (OVPN_SYMMETRIC_ID == 0) ))
21
22OVPN_JQ_FILTER='map(if type == "array" then .[] else . end) |
23	map(select(.msg.peer | has("remote-ipv6") | not)) |
24	map(del(.msg.ifindex)) | sort_by(.msg.peer.id)[]'
25OVPN_LAN_IP="11.11.11.11"
26
27declare -A OVPN_TMP_JSONS=()
28declare -A OVPN_LISTENER_PIDS=()
29OVPN_CURRENT_STAGE=""
30
31ovpn_is_verbose() {
32	[[ "${OVPN_VERBOSE}" == "1" ]]
33}
34
35ovpn_log() {
36	ovpn_is_verbose || return 0
37	printf '%s\n' "$*"
38}
39
40ovpn_print_cmd_output() {
41	local output_file="$1"
42	local line
43
44	[[ -s "${output_file}" ]] || return 0
45
46	while IFS= read -r line; do
47		ovpn_log "${line}"
48	done < "${output_file}"
49}
50
51ovpn_cmd_run() {
52	local mode="$1"
53	local label="$2"
54	local output_file
55	local rc
56	local ret=0
57
58	shift 2
59
60	output_file=$(mktemp)
61	if "$@" >"${output_file}" 2>&1; then
62		rc=0
63	else
64		rc=$?
65	fi
66
67	case "${mode}" in
68	ok)
69		if [[ "${rc}" -ne 0 ]]; then
70			cat "${output_file}"
71			printf '%s\n' \
72				"${label}: command failed with rc=${rc}: $*"
73			ret="${rc}"
74		fi
75		;;
76	mayfail)
77		;;
78	fail)
79		[[ "${rc}" -eq 0 ]] && ret=1
80		;;
81	esac
82
83	if ovpn_is_verbose && [[ "${rc}" -eq 0 || "${mode}" != "ok" ]]; then
84		ovpn_print_cmd_output "${output_file}"
85	fi
86
87	rm -f "${output_file}"
88	return "${ret}"
89}
90
91ovpn_cmd_ok() {
92	ovpn_cmd_run ok "$@"
93}
94
95ovpn_cmd_mayfail() {
96	ovpn_cmd_run mayfail "$@"
97}
98
99ovpn_cmd_fail() {
100	ovpn_cmd_run fail "$@"
101}
102
103ovpn_run_bg() {
104	local pid_var="$1"
105
106	shift
107	if ovpn_is_verbose; then
108		"$@" &
109	else
110		"$@" >/dev/null 2>&1 &
111	fi
112
113	printf -v "${pid_var}" '%s' "$!"
114}
115
116ovpn_run_stage() {
117	local label="$1"
118
119	shift
120	OVPN_CURRENT_STAGE="${label}"
121	"$@"
122	OVPN_CURRENT_STAGE=""
123	ktap_test_pass "${label}"
124}
125
126ovpn_stage_err() {
127	# ERR trap is global under set -eE: only report failures that happen
128	# while ovpn_run_stage() is actively executing a stage body.
129	if [[ -n "${OVPN_CURRENT_STAGE}" ]]; then
130		ktap_test_fail "${OVPN_CURRENT_STAGE}"
131		OVPN_CURRENT_STAGE=""
132	fi
133}
134
135ovpn_create_ns() {
136	ip netns add "ovpn_peer${1}"
137}
138
139ovpn_peer_vpn_addr() {
140	local peer="$1"
141	local file
142
143	if [ "${OVPN_PROTO}" == "UDP" ]; then
144		file="${OVPN_UDP_PEERS_FILE}"
145	else
146		file="${OVPN_TCP_PEERS_FILE}"
147	fi
148
149	awk -v peer="${peer}" '$1 == peer {print $NF; exit}' "${file}"
150}
151
152ovpn_setup_ns() {
153	local peer="ovpn_peer${1}"
154	local server_ns="ovpn_peer0"
155	local peer_ns
156	MODE="P2P"
157
158	if [ ${1} -eq 0 ]; then
159		MODE="MP"
160		for p in $(seq 1 ${OVPN_NUM_PEERS}); do
161			peer_ns="ovpn_peer${p}"
162			ip link add veth${p} netns "${server_ns}" type veth \
163				peer name veth${p} netns "${peer_ns}"
164
165			ip -n "${server_ns}" addr add 10.10.${p}.1/24 dev \
166				veth${p}
167			ip -n "${server_ns}" addr add fd00:0:0:${p}::1/64 dev \
168				veth${p}
169			ip -n "${server_ns}" link set veth${p} up
170
171			ip -n "${peer_ns}" addr add 10.10.${p}.2/24 dev veth${p}
172			ip -n "${peer_ns}" addr add fd00:0:0:${p}::2/64 dev \
173				veth${p}
174			ip -n "${peer_ns}" link set veth${p} up
175		done
176	fi
177
178	ip netns exec "${peer}" ${OVPN_CLI} new_iface tun${1} $MODE
179	ip -n "${peer}" addr add ${2} dev tun${1}
180	# add a secondary IP to peer 1, to test a LAN behind a client
181	if [ ${1} -eq 1 -a -n "${OVPN_LAN_IP}" ]; then
182		ip -n "${peer}" addr add ${OVPN_LAN_IP} dev tun${1}
183		ip -n "${server_ns}" route add ${OVPN_LAN_IP} via \
184			$(echo ${2} |sed -e s'!/.*!!') dev tun0
185	fi
186	if [ -n "${3}" ]; then
187		ip -n "${peer}" link set mtu ${3} dev tun${1}
188	fi
189	ip -n "${peer}" link set tun${1} up
190}
191
192ovpn_build_capture_filter() {
193	# match the first four bytes of the openvpn data payload
194	if [ "${OVPN_PROTO}" == "UDP" ]; then
195		# For UDP, libpcap transport indexing only works for IPv4, so
196		# use an explicit IPv4 or IPv6 expression based on the peer
197		# address. The IPv6 branch assumes there are no extension
198		# headers in the outer packet.
199		if [[ "${2}" == *:* ]]; then
200			printf "ip6 and ip6[6] = 17 and ip6[48:4] = %s" "${1}"
201		else
202			printf "ip and udp[8:4] = %s" "${1}"
203		fi
204	else
205		# openvpn over TCP prepends a 2-byte packet length ahead of the
206		# DATA_V2 opcode, so skip it before matching the payload header
207		printf "ip and tcp[(((tcp[12] & 0xf0) >> 2) + 2):4] = %s" "${1}"
208	fi
209}
210
211ovpn_setup_listener() {
212	local peer="$1"
213	local file
214	local peer_ns="ovpn_peer${peer}"
215
216	file=$(mktemp)
217	PYTHONUNBUFFERED=1 ip netns exec "${peer_ns}" "${OVPN_YNL}" --family \
218		ovpn --subscribe peers --output-json > "${file}" \
219		2>/dev/null &
220	OVPN_LISTENER_PIDS["${peer}"]=$!
221	OVPN_TMP_JSONS["${peer}"]="${file}"
222}
223
224ovpn_add_peer() {
225	labels=("ASYMM" "SYMM")
226	local peer_ns
227	local server_ns="ovpn_peer0"
228	M_ID=${labels[OVPN_SYMMETRIC_ID]}
229
230	if [ "${OVPN_PROTO}" == "UDP" ]; then
231		if [ ${1} -eq 0 ]; then
232			ip netns exec "${server_ns}" ${OVPN_CLI} \
233				new_multi_peer tun0 1 ${M_ID} \
234				${OVPN_UDP_PEERS_FILE}
235
236			for p in $(seq 1 ${OVPN_NUM_PEERS}); do
237				ip netns exec "${server_ns}" ${OVPN_CLI} \
238					new_key tun0 ${p} 1 0 ${OVPN_ALG} 0 \
239					data64.key
240			done
241		else
242			peer_ns="ovpn_peer${1}"
243			if [ "${OVPN_SYMMETRIC_ID}" -eq 1 ]; then
244				PEER_ID=${1}
245				TX_ID="none"
246			else
247				PEER_ID=$(awk "NR == ${1} {print \$2}" \
248					${OVPN_UDP_PEERS_FILE})
249				TX_ID=${1}
250			fi
251			RADDR=$(awk "NR == ${1} {print \$3}" \
252				${OVPN_UDP_PEERS_FILE})
253			RPORT=$(awk "NR == ${1} {print \$4}" \
254				${OVPN_UDP_PEERS_FILE})
255			LPORT=$(awk "NR == ${1} {print \$6}" \
256				${OVPN_UDP_PEERS_FILE})
257			ip netns exec "${peer_ns}" ${OVPN_CLI} new_peer \
258				tun${1} ${PEER_ID} ${TX_ID} ${LPORT} ${RADDR} \
259				${RPORT}
260			ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${1} \
261				${PEER_ID} 1 0 ${OVPN_ALG} 1 data64.key
262		fi
263	else
264		if [ ${1} -eq 0 ]; then
265			(ip netns exec "${server_ns}" ${OVPN_CLI} listen tun0 \
266				1 ${M_ID} ${OVPN_TCP_PEERS_FILE} && {
267				for p in $(seq 1 ${OVPN_NUM_PEERS}); do
268					ip netns exec "${server_ns}" \
269						${OVPN_CLI} new_key tun0 ${p} \
270						1 0 ${OVPN_ALG} 0 data64.key
271				done
272			}) &
273			sleep 5
274		else
275			peer_ns="ovpn_peer${1}"
276			if [ "${OVPN_SYMMETRIC_ID}" -eq 1 ]; then
277				PEER_ID=${1}
278				TX_ID="none"
279			else
280				PEER_ID=$(awk "NR == ${1} {print \$2}" \
281					${OVPN_TCP_PEERS_FILE})
282				TX_ID=${1}
283			fi
284			ip netns exec "${peer_ns}" ${OVPN_CLI} connect tun${1} \
285				${PEER_ID} ${TX_ID} 10.10.${1}.1 1 data64.key
286		fi
287	fi
288}
289
290ovpn_compare_ntfs() {
291	local diff_rc=0
292	local diff_file
293
294	if [ ${#OVPN_TMP_JSONS[@]} -gt 0 ]; then
295		suffix=""
296		[ "${OVPN_SYMMETRIC_ID}" -eq 1 ] && suffix="${suffix}-symm"
297		[ "$OVPN_FLOAT" == 1 ] && suffix="${suffix}-float"
298		expected="json/peer${1}${suffix}.json"
299		received="${OVPN_TMP_JSONS[$1]}"
300		diff_file=$(mktemp)
301
302		ovpn_stop_listener "${1}" 1
303		printf "Checking notifications for peer ${1}... "
304		if diff <(jq -s "${OVPN_JQ_FILTER}" ${expected}) \
305			<(jq -s "${OVPN_JQ_FILTER}" ${received}) \
306			>"${diff_file}" 2>&1; then
307			echo "OK"
308		else
309			diff_rc=$?
310			echo "failed"
311			cat "${diff_file}"
312		fi
313
314		rm -f "${diff_file}" || true
315		rm -f "${received}" || true
316		unset "OVPN_TMP_JSONS[$1]"
317	fi
318
319	return "${diff_rc}"
320}
321
322ovpn_stop_listener() {
323	local peer="$1"
324	local keep_json="${2:-0}"
325	local pid="${OVPN_LISTENER_PIDS[$peer]:-}"
326	local json="${OVPN_TMP_JSONS[$peer]:-}"
327
328	if [[ -n "${pid}" ]]; then
329		kill -TERM "${pid}" 2>/dev/null || true
330		wait "${pid}" 2>/dev/null || true
331		unset "OVPN_LISTENER_PIDS[$peer]"
332	fi
333
334	if [[ -n "${json}" && "${keep_json}" -eq 0 ]]; then
335		rm -f "${json}" || true
336		unset "OVPN_TMP_JSONS[$peer]"
337	fi
338}
339
340ovpn_cleanup_peer_ns() {
341	local peer="$1"
342	local peer_id="${peer#ovpn_peer}"
343
344	ip -n "${peer}" link set tun${peer_id} down 2>/dev/null || true
345	ip netns exec "${peer}" ${OVPN_CLI} del_iface tun${peer_id} \
346		1>/dev/null 2>&1 || true
347	ip netns del "${peer}" 2>/dev/null || true
348}
349
350ovpn_cleanup() {
351	local peer
352
353	# some ovpn-cli processes sleep in background so they need manual poking
354	killall "$(basename "${OVPN_CLI}")" 2>/dev/null || true
355
356	for peer in "${!OVPN_LISTENER_PIDS[@]}"; do
357		ovpn_stop_listener "${peer}" 2>/dev/null
358	done
359
360	for p in $(seq 1 10); do
361		ip -n ovpn_peer0 link del veth${p} 2>/dev/null || true
362	done
363
364	# remove from ovpn's netns pool
365	while IFS= read -r peer; do
366		[[ -n "${peer}" ]] || continue
367		ovpn_cleanup_peer_ns "${peer}" 2>/dev/null
368	done < <(ip netns list 2>/dev/null | awk '/^ovpn_/ {print $1}')
369}
370
371if [ "${OVPN_PROTO}" == "UDP" ]; then
372	OVPN_NUM_PEERS=${OVPN_NUM_PEERS:-$(wc -l ${OVPN_UDP_PEERS_FILE} | \
373		awk '{print $1}')}
374else
375	OVPN_NUM_PEERS=${OVPN_NUM_PEERS:-$(wc -l ${OVPN_TCP_PEERS_FILE} | \
376		awk '{print $1}')}
377fi
378