Skip to content

feat(net): add TCP-level allowlist filtering with SNI/Host inspection#411

Merged
DorianZheng merged 1 commit into
mainfrom
feat/tcp-allownet-filter
Mar 28, 2026
Merged

feat(net): add TCP-level allowlist filtering with SNI/Host inspection#411
DorianZheng merged 1 commit into
mainfrom
feat/tcp-allownet-filter

Conversation

@DorianZheng

Copy link
Copy Markdown
Member

Summary

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

  • Port 443: Extracts hostname from TLS ClientHello SNI extension
  • 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.

Behavior by port

Port Has hostname rules Has IP/CIDR rules Behavior
443 yes - Accept → Peek TLS SNI → check hostname
80 yes - Accept → Peek HTTP Host → check hostname
other - yes Standard flow → check IP/CIDR
other yes only - Blocked (can't extract hostname)
any - - filter=nil → upstream flow (no filtering)

Files

New Go files (7):

  • tcp_filter.go — AllowNet matcher (IP/CIDR/hostname/wildcard)
  • tcp_filter_test.go — 12 unit tests
  • sni_peek.go — TLS SNI + HTTP Host extraction via bufio.Peek()
  • sni_peek_test.go — 10 unit tests
  • forked_tcp.go — TCP forwarder with filter + SNI inspection
  • forked_network.go — Override TCP handler via reflect
  • forked_network_test.go — Structural guard test

Modified:

  • main.go — Wire filter after virtualnetwork.New()
  • network_spec.rs — 2 new ignored VM integration tests
  • types.rs — Fix pre-existing server test (wrong serde format)

New Python tests:

  • test_tcp_filter.py — 21 end-to-end VM integration tests across 7 categories

Test plan

  • 25 Go unit tests pass (filter, SNI peek, structural guard)
  • 6 Rust network_spec tests pass (non-ignored)
  • 68 boxlite-server tests pass (including fixed deserialization test)
  • 21 Python VM integration tests pass (default access, hostname allowlist, wildcard, IP/CIDR, mixed rules, disabled network, edge cases)
  • CI: Run make test:integration:rust FILTER=network_spec for VM tests

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 DorianZheng merged commit c75c99b into main Mar 28, 2026
31 checks passed
@DorianZheng DorianZheng deleted the feat/tcp-allownet-filter branch March 28, 2026 13:23
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