Watch
0
0
Fork
You've already forked hyperhive
0

egress anti-spoof: back to log by default; the lease set does not survive a suspend/resume

This commit is contained in:
atlas 2026-11-14 18:12:20 +01:00 • committed by mara
commit b8098686c4
3 changed files with 27 additions and 27 deletions

View file

@ -803,7 +803,7 @@ Every container on the agent bridge is root in its own network namespace and
shares one L2 segment with the others, so it can send with another container's shares one L2 segment with the others, so it can send with another container's
MAC or address. `services.hyperhive.deploy.hive-controller.egress.antiSpoof` sets what the MAC or address. `services.hyperhive.deploy.hive-controller.egress.antiSpoof` sets what the
`bridge hive_egress` table (`nix/host-modules/hive-egress.nix`) does about it. It `bridge hive_egress` table (`nix/host-modules/hive-egress.nix`) does about it. It
takes `drop` (the default) or `log`. Two tables, not one: the bridge carries traffic between two takes `drop` or `log` (the default, until the lease set survives a host suspend/resume). Two tables, not one: the bridge carries traffic between two
ports, and the `inet` hooks never see it as coming from a port. Only a ports, and the `inet` hooks never see it as coming from a port. Only a
`bridge`-family hook at `prerouting` sees which veth a frame came in on, so the `bridge`-family hook at `prerouting` sees which veth a frame came in on, so the
source check lives in that table and the host-side filtering stays in source check lives in that table and the host-side filtering stays in
@ -872,14 +872,12 @@ source check lives in that table and the host-side filtering stays in
sent; the port names the sender, because a bound port sends only its own MAC. sent; the port names the sender, because a bound port sends only its own MAC.
The counters in `nft list chain bridge hive_egress spoof` count every refused The counters in `nft list chain bridge hive_egress spoof` count every refused
frame. frame.
- **Reading the log.** A container that sends from its leased address and its - **Why `log` is the default.** A container that sends from its leased address
own MAC never gets a line, so a line is a frame to look at. A container that and its own MAC is never refused by `drop`, but only while the pair stays in
has no lease yet logs a few lines in its first seconds: DNS queries from the table, and a host suspend/resume empties it. `log` keeps the lines and
`0.0.0.0`, and the DHCP replies and ICMP redirects its own stack reflects the counters and blocks nothing; `drop` returns once a resume test proves the
while the address isn't configured. They stop at the first lease, and `drop` lease set survives it. `drop`
loses nothing by refusing them. Set `antiSpoof = "log"` to let the frames is what the table is for: without it the egress log can't tell agents apart.
through while you check whether a refusal is a false positive. `drop` is what
the table is for: without it the egress log can't tell agents apart.
A binding that failed shows as `hive-egress-pin` errors in the journal and A binding that failed shows as `hive-egress-pin` errors in the journal and
`IN=` lines for that port. `IN=` lines for that port.
- **What it doesn't cover.** A container can still send from any IPv6 - **What it doesn't cover.** A container can still send from any IPv6

View file

@ -339,7 +339,7 @@ in
"drop" "drop"
"log" "log"
]; ];
default = "drop"; default = "log";
description = '' description = ''
What the `bridge hive_egress` table does with a frame whose source is not What the `bridge hive_egress` table does with a frame whose source is not
the sending container's own. Every port of the agent bridge is checked: the sending container's own. Every port of the agent bridge is checked:
@ -350,13 +350,15 @@ in
address whose `chaddr` is the port's MAC, ARP probes, and IPv6 link-local address whose `chaddr` is the port's MAC, ARP probes, and IPv6 link-local
sources (and `::` in neighbour discovery and listener reports). sources (and `::` in neighbour discovery and listener reports).
- `drop` (the default): the frame is dropped and logged with the prefix - `drop`: the frame is dropped and logged with the prefix
`hive-egress-spoof `, rate limited per port. A packet the per-port limit `hive-egress-spoof `, rate limited per port. A packet the per-port limit
cannot log gets a line with the prefix `hive-egress-spoof-unmetered ` cannot log gets a line with the prefix `hive-egress-spoof-unmetered `
from a limit shared by all ports. A container sending from its leased from a limit shared by all ports. A container sending from its leased
address is never affected, so nothing legitimate breaks. address is never affected, so nothing legitimate breaks.
- `log`: the same lines and counters, and the frame goes through. For - `log` (the default): the same lines and counters, and the frame goes
finding out whether a suspected false positive of `drop` is one. through. For finding out why a legitimate container is refused on a
live hive. It is the default because the lease set does not yet survive
a host suspend/resume; `drop` returns once a resume test proves it.
With `log`, the identity squid takes from the source of an intercepted With `log`, the identity squid takes from the source of an intercepted
flow (`egress.intercept`) can be spoofed: use it to diagnose, not to run. flow (`egress.intercept`) can be spoofed: use it to diagnose, not to run.

View file

