Files
2026-09-15 15:24:03 -04:00

71 lines
2.9 KiB
Plaintext

# =====================================================================
# 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
}
}