nixos/scripts/router-firewall-check.py

142 lines
4.5 KiB
Python

"""Exercise evaluated router rules in private user/network namespaces.
Run: python3 scripts/router-firewall-check.py /tmp/router-rules.nft
Requires ip, nft, nsenter and unshare on PATH. Never runs against the host network.
The extra NAT table models Docker publishing allowed and unlisted host ports.
"""
import os
from pathlib import Path
import subprocess as sp
import sys
original = os.environ.get("FRED_TEST_ORIGINAL_NETNS")
if not original:
os.execvpe(
"unshare",
["unshare", "-Urn", sys.executable, *sys.argv],
{**os.environ, "FRED_TEST_ORIGINAL_NETNS": os.readlink("/proc/self/ns/net")},
)
if os.readlink("/proc/self/ns/net") == original:
sys.exit("Refusing to run outside a private network namespace")
children = []
def run(*args, **kw):
return sp.run(args, check=True, text=True, **kw)
def net(pid, *args, **kw):
return run("nsenter", "-t", str(pid), "-n", *args, **kw)
def peer(interface, address, peer_address):
proc = sp.Popen(["unshare", "-n", "sleep", "300"])
children.append(proc)
# unshare must finish before moving the peer into its namespace.
import time
for _ in range(100):
if os.readlink(f"/proc/{proc.pid}/ns/net") != os.readlink("/proc/self/ns/net"):
break
time.sleep(0.01)
run("ip", "link", "add", interface, "type", "veth", "peer", "name", "peer0")
run("ip", "link", "set", "peer0", "netns", str(proc.pid))
run("ip", "addr", "add", address + "/24", "dev", interface)
run("ip", "link", "set", interface, "up")
net(proc.pid, "ip", "addr", "add", peer_address + "/24", "dev", "peer0")
net(proc.pid, "ip", "link", "set", "peer0", "up")
net(proc.pid, "ip", "link", "set", "lo", "up")
net(proc.pid, "ip", "route", "add", "default", "via", address)
return proc.pid
server = """
import socket,threading,time
ports=PORTS
for port in ports:
for kind in (socket.SOCK_STREAM,socket.SOCK_DGRAM):
s=socket.socket(socket.AF_INET,kind);s.bind(('0.0.0.0',port))
if kind==socket.SOCK_STREAM:s.listen()
def serve(s=s,kind=kind):
while True:
if kind==socket.SOCK_STREAM:
c,a=s.accept()
with c:
data=c.recv(16)
if data:c.sendall(data)
else:
data,a=s.recvfrom(16);s.sendto(data,a)
threading.Thread(target=serve,daemon=True).start()
print('ready',flush=True)
time.sleep(300)
"""
def serve(pid, ports):
cmd = [sys.executable, "-u", "-c", server.replace("PORTS", repr(ports))]
if pid:
cmd = ["nsenter", "-t", str(pid), "-n"] + cmd
proc = sp.Popen(cmd, stdout=sp.PIPE, text=True)
children.append(proc)
assert proc.stdout.readline().strip() == "ready"
def check(pid, port, proto, expected):
client = f"""
import socket
s=socket.socket(socket.AF_INET,socket.SOCK_{"STREAM" if proto == "tcp" else "DGRAM"})
s.settimeout(.4)
try:
s.connect(('{"10.0.0.1" if pid == lan else "198.18.0.1"}',{port}))
s.send(b'hello')
ok=s.recv(16)==b'hello'
except OSError:ok=False
assert ok=={expected!r}, 'unexpected reachability: {proto}/{port}, expected {expected}'
"""
net(pid, sys.executable, "-c", client)
print(
f"{'WAN' if pid == wan else 'LAN'} {proto}/{port}: {'allowed' if expected else 'blocked'}"
)
try:
run("ip", "link", "set", "lo", "up")
wan = peer("eno1", "198.18.0.1", "198.18.0.2")
lan = peer("eth0", "10.0.0.1", "10.0.0.2")
container = peer("docker0", "172.18.0.1", "172.18.0.2")
Path("/proc/sys/net/ipv4/ip_forward").write_text("1")
run("nft", "-f", sys.argv[1])
run(
"nft",
"-f",
"-",
input="""
table ip docker_test {
chain prerouting {
type nat hook prerouting priority -101; policy accept;
iifname "eno1" tcp dport { 25565, 25600, 2022 } dnat to 172.18.0.2
iifname "eno1" udp dport { 25565, 25600, 2022 } dnat to 172.18.0.2
iifname "eno1" tcp dport { 25564, 25601, 9999 } dnat to 172.18.0.2:9000
}
chain forward { type filter hook forward priority 0; policy accept; }
}
""",
)
serve(None, [22, 443, 2022])
serve(container, [25565, 25600, 2022, 9000])
for port in [22, 443, 25565, 25600]:
check(wan, port, "tcp", True)
for port in [25565, 25600]:
check(wan, port, "udp", True)
for port in [2022, 25564, 25601, 9999]:
check(wan, port, "tcp", False)
check(wan, 2022, "udp", False)
check(lan, 2022, "tcp", True)
print("Router namespace checks passed")
finally:
for proc in reversed(children):
proc.terminate()
for proc in children:
proc.wait()