You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

89 lines
5.9 KiB

"""STUN на другом порту: реальные NAT и переключение на независимый ETCP-раунд."""
import json
from pathlib import Path
import select
import sys
from nat_scenario import profile
from network import nat_rules
from scenario import Agent
from topology import NATS, NODES
def check_stun(scenario):
network, agents = scenario.network, scenario.agents
nodes = {node.name: node for node in NODES}
script = Path(__file__).with_name("stun_server.py")
servers = []
outputs = []
verified = {}
try:
for name, peer in (("n01", "n02"), ("n02", "n01")):
output = (network.directory / f"{name}-stun.log").open("w", buffering=1)
outputs.append(output)
process = network.spawn(name, [sys.executable, "-u", str(script), "--ip", nodes[name].ip,
"--peer", nodes[peer].ip], stdout=-1, stderr=output, text=True, bufsize=1)
if not select.select([process.stdout], [], [], 5)[0] or process.stdout.readline().strip() != "READY":
raise RuntimeError(f"STUN server {name} did not start")
# Журнал читается процессом-копировщиком, чтобы не забить stdout pipe.
network.spawn(name, [sys.executable, "-u", "-c", "import sys; sys.stdout.write(sys.stdin.read())"],
stdin=process.stdout, stdout=output)
servers.append(process)
cases = (("n03", "n07", "n12", "EIM", "OPEN"),
("n14", "n08", "n13", "EIM", "CONTACT_REQUIRED"),
("n05", "n01", "n02", "DEPENDENT", "CONTACT_REQUIRED"))
for owner_name, observer_name, helper_name, mapping, inbound in cases:
owner = nodes[owner_name]
gateway = next(item for item in NATS if item.name == owner.nat)
network.command(gateway.name, ["nft", "flush", "ruleset"])
network.command(gateway.name, ["nft", "-f", "-"], input=nat_rules(gateway, stun_trial=True))
config = (f"[stun]\nenabled=yes\nserver={nodes['n01'].ip}:3478\nserver={nodes['n02'].ip}:3478\n"
f"[server:duplicate]\naddr={owner.ip}:32001\ntype=nat\n")
agents[owner_name] = Agent(network, owner, Path(scenario.args.binary).resolve(), scenario.args.seed,
False, False, True, config)
agents[owner_name].rpc("nat_auto", enabled=1)
data = scenario.wait(f"stun-fallback-{owner_name}", lambda statistics:
all(item["source"] == "STUN" for item in statistics[owner_name]["address_profiles"]), 130)
udp, duplicate = profile(data[owner_name], "udp"), profile(data[owner_name], "duplicate")
if (udp["mapping"], udp["inbound"]) != (mapping, inbound):
raise RuntimeError(f"{owner_name}: STUN class mismatch: {udp}")
if udp["stun"]["evidence"] != duplicate["stun"]["evidence"] or udp["stun"]["members"] != 2 or udp["external"]:
raise RuntimeError(f"{owner_name}: duplicated probe or foreign external port: {udp}, {duplicate}")
for name in (observer_name,):
agents[name] = Agent(network, nodes[name], Path(scenario.args.binary).resolve(), scenario.args.seed,
False, False, True)
agents[owner_name].rpc("nat_link", snapshot=agents[observer_name].rpc("snapshot")["snapshot"], socket="udp")
scenario.wait(f"stun-observer-link-{owner_name}", lambda statistics:
agents[observer_name].identity in statistics[owner_name]["direct_up"], 20)
agents[owner_name].rpc("nat_check", socket="udp", observer=agents[observer_name].identity)
data = scenario.wait(f"stun-no-helper-{owner_name}", lambda statistics:
profile(statistics[owner_name], "udp")["reason"] == "no_helper", 40)
if profile(data[owner_name], "udp")["source"] != "STUN":
raise RuntimeError(f"{owner_name}: missing fallback with single observer")
agents[helper_name] = Agent(network, nodes[helper_name], Path(scenario.args.binary).resolve(), scenario.args.seed,
False, False, True)
agents[observer_name].rpc("nat_link", snapshot=agents[helper_name].rpc("snapshot")["snapshot"], socket="udp")
scenario.wait(f"stun-helper-link-{owner_name}", lambda statistics:
agents[helper_name].identity in statistics[observer_name]["direct_up"], 20)
agents[owner_name].rpc("nat_check", socket="udp", observer=agents[observer_name].identity)
data = scenario.wait(f"stun-etcp-promotion-{owner_name}", lambda statistics:
profile(statistics[owner_name], "udp")["source"] == "ETCP", 40)
measured = profile(data[owner_name], "udp")
expected = ("DEPENDENT", "UNKNOWN") if gateway.mode == "strict" else (mapping, inbound)
if (measured["mapping"], measured["inbound"]) != expected:
raise RuntimeError(f"{owner_name}: ETCP class mismatch: {measured}")
verified[owner_name] = {"stun": udp, "etcp": measured}
print(f"PASS {owner_name}: STUN {mapping}/{inbound} -> ETCP {expected[0]}/{expected[1]}", flush=True)
# Каждый следующий случай начинает с действительно одного наблюдателя.
for name in (owner_name, observer_name, helper_name):
if agents.pop(name).close():
raise RuntimeError(f"{name}: nonzero exit")
(network.directory / "stun-result.json").write_text(json.dumps(verified, indent=2) + "\n")
return {"pass": True, "stun": verified}
finally:
for process in servers:
process.terminate()
process.wait(timeout=5)
for output in outputs:
output.close()