egress: forward = reject for intercepted agents, direct_mac exempt in log mode too
This commit is contained in:
parent
dff662c6b3
commit
dad15ca855
1 changed files with 248 additions and 70 deletions
|
|
@ -225,6 +225,10 @@ pkgs.testers.runNixOSTest {
|
|||
pkgs.mkpasswd
|
||||
pkgs.nftables
|
||||
];
|
||||
# The reject-mode half of the test switches to this at its end. The
|
||||
# forward mode is fixed when the node evaluates; the switch reloads
|
||||
# `hive-egress` with the other ruleset.
|
||||
specialisation.reject.configuration.services.hyperhive.network.egress.forward = "reject";
|
||||
services.hyperhive.deploy.hive-controller.squid.selfcheck.unrestrictedCanary = "other.t.local";
|
||||
services.hyperhive.deploy.hive-controller.squid.selfcheck.canary = "cache.t.local";
|
||||
services.hyperhive.deploy.hive-controller.squid.baseAllowedDomains = [
|
||||
|
|
@ -1157,11 +1161,71 @@ pkgs.testers.runNixOSTest {
|
|||
assert icpt("https://203.0.113.2/") == "200", "an IP literal, directly"
|
||||
assert agent_lines() == seen, "squid saw nothing of it"
|
||||
assert send_hello("ech") == "16", "a hello with ECH, directly: the relay is not in the path"
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
"'hive-egress-would-drop(-unmetered)? IN=eth1 OUT=eth2 .*SRC=192\\.168\\.1\\.3 DST=203\\.0\\.113\\.2 .*PROTO=TCP .*DPT=443 '"
|
||||
)
|
||||
proxy.succeed(f"nft delete element inet hive_egress direct_mac '{{ {AGENT_MAC} }}'")
|
||||
assert icpt("https://other.t.local/") == "000", "intercepted again"
|
||||
# Control: a new flow of the same MAC, now off the set, is logged, and the
|
||||
# kernel log is in order, so a line of the exempt flows would be before it.
|
||||
agent.execute("echo x > /dev/udp/203.0.113.79/9")
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
"'hive-egress-would-drop(-unmetered)? IN=eth1 OUT=eth2 .*DST=203\\.0\\.113\\.79 '"
|
||||
)
|
||||
assert not would_drop(r"DST=203\.0\.113\.2 .*DPT=443 "), "the exempt MAC's flows leave no line"
|
||||
|
||||
# ---- Reject mode ----
|
||||
# The node switches to its `reject` specialisation, which reloads the
|
||||
# tables with the refusing forward chain. Nothing is in `direct_mac` yet.
|
||||
def reject_lines(pattern):
|
||||
# The reject chain's log lines in the kernel log that match `pattern`.
|
||||
out = proxy.succeed("journalctl -k -o cat --no-pager | grep hive-egress-reject || true")
|
||||
return [line for line in out.splitlines() if re.search(pattern, line)]
|
||||
|
||||
def wait_reject_line(pattern):
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
+ shlex.quote(f"hive-egress-reject(-unmetered)? IN=eth1 OUT=eth2 .*{pattern}")
|
||||
)
|
||||
|
||||
def dial_status(addr, port):
|
||||
# 0 connected, 1 refused, 124 no answer in 3 seconds.
|
||||
return agent.execute(f"timeout 3 bash -c 'echo > /dev/tcp/{addr}/{port}'")[0]
|
||||
|
||||
def udp_status(addr):
|
||||
# A connected UDP socket reports the ICMP error of the first datagram
|
||||
# on the second write: non-zero when the host refused it. Nothing
|
||||
# answers a datagram that is let through, so that case is 0.
|
||||
return agent.execute(f"bash -c 'exec 3<>/dev/udp/{addr}/9; echo x >&3; sleep 1; echo y >&3'")[0]
|
||||
|
||||
with checked("reject mode: web still goes through squid and everything else from a restricted agent is refused"):
|
||||
proxy.succeed("/run/current-system/specialisation/reject/bin/switch-to-configuration test")
|
||||
out = proxy.succeed("nft list chain inet hive_egress forward")
|
||||
assert "tcp reset" in out and "admin-prohibited" in out, out
|
||||
assert icpt("https://forge.t.local/") == "200", "control: web still goes through squid"
|
||||
assert dial_status("203.0.113.2", 8443) == 1, "TCP 8443 is reset at once, not left to time out"
|
||||
wait_reject_line(r"SRC=192\.168\.1\.3 DST=203\.0\.113\.2 .*PROTO=TCP .*DPT=8443 ")
|
||||
status, out = agent.execute("ping -c1 -W3 203.0.113.2")
|
||||
assert status != 0 and "filtered" in out.lower(), f"ICMP is refused with admin-prohibited: {out}"
|
||||
wait_reject_line(r"SRC=192\.168\.1\.3 DST=203\.0\.113\.2 .*PROTO=ICMP ")
|
||||
assert udp_status("203.0.113.77") != 0, "a datagram is refused with an ICMP error"
|
||||
wait_reject_line(r"DST=203\.0\.113\.77 .*PROTO=UDP ")
|
||||
out = proxy.succeed("nft list chain inet hive_egress forward")
|
||||
found = re.search(r'oifname != "eth1" counter packets (\d+) bytes \d+$', out, re.M)
|
||||
assert found and int(found.group(1)) > 0, out
|
||||
|
||||
with checked("reject mode: a MAC in direct_mac passes and is refused again once it leaves, and the mesh passes"):
|
||||
proxy.succeed(f"nft add element inet hive_egress direct_mac '{{ {AGENT_MAC} }}'")
|
||||
assert dial_status("203.0.113.2", 8443) == 0, "an exempt MAC (a service container's stand-in) reaches TCP 8443"
|
||||
agent.succeed("ping -c1 -W3 203.0.113.2")
|
||||
assert udp_status("203.0.113.77") == 0, "control: an exempt MAC's datagram is not refused"
|
||||
proxy.succeed(f"nft delete element inet hive_egress direct_mac '{{ {AGENT_MAC} }}'")
|
||||
assert dial_status("203.0.113.2", 8443) == 1, "refused again"
|
||||
# The mesh comes first in the chain. A dummy interface answers nothing,
|
||||
# so the proof is that the chain refuses neither the ping nor the UDP
|
||||
# datagram, while the same flows to the uplink are refused.
|
||||
status, out = agent.execute("ping -c1 -W5 198.51.100.7")
|
||||
assert "filtered" not in out.lower(), f"the mesh interface is not egress: {out}"
|
||||
agent.execute("echo x > /dev/udp/198.51.100.7/9; echo x > /dev/udp/203.0.113.80/9")
|
||||
wait_reject_line(r"DST=203\.0\.113\.80 ")
|
||||
assert not reject_lines(r"DST=198\.51\.100\.7 "), "the mesh interface is not egress"
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue