Skip to content

feat(net): add network allowlist with DNS sinkhole filtering#410

Merged
DorianZheng merged 2 commits into
mainfrom
feat/dns-sinkhole
Mar 28, 2026
Merged

feat(net): add network allowlist with DNS sinkhole filtering#410
DorianZheng merged 2 commits into
mainfrom
feat/dns-sinkhole

Conversation

@DorianZheng

Copy link
Copy Markdown
Member

Summary

Rename NetworkSpec enum and add DNS sinkhole for outbound network filtering:

  • NetworkSpec::Enabled { allow_net: [] } — full internet access (default)
  • NetworkSpec::Enabled { allow_net: ["api.openai.com"] } — only listed hosts reachable via DNS
  • NetworkSpec::Disabled — no network (gvproxy not started)

DNS sinkhole: blocked hostnames resolve to 0.0.0.0 via gvproxy DNS zones. Direct IP connections are not filtered (TCP-level filtering in follow-up PR).

Changes

  • Rust: NetworkSpec enum with Enabled{allow_net}/Disabled, plumbing through config chain
  • Go: dns_filter.go with AllowNetMatcher + buildAllowNetDNSZones (11 Go tests)
  • SDKs: Python/Node accept "enabled"/"disabled" strings
  • Server test + OpenAPI spec updated

Not included (follow-up PRs)

  • TCP-level filtering (proxy crate + DialFunc)
  • Secret substitution (MITM)

Test plan

  • 593 Rust unit tests pass
  • Go: 11 DNS filter tests (hostname, wildcard, IP, CIDR, zone builder)
  • Backward compatible (default is Enabled { allow_net: [] } = full access)

Rename NetworkSpec: Isolated → Enabled{allow_net}/Disabled.
Add DNS sinkhole: blocked hosts resolve to 0.0.0.0 via gvproxy DNS zones.

- NetworkSpec::Enabled{allow_net: []} — full access (default)
- NetworkSpec::Enabled{allow_net: ["host"]} — DNS sinkhole for unlisted hosts
- NetworkSpec::Disabled — no network (no gvproxy)

Changes:
- Rust: NetworkSpec enum, allow_net plumbing through config chain
- Go: dns_filter.go with AllowNetMatcher + buildAllowNetDNSZones
- SDKs: Python/Node accept "enabled"/"disabled" strings
- Server/OpenAPI: updated test + spec

No proxy crate in this PR (TCP-level filtering in follow-up).
Three fixes for the network allowlist feature:

1. DNS sinkhole: per-TLD zones now include DefaultIP 0.0.0.0 so blocked
   hosts resolve to 0.0.0.0 instead of NXDOMAIN (industry standard per
   Pi-hole/AdGuard — prevents DNS fallback bypass in browsers).

2. NetworkSpec::Disabled: use dead socket trick to prevent libkrun's TSI
   from auto-enabling. Creates a non-functional virtio-net interface via
   UnixStream::pair() so TSI sees non-empty net.list and stays disabled.

3. Guest init: skip network config when NetworkSpec::Disabled (send
   network: None to guest). Use constants instead of hardcoded IPs.

Also exposes allow_net parameter in Python SDK.
@DorianZheng DorianZheng merged commit 5318496 into main Mar 28, 2026
39 checks passed
@DorianZheng DorianZheng deleted the feat/dns-sinkhole branch March 28, 2026 05:45
DorianZheng added a commit that referenced this pull request Mar 28, 2026
Complements the DNS sinkhole from #410 with transport-layer filtering.
Guests can no longer bypass the allowlist by connecting to raw IPs.

For port 443: extracts hostname from TLS ClientHello SNI extension.
For port 80: extracts hostname from HTTP Host header.
Other ports: filters by IP/CIDR rules only.
Zero overhead when no allowlist is configured (filter=nil).

Uses bufio.Reader.Peek() + crypto/tls.Server.GetConfigForClient for
non-consuming SNI extraction (same technique as inet.af/tcpproxy).
Overrides gvproxy's TCP handler via reflect to inject the filter
after virtualnetwork.New() — guarded by structural test.

Go: 25 unit tests (filter, SNI peek, structural guard)
Rust: 2 new ignored VM integration tests
Python: 21 end-to-end integration tests across 7 categories

Also fixes pre-existing boxlite-server test that used wrong serde
format for network field (JSON object instead of string).
DorianZheng added a commit that referenced this pull request Mar 28, 2026
…#411)

Complements the DNS sinkhole from #410 with transport-layer filtering.
Guests can no longer bypass the allowlist by connecting to raw IPs.

For port 443: extracts hostname from TLS ClientHello SNI extension.
For port 80: extracts hostname from HTTP Host header.
Other ports: filters by IP/CIDR rules only.
Zero overhead when no allowlist is configured (filter=nil).

Uses bufio.Reader.Peek() + crypto/tls.Server.GetConfigForClient for
non-consuming SNI extraction (same technique as inet.af/tcpproxy).
Overrides gvproxy's TCP handler via reflect to inject the filter
after virtualnetwork.New() — guarded by structural test.

Go: 25 unit tests (filter, SNI peek, structural guard)
Rust: 2 new ignored VM integration tests
Python: 21 end-to-end integration tests across 7 categories

Also fixes pre-existing boxlite-server test that used wrong serde
format for network field (JSON object instead of string).
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.

1 participant