log the agent bridge's forwarded flows in the hive_egress table
A forward chain in the hive-owned nft table logs each new flow from the agent bridge to the uplink (v4 and v6) and counts every packet. It never blocks. The mesh interface is skipped. Logging is rate limited per source address; a flow that limit cannot log gets one line from a shared limit with its own prefix. services.hyperhive.network.egress.forward selects accept, log (default) or drop; drop is refused at evaluation until the interception it needs exists.
This commit is contained in:
parent
f5e7caa2eb
commit
bd79b58e78
1 changed files with 321 additions and 8 deletions
|
|
@ -10,7 +10,9 @@
|
|||
# covers the real option wiring.
|
||||
#
|
||||
# It also covers ../host-modules/hive-egress.nix: `proxy` loads the real module, so the
|
||||
# agent's dials to the host's loopback run against the table the module installs.
|
||||
# agent's dials to the host's loopback, and its direct connects to the uplink
|
||||
# (`proxy` forwards and masquerades like the hive host), run against the table
|
||||
# the module installs.
|
||||
{ pkgs }:
|
||||
let
|
||||
bridgeIp = "192.168.1.1";
|
||||
|
|
@ -98,6 +100,10 @@ let
|
|||
address = "fd00:99::${toString host}";
|
||||
prefixLength = 64;
|
||||
};
|
||||
bridgeUla = host: {
|
||||
address = "fd00:98::${toString host}";
|
||||
prefixLength = 64;
|
||||
};
|
||||
|
||||
# Plain TCP on 443: a CONNECT is answered before any TLS, which is all the
|
||||
# test reads.
|
||||
|
|
@ -151,6 +157,14 @@ pkgs.testers.runNixOSTest {
|
|||
++ privateAddrs 1
|
||||
);
|
||||
networking.interfaces.eth2.ipv6.addresses = [ (ulaAddr 1) ];
|
||||
networking.interfaces.eth1.ipv6.addresses = [ (bridgeUla 1) ];
|
||||
# What hive-network.nix sets for the agent bridge: forwarding, and NAT
|
||||
# for everything that leaves it. The agent's direct path out exists.
|
||||
networking.nat = {
|
||||
enable = true;
|
||||
internalInterfaces = [ "eth1" ];
|
||||
};
|
||||
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = 1;
|
||||
# Names the proxy resolves, each a different destination class.
|
||||
networking.hosts = {
|
||||
# `other.t.local` and the self-check's deny canary resolve, are
|
||||
|
|
@ -225,6 +239,13 @@ pkgs.testers.runNixOSTest {
|
|||
++ privateAddrs 2
|
||||
);
|
||||
networking.interfaces.eth1.ipv6.addresses = [ (ulaAddr 2) ];
|
||||
networking.interfaces.eth1.ipv6.routes = [
|
||||
{
|
||||
address = "fd00:98::";
|
||||
prefixLength = 64;
|
||||
via = "fd00:99::1";
|
||||
}
|
||||
];
|
||||
networking.firewall.allowedTCPPorts = [ 443 ];
|
||||
};
|
||||
|
||||
|
|
@ -240,6 +261,26 @@ pkgs.testers.runNixOSTest {
|
|||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
networking.interfaces.eth1.ipv4.routes = [
|
||||
{
|
||||
address = "203.0.113.0";
|
||||
prefixLength = 24;
|
||||
via = bridgeIp;
|
||||
}
|
||||
{
|
||||
address = "198.51.100.0";
|
||||
prefixLength = 24;
|
||||
via = bridgeIp;
|
||||
}
|
||||
];
|
||||
networking.interfaces.eth1.ipv6.addresses = [ (bridgeUla 3) ];
|
||||
networking.interfaces.eth1.ipv6.routes = [
|
||||
{
|
||||
address = "fd00:99::";
|
||||
prefixLength = 64;
|
||||
via = "fd00:98::1";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -735,5 +776,68 @@ pkgs.testers.runNixOSTest {
|
|||
proxy.succeed("nft delete table inet hive_egress")
|
||||
assert not dial("127.0.0.5"), "route_localnet=0 refuses it alone"
|
||||
proxy.succeed("systemctl reload hive-egress.service")
|
||||
|
||||
def would_drop(pattern):
|
||||
# The forward chain's log lines in the kernel log that match `pattern`.
|
||||
out = proxy.succeed("journalctl -k -o cat --no-pager | grep hive-egress-would-drop || true")
|
||||
return [line for line in out.splitlines() if re.search(pattern, line)]
|
||||
|
||||
with checked("a direct connect from the agent bridge still works in log mode, and the flow is logged with the agent's address"):
|
||||
# Through squid, to the gateway on the bridge and out of the proxy itself
|
||||
# first: none of them is forwarded traffic, so none may be logged.
|
||||
assert connect("forge.t.local", ATLAS) == "200", "through squid"
|
||||
assert dial("${bridgeIp}"), "the gateway on the bridge IP"
|
||||
assert dial("203.0.113.2"), "a direct connect to an outside IP works"
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
"'hive-egress-would-drop IN=eth1 OUT=eth2 .*SRC=192\\.168\\.1\\.3 DST=203\\.0\\.113\\.2 .*PROTO=TCP .*DPT=443 '"
|
||||
)
|
||||
agent.succeed("ping -c1 -W2 203.0.113.2")
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
"'hive-egress-would-drop IN=eth1 OUT=eth2 .*SRC=192\\.168\\.1\\.3 DST=203\\.0\\.113\\.2 .*PROTO=ICMP '"
|
||||
)
|
||||
# Control: the same journal holds no line for the traffic that is not forwarded.
|
||||
assert not would_drop(r"DST=192\.168\.1\.1 "), "the bridge IP is the host's own address"
|
||||
assert not would_drop(r"DPT=3128 "), "squid's port is the host's own address"
|
||||
assert not would_drop(r"SRC=203\.0\.113\.1 "), "squid's own connections leave the host and are not forwarded"
|
||||
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("the same holds over IPv6"):
|
||||
agent.wait_until_succeeds("ping -6 -c1 -W2 fd00:98::1")
|
||||
assert dial("fd00:99::2"), "a direct v6 connect works"
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
"'hive-egress-would-drop IN=eth1 OUT=eth2 .*SRC=fd00:0*98:(0+:)*0*3 DST=fd00:0*99:(0+:)*0*2 .*PROTO=TCP .*DPT=443 '"
|
||||
)
|
||||
|
||||
with checked("a flow to the mesh interface is not logged and one to the uplink is"):
|
||||
proxy.succeed("ip link add wg-hive type dummy && ip link set wg-hive up")
|
||||
proxy.succeed("ip route add 198.51.100.0/24 dev wg-hive")
|
||||
agent.execute("echo x > /dev/udp/198.51.100.7/9; echo x > /dev/udp/203.0.113.77/9")
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E "
|
||||
"'hive-egress-would-drop IN=eth1 OUT=eth2 .*DST=203\\.0\\.113\\.77 '"
|
||||
)
|
||||
assert not would_drop(r"DST=198\.51\.100\.7 "), "the mesh interface is not egress"
|
||||
|
||||
with checked("one busy agent address is cut to a burst plus a trickle, the rest still leave a trace, and every flow is logged once at most"):
|
||||
agent.execute("for i in $(seq 1 100); do echo x > /dev/udp/203.0.113.78/$((3000 + i)); done")
|
||||
proxy.wait_until_succeeds(
|
||||
"journalctl -k -o cat --no-pager | grep -E 'hive-egress-would-drop-unmetered .*DST=203\\.0\\.113\\.78 '"
|
||||
)
|
||||
time.sleep(3)
|
||||
lines = would_drop(r"DST=203\.0\.113\.78 ")
|
||||
metered = [line for line in lines if "hive-egress-would-drop IN=" in line]
|
||||
unmetered = [line for line in lines if "hive-egress-would-drop-unmetered IN=" in line]
|
||||
assert 1 <= len(metered) <= 30, f"100 new flows: {len(metered)} per-source lines"
|
||||
assert 1 <= len(unmetered) <= 80, f"100 new flows: {len(unmetered)} unmetered lines"
|
||||
ports = [re.search(r"DPT=(\d+) ", line).group(1) for line in lines]
|
||||
assert len(ports) == len(set(ports)), "a flow was logged twice"
|
||||
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)) >= 100, out
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue