Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/CI_test_suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ jobs:
run: pip install -r python_requirements.txt
working-directory: test_suite

- name: Run system tests sequentially
- name: Run system tests sequentially with NAT IP pooling
id: system-test
run: ./system_tests.sh -b
run: ./system_tests.sh -b -n 2
working-directory: test_suite
continue-on-error: true

Expand Down Expand Up @@ -69,9 +69,9 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Run system tests in parallel
- name: Run system tests in parallel with NAT IP pooling
id: system-test
run: GITHUB_ACTION=true ./system_tests.sh -t 4
run: GITHUB_ACTION=true ./system_tests.sh -t 4 -n 2
working-directory: test_suite
continue-on-error: true

Expand Down
12 changes: 12 additions & 0 deletions test_suite/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

In this file, the test suite features that have been made possible thanks to [funding from NLnet](./README.md#funding) are documented.

## 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).
- The `-n` flag to [`system_tests.sh`](system_tests.sh) and a positional parameter to [`system_test.sh`](system_test.sh),[`nat_simulation/setup_networks.sh`](nat_simulation/setup_networks.sh) and [`nat_simulation/setup_router.sh`](nat_simulation/setup_router.sh), which specify the amount of IP addresses available to the routers for NAT IP pooling.
- New expected test result `TS_PASS`, which accepts both a direct and relayed connection between the peers. This new result is necessary because for some NAT combinations with IP pooling, it is uncertain whether a direct connection can be established.
- Report on which system tests are affected by IP pooling in the [system test results](./README.md#effect-of-ip-address-pooling-on-system-test-results).

### Changed
- The logic in [`system_tests.sh`](system_tests.sh) which decides the expected test result based on the specified NAT combination of the peers. This logic now uses the new `TS_PASS` result for certain combinations if NAT IP pooling is enabled.
- Branches on the (expected) test result in [`system_test.sh`](system_test.sh) and [`test_client/setup_client.sh`](test_client/setup_client.sh), such that they also take the new `TS_PASS` result into account.
- Older results of the system tests without NAT IP pooling. They now contain a reference to the new results, and their visualization has been changed to align with the new results.
- The implementation of the NAT mapping & filtering behaviour, respectively found in [`nat_simulation/setup_networks.sh`](nat_simulation/setup_networks.sh) and [`nat_simulation/setup_router.sh`](nat_simulation/setup_router.sh). The implementation of the mapping behaviour now also does NAT IP pooling, and the filtering behaviour had to be adjusted to take the multiple IP addresses into account.

## Parallel system tests (April 11, 2025)

Expand Down
200 changes: 159 additions & 41 deletions test_suite/README.md

Large diffs are not rendered by default.

36 changes: 19 additions & 17 deletions test_suite/nat_simulation/setup_nat_filtering_hairpinning.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

pub_nat_iface=$1
priv_nat_iface=$2
pub_ip=$3
priv_subnet=$4
nat_filter=$5
priv_subnet=$3
nat_filter=$4

# Make sure all arguments have been passed, and nat_filter is between 0 and 2
if [[ $# -ne 5 || ! ($nat_filter =~ ^[0-2]$) ]]; then
if [[ $# -ne 4 || ! ($nat_filter =~ ^[0-2]$) ]]; then
echo """
Usage: ${0} <PUBLIC NAT NETWORK INTERFACE> <PRIVATE NAT NETWORK INTERFACE> <PUBLIC IP> <PRIVATE SUBNET> <NAT FILTERING TYPE>
Usage: ${0} <PUBLIC NAT NETWORK INTERFACE> <PRIVATE NAT NETWORK INTERFACE> <PRIVATE SUBNET> <NAT FILTERING TYPE>

<NAT FILTERING TYPE> may be one of the following numbers:
0 - Endpoint-Independent
Expand All @@ -21,30 +20,33 @@ This script must be run with root permissions, and assumes the nftables postrout
fi

# Configure NAT filtering type with nftables
nft add chain nat prerouting { type nat hook prerouting priority -100\; }

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

# This pattern captures 1) the source IP, 2) the source port, 3) the destination IP, and 4) the translated source port from a conntrack event
pattern=".*src=(\S+).*sport=(\S+).*src=(\S+).*dport=(\S+)$"
# This pattern captures the following info from a conntrack event:
# 1) the source IP
# 2) the source port
# 3) the destination IP,
# 4) the translated source IP,
# 5) the translated source port.
pattern=".*src=(\S+).*sport=(\S+).*src=(\S+).*dst=(\S+).*dport=(\S+).*$"

# Hairpinning: if a mapping is created with source IP \1, source port \2 and translated source port \4:
hairpin_rule1="nat prerouting iif $priv_nat_iface ip saddr $priv_subnet ip daddr $pub_ip meta l4proto {tcp, udp} th dport \4 counter dnat to \1:\2" # All traffic from the private network destined to $pub_ip:\4 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 $pub_ip:\4" # For all hairpinned packets from \1:\2, the source becomes $pub_ip:\4
# Hairpinning: if a mapping is created where source IP \1 and source port \2 are respectively translated to \4 and \5:
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)
case $nat_filter in
0)
# If a mapping is created with source IP \1, source port \2 and translated source port \4, all traffic destined to \4 should be DNATed to \1:\2
filter_rule="nat prerouting iif $pub_nat_iface meta l4proto {tcp, udp} th dport \4 counter dnat to \1:\2";;
# 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
filter_rule="nat prerouting iif $pub_nat_iface meta l4proto {tcp, udp} th dport \5 counter dnat to \1:\2";;
1)
# If a mapping is created with source IP \1, source port \2, translated source IP \3 and translated source port \4, all traffic from \3 destined to \4 should be DNATed to \1:\2
filter_rule="nat prerouting ip saddr \3 iif $pub_nat_iface meta l4proto {tcp, udp} th dport \4 counter dnat to \1:\2";;
# If a mapping is created with source IP \1, source port \2, translated source IP \3 and translated source port \5, all traffic from \3 destined to \5 should be DNATed to \1:\2
filter_rule="nat prerouting ip saddr \3 iif $pub_nat_iface meta l4proto {tcp, udp} th dport \5 counter dnat to \1:\2";;
esac

# Only monitor new source NAT connections that are created by the nftables masquerade rule
# 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
else
Expand Down
41 changes: 31 additions & 10 deletions test_suite/nat_simulation/setup_nat_mapping.sh
Original file line number Diff line number Diff line change
@@ -1,44 +1,65 @@
#!/usr/bin/env bash

nat_iface=$1
priv_subnet=$2
nat_map=$3
adm_ips=$4
pub_prefix=$2
priv_subnet=$3
nat_map=$4
n_pooling_ips=$5
adm_ips=$6

# Make sure all arguments have been passed, and nat_map and nat_filter are both integers between 0 and 2
if [[ $# -ne 4 || ! ($nat_map =~ ^[0-2]+$ ) ]]; then
if [[ $# -ne 6 || ! ($nat_map =~ ^[0-2]+$ ) ]]; then
echo """
Usage: ${0} <NAT NETWORK INTERFACE> <PRIVATE SUBNET> <NAT MAPPING TYPE> <IP ADDRESS LIST>
Usage: ${0} <NAT NETWORK INTERFACE> <PUBLIC NETWORK PREFIX> <PRIVATE SUBNET> <NAT MAPPING TYPE> <AMOUNT OF IPS> <IP ADDRESS LIST>

<NAT MAPPING TYPE> and <NAT FILTERING TYPE> may be one of the following numbers:
0 - Endpoint-Independent
1 - Address-Dependent
2 - Address and Port-Dependent

<AMOUNT OF IPS> specifies the number of IP addresses assigned to each NAT; setting this argument >1 allows NAT IP pooling to be simulated

<IP ADDRESS LIST> is a string of IP addresses separated by a space that may be the destination IP of packets crossing this NAT device (also generated by setup_networks.sh), and is necessary to simulate an Address-Dependent Mapping

This script must be run with root permissions"""
exit 1
fi

# Configure NAT mapping type with nftables rules
# Create NAT table and chains to configure NAT mapping type
nft add table ip nat
nft add chain nat prerouting { type nat hook prerouting priority -100\; }
nft add chain ip nat postrouting { type nat hook postrouting priority 100\; }

# Dictionary with packet mark key and IP address value, so that we can mark packets to control how their address will be translated
nft add map nat ip_pooling { type mark : ipv4_addr\; }
for ((i=0; i<$n_pooling_ips; i++)); do
nft add element nat ip_pooling { $i : $pub_prefix.$((254-i)) } # For NAT IPs, host identifiers range from (254 - n_pooling_ips) to 254
done

case $nat_map in
0)
nft add rule ip nat postrouting ip saddr $priv_subnet oif $nat_iface counter masquerade persistent;;
1)
# In EIM the same host always reuses the same mapping, so we set the packet mark by hashing the source IP + UDP/TCP source port
nft add rule nat prerouting meta l4proto { tcp, udp } ct mark set jhash ip saddr . th sport mod $n_pooling_ips

nft add rule nat postrouting ip saddr $priv_subnet oif $nat_iface counter snat to ct mark map @ip_pooling persistent;;
1)
# In ADM the mapping is reused as long as the destination IP does not change, so we also make the hash depend on the destination IP
nft add rule nat prerouting meta l4proto { tcp, udp } ct mark set jhash ip saddr . th sport . ip daddr mod $n_pooling_ips

# Assign a block of 100 ports to each IP in adm_ips
port_range_start=50000
port_range_width=100

# Iterate over each IP
for ip in $adm_ips; do
port_range_end=$(($port_range_start + $port_range_width - 1))
nft add rule ip nat postrouting ip protocol {tcp, udp} ip saddr $priv_subnet ip daddr $ip oif $nat_iface counter masquerade to :${port_range_start}-${port_range_end} persistent
port_range="${port_range_start}-${port_range_end}"
nft add rule ip nat postrouting ip protocol {tcp, udp} ip saddr $priv_subnet ip daddr $ip oif $nat_iface counter snat to ct mark map @ip_pooling:$port_range persistent
port_range_start=$(($port_range_end+1))
done ;;
2)
nft add rule ip nat postrouting ip saddr $priv_subnet oif $nat_iface counter masquerade random;;
# In APDM each connection gets a new mapping, so the packet mark can be completely random
nft add rule nat prerouting ct mark set numgen random mod $n_pooling_ips

nft add rule nat postrouting ip saddr $priv_subnet oif $nat_iface counter snat to ct mark map @ip_pooling random;;
esac
20 changes: 14 additions & 6 deletions test_suite/nat_simulation/setup_networks.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
#!/usr/bin/env bash

if [[ $1 = "-h" ]]; then
echo """
Usage: ${0}
usage_str="""
Usage: ${0} <AMOUNT OF IPS>

<AMOUNT OF IPS> specifies the number of IP addresses assigned to each NAT; setting this argument >1 allows NAT IP pooling to be simulated

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

This script must be run with root permissions"""

if [[ $1 = "-h" || $# -ne 1 ]]; then
echo $usage_str
exit 1
fi

# Number of IPs per router to test NAT IP pooling
n_pooling_ips=$1

# Enable IP forwarding to allow for routing between namespaces
sysctl -w net.ipv4.ip_forward=1 &> /dev/null

Expand Down Expand Up @@ -60,13 +67,14 @@ for ((i=1; i<=n_priv_nets; i++)); do
priv_subnet="${priv_prefix}.0/24"
router_priv_ip="${priv_prefix}.254"
pub_prefix="192.168.${i}"
pub_subnet="${pub_prefix}.0/24"
router_pub_ip="${pub_prefix}.254"

# Add router's public IP to list created earlier
adm_ips+=($router_pub_ip)
# Add router's public subnet to list created earlier
adm_ips+=($pub_subnet)

# Setup router
ip netns exec $router_name ./setup_router.sh $router_name $priv_name $priv_subnet $router_priv_ip $router_pub_ip $switch_ip
ip netns exec $router_name ./setup_router.sh $router_name $priv_name $priv_subnet $router_priv_ip $pub_prefix $n_pooling_ips $switch_ip

# Setup private network
ip netns exec $priv_name ./setup_private.sh $router_name $router_pub_ip $priv_subnet
Expand Down
20 changes: 14 additions & 6 deletions test_suite/nat_simulation/setup_router.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ router_name=$1
priv_name=$2
priv_subnet=$3
priv_ip=$4
pub_ip=$5
switch_ip=$6
pub_subnet_prefix=$5
n_ips=$6
switch_ip=$7

if [[ $# -ne 6 ]]; then
if [[ $# -ne 7 ]]; then
echo """
Usage: ${0} <ROUTER NAME> <PRIVATE NETWORK NAME> <PRIVATE SUBNET> <ROUTER PRIVATE IP> <ROUTER PUBLIC IP> <SWITCH IP>
Usage: ${0} <ROUTER NAME> <PRIVATE NETWORK NAME> <PRIVATE SUBNET> <ROUTER PRIVATE IP> <PUBLIC /24 NETWORK PREFIX> <NUMBER OF IPS> <SWITCH IP>

This script must be run with root permissions"""
exit 1
fi

pub_subnet="$pub_subnet_prefix.0/24"

# Create veth pair to place the router's private interface in the private and router namespaces
router_priv="${router_name}_priv"
ip link add $router_priv type veth peer $router_name netns $priv_name
Expand All @@ -25,7 +28,12 @@ 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

for host in $(seq $((254 - $n_ips + 1)) 254); do
ip="$pub_subnet_prefix.$host/24"
ip addr add $ip dev $router_pub
done

ip link set $router_pub up
ip netns exec public ip link set $router_name up

Expand All @@ -38,4 +46,4 @@ 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
ip netns exec public ip route add $pub_subnet dev $router_name
27 changes: 15 additions & 12 deletions test_suite/system_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
SYSTEM_TEST_TIMEOUT=60

usage_str="""
Usage: ${0} [OPTIONAL ARGUMENTS] <TEST TARGET> <NAMESPACE CONFIGURATION> [NAT CONFIGURATION 1]:[NAT CONFIGURATION 2] [WIREGUARD INTERFACE 1]:[WIREGUARD INTERFACE 2] <TEST INDEX> <CONTROL SERVER PUBLIC KEY> <CONTROL SERVER IP> <CONTROL SERVER PORT> <IP ADDRESS LIST> <LOG LEVEL> <LOG DIRECTORY> <REPOSITORY DIRECTORY>
Usage: ${0} [OPTIONAL ARGUMENTS] <TEST TARGET> <NAMESPACE CONFIGURATION> [NAT CONFIGURATION 1]:[NAT CONFIGURATION 2] [WIREGUARD INTERFACE 1]:[WIREGUARD INTERFACE 2] <TEST INDEX> <CONTROL SERVER PUBLIC KEY> <CONTROL SERVER IP> <CONTROL SERVER PORT> <AMOUNT OF IPS> <IP ADDRESS LIST> <LOG LEVEL> <LOG DIRECTORY> <REPOSITORY DIRECTORY>

<TEST TARGET> is the expected result of the system test:
1. TS_PASS_DIRECT: the peers have established a direct connection
Expand Down Expand Up @@ -33,6 +33,8 @@ Usage: ${0} [OPTIONAL ARGUMENTS] <TEST TARGET> <NAMESPACE CONFIGURATION> [NAT CO
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)

<AMOUNT OF IPS> specifies the number of IP addresses assigned to each NAT; setting this argument >1 allows NAT IP pooling to be simulated

If [WIREGUARD INTERFACE 1] or [WIREGUARD INTERFACE 2] is not provided, the corresponding peer will use userspace WireGuard

<IP ADDRESS LIST> 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
Expand Down Expand Up @@ -95,8 +97,8 @@ done
shift $((OPTIND-1))

# Make sure all required arguments have been passed
if [[ $# -ne 12 ]]; then
exit_with_error "expected 12 positional parameters, but received $#"
if [[ $# -ne 13 ]]; then
exit_with_error "expected 13 positional parameters, but received $#"
fi

test_target=$1
Expand All @@ -107,10 +109,11 @@ test_idx=$5
control_pub_key=$6
control_ip=$7
control_port=$8
adm_ips=$9
log_lvl=${10}
log_dir=${11}
repo_dir=${12}
n_pooling_ips=$9
adm_ips=${10}
log_lvl=${11}
log_dir=${12}
repo_dir=${13}

# Validate namespace configuration string
ns_regex="([^-:]+)" # One or more occurence of every character except '-' and ':' (these are used to separate the namespaces)
Expand Down Expand Up @@ -255,12 +258,12 @@ 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"
priv_prefix="10.0.$((i+1)).0/24"
router_pub_prefix="192.168.$((i+1))"
priv_subnet="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}"
sudo ip netns exec $router_ns ./setup_nat_mapping.sh $router_pub $router_pub_prefix $priv_subnet ${nat_map[$i]} $n_pooling_ips "${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 | \
sudo ip netns exec $router_ns ./setup_nat_filtering_hairpinning.sh $router_pub $router_priv $priv_subnet ${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
done

Expand Down Expand Up @@ -302,7 +305,7 @@ else
fi

# Output test result
if [[ $test_target != $test_result ]]; then
if [[ ! ( $test_result =~ $test_target ) ]]; then
echo -e "${RED}$test_result${NC}"
clean_exit 1
fi
Expand Down
Loading