#609

feat: add outbound filtering with SNI-based allowlist and security hardening

Openclemlesnewants to merge intofrom

Problem

Sandboxed environments — untrusted code execution, CI/CD pipelines, isolated testing — need to control outbound network access from VMs/containers while keeping host-to-guest communication working (orchestrators need to run health checks, collect metrics, or interact with services inside the guest).

Today there is no mechanism to restrict what a guest can reach on the network. A guest can connect to any IP or resolve any domain.

Replaces #599 and #600 — this PR combines the functionality of both with a stronger security posture.

Functional needs

  1. Full outbound block (blockAllOutbound): kill all guest-initiated outbound traffic (TCP + UDP), while host-to-guest port forwards keep working for orchestrator use cases (health checks, API calls into the guest, etc.)
  2. Domain allowlist (outboundAllow): allow outbound connections only to a list of approved domains. This must filter at both layers — DNS resolution (unauthorized domains get NXDOMAIN) and actual IP connections (so hardcoded IPs or SNI spoofing don't bypass the filter)
  3. Gateway exemption: traffic to the gateway address is always allowed (internal services, DNS)

Implementation

blockAllOutbound

TCP and UDP forwarders reject all guest-initiated connections before net.Dial(). Port forwards (host-to-guest) are unaffected since they use a separate path.

outboundAllow

A list of regex patterns matching allowed domains. When configured:

TrafficBehavior
DNS queriesChecked against allowlist before upstream resolution — blocked domains get NXDOMAIN
TCP port 443TLS ClientHello peeked → SNI extracted → checked against allowlist → SNI resolved via DNS → resolved IPs compared against actual destination IP
TCP non-443Blocked (no SNI available to verify)
UDPOnly gateway-bound allowed (DNS)
GatewayAlways exempt

Files changed

FileWhat
pkg/types/configuration.goBlockAllOutbound and OutboundAllow config fields
pkg/virtualnetwork/services.goPass new config to forwarders and DNS server
pkg/services/forwarder/sni.goTLS ClientHello parser (SNI + ALPN extraction)
pkg/services/forwarder/tcp.goTCP routing, TLS allowlist handler, DNS cross-check
pkg/services/forwarder/udp.goUDP routing with allowlist support
pkg/services/dns/dns.goDNS allowlist filtering
+ *_test.go filesComprehensive tests for all of the above

Security posture

The SNI parser and allowlist matching are hardened against known bypass techniques:

AttackRiskMitigation
IP literal in SNIRFC 6066 forbids it; CVE-2025-25255 showed Fortinet bypassed via IP-only connectionsnet.ParseIP() rejects IPv4/IPv6 literals in parseSNIExtension
Case mismatchDNS is case-insensitive (RFC 4343) but regex isn't; EXAMPLE.COM wouldn't match ^example\.com$strings.ToLower() at the source — both SNI parser and DNS handler normalize before any matching
SNI spoofing#1 practical bypass — client sends allowed.com in SNI but connects to evil.com's IPDNS cross-check: resolve SNI hostname, verify at least one IP matches the destination; fail-closed on lookup failure
Tunneling detectionUnusual ALPN values (e.g. VLESS+WebSocket using h2) can indicate tunnelingALPN protocols extracted from ClientHello and logged for observability
TLS record fragmentationAttacker splits ClientHello across multiple TLS records to evade parsersParser reassembles fragmented handshake messages before extraction
ECH/ESNIEncrypted Client Hello hides the real SNI; outer SNI can't be trustedParser detects ECH (0xfe0d) and legacy ESNI (0xffce) extensions → connection blocked

Test plan

  • go build ./cmd/gvproxy/
  • go test ./pkg/services/forwarder/ -count=1 — 100+ tests pass
  • go test ./pkg/services/dns/ -count=1 — all pass
  • go vet ./pkg/services/forwarder/ ./pkg/services/dns/ — clean
  • Integration test with actual VM

🤖 Generated with Claude Code

Activity

18
openshift-ci[bot]8mo ago

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: clemlesne Once this PR has been reviewed and has the lgtm label, please assign lstocchi for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

openshift-ci[bot]8mo ago

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: clemlesne Once this PR has been reviewed and has the lgtm label, please assign lstocchi for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

openshift-ci[bot]8mo ago

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: clemlesne Once this PR has been reviewed and has the lgtm label, please assign lstocchi for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

openshift-ci[bot]8mo ago

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: clemlesne Once this PR has been reviewed and has the lgtm label, please assign lstocchi for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

openshift-ci[bot]8mo ago

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: clemlesne Once this PR has been reviewed and has the lgtm label, please assign lstocchi for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

clemlesne8mo ago

ECH/GREASE compatibility (04a0b9ce)

Chrome 117+ and Firefox 119+ send ECH GREASE (0xfe0d) on every TLS 1.3 handshake. The proxy was dropping these — breaking HTTPS for modern clients.

Removed the ECH rejection (3 lines in parseClientHello). Per RFC 9849 §8.1.2, the proxy acts on the outer SNI — which the allowlist + DNS cross-validation already enforce. ECH doesn't weaken this: the SNI must still match the regex AND resolve to the destination IP.

Security table correction — ECH row: Outer SNI used per RFC 9849 §8.1.2; allowlist + DNS cross-validation enforced; ECH extensions silently ignored.

clemlesne8mo ago

ECH/GREASE compatibility (04a0b9ce)

Chrome 117+ and Firefox 119+ send ECH GREASE (0xfe0d) on every TLS 1.3 handshake. The proxy was dropping these — breaking HTTPS for modern clients.

Removed the ECH rejection (3 lines in parseClientHello). Per RFC 9849 §8.1.2, the proxy acts on the outer SNI — which the allowlist + DNS cross-validation already enforce. ECH doesn't weaken this: the SNI must still match the regex AND resolve to the destination IP.

Security table correction — ECH row: Outer SNI used per RFC 9849 §8.1.2; allowlist + DNS cross-validation enforced; ECH extensions silently ignored.

clemlesne8mo ago

ECH/GREASE compatibility (04a0b9ce)

Chrome 117+ and Firefox 119+ send ECH GREASE (0xfe0d) on every TLS 1.3 handshake. The proxy was dropping these — breaking HTTPS for modern clients.

Removed the ECH rejection (3 lines in parseClientHello). Per RFC 9849 §8.1.2, the proxy acts on the outer SNI — which the allowlist + DNS cross-validation already enforce. ECH doesn't weaken this: the SNI must still match the regex AND resolve to the destination IP.

Security table correction — ECH row: Outer SNI used per RFC 9849 §8.1.2; allowlist + DNS cross-validation enforced; ECH extensions silently ignored.

clemlesne8mo ago

ECH/GREASE compatibility (04a0b9ce)

Chrome 117+ and Firefox 119+ send ECH GREASE (0xfe0d) on every TLS 1.3 handshake. The proxy was dropping these — breaking HTTPS for modern clients.

Removed the ECH rejection (3 lines in parseClientHello). Per RFC 9849 §8.1.2, the proxy acts on the outer SNI — which the allowlist + DNS cross-validation already enforce. ECH doesn't weaken this: the SNI must still match the regex AND resolve to the destination IP.

Security table correction — ECH row: Outer SNI used per RFC 9849 §8.1.2; allowlist + DNS cross-validation enforced; ECH extensions silently ignored.

openshift-ci[bot]6mo ago

PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

openshift-ci[bot]6mo ago

PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.