Skip to content

Add a dial filter and a DNS answer observer for embedders - #750

Open
gorkem wants to merge 2 commits into
containers:mainfrom
gorkem:forwarder-dial-filter-dns-observer
Open

gorkem wants to merge 2 commits into
containers:mainfrom
gorkem:forwarder-dial-filter-dns-observer

Conversation

@gorkem

@gorkem gorkem commented Oct 14, 2026 •

Copy link
Copy Markdown

An embedder that runs untrusted workloads in the guest, and wants to hold them to an allow list of destinations on the host side, has no point where it can refuse a connection the forwarders open for the guest, or learn the addresses the embedded DNS server resolved. This adds two optional hooks to types.Configuration:

  • DialFilter func(network, address string) error is called by the TCP, UDP and ICMP forwarders with the destination after NAT translation, before they dial. A non-nil error refuses the connection: TCP answers the guest with a reset, UDP and ICMP are dropped.
  • DNSObserver func(name string, ips []net.IP) is called by the DNS server with the addresses of every A or AAAA answer it takes from the upstream resolver, before the answer is sent, so a connection the guest opens after the lookup already finds its address allowed.

Both are yaml:"-" and nil by default, which leaves current behaviour unchanged. One thing to note for review: forwarder.TCP, forwarder.UDP and forwarder.ICMP take the filter as a new trailing parameter, so a caller of those functions outside pkg/virtualnetwork needs to pass nil. If an options struct or a setter would suit the project better, I can change it.

The change is split in two commits, one per hook, each building on its own. pkg/services/forwarder and pkg/services/dns gain tests for the refusal and the observed answers. make cross-lint and go test ./pkg/... pass; the VM-based suites were not run locally.

@openshift-ci

openshift-ci Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

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

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

Details 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

@evidolob
evidolob self-requested a review November 2, 2026 13:08
@evidolob

evidolob commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

@gorkem sorry for long reply, I have a quite similar PR #687 would it fit your needs? Otherwise I would think on how to integrate this PR in to my.

An embedder that runs untrusted workloads in the guest needs to decide,
per connection, whether the host may open it on the guest's behalf, for
example to keep the guest to an allow list of destinations. The TCP, UDP
and ICMP forwarders open those connections with no point where the
embedder can refuse one.

Configuration gains DialFilter. When it is set, each forwarder calls it
with the network ("tcp", "udp" or "icmp") and the destination after NAT
translation before it dials, and a non-nil error refuses the connection:
TCP answers the guest with a reset, UDP and ICMP are dropped. When it is
nil, nothing changes.

Signed-off-by: Gorkem Ercan <gorkem.ercan@gmail.com>
An embedder that limits the guest to named destinations needs the
addresses those names resolve to, so it can allow connections to them.
The embedded DNS server answers the guest from its upstream resolver
without exposing those answers.

Configuration gains DNSObserver. When it is set, the DNS server calls it
with the name, without its trailing dot, and the addresses of every A or
AAAA answer it takes from the upstream resolver, before the answer is sent,
so a connection the guest opens after the lookup already finds its
address allowed. When it is nil, nothing changes.

Signed-off-by: Gorkem Ercan <gorkem.ercan@gmail.com>
@gorkem
gorkem force-pushed the forwarder-dial-filter-dns-observer branch from 70159b9 to a3159d4 Compare November 7, 2026 03:02
@gorkem

gorkem commented Nov 7, 2026

Copy link
Copy Markdown
Author

@gorkem sorry for long reply, I have a quite similar PR #687 would it fit your needs? Otherwise I would think on how to integrate this PR in to my.

Thanks @evidolob, and no worries.

I went through #687 to see if it covers what I need. Unfortunately it doesn't quite fit.

My use case: a program embeds gvisor-tap-vsock as a Go library to give a VM networking, and that VM runs untrusted workloads. The host process decides which destinations the guest may reach. The allowed set comes from the embedding program's own configuration, not from gvproxy's YAML, and it changes while the VM is running. To enforce that on the host side, I need things #687's policy doesn't express:

  • Ports other than 443. Destinations are ip:port pairs on arbitrary TCP ports. That includes a few ports on the host's loopback, reached through a NAT'd virtual IP. With OutboundAllow set, Allow filtering of outgoing connectins #687 only exempts the gateway IP, so those connections get blocked.
  • IP literals and non-TLS traffic. Some allowed destinations are plain addresses, and not every allowed connection is TLS with an SNI.
  • Changes during a run. The host adds destinations while the VM runs. In Allow filtering of outgoing connectins #687 the runtime allowlist is checked by DNS, but handleTLSWithAllowlist only consults the static outboundAllow list.
  • Opening addresses from DNS answers. When the guest resolves an allowed name, I open exactly the addresses in the answer it received. Re-resolving on the host and requiring a match can give false blocks for CDNs that rotate their answers.

That's why #750 adds two hooks rather than a policy: a dial filter called with the destination after NAT, and an observer for upstream DNS answers. The embedding program decides what's allowed, and when both hooks are nil, nothing changes.

The two could fit together if #687's allowlist, blocklist and SNI checks were built on top of the same hooks. #687's policy would then be one implementation, and gvproxy users would get it from config and the REST API as you've designed it. Library users with their own policy could plug in theirs. For TLS, the hook would need to be called with the SNI once it's been peeked. I'd be happy to extend #750's filter signature for that, or rebase onto yours if it lands first.

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