Repository navigation
egress: keep guests off non-public networks and mail ports in every mode - #12
Merged
Merged
Conversation
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.
There was a problem hiding this comment.
🟡 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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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:
--dns-servers, on port 53 only.In allowlist mode the guards sit after the resolver accept and before the allowlist accept.
ct status & dnat == 0).ct status != dnatcompiles to an exact compare against0x20and 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
make netns-test+ newnetnsCI 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.