143 lines
4.5 KiB
Python
143 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()
|