Skip to content

egress: keep guests off non-public networks and mail ports in every mode - #12

Merged
linyiru merged 3 commits into
mainfrom
fix/egress-private-range-drop
Oct 13, 2026
Merged

linyiru merged 3 commits into
mainfrom
fix/egress-private-range-drop

Conversation

@linyiru

@linyiru linyiru commented Oct 12, 2026

Copy link
Copy Markdown
Member

Problem

In --egress open (what the falcon CI hosts run) the forward chain only dropped spoofed sources and guest-to-guest traffic, so a guest could reach anything the host routes to: the home LAN, other RFC1918 ranges, the tailnet (100.64/10), link-local metadata, and outbound mail ports. In allowlist mode, mail ports on allowlisted ranges were open.

Verified against the real kernel (nft 1.1.3, throwaway network namespaces) before the fix: every one of those destinations was reachable from a guest.

Fix

In every mode, guest traffic to non-globally-reachable IPv4 (RFC 6890) and to TCP 25/465/587/2525 is dropped. Two host-chosen exceptions:

  • a destination the host itself DNAT'd (the CI hosts redirect guest DNS to a resolver on a private veth); a guest cannot create DNAT entries;
  • in allowlist mode, a resolver listed in --dns-servers, on port 53 only.

In allowlist mode the guards sit after the resolver accept and before the allowlist accept.

⚠️ The DNAT exemption tests the bit (ct status & dnat == 0). ct status != dnat compiles to an exact compare against 0x20 and dropped every DNAT'd connection, which would have broken all guest DNS on the falcons. Caught only by the netns test; now pinned by a unit test too.

Tests

  • Unit: guards present in every mode, forward-rule order for both modes, the dnat bit test. Reintroducing either bug fails them.
  • make netns-test + new netns CI job (ubuntu-latest): loads the generated ruleset into network namespaces and probes, TCP RST vs timeout, LAN / RFC1918 / tailnet / metadata / mail ports / other guests / host addresses / cache port / configured LAN resolver / host-DNAT'd DNS. Passes in both modes, 3 repeated runs stable.
  • go vet, go test -race ./... on Linux, coverage 80.2%.

Rollout

Ships to the falcons via a release + fleetranch ansible sha256 pin bump, canary on falcon-2 first. No known CI job needs LAN/tailnet/SMTP from a guest, but watch for jobs stalling on connect timeouts after the canary.

Open mode forwarded a guest to anything the host could route to: the
forward chain only dropped spoofed sources and guest-to-guest traffic, so
a CI job could reach the home LAN, the tailnet (100.64/10) and link-local
metadata, and send mail from the host's address. Allowlist mode blocked
the networks but not mail ports on allowlisted ranges.

Add, in every mode, a drop for guest traffic to non-globally-reachable
IPv4 (RFC 6890) and to TCP 25/465/587/2525. A destination the host itself
DNAT'd is exempt from the non-public drop (the CI hosts redirect guest DNS
to a resolver on a private veth); a guest cannot create DNAT entries. In
allowlist mode the guards sit after the explicit resolver accept and
before the allowlist accept.

The exemption tests the dnat bit (ct status & dnat == 0): on nft 1.1.3
'ct status != dnat' compiles to an exact compare against 0x20 and drops
every DNAT'd connection.
Load the generated ruleset into throwaway network namespaces and probe,
TCP RST vs timeout, what a guest reaches in open and allowlist mode: LAN,
RFC1918, tailnet, metadata, mail ports, other guests, host addresses,
the cache port, a configured LAN resolver and host-DNAT'd DNS. Run by
'make netns-test' and a new ubuntu-latest CI job.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The RFC 6890 blocklist omits multiple non-globally-reachable ranges, leaving them accessible in open mode.

1 open finding
What changed in this PR

Hardens guest egress by blocking non-public IPv4 destinations and mail ports across all modes.

Changes:

  • Adds nftables egress guards and unit tests.
  • Adds kernel-level namespace integration tests and CI coverage.
  • Documents the restrictions and test workflow.
File Description
README.md Documents egress restrictions and exceptions.
Makefile Adds the network-namespace test target.
internal/​provisioner/​egress.go Generates the new firewall guards.
internal/​provisioner/​egress_test.go Tests guard generation and ordering.
internal/​provisioner/​egress_netns_test.go Tests firewall behavior against nftables.
.gitignore Ignores the generated test binary.
.github/​workflows/​ci.yml Runs namespace tests in CI.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/provisioner/egress.go
192.0.2.0/24, 198.51.100.0/24 and 203.0.113.0/24 are not globally
reachable (RFC 6890) but were missing from nonpublic, and the netns test
used TEST-NET addresses as its public stand-ins, which hid the gap. The
lab now uses genuinely global addresses for the reachable fixtures and
probes a TEST-NET address as blocked. 192.88.99.0/24 stays out: RFC 6890
marks the deprecated 6to4 relay anycast range global.
@linyiru
linyiru merged commit 40e3b79 into main Oct 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants