Watch
0
0
Fork
You've already forked hyperhive
0

agent bridge: drop frames whose source MAC or address isn't the container's own (egress.antiSpoof)

This commit is contained in:
atlas 2026-11-13 02:36:38 +01:00 • committed by mara
commit c4ac01ef88

View file

@ -72,10 +72,13 @@ let
# The two settings of `egress.forward` the ordinary host (`log`) does not
# render, for ../host-modules/hive-egress.nix: `accept` leaves the table
# without a forward chain, `drop` is refused by an assertion.
# without a forward chain, `drop` is refused by an assertion. The `accept`
# host also sets `egress.antiSpoof = "log"`, the one setting the ordinary
# host (`drop`) does not render.
egressForwardAccept = hive {
network.enable = true;
network.egress.forward = "accept";
network.egress.antiSpoof = "log";
};
egressForwardDrop = hive {
network.enable = true;
@ -1529,7 +1532,8 @@ let
unit != null
&& unit.serviceConfig.Type == "oneshot"
&& lib.hasInfix "hive-egress.nft" unit.serviceConfig.ExecStart
&& lib.hasInfix "delete table inet hive_egress" unit.serviceConfig.ExecStop
&& lib.hasInfix "delete table inet hive_egress" (toString unit.serviceConfig.ExecStop)
&& lib.hasInfix "delete table bridge hive_egress" (toString unit.serviceConfig.ExecStop)
&& bare.networking.firewall.extraInputRules == ""
&& !(centralToggleOff.systemd.services ? hive-egress);
}
@ -1563,6 +1567,83 @@ let
&& !(lib.any (l: lib.hasSuffix " drop" l || lib.hasSuffix " reject" l) rules)
&& lib.hasInfix "chain input" text;
}
{
# The bridge table drops what a port sends from an address that is not
# 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
# let the leased source through, which is the presence arm.
name = "the default egress table drops frames whose source is not the port's own";
ok =
let
text = bare.services.hyperhive.network.egress.ruleset;
bridgeText = lib.last (lib.splitString "table bridge hive_egress" text);
section =
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")));
ip4 = section "l3_v4";
in
bare.services.hyperhive.network.egress.antiSpoof == "drop"
&& lib.hasInfix "hook prerouting priority -300" bridgeText
&& lib.hasInfix ''meta ibrname != "hive-br0" accept'' bridgeText
&& lib.hasInfix "iifname . ether saddr != @port_mac goto spoof" bridgeText
&& lib.hasInfix "ether type { 8021q, 8021ad } goto spoof" bridgeText
&& lib.hasInfix "udp dport 67 iifname . @th,288,48 != @port_chaddr goto spoof" bridgeText
&& lib.hasInfix "ether saddr . ip saddr @lease_v4 accept" bridgeText
&& lib.hasInfix "ip6 saddr fe80::/10 accept" bridgeText
&& lib.hasInfix "arp saddr ether . arp saddr ip @lease_v4 accept" bridgeText
&& lib.hasInfix ''log prefix "hive-egress-spoof "'' bridgeText
&& lib.hasInfix ''log prefix "hive-egress-spoof-unmetered "'' bridgeText
&& lib.hasInfix "update @spoof_ports { iifname limit rate 1/second burst 20 packets }" bridgeText
&& lib.length spoof == 3
&& lib.all (l: lib.hasSuffix " drop" l) spoof
&& lib.hasInfix "goto spoof" ip4;
}
{
# `log` renders the same chains with `accept` where `drop` was: the lines
# and counters stay, the frame goes through. The `accept` host is the
# only fixture that sets it; `bare` is the control that the two differ.
name = "egress.antiSpoof log renders the spoof chain with accept and no drop";
ok =
let
textOf = m: m.services.hyperhive.network.egress.ruleset;
spoofOf =
m:
let
t = lib.last (lib.splitString "chain spoof {" (textOf m));
in
lib.filter (l: l != "" && l != "}") (
map lib.trim (lib.splitString "\n" (lib.head (lib.splitString "chain" t)))
);
logged = spoofOf egressForwardAccept;
in
egressForwardAccept.services.hyperhive.network.egress.antiSpoof == "log"
&& lib.length logged == 3
&& lib.all (l: lib.hasSuffix " accept" l) logged
&& lib.hasInfix ''log prefix "hive-egress-spoof "'' (textOf egressForwardAccept)
&& !(lib.any (l: lib.hasSuffix " drop" l) logged)
&& lib.all (l: lib.hasSuffix " drop" l) (spoofOf bare);
}
{
# The sets are filled at run time, from three places: the unit at start
# and reload, the udev rule for each new port, and dnsmasq for each
# lease. Each is present on `bare`; the host without the bridge has none.
name = "the bridge table's sets are filled by the unit, a udev rule per port and dnsmasq's lease script";
ok =
let
unit = bare.systemd.services.hive-egress;
pin = bare.systemd.services."hive-egress-pin@";
in
lib.hasInfix "egress-pin --seed" unit.serviceConfig.ExecStartPost
&& lib.any (lib.hasInfix "egress-pin --seed") unit.serviceConfig.ExecReload
&& unit.environment.HIVE_EGRESS_BRIDGE == "hive-br0"
&& lib.hasInfix "egress-pin %i" pin.serviceConfig.ExecStart
&& lib.hasInfix ''KERNEL=="vb-*"'' bare.services.udev.extraRules
&& lib.hasInfix "hive-egress-pin@%k.service" bare.services.udev.extraRules
&&
bare.services.dnsmasq.settings.dhcp-script
== toString bare.services.hyperhive.network.egress.leaseScript
&& !(centralToggleOff.systemd.services ? "hive-egress-pin@");
}
{
# `accept` renders the table without the forward chain and without the
# log prefix; the input chain is the presence arm that the text it