# ===================================================================== # Second-layer egress filter for the VM sandbox. # # WHY THIS IS THE SECOND LAYER AND NOT THE FIRST # # The primary control is in owuivm.service: systemd's IPAddressDeny= # attaches a BPF filter to the unit's cgroup, which `nft flush ruleset` # cannot reach and which applies to every socket the process opens. # This table says the same thing a second time, independently. # # It is deliberately NOT trusted on its own. nft uid-matching is # fail-open: a `ufw reload`, an `nft -f` carrying `flush ruleset`, a # package upgrade, or this unit simply failing to start, and the rules # are gone with nothing to notice. netguard/firewall.sh exists in this # suite for exactly that class of failure - "a firewall that silently # failed to apply is worse than none, because everything downstream # carries on as though it were enforced". # # So the test that proves the design is not "these rules are loaded". # It is: delete this table, re-run the whole guest negative suite, and # watch it still pass. Then strip IPAddressDeny from a scratch unit and # run it again. Two proofs, neither trusting the other. # # WHAT IT MATCHES # # Every guest packet is re-originated by the qemu process as an ordinary # host socket (see the -netdev user decision in vmd), so the whole guest # is reachable as "traffic owned by uid owuivm". There is no tap, no # bridge and no forwarding path, which is why nothing here hooks forward. # # Note 10.0.0.0/8 does NOT block the guest's own 10.0.2.x addresses. # Those exist only inside libslirp and never become host packets. The # 127.0.0.0/8 line is what closes SLIRP's host aliases - 10.0.2.2 is the # host's loopback and 10.0.2.3 its DNS forwarder - which is why the # guest images carry a static public resolver instead of asking DHCP. # ===================================================================== table inet owuivm_guard { set rfc1918_and_local { type ipv4_addr flags interval elements = { 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4 } } chain out { # priority filter - 10, so this runs ahead of ufw's own output # chain. policy accept: this table only ever drops, exactly like # /etc/nftables.d/50-node_exporter_guard.nft does for input. type filter hook output priority filter - 10; policy accept; meta skuid "owuivm" ip daddr @rfc1918_and_local \ counter log prefix "owuivm-blocked: " level info drop # The guest has no IPv6 at all: the netdev is ipv6=off and the # unit omits AF_INET6 from RestrictAddressFamilies. This is the # third statement of the same thing, and it costs nothing. meta skuid "owuivm" meta nfproto ipv6 counter drop } }