From 24f76defb553cdf9ccbb2aae9da1c45e2ab610ef Mon Sep 17 00:00:00 2001 From: Henk Date: Sun, 7 Sep 2025 17:23:25 +0200 Subject: [PATCH 01/11] Add Double NAT support --- test_suite/nat_simulation/setup_networks.sh | 59 +++++-- test_suite/nat_simulation/setup_router.sh | 49 +++--- test_suite/performance_test.sh | 2 +- test_suite/system_test.sh | 169 ++++++++++++++------ test_suite/system_tests.sh | 124 +++++++++++--- 5 files changed, 300 insertions(+), 103 deletions(-) diff --git a/test_suite/nat_simulation/setup_networks.sh b/test_suite/nat_simulation/setup_networks.sh index cdc3725..502661e 100755 --- a/test_suite/nat_simulation/setup_networks.sh +++ b/test_suite/nat_simulation/setup_networks.sh @@ -1,16 +1,33 @@ #!/usr/bin/env bash -if [[ $1 = "-h" ]]; then - echo """ -Usage: ${0} +usage_str="""Usage: ${0} [OPTIONAL ARGUMENTS] Simulates a network setup containing two private networks connected via the public network. Each private network contains two peers using eduP2P -To allow traffic to flow between the public and private networks, the scripts setup_nat_mapping.sh should also be executed -To allow traffic to flow between peers in the same private network, the scripts setup_nat_filtering_hairpinning.sh should also be executed + +By default, a single NAT facilitates the communication between a private and public network. With the -2 flag, Double NAT is simulated by adding another level of private networks on top of the existing ones + +To allow traffic to flow between the public and private networks, the script setup_nat_mapping.sh should also be executed. To allow traffic to flow between peers in the same private network, the script setup_nat_filtering_hairpinning.sh should also be executed This script must be run with root permissions""" - exit 1 -fi + +# Use functions and constants from util.sh +. ../util.sh + +# Validate optional arguments +while getopts ":2h" opt; do + case $opt in + 2) + double_nat=true + ;; + h) + echo "$usage_str" + exit 0 + ;; + *) + exit_with_error "invalid option" + ;; + esac +done # Enable IP forwarding to allow for routing between namespaces sysctl -w net.ipv4.ip_forward=1 &> /dev/null @@ -65,11 +82,31 @@ for ((i=1; i<=n_priv_nets; i++)); do # Add router's public IP to list created earlier adm_ips+=($router_pub_ip) - # Setup router - ip netns exec $router_name ./setup_router.sh $router_name $priv_name $priv_subnet $router_priv_ip $router_pub_ip $switch_ip + if [[ -z $double_nat ]]; then + # Setup router + ip netns exec $router_name ./setup_router.sh $router_name $priv_name public $priv_subnet $router_priv_ip $router_pub_ip $switch_ip 0 + + # Setup private network + ip netns exec $priv_name ./setup_private.sh $router_name $router_pub_ip $priv_subnet + else + # Create namespace for the additional private network + double_name="double${i}" + ./create_namespace.sh $double_name + + # Variables related to the additional private network and its router + double_prefix="172.16.${i}" + double_subnet="${priv_prefix}.0/24" + double_ip="${double_prefix}.254" + + # Setup first router + ip netns exec $router_name ./setup_router.sh $router_name $double_name public $double_subnet $double_ip $router_pub_ip $switch_ip 0 + + # Setup additional router + ip netns exec $double_name ./setup_router.sh $double_name $priv_name $router_name $priv_subnet $router_priv_ip $double_ip $router_pub_ip 1 - # Setup private network - ip netns exec $priv_name ./setup_private.sh $router_name $router_pub_ip $priv_subnet + # Setup private network + ip netns exec $priv_name ./setup_private.sh $double_name $double_ip $priv_subnet + fi # Setup peers in each private network for ((j=1; j<=n_peers; j++)); do diff --git a/test_suite/nat_simulation/setup_router.sh b/test_suite/nat_simulation/setup_router.sh index 0444074..42bbbe4 100755 --- a/test_suite/nat_simulation/setup_router.sh +++ b/test_suite/nat_simulation/setup_router.sh @@ -2,14 +2,16 @@ router_name=$1 priv_name=$2 -priv_subnet=$3 -priv_ip=$4 -pub_ip=$5 -switch_ip=$6 - -if [[ $# -ne 6 ]]; then +public_name=$3 +priv_subnet=$4 +priv_ip=$5 +pub_ip=$6 +switch_ip=$7 +router_index=$8 + +if [[ $# -ne 8 ]]; then echo """ -Usage: ${0} +Usage: ${0} This script must be run with root permissions""" exit 1 @@ -22,20 +24,27 @@ ip netns exec $priv_name ip addr add "${priv_ip}/24" dev $router_name ip link set $router_priv up ip netns exec $priv_name ip link set $router_name up -# Create veth pair to place the router's public interface in the public and router namespaces -router_pub="${router_name}_pub" -ip link add $router_pub type veth peer $router_name netns public -ip addr add "${pub_ip}/24" dev $router_pub -ip link set $router_pub up -ip netns exec public ip link set $router_name up - -# Add switch as default gateway -ip route add $switch_ip dev $router_pub -ip route add default via $switch_ip dev $router_pub - # Add route for traffic to router's private network ip route add $priv_ip dev $router_priv ip route add $priv_subnet via $priv_ip dev $router_priv -# Create route to router in the public network -ip netns exec public ip route add $pub_ip dev $router_name \ No newline at end of file +# $router_index is equal to 0 for first router, 1 for additional router +if [[ $router_index -eq 0 ]]; then + # Create veth pair to place the router's public interface in the public and router namespaces + router_pub="${router_name}_pub" + ip link add $router_pub type veth peer $router_name netns public + ip addr add "${pub_ip}/24" dev $router_pub + ip link set $router_pub up + ip netns exec public ip link set $router_name up + + # Create route to first router in the public network + ip netns exec $public_name ip route add $pub_ip dev $router_name +else + # Veth pair already created by first router + router_pub=$public_name +fi + +# Show switch is routable via router as default gateway +ip route add $switch_ip dev $router_pub +ip route add default via $switch_ip dev $router_pub + diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index 12096f2..6ece76c 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -28,7 +28,7 @@ while getopts ":b:h" opt; do case $opt in b) baseline=$OPTARG - validate_str $baseline "^direct|wireguard|both$" + validate_str $baseline "^direct$|^wireguard$|^both$" case $baseline in "direct") diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index 43d0a82..a21c48a 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -4,14 +4,11 @@ SYSTEM_TEST_TIMEOUT=60 usage_str=""" -Usage: ${0} [OPTIONAL ARGUMENTS] [NAT CONFIGURATION 1]:[NAT CONFIGURATION 2] [WIREGUARD INTERFACE 1]:[WIREGUARD INTERFACE 2] +Usage: ${0} [OPTIONAL ARGUMENTS] [WIREGUARD INTERFACE 1]/[WIREGUARD INTERFACE 2] - is the expected result of the system test: - 1. TS_PASS_DIRECT: the peers have established a direct connection - 2. TS_PASS_RELAY: the peers have established a connection via the eduP2P relay server - 3. TS_FAIL: the peers failed to establish a connection - -[OPTIONAL ARGUMENTS] can be provided for a performance test: +The -2 option can be used to perform a test where both peers are behind two NATs instead of one. + +The remaining optional arguments can be provided for a performance test: -k -v -d @@ -19,25 +16,44 @@ Usage: ${0} [OPTIONAL ARGUMENTS] [NAT CO -b With this flag, eduP2P's performance is compared to the performance of a direct connection and/or a connection using only WireGuard This flag should only be used when both peers reside in the 'public' network + + is the expected result of the system test: + 1. TS_PASS_DIRECT: the peers have established a direct connection + 2. TS_PASS_RELAY: the peers have established a connection via the eduP2P relay server + 3. TS_FAIL: the peers failed to establish a connection + + specifies the peer and NAT namespaces to be used in this system test. It should be a string with one of the following formats: + 1. /, for peers in the public network + 2. :/, for one peer in a private network and the other in the public network + 3. :: for peers in the same private network + 4. :/: for peers in different private networks + +By default, and should be the namespaces of the routers used by the corresponding peers. If Double NAT is enabled with the -2 option, they should both have the format : + + specifies the type of NAT applied by each router. It follows a format similar to : + 1. /, for peers in the public network + 2. /, for one peer in a private network and the other in the public network + 3. for peers in the same private network + 4. / for peers in different private networks + +By default, and , should follow the format -, where both may be one of the following numbers: + 0 - Endpoint-Independent + 1 - Address-Dependent + 2 - Address and Port-Dependent + +If Double NAT is enabled with the -2 option, they should both have the format -:- - specifies the peer and router namespaces to be used in this system test. It should be a string with one of the following formats: - 1. -, for peers in the public network - 2. -:, for one peer in a private network and the other in the public network - 3. -- for peers in the same private network - 4. -:- for peers in different private networks - -[NAT CONFIGURATION 1] and [NAT CONFIGURATION 2] specify the type of NAT applied to packets sent by peer 1 and 2 respectively. They should equal an empty string if the corresponding peer is in the public network, and otherwise follow this format: - -, where both may be one of the following numbers: - 0 - Endpoint-Independent - 1 - Address-Dependent - 2 - Address and Port-Dependent -Examples of valid NAT configurations: 0-1:1-2 (both peers in private networks), 0-1: (peer 2 in public network), : (both peers in public network) +Examples of valid NAT configurations: + '/' for both peers in public network) + '0-1/' for peer 2 in public network + '0-1/1-2' for both peers in private networks + '0-0:1-2/2-1:1-0' for both peers in private networks behind two NATs If [WIREGUARD INTERFACE 1] or [WIREGUARD INTERFACE 2] is not provided, the corresponding peer will use userspace WireGuard is a string of IP addresses separated by a space that may be the destination IP of packets crossing this NAT device, and is necessary to simulate an Address-Dependent Mapping - should be one of {trace|debug|info|warn|error}, and MUST be trace/debug if one of the peers uses userspace WireGuard (the other peer's IP address is not logged otherwise)""" + should be one of {trace/debug/info/warn/error}, and MUST be trace/debug if one of the peers uses userspace WireGuard (the other peer's IP address is not logged otherwise)""" # Use functions and constants from util.sh . ./util.sh @@ -46,11 +62,11 @@ performance_test_duration=5 # Default value in case -d is not used performance_test_reps=1 # Default value in case -r is not used # Validate optional arguments -while getopts ":k:v:d:r:b:h" opt; do +while getopts ":k:v:d:r:b:2h" opt; do case $opt in k) performance_test_var=$OPTARG - validate_str $performance_test_var "^bitrate|delay|packet_loss$" + validate_str $performance_test_var "^bitrate$|^delay$|^packet_loss$" ;; v) performance_test_values=$OPTARG @@ -74,10 +90,12 @@ while getopts ":k:v:d:r:b:h" opt; do exit_with_error "value of -r should be at least 1" fi ;; - + 2) + double_nat=true + ;; b) performance_test_baseline=$OPTARG - validate_str $performance_test_baseline "^direct|wireguard|both$" + validate_str $performance_test_baseline "^direct$|^wireguard$|^both$" baseline="-b $performance_test_baseline" ;; @@ -113,11 +131,18 @@ log_dir=${11} repo_dir=${12} # Validate namespace configuration string -ns_regex="([^-:]+)" # One or more occurence of every character except '-' and ':' (these are used to separate the namespaces) -ns_config1_regex="^${ns_regex}-${ns_regex}$" -ns_config2_regex="^${ns_regex}-${ns_regex}:${ns_regex}$" -ns_config3_regex="^${ns_regex}-${ns_regex}-${ns_regex}$" -ns_config4_regex="^${ns_regex}-${ns_regex}:${ns_regex}-${ns_regex}$" +peer_ns_regex="([^:/]+)" # One or more occurence of every character except ':' and '/' (these are used to separate the namespaces) + +if [[ -z $double_nat ]]; then + router_ns_regex=$peer_ns_regex +else + router_ns_regex="${peer_ns_regex}:${peer_ns_regex}" +fi + +ns_config1_regex="^${peer_ns_regex}/${peer_ns_regex}$" +ns_config2_regex="^${peer_ns_regex}:${router_ns_regex}/${peer_ns_regex}$" +ns_config3_regex="^${peer_ns_regex}:${router_ns_regex}:${peer_ns_regex}$" +ns_config4_regex="^${peer_ns_regex}:${router_ns_regex}/${router_ns_regex}:${peer_ns_regex}$" validate_str $ns_config_str "$ns_config1_regex|$ns_config2_regex|$ns_config3_regex|$ns_config4_regex" # Remove empty string elements in BASH_REMATCH, so that it only contains the matches of exactly one configuration @@ -138,18 +163,27 @@ fi # NAT configuration parsing depends on the amount of routers n_routers=${#router_ns_list[@]} -nat_config_regex="([0-2])-([0-2])" -nat_map=() -nat_filter=() + +if [[ -z $double_nat ]]; then + n_private=$n_routers + nat_config_regex="([0-2])-([0-2])" +else + n_private=$(($n_routers / 2)) + nat_config_regex="([0-2])-([0-2]):([0-2])-([0-2])" +fi # Ensure the NAT configuration is provided for all routers -case $n_routers in - 0) validate_str $nat_config_str "^:$";; - 1) validate_str $nat_config_str "^$nat_config_regex:$";; - 2) validate_str $nat_config_str "^$nat_config_regex:$nat_config_regex$";; +case $n_private in + 0) validate_str $nat_config_str "^/$";; + 1) validate_str $nat_config_str "^$nat_config_regex$|^$nat_config_regex/$" + BASH_REMATCH=(${BASH_REMATCH[@]}) ;; + 2) validate_str $nat_config_str "^$nat_config_regex/$nat_config_regex$";; esac # Store the individual Mapping and Filtering types +nat_map=() +nat_filter=() + for ((i=0; i<$n_routers; i++)); do map_idx=$((1 + 2 * $i)) filter_idx=$((2 + 2 * $i)) @@ -158,7 +192,7 @@ for ((i=0; i<$n_routers; i++)); do done # Parse WireGuard interfaces string into individual interfaces -wg_interface_regex="^([^:]*):([^:]*)$" +wg_interface_regex="^([^/]*)/([^/]*)$" validate_str $wg_interface_str $wg_interface_regex wg_interfaces=(${BASH_REMATCH[1]} ${BASH_REMATCH[2]}) @@ -173,10 +207,21 @@ NAT_TYPES=("EI" "AD" "APD") function describe_nat() { i=$1 - if [[ $i < $n_routers ]]; then + function helper() { + i=$1 + echo "${NAT_TYPES[${nat_map[$i]}]}M-${NAT_TYPES[${nat_filter[$i]}]}F" - else + } + + if [[ $i -ge $n_private ]]; then echo "No-NAT" + elif [[ -z $double_nat ]]; then + echo $(helper $i) + else + idx1=$((2*i)) + idx2=$((2*i+1)) + + echo "$(helper $idx1):$(helper $idx2)" fi } @@ -186,7 +231,7 @@ if [[ $hairpinning == true ]]; then else nat1_description=$(describe_nat 0) nat2_description=$(describe_nat 1) - nat_setup="$nat1_description <-> $nat2_description" + nat_setup="$nat1_description/$nat2_description" fi # Prepare a string describing the test setup @@ -251,17 +296,45 @@ trap "clean_exit 1" SIGTERM # Start NAT simulation on each router cd ${repo_dir}/test_suite/nat_simulation -for ((i=0; i<${#router_ns_list[@]}; i++)); do - router_ns=${router_ns_list[$i]} - router_pub="${router_ns}_pub" - router_priv="${router_ns}_priv" - router_pub_ip="192.168.$((i+1)).254" +for ((i=0; i<$n_private; i++)); do priv_prefix="10.0.$((i+1)).0/24" - sudo ip netns exec $router_ns ./setup_nat_mapping.sh $router_pub $priv_prefix ${nat_map[$i]} "${adm_ips}" + if [[ -z $double_nat ]]; then + router_pub_ip="192.168.$((i+1)).254" + router_ns=${router_ns_list[$i]} + router_pub="${router_ns}_pub" + router_priv="${router_ns}_priv" + + sudo ip netns exec $router_ns ./setup_nat_mapping.sh $router_pub $priv_prefix ${nat_map[$i]} "${adm_ips}" + sudo ip netns exec $router_ns ./setup_nat_filtering_hairpinning.sh $router_pub $router_priv $router_pub_ip $priv_prefix ${nat_filter[$i]} 2>&1 | \ + tee ${log_dir}/$router_ns.txt > /dev/null & # Combination of tee and redirect to /dev/null is necessary to avoid weird behaviour caused by redirecting a script run with sudo + else + # With both peers in private networks, the router order is different between the peers + if [[ $i -eq 1 ]]; then + idx1=$((2*i)) + idx2=$((2*i+1)) + else + idx1=$((2*i+1)) + idx2=$((2*i)) + fi + + router1_pub_ip="192.168.$((i+1)).254" + router1_ns=${router_ns_list[$idx1]} + router1_pub="${router1_ns}_pub" + router1_priv="${router1_ns}_priv" + double_prefix="172.16.$((i+1)).0/24" - sudo ip netns exec $router_ns ./setup_nat_filtering_hairpinning.sh $router_pub $router_priv $router_pub_ip $priv_prefix ${nat_filter[$i]} 2>&1 | \ - tee ${log_dir}/$router_ns.txt > /dev/null & # Combination of tee and redirect to /dev/null is necessary to avoid weird behaviour caused by redirecting a script run with sudo + sudo ip netns exec $router1_ns ./setup_nat_mapping.sh $router1_pub $double_prefix ${nat_map[$idx1]} "${adm_ips}" + sudo ip netns exec $router1_ns ./setup_nat_filtering_hairpinning.sh $router1_pub $router1_priv $router1_pub_ip $double_prefix ${nat_filter[$idx1]} 2>&1 | tee ${log_dir}/$router1_ns.txt > /dev/null & + + router2_pub_ip="172.16.$((i+1)).254" + router2_ns=${router_ns_list[$idx2]} + router2_pub="${router1_ns}" + router2_priv="${router2_ns}_priv" + + sudo ip netns exec $router2_ns ./setup_nat_mapping.sh $router2_pub $priv_prefix ${nat_map[$idx2]} "${adm_ips}" + sudo ip netns exec $router2_ns ./setup_nat_filtering_hairpinning.sh $router2_pub $router2_priv $router2_pub_ip $priv_prefix ${nat_filter[$idx2]} 2>&1 | tee ${log_dir}/$router2_ns.txt > /dev/null & + fi done # Execute scripts to start the peers diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 719fc18..5a2c5f6 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -6,6 +6,8 @@ Usage: ${0} [OPTIONAL ARGUMENTS] This script runs system tests between two eduP2P peers sequentially The following options determine the type of tests run: + -2 + Run connectivity tests with Double NAT: both peers are separated from the public network with two NATs, instead of the default single NAT -e Run extended connectivity tests (all combinations of RFC 4787 NAT mapping and filtering behaviour) -f @@ -41,7 +43,7 @@ The following options can be used to configure additional parameters during the log_lvl="debug" # Validate optional arguments -while getopts ":c:d:ef:l:L:t:bph" opt; do +while getopts ":c:d:ef:l:L:t:2bph" opt; do case $opt in c) connectivity=true @@ -79,7 +81,7 @@ while getopts ":c:d:ef:l:L:t:bph" opt; do l) log_lvl=$OPTARG - log_lvl_regex="^trace|debug|info|warn|error?$" + log_lvl_regex="^trace$|^debug$|^info$|^warn$|^error$" validate_str $log_lvl $log_lvl_regex ;; L) @@ -94,6 +96,9 @@ while getopts ":c:d:ef:l:L:t:bph" opt; do threads_regex="^[2-8]$" validate_str $n_threads $int_regex ;; + 2) + double_nat="-2" + ;; b) build=true ;; @@ -145,7 +150,7 @@ function build_go() { function setup_networks() { cd nat_simulation/ - adm_ips=$(sudo ./setup_networks.sh) # setup_networks.sh returns an array of IPs used by hosts in the network simulation setup, this list is needed to simulate a NAT device with an Address-Dependent Mapping + adm_ips=$(sudo ./setup_networks.sh $double_nat) # setup_networks.sh returns an array of IPs used by hosts in the network simulation setup, this list is needed to simulate a NAT device with an Address-Dependent Mapping } function extract_server_pub_key() { @@ -335,6 +340,27 @@ function run_system_test() { fi } +function filter_nat_combinations { + test_target=$1 + ns_config=$2 + nat_config=$3 + wg_config=$4 + nat1=$5 # Optional + nat2=$6 # Optional + nat3=$7 # Optional + nat4=$8 # Optional + + rfc_3489_nats=("0-0" "0-1" "0-2" "2-2") + + # Only test RFC 3489 NATs unless the extended flag was set + if [[ ( ${rfc_3489_nats[*]} =~ $nat1 && ${rfc_3489_nats[*]} =~ $nat2 && ${rfc_3489_nats[*]} =~ $nat3 && ${rfc_3489_nats[*]} =~ $nat4 ) || $extended == true ]]; then + # Only test Double NAT configurations where the two NATs are different + if [[ -z $double_nat || $nat1 != $nat2 && (-z $nat3 || $nat3 != $nat4 ) ]]; then + run_system_test $double_nat $test_target $ns_config $nat_config $wg_config + fi + fi +} + function connectivity_test_logic() { ns_config=$1 wg_config=$2 @@ -364,17 +390,45 @@ function connectivity_test_logic() { nat1=$nat1_mapping-$nat1_filter nat2=$nat2_mapping-$nat2_filter - # Only test RFC 3489 NATs unless the extended flag was set - if [[ ( ${rfc_3489_nats[*]} =~ $nat1 && ${rfc_3489_nats[*]} =~ $nat2 ) || $extended == true ]]; then - nat_config=$nat1:$nat2 - run_system_test $test_target $ns_config $nat_config $wg_config + filter_nat_combinations $test_target $ns_config $nat1/$nat2 $wg_config $nat1 $nat2 + fi +} + +function connectivity_test_logic_double_nat() { + ns_config=$1 + wg_config=$2 + nat1_mapping=$3 + nat1_filter=$4 + nat2_mapping=$5 + nat2_filter=$6 + nat3_mapping=$7 + nat3_filter=$8 + nat4_mapping=$9 + nat4_filter=${10} + + nat1=$nat1_mapping-$nat1_filter + nat2=$nat2_mapping-$nat2_filter + nat3=$nat3_mapping-$nat3_filter + nat4=$nat4_mapping-$nat4_filter + + # Two Double NAT with at least one Symmetric NAT results in a relay connection + if [[ -n $nat4 && "$nat1 $nat2 $nat3 $nat4" =~ 2-2 ]]; then + test_target="TS_PASS_RELAY" + else + test_target="TS_PASS_DIRECT" + fi + + # Skip symmetrical cases + if [[ $nat3_mapping -gt $nat1_mapping || $nat3_mapping -eq $nat1_mapping && $nat3_filter -ge $nat1_filter ]]; then + if [[ $nat4_mapping -gt $nat2_mapping || $nat4_mapping -eq $nat2_mapping && $nat4_filter -ge $nat2_filter ]]; then + filter_nat_combinations $test_target $ns_config $nat1:$nat2/$nat3:$nat4 $wg_config $nat1 $nat2 $nat3 $nat4 fi fi } if [[ $performance == true ]]; then log_sequential "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 100,200,300,400,500 -d 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test $double_nat -k bitrate -v 100,200,300,400,500 -d 3 -b both TS_PASS_DIRECT router1/router2 / wg0/wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" @@ -383,8 +437,6 @@ elif [[ -n $file ]]; then eval $test_cmd done < $file else - rfc_3489_nats=("0-0" "0-1" "0-2" "2-2") - log_sequential """ Starting connectivity tests between two peers (possibly) behind NATs with various combinations of mapping and filtering behaviour: - Endpoint-Independent Mapping/Filtering (EIM/EIF) @@ -392,13 +444,20 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou - Address and Port-Dependent Mapping/Filtering (ADPM/ADPF)""" log_sequential "\nTests with one peer behind a NAT" - for nat_mapping in {0..2}; do - for nat_filter in {0..2}; do - nat=$nat_mapping-$nat_filter + for nat1_mapping in {0..2}; do + for nat1_filter in {0..2}; do + nat1=$nat1_mapping-$nat1_filter - # Only test RFC 3489 NATs unless the extended flag was set - if [[ ${rfc_3489_nats[*]} =~ $nat || $extended == true ]]; then - run_system_test TS_PASS_DIRECT private1_peer1-router1:router2 $nat: wg0: + if [[ -z $double_nat ]]; then + filter_nat_combinations TS_PASS_DIRECT private1_peer1:router1/router2 $nat1/ wg0/ $nat1 + else + for nat2_mapping in {0..2}; do + for nat2_filter in {0..2}; do + nat2=$nat2_mapping-$nat2_filter + + filter_nat_combinations TS_PASS_DIRECT private1_peer1:double1:router1/router2 $nat1:$nat2/ wg0/ $nat1 $nat2 + done + done fi done done @@ -408,20 +467,39 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou for nat1_filter in {0..2}; do for nat2_mapping in {0..2}; do for nat2_filter in {0..2}; do - connectivity_test_logic private1_peer1-router1:router2-private2_peer1 wg0: $nat1_mapping $nat1_filter $nat2_mapping $nat2_filter + if [[ -z $double_nat ]]; then + connectivity_test_logic private1_peer1:router1/router2:private2_peer1 wg0/ $nat1_mapping $nat1_filter $nat2_mapping $nat2_filter + else + for nat3_mapping in {0..2}; do + for nat3_filter in {0..2}; do + for nat4_mapping in {0..2}; do + for nat4_filter in {0..2}; do + connectivity_test_logic_double_nat private1_peer1:double1:router1/router2:double2:private2_peer1 wg0/ $nat1_mapping $nat1_filter $nat2_mapping $nat2_filter $nat3_mapping $nat3_filter $nat4_mapping $nat4_filter + done + done + done + done + fi done done done done log_sequential "\nTest hairpinning" - for nat_mapping in {0..2}; do - for nat_filter in {0..2}; do - nat=$nat_mapping-$nat_filter + for nat1_mapping in {0..2}; do + for nat1_filter in {0..2}; do + nat1=$nat1_mapping-$nat1_filter - # Only test RFC 3489 NATs unless the extended flag was set - if [[ ${rfc_3489_nats[*]} =~ $nat || $extended == true ]]; then - run_system_test TS_PASS_DIRECT private1_peer1-router1-private1_peer2 $nat: wg0: + if [[ -z $double_nat ]]; then + filter_nat_combinations TS_PASS_DIRECT private1_peer1:router1:private1_peer2 $nat1 wg0/ $nat1 + else + for nat2_mapping in {0..2}; do + for nat2_filter in {0..2}; do + nat2=$nat2_mapping-$nat2_filter + + filter_nat_combinations TS_PASS_DIRECT private1_peer1:double1:router1:private1_peer2 $nat1:$nat2 wg0/ $nat1 $nat2 + done + done fi done done From 00d9054a6f5de1fa1600209169f5a79c6254c71a Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 9 Sep 2025 11:08:35 +0200 Subject: [PATCH 02/11] Add logic to calculate expected test result for Double RFC 3489 NATs, and fix compatibility between Double NAT and parallel system tests --- test_suite/system_tests.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 5a2c5f6..e0cb5f2 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -115,6 +115,8 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do esac done +system_test_opts=$@ + # Store repository's root directory for later use repo_dir=$(cd ..; pwd) @@ -260,7 +262,7 @@ function parallel_setup() { Dividing the system tests among $n_threads threads. The output of each thread can be found in the logs.""" # The current system tests command will be run in parallel docker containers with a few modifications: - system_test_opts=$(echo $@ | sed -r -e "s/-f \S+//" `# Potential -f flag is removed, as each docker container will be assigned a file containing a subset of the current system tests` \ + system_test_opts=$(echo $system_test_opts | sed -r -e "s/-f \S+//" `# Potential -f flag is removed, as each docker container will be assigned a file containing a subset of the current system tests` \ -e "s/-t [2-8]//") # -t flag is removed, since each docker container will run the tests in parallel` # Tests will be assigned to the containers in a round-robin manner, so we keep track of the current thread @@ -394,6 +396,7 @@ function connectivity_test_logic() { fi } +# This function currently takes only RFC 3489 NATs into account function connectivity_test_logic_double_nat() { ns_config=$1 wg_config=$2 @@ -411,8 +414,9 @@ function connectivity_test_logic_double_nat() { nat3=$nat3_mapping-$nat3_filter nat4=$nat4_mapping-$nat4_filter - # Two Double NAT with at least one Symmetric NAT results in a relay connection - if [[ -n $nat4 && "$nat1 $nat2 $nat3 $nat4" =~ 2-2 ]]; then + # TS_PASS_RELAY only if one peer is behind at least one Symmetric NAT, and the other peer is behind at least one Symmetric/Port Restricted Cone NAT + # Since we skip symmetrical cases (see below), we can assume the Symmetric NAT is on peer 2's side + if [[ -n $nat4 && ( $nat1_filter -eq 2 || $nat2_filter -eq 2 ) && "$nat3 $nat4" =~ 2-2 ]]; then test_target="TS_PASS_RELAY" else test_target="TS_PASS_DIRECT" @@ -497,7 +501,15 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou for nat2_filter in {0..2}; do nat2=$nat2_mapping-$nat2_filter - filter_nat_combinations TS_PASS_DIRECT private1_peer1:double1:router1:private1_peer2 $nat1:$nat2 wg0/ $nat1 $nat2 + if [[ $nat1_mapping -ge 1 && $nat1_filter -eq 2 ]]; then + # Hairpinning is done by nat2, so its mapping/filtering behaviour is irrelevant + # However, if nat1 is A(P)DM-ADPF, UDP hole punching will fail because both peers are behind a too restrictive NAT + test_target=TS_PASS_RELAY + else + test_target=TS_PASS_DIRECT + fi + + filter_nat_combinations $test_target private1_peer1:double1:router1:private1_peer2 $nat1:$nat2 wg0/ $nat1 $nat2 done done fi From 9f12c51e19c455b0133549236c117dd82a31388f Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 9 Sep 2025 15:06:59 +0200 Subject: [PATCH 03/11] Update regex validation to prevent empty string from always passing check --- test_suite/performance_test.sh | 2 +- test_suite/system_test.sh | 18 +++++++++--------- test_suite/system_tests.sh | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index 6ece76c..3ecb95f 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -28,7 +28,7 @@ while getopts ":b:h" opt; do case $opt in b) baseline=$OPTARG - validate_str $baseline "^direct$|^wireguard$|^both$" + validate_str "$baseline" "^direct$|^wireguard$|^both$" case $baseline in "direct") diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index a21c48a..fdc0e8d 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -66,7 +66,7 @@ while getopts ":k:v:d:r:b:2h" opt; do case $opt in k) performance_test_var=$OPTARG - validate_str $performance_test_var "^bitrate$|^delay$|^packet_loss$" + validate_str "$performance_test_var" "^bitrate$|^delay$|^packet_loss$" ;; v) performance_test_values=$OPTARG @@ -80,11 +80,11 @@ while getopts ":k:v:d:r:b:2h" opt; do ;; d) performance_test_duration=$OPTARG - validate_str $performance_test_duration "^[0-9]+$" + validate_str "$performance_test_duration" "^[0-9]+$" ;; r) performance_test_reps=$OPTARG - validate_str $performance_test_duration "^[0-9]+$" + validate_str "$performance_test_duration" "^[0-9]+$" if [[ $performance_test_reps -eq 0 ]]; then exit_with_error "value of -r should be at least 1" @@ -95,7 +95,7 @@ while getopts ":k:v:d:r:b:2h" opt; do ;; b) performance_test_baseline=$OPTARG - validate_str $performance_test_baseline "^direct$|^wireguard$|^both$" + validate_str "$performance_test_baseline" "^direct$|^wireguard$|^both$" baseline="-b $performance_test_baseline" ;; @@ -143,7 +143,7 @@ ns_config1_regex="^${peer_ns_regex}/${peer_ns_regex}$" ns_config2_regex="^${peer_ns_regex}:${router_ns_regex}/${peer_ns_regex}$" ns_config3_regex="^${peer_ns_regex}:${router_ns_regex}:${peer_ns_regex}$" ns_config4_regex="^${peer_ns_regex}:${router_ns_regex}/${router_ns_regex}:${peer_ns_regex}$" -validate_str $ns_config_str "$ns_config1_regex|$ns_config2_regex|$ns_config3_regex|$ns_config4_regex" +validate_str "$ns_config_str" "$ns_config1_regex|$ns_config2_regex|$ns_config3_regex|$ns_config4_regex" # Remove empty string elements in BASH_REMATCH, so that it only contains the matches of exactly one configuration BASH_REMATCH=(${BASH_REMATCH[@]}) @@ -174,10 +174,10 @@ fi # Ensure the NAT configuration is provided for all routers case $n_private in - 0) validate_str $nat_config_str "^/$";; - 1) validate_str $nat_config_str "^$nat_config_regex$|^$nat_config_regex/$" + 0) validate_str "$nat_config_str" "^/$";; + 1) validate_str "$nat_config_str" "^$nat_config_regex$|^$nat_config_regex/$" BASH_REMATCH=(${BASH_REMATCH[@]}) ;; - 2) validate_str $nat_config_str "^$nat_config_regex/$nat_config_regex$";; + 2) validate_str "$nat_config_str" "^$nat_config_regex/$nat_config_regex$";; esac # Store the individual Mapping and Filtering types @@ -193,7 +193,7 @@ done # Parse WireGuard interfaces string into individual interfaces wg_interface_regex="^([^/]*)/([^/]*)$" -validate_str $wg_interface_str $wg_interface_regex +validate_str "$wg_interface_str" $wg_interface_regex wg_interfaces=(${BASH_REMATCH[1]} ${BASH_REMATCH[2]}) # Remove conntrack entries from potential previous tests diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index e0cb5f2..6390634 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -51,7 +51,7 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do # Make sure packet_loss is a real number real_regex="^[0-9]+[.]?([0-9]+)?$" - validate_str $packet_loss $real_regex + validate_str "$packet_loss" $real_regex # Make sure packet loss is in the interval [0, 100) in_interval=$(echo "$packet_loss >= 0 && $packet_loss < 100" | bc) # 1=true, 0=false @@ -65,7 +65,7 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do # Make sure delay is an integer int_regex="^[0-9]+$" - validate_str $delay $int_regex + validate_str "$delay" $int_regex ;; e) extended=true @@ -82,11 +82,11 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do log_lvl=$OPTARG log_lvl_regex="^trace$|^debug$|^info$|^warn$|^error$" - validate_str $log_lvl $log_lvl_regex + validate_str "$log_lvl" $log_lvl_regex ;; L) alphanum_regex="^[a-zA-Z0-9]+$" - validate_str $OPTARG $alphanum_regex + validate_str "$OPTARG" $alphanum_regex log_dir_rel=system_test_logs/$OPTARG ;; t) @@ -94,7 +94,7 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do # Make sure n_threads is an integer between 2 and 8 threads_regex="^[2-8]$" - validate_str $n_threads $int_regex + validate_str "$n_threads" $int_regex ;; 2) double_nat="-2" From e7fada879d53860eca5603ace445e538df6d501a Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 9 Sep 2025 15:32:35 +0200 Subject: [PATCH 04/11] Remove duplicated code by moving frequently used regex patterns to util.sh --- test_suite/set_delay.sh | 4 ++-- test_suite/set_packet_loss.sh | 6 +++--- test_suite/system_test.sh | 1 - test_suite/system_tests.sh | 4 +--- test_suite/util.sh | 4 ++++ 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/test_suite/set_delay.sh b/test_suite/set_delay.sh index c94c4c9..4b29b38 100755 --- a/test_suite/set_delay.sh +++ b/test_suite/set_delay.sh @@ -7,9 +7,9 @@ Usage: ${0} delay=$1 -# Make sure delay is an integer -int_regex="^[0-9]+$" +. ./util.sh +# Make sure delay is an integer if [[ $# -ne 1 || ! ( $delay =~ $int_regex) ]]; then echo $usage_str exit 1 diff --git a/test_suite/set_packet_loss.sh b/test_suite/set_packet_loss.sh index 11d91d8..118e20a 100755 --- a/test_suite/set_packet_loss.sh +++ b/test_suite/set_packet_loss.sh @@ -7,10 +7,10 @@ Usage: ${0} packet_loss=$1 -# Make sure packet_loss is a real number, and get the amount of decimal digits -real_regex="^[0-9]+[.]?([0-9]+)?$" +. ./util.sh -if [[ $# -ne 1 || ! ( $packet_loss =~ $real_regex) ]]; then +# Make sure packet_loss is a real number, and get the amount of decimal digits +if [[ $# -ne 1 || ! ( $packet_loss =~ ^$real_regex$ ) ]]; then echo $usage_str exit 1 fi diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index fdc0e8d..e040d72 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -75,7 +75,6 @@ while getopts ":k:v:d:r:b:2h" opt; do exit_with_error "-k should be specified before -v" fi - real_regex="[0-9]+(.[0-9]+)?" validate_str "$performance_test_values" "^$real_regex(,$real_regex)*$" ;; d) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 6390634..2f7cf43 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -50,8 +50,7 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do packet_loss=$OPTARG # Make sure packet_loss is a real number - real_regex="^[0-9]+[.]?([0-9]+)?$" - validate_str "$packet_loss" $real_regex + validate_str "$packet_loss" ^$real_regex$ # Make sure packet loss is in the interval [0, 100) in_interval=$(echo "$packet_loss >= 0 && $packet_loss < 100" | bc) # 1=true, 0=false @@ -64,7 +63,6 @@ while getopts ":c:d:ef:l:L:t:2bph" opt; do delay=$OPTARG # Make sure delay is an integer - int_regex="^[0-9]+$" validate_str "$delay" $int_regex ;; e) diff --git a/test_suite/util.sh b/test_suite/util.sh index 8988fb2..26d17f1 100755 --- a/test_suite/util.sh +++ b/test_suite/util.sh @@ -7,6 +7,10 @@ RED="\033[0;31m" GREEN="\033[0;32m" NC="\033[0m" # No color +# Frequently used regular expressions +int_regex="^[0-9]+$" +real_regex="[0-9]+[.]?([0-9]+)?" # Allows for counting the amount of digits after the decimal point + function exit_with_error() { err_reason=$1 From 66bdde511ba6f6f2993b70913474d78f96ff455d Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 9 Sep 2025 15:40:21 +0200 Subject: [PATCH 05/11] Remove trailing underscore in log directory name of system tests with NAT hairpinning --- test_suite/system_test.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index e040d72..7ec2451 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -240,9 +240,13 @@ test_description="Test $test_idx. $nat_setup, target=$test_target, result=" echo -n "$test_description" # Add log subdirectory for this system test -new_dir="${log_dir}/${test_idx}_${nat1_description}_${nat2_description}" -mkdir $new_dir -log_dir=$new_dir +log_dir="${log_dir}/${test_idx}_${nat1_description}" + +if [[ $hairpinning != true ]]; then + log_dir="${log_dir}_${nat2_description}" +fi + +mkdir $log_dir function clean_exit() { exit_code=$1 From a19fbb1fc12e2ffefba2e7e493dd73c9ef8c4f0b Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 21 Oct 2025 21:32:00 +0200 Subject: [PATCH 06/11] Take IP pooling into account in Double NAT test result logic --- test_suite/system_tests.sh | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 59a6634..9602fd0 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -429,22 +429,34 @@ function connectivity_test_logic_double_nat() { nat3=$nat3_mapping-$nat3_filter nat4=$nat4_mapping-$nat4_filter + # TS_PASS_RELAY if peer 1 is behind at least one Port Restricted Cone/Symmetric NAT, and peer 2 is behind at least one Symmetric NAT + if [[ -n $nat4 && \ + ( ( $nat1_filter -eq 2 || $nat2_filter -eq 2 ) && "$nat3 $nat4" =~ 2-2 || \ + ( $nat3_filter -eq 2 || $nat4_filter -eq 2 ) && "$nat1 $nat2" =~ 2-2 )]]; then + test_target="TS_PASS_RELAY" # TS_PASS only if one peer is behind at least one Restricted Cone NAT, and the other peer is behind at least one Symmetric NAT - # Since we skip symmetrical cases (see below), we can assume the Symmetric NAT is on peer 2's side - if [[ -n $nat4 && ( $nat1_filter -eq 1 || $nat2_filter -eq 1 ) && "$nat3 $nat4" =~ 2-2 ]]; then + elif [[ -n $nat4 && \ + ( ( $nat1_filter -eq 1 || $nat2_filter -eq 1 ) && "$nat3 $nat4" =~ 2-2 || \ + ( $nat3_filter -eq 1 || $nat4_filter -eq 1 ) && "$nat1 $nat2" =~ 2-2 )]]; then test_target="TS_PASS" - # TS_PASS_RELAY only if peer 1 is behind at least one Port Restricted Cone/Symmetric NAT, and peer 2 is behind at least one Symmetric NAT - elif [[ -n $nat4 && ( $nat1_filter -eq 2 || $nat2_filter -eq 2 ) && "$nat3 $nat4" =~ 2-2 ]]; then - test_target="TS_PASS_RELAY" else test_target="TS_PASS_DIRECT" fi - # Skip symmetrical cases - if [[ $nat3_mapping -gt $nat1_mapping || $nat3_mapping -eq $nat1_mapping && $nat3_filter -ge $nat1_filter ]]; then - if [[ $nat4_mapping -gt $nat2_mapping || $nat4_mapping -eq $nat2_mapping && $nat4_filter -ge $nat2_filter ]]; then - filter_nat_combinations $test_target $ns_config $nat1:$nat2/$nat3:$nat4 $wg_config $nat1 $nat2 $nat3 $nat4 - fi + # Assign a score to each NAT, such that the RFC 3489 NAT types ordered by score are as follows: + ## 1. Full Cone = 0 + 0 = 0 + ## 2. Restricted Cone = 0 + 1 = 1 + ## 3. Port Restricted Cone = 0 + 2 = 2 + ## 4. Symmetric = 2 + 2 = 4 + nat1_score=$(echo "$nat1_mapping+$nat1_filter" | bc) + nat2_score=$(echo "$nat2_mapping+$nat2_filter" | bc) + nat3_score=$(echo "$nat3_mapping+$nat3_filter" | bc) + nat4_score=$(echo "$nat4_mapping+$nat4_filter" | bc) + + + # Use score to skip symmetrical cases + if [[ $nat3_score -gt $nat1_score || $nat3_score -eq $nat1_score && $nat4_score -ge $nat2_score ]]; then + filter_nat_combinations $test_target $ns_config $nat1:$nat2/$nat3:$nat4 $wg_config $nat1 $nat2 $nat3 $nat4 fi } From a936ee0166bf6192cf2bd8c7b8838b6abfa8434f Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 21 Oct 2025 21:32:23 +0200 Subject: [PATCH 07/11] Add Double NAT documentation + results, and update example system test commands in documentation to reflect new syntax --- test_suite/README.md | 99 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/test_suite/README.md b/test_suite/README.md index 7963c2f..4730567 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -245,8 +245,26 @@ conditions: public “peers” to be simulated by making a router act as a peer, since the routers have a public IP address. -The next section explains the types of NAT in this network setup, and -describes how they are implemented. +### Double NAT + +Until now, we have assumed there is a single NAT between the peers and +the public network. However, in the real world there may be multiple. +For example, Double NAT describes a scenario where a host is separated +from the public network by two NATs. To use Double NAT in the test +suite, the `-2` flag must be added when calling the +[system_tests.sh](system_tests.sh) script. + +Double NAT is implemented by adding the network namespaces `double1` and +`double2`, respectively between `private1`/`router1` and +`private2`/`router2`. The existing veth pair containing the `router1` +device is used to connect the `double1` and `router1` namespaces, while +a new veth pair containing a device called `double1` is used to connect +the `private1` and `router1` namespaces. This new device has IP address +`172.16.1.254`. The setup for the second private network is similar. + +Each simulated NAT device in the test suite can be configured +separately. The next section describes the types of NAT supported in the +test suite, and how they are implemented. ### Applying NAT @@ -462,9 +480,12 @@ RFC 4787 specifies two types of IP address pooling behaviours: sessions belonging to the same internal IP. We implement the second behaviour in the test suite, as it has more -potential to cause issues for P2P protocols. The implementation relies -on two nftables features: packet marking and maps, which act like -dictionaries. +potential to cause issues for P2P protocols. However, when Double NAT is +enabled, IP address pooling is only supported for the NAT facing the +public network. + +The implementation relies on two nftables features: packet marking and +maps, which act like dictionaries. Each packet that flows through the `nat` table is marked in the `prerouting` chain. The range of possible mark values is equal to the @@ -566,7 +587,7 @@ The following command runs tests from a file named Suppose `performance_test.txt` contains the following line: - run_system_test -k bitrate -v 100,200 -d 5 -b wireguard -r 3 TS_PASS_DIRECT router1-router2 : : + run_system_test -k bitrate -v 100,200 -d 5 -b wireguard -r 3 TS_PASS_DIRECT router1/router2 / / Then, running the system tests with the `-f performance_test.txt` option will execute a performance test with the following parameters: @@ -633,6 +654,9 @@ disables IP address pooling. The old results are followed by a section in which the differences resulting from the addition of IP address pooling are described. +Finally, the system tests results section is concluded by analysing how +Double NAT affects the results. + ## System Test Results Without IP Address Pooling Using the test suite’s system tests, we can get an overview of whether @@ -1285,6 +1309,63 @@ process described for the RFC 3489 Restricted Cone and Symmetric NAT combination. This is because the Restricted Cone NAT corresponds to an EIM-ADF NAT. +## Effect of Double NAT on System Test Results + +With Double NAT, we do not discuss the RFC 4787 NATs, as the amount of +possible combination would be too high. Even for the RFC 3489 NATs, +there are quite a lot of possible combinations. Therefore, we split this +section in two parts: + +1. We first discuss the Double NAT combinations where the two NATs + separating a host from the public network are of the *same* type. +2. Then, we discuss the combinations where the NATs are of a + *different* type. + +### Same-type Double NAT + +| Double NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | +|----|:---|:---|:---|:---| +| **Full Cone** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| **Restricted Cone** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :question: | +| **Port Restricted Cone** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | +| **Symmetric** | :white_check_mark: | :question: | :x: | :x: | + +The results in this table are equal to those of the [previous +table](#effect-of-ip-address-pooling-on-system-test-results). Therefore, +if both NATs have the same type, Double NAT does not affect the peers’ +ability to establish a direct connection. + +### Different-type Double NAT + +| Double NAT Type | FC/RC | FC/PRC | FC/Sym | RC/FC | RC/PRC | RC/Sym | PRC/FC | PRC/RC | PRC/Sym | Sym/FC | Sym/RC | Sym/PRC | +|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---| +| **FC/RC** | :white_check_mark: | :white_check_mark: | :question: | :white_check_mark: | :white_check_mark: | :question: | :white_check_mark: | :white_check_mark: | :question: | :question: | :question: | :question: | +| **FC/PRC** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | +| **FC/Sym** | :question: | :x: | :x: | :question: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | +| **RC/FC** | :white_check_mark: | :white_check_mark: | :question: | :white_check_mark: | :white_check_mark: | :question: | :white_check_mark: | :white_check_mark: | :question: | :question: | :question: | :question: | +| **RC/PRC** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | +| **RC/Sym** | :question: | :x: | :x: | :question: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | +| **PRC/FC** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | +| **PRC/RC** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | +| **PRC/Sym** | :question: | :x: | :x: | :question: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | +| **Sym/FC** | :question: | :x: | :x: | :question: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | +| **Sym/RC** | :question: | :x: | :x: | :question: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | +| **Sym/PRC** | :question: | :x: | :x: | :question: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | :x: | + +In this table, we see that the row for Double NAT X/Y contains the same +results as the row for Double NAT Y/X. For example, the row for FC/RC is +equal to the row for RC/FC. We can make the same observation about the +columns in the table. This shows that the order of the NAT types in +Double NAT does not affect the peers’ ability to establish a direct +connection + +We also observe that all rows and columns where one of the two NATs is a +Symmetric NAT contain the same results. The reason for this is that the +Symmetric NAT is the most restrictive of the RFC 3489 NATs, so the other +NAT’s behaviour does not impact the test outcome. In general, it holds +that the ability to establish a direct connection is determined by the +combination of each peer’s most restrictive NAT. + ## Performance Test Results The results in this section were measured on my own laptop with the @@ -1304,7 +1385,7 @@ reproducibility. Command used: - run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b both -r 5 TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b both -r 5 TS_PASS_DIRECT router1/router2 / wg0/wg0 With this command, we compare the performance of eduP2P, WireGuard and a direct connection between two peers in the test suite’s network setup. @@ -1357,7 +1438,7 @@ differ on other machines. Command used: - run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b both -r 5 TS_PASS_DIRECT router1-router2 : : + run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b both -r 5 TS_PASS_DIRECT router1/router2 / / This command repeats the performance test of the previous section, with the only difference being that now both peers use userspace WireGuard @@ -1378,7 +1459,7 @@ further, however: Command used: - run_system_test -k delay -v 0,1,2,3 -d 3 -b both -r 3 TS_PASS_DIRECT router1-router2 : : + run_system_test -k delay -v 0,1,2,3 -d 3 -b both -r 3 TS_PASS_DIRECT router1/router2 / / ![](./images/performance_tests/x_ow_delay_y_http_latency.png) From 451d34207b94ddfde0e5b8e8e4d1cab7edd2a0f7 Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 21 Oct 2025 21:40:27 +0200 Subject: [PATCH 08/11] Fix incorrect abbreviation for Address and Port-Dependent Filtering: ADPF -> APDF --- test_suite/README.md | 34 +++++++++---------- .../setup_nat_filtering_hairpinning.sh | 6 ++-- test_suite/system_tests.sh | 4 +-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/test_suite/README.md b/test_suite/README.md index 4730567..abd67e0 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -369,7 +369,7 @@ three different types of behaviour with the same naming convention: or port. 2. **Address-Dependent Filtering (ADF):** packets destined to `X':x1'` are filtered only if their source IP address does not equal `Y` -3. **Address and Port-Dependent Filtering (ADPF):** packets destined to +3. **Address and Port-Dependent Filtering (APDF):** packets destined to `X':x1'` are filtered only if their source endpoint does not equal `Y:y1`. @@ -383,7 +383,7 @@ these packets may be filtered, which is indicated by a dashed arrow: destined to a port on the NAT for which a mapping exists. 2. **ADF:** The packet from `Z:z` is filtered, because incoming packets to `X':x1'` are only accepted if they have source IP address `Y`. -3. **ADPF:** The packets from `Y:y2` and `Z:z` are filtered, because +3. **APDF:** The packets from `Y:y2` and `Z:z` are filtered, because incoming packets to `X':x1'` are only accepted if they have source IP address `Y` and source port `y1`. @@ -398,7 +398,7 @@ do not belong to an existing session are filtered. Each time an internal endpoint establishes a connection to a new external endpoint, a new session is also created. Therefore, the above -nftables rule is sufficient to simulate ADPF, since only the original +nftables rule is sufficient to simulate APDF, since only the original session’s endpoint can send packets to the corresponding mapped IP address. @@ -682,9 +682,9 @@ to: Mapping (EIM) and Address-Dependent Filtering (ADF). - **Port Restricted Cone NAT:** equivalent to a NAT with Endpoint-Independent Mapping (EIM) and Address and Port-Dependent - Filtering (ADPF). + Filtering (APDF). - **Symmetric NAT:** equivalent to a NAT with Address and Port-Dependent - Mapping (ADPM) and Address and Port-Dependent Filtering (ADPF). + Mapping (ADPM) and Address and Port-Dependent Filtering (APDF). The expected results are shown in the table below. A cell is marked with an ‘X’ if UDP hole punching is successful in the scenario where one peer @@ -1101,19 +1101,19 @@ conditions with packet loss. The results of extending the UDP hole punching experiment to all combinations of RFC 4787 mapping (EIM, ADM, ADPM) and filtering (EIF, -ADF, ADPF) behaviours are shown in the table below: +ADF, APDF) behaviours are shown in the table below: -| NAT Type | EIM-EIF | EIM-ADF | EIM-ADPF | ADM-EIF | ADM-ADF | ADM-ADPF | ADPM-EIF | ADPM-ADF | ADPM-ADPF | +| NAT Type | EIM-EIF | EIM-ADF | EIM-APDF | ADM-EIF | ADM-ADF | ADM-APDF | ADPM-EIF | ADPM-ADF | ADPM-APDF | |:---|:---|:---|:---|:---|:---|:---|:---|:---|:---| | **EIM-EIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **EIM-ADF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| **EIM-ADPF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | +| **EIM-APDF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | | **ADM-EIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **ADM-ADF** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | -| **ADM-ADPF** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | +| **ADM-APDF** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | | **ADPM-EIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **ADPM-ADF** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | -| **ADPM-ADPF** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | +| **ADPM-APDF** | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | Based on these results, we can conclude that there are three (overlapping) types of NAT scenarios where the UDP hole punching process @@ -1160,7 +1160,7 @@ To substantiate this claim, we examine the general UDP hole punching process for the two least restrictive NAT combinations in the above table where a direct connection could not be established: -1. **One peer behind an EIM-ADPF NAT, and the other behind an ADM-ADF +1. **One peer behind an EIM-APDF NAT, and the other behind an ADM-ADF NAT.** The UDP hole punching between the peers in this NAT scenario is @@ -1171,7 +1171,7 @@ table where a direct connection could not be established: autonumber actor p1 as Peer 1 (X:x) - participant nat1 as EIM-ADPF NAT + participant nat1 as EIM-APDF NAT participant nat2 as ADM-ADF NAT actor p2 as Peer 2 (Y:y) @@ -1199,7 +1199,7 @@ table where a direct connection could not be established: The problem in this scenario is that Peer 1 is sending pings to `Y':y1'`, while Peer 2 is sending them from `Y':y2'`. Peer 1’s NAT - will always drop the packets from Peer 2 because it has ADPF + will always drop the packets from Peer 2 because it has APDF behaviour and `y1'` is not equal to `y2'`. Peer 2’s NAT will accept packets from source IP `X'` destined to `Y':y2'` after sending its first ping from `Y':y2'` to `X':x1'`, but Peer 1 is sending packets @@ -1288,17 +1288,17 @@ destination IP of an existing session. ### Experiment with RFC 4787 NAT mapping & filtering behaviours -| NAT Type | EIM-EIF | EIM-ADF | EIM-ADPF | ADM-EIF | ADM-ADF | ADM-ADPF | ADPM-EIF | ADPM-ADF | ADPM-ADPF | +| NAT Type | EIM-EIF | EIM-ADF | EIM-APDF | ADM-EIF | ADM-ADF | ADM-APDF | ADPM-EIF | ADPM-ADF | ADPM-APDF | |:---|:---|:---|:---|:---|:---|:---|:---|:---|:---| | **EIM-EIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **EIM-ADF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :question: | :question: | :white_check_mark: | :question: | :question: | -| **EIM-ADPF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | +| **EIM-APDF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | | **ADM-EIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **ADM-ADF** | :white_check_mark: | :question: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | -| **ADM-ADPF** | :white_check_mark: | :question: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | +| **ADM-APDF** | :white_check_mark: | :question: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | | **ADPM-EIF** | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | **ADPM-ADF** | :white_check_mark: | :question: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | -| **ADPM-ADPF** | :white_check_mark: | :question: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | +| **ADPM-APDF** | :white_check_mark: | :question: | :x: | :white_check_mark: | :x: | :x: | :white_check_mark: | :x: | :x: | Just like with the RFC 3489 experiment, the change in results caused by NAT IP address pooling is fairly limited. The outcome of UDP hole diff --git a/test_suite/nat_simulation/setup_nat_filtering_hairpinning.sh b/test_suite/nat_simulation/setup_nat_filtering_hairpinning.sh index a8079da..eea939e 100755 --- a/test_suite/nat_simulation/setup_nat_filtering_hairpinning.sh +++ b/test_suite/nat_simulation/setup_nat_filtering_hairpinning.sh @@ -22,7 +22,7 @@ fi # Configure NAT filtering type with nftables nft add table inet filter nft add chain inet filter input { type filter hook input priority 0\; policy drop\; } -nft add rule inet filter input ct state related,established counter accept # This rule is sufficient to simulate ADPF +nft add rule inet filter input ct state related,established counter accept # This rule is sufficient to simulate APDF # This pattern captures the following info from a conntrack event: # 1) the source IP @@ -36,7 +36,7 @@ pattern=".*src=(\S+).*sport=(\S+).*src=(\S+).*dst=(\S+).*dport=(\S+).*$" hairpin_rule1="nat prerouting iif $priv_nat_iface ip saddr $priv_subnet ip daddr \4 meta l4proto {tcp, udp} th dport \5 counter dnat to \1:\2" # All traffic from the private network destined to \4:\5 should be hairpinned pack to \1:\2 hairpin_rule2="nat postrouting iif $priv_nat_iface ip saddr \1 ip daddr $priv_subnet meta l4proto {tcp, udp} th sport \2 counter snat to \4:\5" # For all hairpinned packets from \1:\2, the source becomes \4:\5 -# Filtering (not necessary for ADPF because of filter rule above) +# Filtering (not necessary for APDF because of filter rule above) case $nat_filter in 0) # If a mapping is created with source IP \1, source port \2 and translated source port \5, all traffic destined to \5 should be DNATed to \1:\2 @@ -48,7 +48,7 @@ esac # Only monitor new source NAT connections that are created by the nftables NAT mapping rules if [[ $nat_filter -eq 2 ]]; then - conntrack -En -s $priv_subnet -e NEW | sed -rn -e "s#$pattern#nft add rule $hairpin_rule1; nft add rule $hairpin_rule2#e" # No filter rule for ADPF NAT + conntrack -En -s $priv_subnet -e NEW | sed -rn -e "s#$pattern#nft add rule $hairpin_rule1; nft add rule $hairpin_rule2#e" # No filter rule for APDF NAT else conntrack -En -s $priv_subnet -e NEW | sed -rn -e "s#$pattern#nft add rule $hairpin_rule1; nft add rule $hairpin_rule2; nft add rule $filter_rule#e" fi \ No newline at end of file diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 9602fd0..975d042 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -475,7 +475,7 @@ else Starting connectivity tests between two peers (possibly) behind NATs with various combinations of mapping and filtering behaviour: - Endpoint-Independent Mapping/Filtering (EIM/EIF) - Address-Dependent Mapping/Filtering (ADM/ADF) - - Address and Port-Dependent Mapping/Filtering (ADPM/ADPF)""" + - Address and Port-Dependent Mapping/Filtering (ADPM/APDF)""" log_sequential "\nTests with one peer behind a NAT" for nat1_mapping in {0..2}; do @@ -533,7 +533,7 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou if [[ $nat1_mapping -ge 1 && $nat1_filter -eq 2 ]]; then # Hairpinning is done by nat2, so its mapping/filtering behaviour is irrelevant - # However, if nat1 is A(P)DM-ADPF, UDP hole punching will fail because both peers are behind a too restrictive NAT + # However, if nat1 is A(P)DM-APDF, UDP hole punching will fail because both peers are behind a too restrictive NAT test_target=TS_PASS_RELAY else test_target=TS_PASS_DIRECT From 08cda1a0c3b0a2590778fecf406585d3eb6cac63 Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 21 Oct 2025 21:44:48 +0200 Subject: [PATCH 09/11] Add missing docker build command required to run system tests in parallel --- test_suite/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test_suite/README.md b/test_suite/README.md index abd67e0..803fcfb 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -66,7 +66,11 @@ sudo and xargs packages): Optionally, the system tests can be run in parallel. In this mode the system tests are distributed over Docker containers, so it requires -installing [Docker Engine](https://docs.docker.com/engine/install/). +installing [Docker Engine](https://docs.docker.com/engine/install/). To +build the Docker image the containers are based on, run the following +command in the `test_suite` directory: + + docker build -t system_tests . ### Performance test-specific requirements From 328e11789ca2a33bd1ccbb617c9723fbfc0e8f78 Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 21 Oct 2025 22:10:58 +0200 Subject: [PATCH 10/11] Update CHANGELOG.md --- test_suite/CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test_suite/CHANGELOG.md b/test_suite/CHANGELOG.md index 0c87228..04d3e3e 100644 --- a/test_suite/CHANGELOG.md +++ b/test_suite/CHANGELOG.md @@ -2,6 +2,23 @@ In this file, the test suite features that have been made possible thanks to [funding from NLnet](./README.md#funding) are documented. +## Double NAT(Oct 21, 2025) +### Added +- Explanation of Double NAT and how it is implemented in the test suite in the [system test documentation](./README.md#double-nat). +- The `-2` flag to [`system_tests.sh`](system_tests.sh) and [`nat_simulation/setup_networks.sh`](nat_simulation/setup_networks.sh), which enables Double NAT. +- New logic in [`system_tests.sh`](system_tests.sh) which decides the expected test result if the peers are behind Double NAT. This logic assumes the NAT behaviour is limited to the 4 NATs described in RFC 3489 +- Functionality in [`nat_simulation/setup_router.sh`](nat_simulation/setup_router.sh) to perform different actions depending on which of the two NATs is being set up. +- Report on the system tests results with Double NAT for each combination of two RFC 3489 NATs in the [system test results](./README.md#effect-of-double-nat-on-system-test-results). + +### Changed +- The syntax of the NAT and network namespace configurations which are passed as parameters to [`system_test.sh`](system_test.sh), such that a second NAT layer can be specified. + +### Fixed +- Small miscellaneous improvements, such as: + - Less duplicated code for regex validation by placing regular expressions which are used multiple times in [util.sh](util.sh). + - Fix incorrect abbreviation ADPF -> APDF across documentation and code comments. + - Add missing command necessary before running parallel system tests in the [system test requirements](./README.md#system-test-specific-requirements). + ## NAT IP pooling (June 20, 2025) ### Added - Explanation of NAT IP pooling and how it is implemented in the test suite in the [system test documentation](./README.md#ip-address-pooling). From a704cf22816cd1993e58f56f6d1bb5a4312d67b7 Mon Sep 17 00:00:00 2001 From: Henk Berendsen <61596108+henkberendsen@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:43:27 +0200 Subject: [PATCH 11/11] Apply regex improvements from code review Co-authored-by: Jonathan de Jong --- test_suite/performance_test.sh | 2 +- test_suite/util.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index 3ecb95f..75180f9 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -28,7 +28,7 @@ while getopts ":b:h" opt; do case $opt in b) baseline=$OPTARG - validate_str "$baseline" "^direct$|^wireguard$|^both$" + validate_str "$baseline" "^(direct|wireguard|both)$" case $baseline in "direct") diff --git a/test_suite/util.sh b/test_suite/util.sh index 26d17f1..a5cb130 100755 --- a/test_suite/util.sh +++ b/test_suite/util.sh @@ -9,7 +9,7 @@ NC="\033[0m" # No color # Frequently used regular expressions int_regex="^[0-9]+$" -real_regex="[0-9]+[.]?([0-9]+)?" # Allows for counting the amount of digits after the decimal point +real_regex="[0-9]+([.][0-9]+)?" # Allows for counting the amount of digits after the decimal point function exit_with_error() { err_reason=$1