Skip to content

[Prototype] Send actor DNS to the egress PEP over DNS over HTTPS - #3854

Draft
Chuang Wang (chuangw6) wants to merge 5 commits into
agent-substrate:mainfrom
chuangw6:dns-doh-pep-prototype
Draft

Chuang Wang (chuangw6) wants to merge 5 commits into
agent-substrate:mainfrom
chuangw6:dns-doh-pep-prototype

Conversation

@chuangw6

Copy link
Copy Markdown
Collaborator

Prototype for the DNS contract with the egress PEP discussed in #2313. This is a draft for discussion, not for merge.

Today the worker's DNS relay checks each actor DNS query against the actor's EgressPolicy and resolves it itself, so a PEP that keeps its own policy cannot govern DNS. This prototype moves the decision to the PEP.

What it does

--dns-egress-policy=enforce-in-pep is a new mode. enforce-in-atunnel is unchanged.

  • atunnel's DNS relay applies no policy. It sends each query to the PEP as an RFC 8484 GET /dns-query?dns=<base64url> on the pooled mTLS connection that carries the actor's CONNECT tunnels, and returns the PEP's response to the actor. No new port or address.
  • The atenet-egress ext_proc answers the query. It runs the actor checks a CONNECT gets, resolves a name that a rule in the actor's EgressPolicy names, and answers any other name NXDOMAIN (SOA with TTL 0, Extended DNS Error 15). Both are HTTP 200 with a DNS message, returned as an ext_proc immediate response. The Envoy config does not change.
  • The relay answers SERVFAIL when the PEP gives no usable response.
  • The PEP logs each denied lookup with the actor, name and type.

The commits are ordered for review: a refactor of the CONNECT actor checks, shared helpers, the PEP side, the atunnel side, and a latency test.

Findings

On kind with a gVisor actor, the unchanged TestActorDNSRequiresPolicy, TestActorEgress, TestActorEgressPolicyDeniesUnlistedHost and TestActorEgressHTTPSByHostname pass in both modes.

Lookup latency inside the actor, median of three alternating runs (single-node kind on Docker Desktop, arm64):

Lookup enforce-in-atunnel enforce-in-pep
Allowed name, one at a time, p50 0.51 ms 3.03 ms
Blocked name, one at a time, p50 0.25 ms 2.58 ms
Allowed name, 16 in parallel, p50 1.19 ms 8.47 ms
Allowed name, 16 in parallel, p99 62.6 ms 21.2 ms
Allowed name after 35 s idle, p50 10.4 ms 17.2 ms

Of the 2.5 ms added to a sequential lookup, about 1.1 ms is the GetActor call the PEP makes on every lookup (CONNECT makes it once per tunnel), about 0.13 ms is re-verifying the certificate chain, and the remaining 1.3 ms is the extra hop. The GetActor call rises to a median of 3.0 ms at 16 parallel lookups and 5.5 ms at 48. The split came from temporary timing that is not in this PR.

With atenet-egress scaled to zero, the actor gets SERVFAIL after 5 s.

How to reproduce

On a kind cluster installed from this branch with the e2e job's settings, switch the mode and wait for the worker pools to roll:

kubectl -n ate-system get deploy ate-controller -o yaml \
  | sed 's/--dns-egress-policy=enforce-in-atunnel/--dns-egress-policy=enforce-in-pep/' \
  | kubectl apply -f -

Then run the latency test, which is skipped unless E2E_DNS_LATENCY_OUT is set:

E2E_DNS_LATENCY_OUT=/tmp/enforce-in-pep.json E2E_DNS_EGRESS_POLICY=enforce-in-pep \
  hack/run-e2e-kind.sh ./internal/e2e/suites/networking -run '^TestActorDNSLatency$' -v -args --no-color

Open questions and gaps

  • GET or POST. The relay sends GET because header-only proxy hooks such as ext_proc can read the query from the URL. The prototype PEP answers POST with 405, which RFC 8484 does not allow a server to do.
  • Whether a PEP should confirm the actor with the control plane on every lookup, cache that, or rely on the certificate.
  • DNS stops when the PEP is unreachable. In enforce-in-atunnel the relay keeps answering from its last good policy.
  • An actor without an egress gateway gets SERVFAIL for every lookup in the new mode.
  • MicroVM actors were not run. The agentgateway dataplane does not answer /dns-query.
  • docs/guides/network-egress.md still says port 53 bypasses the PEP. It is left alone until the contract is agreed.
  • TestSandboxNamespacesDoNotInheritNodeSettings and TestSetupSandboxNetworkWithoutVeth fail for me under Docker Desktop on unmodified main as well.

AI use: AI-assisted (Claude Code). I reviewed and tested it.

  • Tests pass
  • Docs updated

handleConnect verified the actor certificate and confirmed the actor with the control plane inline. Move both into a helper so another handler on the egress filter chain can run the same checks.

Signed-off-by: Chuang Wang <chuangw@google.com>
enforce-in-pep is a --dns-egress-policy value under which atunnel sends actor DNS queries to the egress PEP over DNS over HTTPS (RFC 8484) and the PEP enforces policy. Nothing reads the value yet.

Policy.AllowsHostname reports whether any rule names a hostname on any port, which is the check for a DNS query. The dns/protocol package gains the DoH path and media type, the EDNS payload size of a query, and a truncated response. ResolvConfNameservers is exported for the egress gateway.

Signed-off-by: Chuang Wang <chuangw@google.com>
The egress ext_proc answers GET /dns-query on the egress filter chain. It runs the actor checks a CONNECT gets, then resolves a name that a rule in the actor's EgressPolicy names, on any port, with the gateway pod's resolvers. Any other name, and every name for an actor without a policy, is answered NXDOMAIN with an SOA of TTL 0 and Extended DNS Error 15. Both are HTTP 200 with a DNS message. A denied lookup is logged with the actor, name and type.

Envoy needs no route for the request: the handler returns the DNS response as an ext_proc immediate response, which a handler can now do through Result.Immediate.

Signed-off-by: Chuang Wang <chuangw@google.com>
With --dns-egress-policy=enforce-in-pep the DNS relay applies no policy. It validates each query as before, then sends it to the egress PEP as an RFC 8484 GET on the pooled mTLS connection that carries the actor's CONNECT tunnels, and returns the PEP's response to the actor. The query goes out with ID 0 and the actor's ID is restored on the response.

The relay finds the actor's egress client per query, because the client is activated after the relay starts serving. It answers SERVFAIL when there is no client, the PEP gives no usable response, or the response answers a different question. Over UDP, a response larger than the actor accepts is returned truncated so the resolver retries over TCP.

enforce-in-atunnel is unchanged.

Signed-off-by: Chuang Wang <chuangw@google.com>
The egress demo gains a /dns endpoint that sends raw A queries with EDNS to the actor's nameserver and reports response codes, Extended DNS Errors and the time each query took.

TestActorDNSLatency drives it for an allowed name sequentially and concurrently, a blocked name, lookups over TCP, and lookups after the gateway connection has idled out, and writes the percentiles to the file E2E_DNS_LATENCY_OUT names. It is skipped when that is unset.

Signed-off-by: Chuang Wang <chuangw@google.com>

This branch has not been deployed

No deployments
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