@ -73,19 +73,19 @@ let
# The two settings of `egress.forward` the ordinary host (`log`) does not # The two settings of `egress.forward` the ordinary host (`log`) does not
# render, for ../host-modules/hive-egress.nix: `accept` leaves the table # render, for ../host-modules/hive-egress.nix: `accept` leaves the table
# without a forward chain, `reject` renders the chain that refuses. The # without a forward chain, `reject` renders the chain that refuses. The
# `accept` host also sets `egress.antiSpoof = "log"`, the one setting the # `accept` host also sets `egress.antiSpoof = "drop"`, the one setting the
# ordinary host (`drop`) does not render. # ordinary host (`log`) does not render.
egressForwardAccept = hive { egressForwardAccept = hive {
network.enable = true; network.enable = true;
deploy.hive-controller.egress.forward = "accept"; deploy.hive-controller.egress.forward = "accept";
deploy.hive-controller.egress.antiSpoof = "log"; deploy.hive-controller.egress.antiSpoof = "drop";
}; };
# The same two settings under their old names, `network.egress.*`, which # The same two settings under their old names, `network.egress.*`, which
# `deploy.nix` still renames. # `deploy.nix` still renames.
egressForwardAcceptLegacy = hive { egressForwardAcceptLegacy = hive {
network.enable = true; network.enable = true;
network.egress.forward = "accept"; network.egress.forward = "accept";
network.egress.antiSpoof = "log"; network.egress.antiSpoof = "drop";
}; };
egressForwardReject = hiveWithController { egressForwardReject = hiveWithController {
deploy.hive-controller.egress.forward = "reject"; deploy.hive-controller.egress.forward = "reject";
@ -1639,17 +1639,17 @@ let
# its own, and logs it. The ruleset is read as text: the spoof chain # its own, and logs it. The ruleset is read as text: the spoof chain
# ends every path in `drop`, and the accepts that remain are the ones that # ends every path in `drop`, and the accepts that remain are the ones that
# let the leased source through, which is the presence arm. # let the leased source through, which is the presence arm.
name = "egress.antiSpoof defaults to drop and renders the bridge table that drops frames whose source is not the port's own"; name = "egress.antiSpoof drop renders the bridge table that drops frames whose source is not the port's own";
ok = ok =
let let
text = bare.services.hyperhive.network.egress.ruleset; text = egressForwardAccept.services.hyperhive.network.egress.ruleset;
bridgeText = lib.last (lib.splitString "table bridge hive_egress" text); bridgeText = lib.last (lib.splitString "table bridge hive_egress" text);
section = section =
name: lib.head (lib.splitString "chain" (lib.last (lib.splitString "chain ${name} {" bridgeText))); name: lib.head (lib.splitString "chain" (lib.last (lib.splitString "chain ${name} {" bridgeText)));
spoof = lib.filter (l: l != "" && l != "}") (map lib.trim (lib.splitString "\n" (section "spoof"))); spoof = lib.filter (l: l != "" && l != "}") (map lib.trim (lib.splitString "\n" (section "spoof")));
ip4 = section "l3_v4"; ip4 = section "l3_v4";
in in
bare.services.hyperhive.deploy.hive-controller.egress.antiSpoof == "drop" egressForwardAccept.services.hyperhive.deploy.hive-controller.egress.antiSpoof == "drop"
&& lib.hasInfix "hook prerouting priority -300" bridgeText && lib.hasInfix "hook prerouting priority -300" bridgeText
&& lib.hasInfix ''meta ibrname != "hive-br0" accept'' bridgeText && lib.hasInfix ''meta ibrname != "hive-br0" accept'' bridgeText
&& lib.hasInfix "iifname . ether saddr != @port_mac goto spoof" bridgeText && lib.hasInfix "iifname . ether saddr != @port_mac goto spoof" bridgeText
@ -1666,10 +1666,10 @@ let
&& lib.hasInfix "goto spoof" ip4; && lib.hasInfix "goto spoof" ip4;
} }
{ {
# `log` renders the same chains with `accept` where `drop` # `log`, the default, renders the same chains with `accept` where `drop`
# was: the lines and counters stay, the frame goes through. The `accept` # was: the lines and counters stay, the frame goes through. The `accept`
# host sets `log` and `bare` is the control that the two differ. # host sets `drop` and is the control that the two differ.
name = "egress.antiSpoof log renders the spoof chain with accept and no drop"; name = "egress.antiSpoof defaults to log and renders the spoof chain with accept and no drop";
ok = ok =
let let
textOf = m: m.services.hyperhive.network.egress.ruleset; textOf = m: m.services.hyperhive.network.egress.ruleset;
@ -1681,14 +1681,14 @@ let
lib.filter (l: l != "" && l != "}") ( lib.filter (l: l != "" && l != "}") (
map lib.trim (lib.splitString "\n" (lib.head (lib.splitString "chain" t))) map lib.trim (lib.splitString "\n" (lib.head (lib.splitString "chain" t)))
); );
logged = spoofOf egressForwardAccept; logged = spoofOf bare;
in in
egressForwardAccept.services.hyperhive.deploy.hive-controller.egress.antiSpoof == "log" bare.services.hyperhive.deploy.hive-controller.egress.antiSpoof == "log"
&& lib.length logged == 3 && lib.length logged == 3
&& lib.all (l: lib.hasSuffix " accept" l) logged && lib.all (l: lib.hasSuffix " accept" l) logged
&& lib.hasInfix ''log prefix "hive-egress-spoof "'' (textOf egressForwardAccept) && lib.hasInfix ''log prefix "hive-egress-spoof "'' (textOf bare)
&& !(lib.any (l: lib.hasSuffix " drop" l) logged) && !(lib.any (l: lib.hasSuffix " drop" l) logged)
&& lib.all (l: lib.hasSuffix " drop" l) (spoofOf bare); && lib.all (l: lib.hasSuffix " drop" l) (spoofOf egressForwardAccept);
} }
{ {
# The sets are filled at run time, from four places: the unit at start and # The sets are filled at run time, from four places: the unit at start and