#!/bin/bash # SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2020-2025 OpenVPN, Inc. # # Author: Antonio Quartulli OVPN_COMMON_DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")") source "$OVPN_COMMON_DIR/../../kselftest/ktap_helpers.sh" OVPN_UDP_PEERS_FILE=${OVPN_UDP_PEERS_FILE:-udp_peers.txt} OVPN_TCP_PEERS_FILE=${OVPN_TCP_PEERS_FILE:-tcp_peers.txt} OVPN_CLI=${OVPN_CLI:-${OVPN_COMMON_DIR}/ovpn-cli} OVPN_YNL=${OVPN_YNL:-${OVPN_COMMON_DIR}/../../../../net/ynl/pyynl/cli.py} OVPN_ALG=${OVPN_ALG:-aes} OVPN_PROTO=${OVPN_PROTO:-UDP} OVPN_FLOAT=${OVPN_FLOAT:-0} OVPN_SYMMETRIC_ID=${OVPN_SYMMETRIC_ID:-0} OVPN_VERBOSE=${OVPN_VERBOSE:-0} export OVPN_ID_OFFSET=$(( 9 * (OVPN_SYMMETRIC_ID == 0) )) OVPN_JQ_FILTER='map(if type == "array" then .[] else . end) | map(select(.msg.peer | has("remote-ipv6") | not)) | map(del(.msg.ifindex)) | sort_by(.msg.peer.id)[]' OVPN_LAN_IP="11.11.11.11" declare -A OVPN_TMP_JSONS=() declare -A OVPN_LISTENER_PIDS=() OVPN_CURRENT_STAGE="" ovpn_is_verbose() { [[ "${OVPN_VERBOSE}" == "1" ]] } ovpn_log() { ovpn_is_verbose || return 0 printf '%s\n' "$*" } ovpn_print_cmd_output() { local output_file="$1" local line [[ -s "${output_file}" ]] || return 0 while IFS= read -r line; do ovpn_log "${line}" done < "${output_file}" } ovpn_cmd_run() { local mode="$1" local label="$2" local output_file local rc local ret=0 shift 2 output_file=$(mktemp) if "$@" >"${output_file}" 2>&1; then rc=0 else rc=$? fi case "${mode}" in ok) if [[ "${rc}" -ne 0 ]]; then cat "${output_file}" printf '%s\n' \ "${label}: command failed with rc=${rc}: $*" ret="${rc}" fi ;; mayfail) ;; fail) [[ "${rc}" -eq 0 ]] && ret=1 ;; esac if ovpn_is_verbose && [[ "${rc}" -eq 0 || "${mode}" != "ok" ]]; then ovpn_print_cmd_output "${output_file}" fi rm -f "${output_file}" return "${ret}" } ovpn_cmd_ok() { ovpn_cmd_run ok "$@" } ovpn_cmd_mayfail() { ovpn_cmd_run mayfail "$@" } ovpn_cmd_fail() { ovpn_cmd_run fail "$@" } ovpn_run_bg() { local pid_var="$1" shift if ovpn_is_verbose; then "$@" & else "$@" >/dev/null 2>&1 & fi printf -v "${pid_var}" '%s' "$!" } ovpn_run_stage() { local label="$1" shift OVPN_CURRENT_STAGE="${label}" "$@" OVPN_CURRENT_STAGE="" ktap_test_pass "${label}" } ovpn_stage_err() { # ERR trap is global under set -eE: only report failures that happen # while ovpn_run_stage() is actively executing a stage body. if [[ -n "${OVPN_CURRENT_STAGE}" ]]; then ktap_test_fail "${OVPN_CURRENT_STAGE}" OVPN_CURRENT_STAGE="" fi } ovpn_create_ns() { ip netns add "ovpn_peer${1}" } ovpn_setup_ns() { local peer="ovpn_peer${1}" local server_ns="ovpn_peer0" local peer_ns MODE="P2P" if [ ${1} -eq 0 ]; then MODE="MP" for p in $(seq 1 ${OVPN_NUM_PEERS}); do peer_ns="ovpn_peer${p}" ip link add veth${p} netns "${server_ns}" type veth \ peer name veth${p} netns "${peer_ns}" ip -n "${server_ns}" addr add 10.10.${p}.1/24 dev \ veth${p} ip -n "${server_ns}" addr add fd00:0:0:${p}::1/64 dev \ veth${p} ip -n "${server_ns}" link set veth${p} up ip -n "${peer_ns}" addr add 10.10.${p}.2/24 dev veth${p} ip -n "${peer_ns}" addr add fd00:0:0:${p}::2/64 dev \ veth${p} ip -n "${peer_ns}" link set veth${p} up done fi ip netns exec "${peer}" ${OVPN_CLI} new_iface tun${1} $MODE ip -n "${peer}" addr add ${2} dev tun${1} # add a secondary IP to peer 1, to test a LAN behind a client if [ ${1} -eq 1 -a -n "${OVPN_LAN_IP}" ]; then ip -n "${peer}" addr add ${OVPN_LAN_IP} dev tun${1} ip -n "${server_ns}" route add ${OVPN_LAN_IP} via \ $(echo ${2} |sed -e s'!/.*!!') dev tun0 fi if [ -n "${3}" ]; then ip -n "${peer}" link set mtu ${3} dev tun${1} fi ip -n "${peer}" link set tun${1} up } ovpn_build_capture_filter() { # match the first four bytes of the openvpn data payload if [ "${OVPN_PROTO}" == "UDP" ]; then # For UDP, libpcap transport indexing only works for IPv4, so # use an explicit IPv4 or IPv6 expression based on the peer # address. The IPv6 branch assumes there are no extension # headers in the outer packet. if [[ "${2}" == *:* ]]; then printf "ip6 and ip6[6] = 17 and ip6[48:4] = %s" "${1}" else printf "ip and udp[8:4] = %s" "${1}" fi else # openvpn over TCP prepends a 2-byte packet length ahead of the # DATA_V2 opcode, so skip it before matching the payload header printf "ip and tcp[(((tcp[12] & 0xf0) >> 2) + 2):4] = %s" "${1}" fi } ovpn_setup_listener() { local peer="$1" local file local peer_ns="ovpn_peer${peer}" file=$(mktemp) PYTHONUNBUFFERED=1 ip netns exec "${peer_ns}" "${OVPN_YNL}" --family \ ovpn --subscribe peers --output-json > "${file}" \ 2>/dev/null & OVPN_LISTENER_PIDS["${peer}"]=$! OVPN_TMP_JSONS["${peer}"]="${file}" } ovpn_add_peer() { labels=("ASYMM" "SYMM") local peer_ns local server_ns="ovpn_peer0" M_ID=${labels[OVPN_SYMMETRIC_ID]} if [ "${OVPN_PROTO}" == "UDP" ]; then if [ ${1} -eq 0 ]; then ip netns exec "${server_ns}" ${OVPN_CLI} \ new_multi_peer tun0 1 ${M_ID} \ ${OVPN_UDP_PEERS_FILE} for p in $(seq 1 ${OVPN_NUM_PEERS}); do ip netns exec "${server_ns}" ${OVPN_CLI} \ new_key tun0 ${p} 1 0 ${OVPN_ALG} 0 \ data64.key done else peer_ns="ovpn_peer${1}" if [ "${OVPN_SYMMETRIC_ID}" -eq 1 ]; then PEER_ID=${1} TX_ID="none" else PEER_ID=$(awk "NR == ${1} {print \$2}" \ ${OVPN_UDP_PEERS_FILE}) TX_ID=${1} fi RADDR=$(awk "NR == ${1} {print \$3}" \ ${OVPN_UDP_PEERS_FILE}) RPORT=$(awk "NR == ${1} {print \$4}" \ ${OVPN_UDP_PEERS_FILE}) LPORT=$(awk "NR == ${1} {print \$6}" \ ${OVPN_UDP_PEERS_FILE}) ip netns exec "${peer_ns}" ${OVPN_CLI} new_peer \ tun${1} ${PEER_ID} ${TX_ID} ${LPORT} ${RADDR} \ ${RPORT} ip netns exec "${peer_ns}" ${OVPN_CLI} new_key tun${1} \ ${PEER_ID} 1 0 ${OVPN_ALG} 1 data64.key fi else if [ ${1} -eq 0 ]; then (ip netns exec "${server_ns}" ${OVPN_CLI} listen tun0 \ 1 ${M_ID} ${OVPN_TCP_PEERS_FILE} && { for p in $(seq 1 ${OVPN_NUM_PEERS}); do ip netns exec "${server_ns}" \ ${OVPN_CLI} new_key tun0 ${p} \ 1 0 ${OVPN_ALG} 0 data64.key done }) & sleep 5 else peer_ns="ovpn_peer${1}" if [ "${OVPN_SYMMETRIC_ID}" -eq 1 ]; then PEER_ID=${1} TX_ID="none" else PEER_ID=$(awk "NR == ${1} {print \$2}" \ ${OVPN_TCP_PEERS_FILE}) TX_ID=${1} fi ip netns exec "${peer_ns}" ${OVPN_CLI} connect tun${1} \ ${PEER_ID} ${TX_ID} 10.10.${1}.1 1 data64.key fi fi } ovpn_compare_ntfs() { local diff_rc=0 local diff_file if [ ${#OVPN_TMP_JSONS[@]} -gt 0 ]; then suffix="" [ "${OVPN_SYMMETRIC_ID}" -eq 1 ] && suffix="${suffix}-symm" [ "$OVPN_FLOAT" == 1 ] && suffix="${suffix}-float" expected="json/peer${1}${suffix}.json" received="${OVPN_TMP_JSONS[$1]}" diff_file=$(mktemp) ovpn_stop_listener "${1}" 1 printf "Checking notifications for peer ${1}... " if diff <(jq -s "${OVPN_JQ_FILTER}" ${expected}) \ <(jq -s "${OVPN_JQ_FILTER}" ${received}) \ >"${diff_file}" 2>&1; then echo "OK" else diff_rc=$? echo "failed" cat "${diff_file}" fi rm -f "${diff_file}" || true rm -f "${received}" || true unset "OVPN_TMP_JSONS[$1]" fi return "${diff_rc}" } ovpn_stop_listener() { local peer="$1" local keep_json="${2:-0}" local pid="${OVPN_LISTENER_PIDS[$peer]:-}" local json="${OVPN_TMP_JSONS[$peer]:-}" if [[ -n "${pid}" ]]; then kill -TERM "${pid}" 2>/dev/null || true wait "${pid}" 2>/dev/null || true unset "OVPN_LISTENER_PIDS[$peer]" fi if [[ -n "${json}" && "${keep_json}" -eq 0 ]]; then rm -f "${json}" || true unset "OVPN_TMP_JSONS[$peer]" fi } ovpn_cleanup_peer_ns() { local peer="$1" local peer_id="${peer#ovpn_peer}" ip -n "${peer}" link set tun${peer_id} down 2>/dev/null || true ip netns exec "${peer}" ${OVPN_CLI} del_iface tun${peer_id} \ 1>/dev/null 2>&1 || true ip netns del "${peer}" 2>/dev/null || true } ovpn_cleanup() { local peer # some ovpn-cli processes sleep in background so they need manual poking killall "$(basename "${OVPN_CLI}")" 2>/dev/null || true for peer in "${!OVPN_LISTENER_PIDS[@]}"; do ovpn_stop_listener "${peer}" 2>/dev/null done for p in $(seq 1 10); do ip -n ovpn_peer0 link del veth${p} 2>/dev/null || true done # remove from ovpn's netns pool while IFS= read -r peer; do [[ -n "${peer}" ]] || continue ovpn_cleanup_peer_ns "${peer}" 2>/dev/null done < <(ip netns list 2>/dev/null | awk '/^ovpn_/ {print $1}') } if [ "${OVPN_PROTO}" == "UDP" ]; then OVPN_NUM_PEERS=${OVPN_NUM_PEERS:-$(wc -l ${OVPN_UDP_PEERS_FILE} | \ awk '{print $1}')} else OVPN_NUM_PEERS=${OVPN_NUM_PEERS:-$(wc -l ${OVPN_TCP_PEERS_FILE} | \ awk '{print $1}')} fi