Watch
0
0
Fork
You've already forked hyperhive
0

egress anti-spoofing: keep established bindings, load tables with bindings and leases in one transaction

This commit is contained in:
atlas 2026-11-13 02:53:37 +01:00 • committed by mara
commit 70179b6c1f
2 changed files with 400 additions and 134 deletions

View file

@ -18,9 +18,11 @@
# traffic between two ports is bridged: the `inet` hooks see it as coming from
# the bridge device, and only a bridge hook knows the port it came in on
# (`iifname` is the veth). Which MAC a port may use and which IPv4 address a MAC
# may use are not known when the ruleset loads, so they are set contents that
# `hive-priv egress-pin` (per port, from udev) and dnsmasq's `dhcp-script`
# (per lease) fill; `hive-priv`'s `egress.rs` documents how.
# may use are not known until a container runs, so they are set contents.
# `hive-priv egress-load` puts the current ones into the same transaction as the
# tables; `egress-pin` (per new port from udev, and from a timer for the ports
# still unbound) and dnsmasq's `dhcp-script` (per lease) keep them current.
# `hive-priv`'s `egress.rs` documents how.
{
lib,
config,
@ -39,7 +41,8 @@ let
# the one every platform container's unit uses (hive-priv.nix points it at the
# current build).
hivePriv = (import ./lib/container-rootfs-check.nix { inherit config; }).hivePriv;
pinCommand = args: "${hivePriv} egress-pin ${args}";
pinCommand = args: lib.trim "${hivePriv} egress-pin ${args}";
loadCommand = "${hivePriv} egress-load ${ruleset}";
pinPath = [
pkgs.nftables
pkgs.iproute2
@ -308,14 +311,11 @@ in
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${nft} -f ${ruleset}";
# The sets of a freshly loaded bridge table are empty: bind the ports of
# the running containers and load the leases from dnsmasq's file.
ExecStartPost = pinCommand "--seed";
ExecReload = [
"${nft} -f ${ruleset}"
(pinCommand "--seed")
];
# The tables load with the bindings of the running containers' ports and
# the leases of dnsmasq's file in the same transaction, so a reload never
# leaves the bridge with empty sets.
ExecStart = loadCommand;
ExecReload = loadCommand;
ExecStop = [
"-${nft} delete table inet hive_egress"
"-${nft} delete table bridge hive_egress"
@ -337,6 +337,30 @@ in
};
};
# Looks again at the ports still unbound. A port a rival MAC kept unbound
# sends nothing, so nothing else would tell the hive it can be bound now.
# Ports with a binding cost a listing of the bridge and no container is read.
systemd.services.hive-egress-pin = {
description = "bind the agent bridge ports that have no binding yet";
wants = [ "hive-egress.service" ];
after = [ "hive-egress.service" ];
path = pinPath;
environment = pinEnvironment;
serviceConfig = {
Type = "oneshot";
ExecStart = pinCommand "";
};
};
systemd.timers.hive-egress-pin = {
description = "bind the agent bridge ports that have no binding yet";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "1min";
OnUnitActiveSec = "30s";
AccuracySec = "5s";
};
};
# nspawn names the host end of a container's veth `vb-<machine>` when the
# container joins a bridge.
services.udev.extraRules = ''

View file

@ -81,7 +81,7 @@ in
# bridgeIp/bridgePrefixLength. All containers (agents and service
# containers such as swarm-ci-runner) receive their IPs dynamically.
dhcp-range = "${networkCfg.dhcpRangeStart},${networkCfg.dhcpRangeEnd},1h";
# `hive-priv egress-pin --seed` reads this file back (hive-egress.nix).
# `hive-priv egress-load` reads this file back (hive-egress.nix).
dhcp-leasefile = "/var/lib/dnsmasq/dnsmasq.leases";
# Each lease change updates the leased (MAC, IPv4) pairs the bridge
# anti-spoofing table checks sources against (hive-egress.nix).