| #!/bin/bash |
| # SPDX-License-Identifier: GPL-2.0 |
| # Copyright (C) 2020-2025 OpenVPN, Inc. |
| # |
| # Author: Antonio Quartulli <antonio@openvpn.net> |
| |
| 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 |