# The hive's own nftables table, `inet hive_egress`, loaded by a systemd # oneshot. It is independent of the NixOS firewall: `networking.firewall` # renders only the rules of the backend in use (`iptables` unless # `networking.nftables.enable` is set), so a rule that has to hold on every # host lives here, in a table no firewall backend writes. Only # `networking.nftables.flushRuleset` removes it; the unit re-applies it then. # # The table holds one chain: traffic from the agent bridge to host # loopback is dropped. The kernel does the same by default (a packet for # 127.0.0.0/8 arriving on a non-loopback interface is a martian while # `route_localnet` is 0); this rule is the second layer, and the assertion # below keeps the first one on. { lib, config, pkgs, ... }: let cfg = config.services.hyperhive.network; nft = lib.getExe pkgs.nftables; # One transaction: `add` + `delete` drop an old copy of the table and the # definition recreates it, so the file loads the same on first start, on a # reload and after something else flushed the ruleset. ruleset = pkgs.writeText "hive-egress.nft" '' add table inet hive_egress delete table inet hive_egress table inet hive_egress { chain input { type filter hook input priority filter - 10; policy accept; iifname "${cfg.bridgeName}" ip daddr 127.0.0.0/8 counter drop iifname "${cfg.bridgeName}" ip6 daddr ::1 counter drop } } ''; routeLocalnetKeys = [ "net.ipv4.conf.all.route_localnet" "net.ipv4.conf.default.route_localnet" "net.ipv4.conf.${cfg.bridgeName}.route_localnet" ]; isOn = v: builtins.elem v [ 1 "1" true ]; in { config = lib.mkIf cfg.enable { assertions = [ { assertion = !(lib.any (k: isOn (config.boot.kernel.sysctl.${k} or 0)) routeLocalnetKeys); message = '' boot.kernel.sysctl must not turn on route_localnet (${lib.concatStringsSep ", " routeLocalnetKeys}). With it on, the kernel routes a packet from the agent bridge to 127.0.0.0/8 instead of dropping it as a martian, which leaves the hive_egress table as the only barrier between an agent and the host's loopback services. ''; } ]; systemd.services.hive-egress = { description = "hive nftables table (agent bridge to host loopback drop)"; wantedBy = [ "multi-user.target" ]; wants = [ "network-pre.target" ]; before = [ "network-pre.target" ]; # `nftables.service` runs `flush ruleset` where `networking.nftables.flushRuleset` # is on: start after it, follow its restarts and re-apply on its reload. after = [ "nftables.service" ]; partOf = [ "nftables.service" ]; unitConfig.ReloadPropagatedFrom = [ "nftables.service" ]; reloadIfChanged = true; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${nft} -f ${ruleset}"; ExecReload = "${nft} -f ${ruleset}"; ExecStop = "-${nft} delete table inet hive_egress"; }; }; }; }