From a581cb2086939a95ffc50c909254c265367c8682 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 12 Oct 2026 01:12:28 +0200 Subject: [PATCH] Add isolated STUN fallback scenario and equivalent NAT for test ports --- tests/stream_netns/network.py | 30 +++++++++++++++++++++++++++++- tests/stream_netns/run.py | 3 +++ tests/stream_netns/scenario.py | 7 +++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/tests/stream_netns/network.py b/tests/stream_netns/network.py index 9c71639b..1ef5372f 100644 --- a/tests/stream_netns/network.py +++ b/tests/stream_netns/network.py @@ -10,7 +10,7 @@ import time from topology import COUNTRIES, NATS, NODES, WANS, wan -def nat_rules(gateway): +def nat_rules(gateway, stun_trial=False): clients = [node for node in NODES if node.nat == gateway.name] destinations = sorted({n.ip for n in NODES if not n.nat} | {g.ip for g in NATS}) lines = ["table ip lab {", @@ -27,6 +27,19 @@ def nat_rules(gateway): for index, destination in enumerate(destinations): port = 40000 + node.number * 100 + index lines.append(f"{match} ip daddr {destination} update @contacts {{ ip daddr . udp dport . {port} timeout 60s }}") + # STUN-сценарий использует одного клиента шлюза. Его случайные тестовые порты + # получают тот же класс NAT, что рабочий 32000; внешние порты остаются другими. + trial = clients[0] + if stun_trial: + match = f' oifname "wan" ip saddr {trial.ip} udp sport 49152-65535' + if gateway.mode == "cone": + lines.append(f"{match} update @active {{ udp sport timeout 60s }}") + elif gateway.mode == "restricted": + lines.append(f"{match} update @contacts {{ ip daddr . udp dport . udp sport timeout 60s }}") + else: + for index, destination in enumerate(destinations): + port = 40000 + trial.number * 100 + index + lines.append(f"{match} ip daddr {destination} update @contacts {{ ip daddr . udp dport . {port} timeout 60s }}") # Запрещённый вход отсекается ДО conntrack. Иначе первая неуспешная # входящая проба закрепляет tuple без DNAT и блокирует последующее открытие. lines += [" }", " chain ingress_guard { type filter hook prerouting priority raw; policy accept;"] @@ -40,6 +53,10 @@ def nat_rules(gateway): else: lines.append(f"{match} ip saddr . udp sport . udp dport @contacts accept") lines.append(f"{match} counter drop") + if stun_trial and gateway.mode != "strict": + match = ' iifname "wan" udp dport 49152-65535' + guard = "udp dport @active" if gateway.mode == "cone" else "ip saddr . udp sport . udp dport @contacts" + lines += [f"{match} {guard} accept", f"{match} counter drop"] lines += [" }", " chain prerouting { type nat hook prerouting priority dstnat; policy accept;"] for node in clients: match = f' iifname "wan" udp dport {node.mapped_port}' @@ -47,6 +64,9 @@ def nat_rules(gateway): lines.append(f"{match} udp dport @active dnat to {node.ip}:{node.udp_port}") elif gateway.mode == "restricted": lines.append(f"{match} ip saddr . udp sport . udp dport @contacts dnat to {node.ip}:{node.udp_port}") + if stun_trial and gateway.mode != "strict": + guard = "udp dport @active" if gateway.mode == "cone" else "ip saddr . udp sport . udp dport @contacts" + lines.append(f' iifname "wan" udp dport 49152-65535 {guard} dnat to {trial.ip}') lines += [" }", " chain postrouting { type nat hook postrouting priority srcnat; policy accept;"] for node in clients: match = f' oifname "wan" ip saddr {node.ip} udp sport {node.udp_port}' @@ -56,6 +76,14 @@ def nat_rules(gateway): lines.append(f"{match} ip daddr {destination} snat to {gateway.ip}:{port}") else: lines.append(f"{match} snat to {gateway.ip}:{node.mapped_port}") + if stun_trial: + match = f' oifname "wan" ip saddr {trial.ip} udp sport 49152-65535' + if gateway.mode == "strict": + for index, destination in enumerate(destinations): + port = 40000 + trial.number * 100 + index + lines.append(f"{match} ip daddr {destination} snat to {gateway.ip}:{port}") + else: + lines.append(f"{match} snat to {gateway.ip}") lines += [' oifname "wan" masquerade', " }", " chain forward { type filter hook forward priority filter; policy drop;", " ct state established,related accept", ' iifname "lan" oifname "wan" accept', diff --git a/tests/stream_netns/run.py b/tests/stream_netns/run.py index 394788f7..55244ea8 100644 --- a/tests/stream_netns/run.py +++ b/tests/stream_netns/run.py @@ -94,6 +94,7 @@ def main(): parser.add_argument("--network-only", action="store_true", help="Only topology, WAN and NAT calibration") parser.add_argument("--nat-check", action="store_true", help="Verify owned endpoint profiles before real JOIN/streaming") parser.add_argument("--nat-only", action="store_true", help="Only real uTun endpoint detection after network calibration") + parser.add_argument("--stun-only", action="store_true", help="STUN interface fallback and promotion to ETCP on three real NAT classes") parser.add_argument("--binary", default=str(Path(__file__).parent / "stream_lab_node")) parser.add_argument("--publisher", choices=[f"n{i:02d}" for i in range(1, 17)], default="n01") parser.add_argument("--rate", type=int, default=1000000, help="Application bitrate in bits/s, before FEC") @@ -131,6 +132,8 @@ def main(): command.append("--nat-check") if args.nat_only: command.append("--nat-only") + if args.stun_only: + command.append("--stun-only") if args.faults: command.append("--faults") if args.tcp: diff --git a/tests/stream_netns/scenario.py b/tests/stream_netns/scenario.py index 7e1b6a6d..44935040 100644 --- a/tests/stream_netns/scenario.py +++ b/tests/stream_netns/scenario.py @@ -11,7 +11,7 @@ from topology import NODES, WANS class Agent: - def __init__(self, network, node, binary, seed, tcp, tcp_only, core_only=False): + def __init__(self, network, node, binary, seed, tcp, tcp_only, core_only=False, extra_config=""): self.node = node directory = network.directory / node.name directory.mkdir() @@ -26,7 +26,7 @@ class Agent: f"db_path={directory / 'db'}\ndebug_level=warn\n" "[ntp]\nenabled=no\n[allowed_keys]\nallow_all=1\n" "[chatserver]\ngroup_autoconnect=1\nstorage_autoload=0\nmember_files_enabled=0\n" - "[debug]\nstream=debug\nnat=debug\nchat_sync=info\nmember_sync=info\nbgp=info\nconnection=info\n" + sockets) + "[debug]\nstream=debug\nnat=debug\nchat_sync=info\nmember_sync=info\nbgp=info\nconnection=info\n" + sockets + extra_config) path = directory / "node.conf" path.write_text(config) self.stderr = (directory / "stderr.log").open("w") @@ -136,6 +136,9 @@ class Scenario: def run(self): binary = Path(self.args.binary).resolve() + if self.args.stun_only: + from stun_scenario import check_stun + return check_stun(self) nat_check = self.args.nat_check or self.args.nat_only for node in NODES: self.agents[node.name] = Agent(self.network, node, binary, self.args.seed, self.args.tcp, self.args.tcp_only, nat_check)