Skip to content

Support CONNECT in atenet router - #715

Open
Keith Mattix II (keithmattix) wants to merge 4 commits into
agent-substrate:mainfrom
keithmattix:ate-router-connect
Open

Support CONNECT in atenet router#715
Keith Mattix II (keithmattix) wants to merge 4 commits into
agent-substrate:mainfrom
keithmattix:ate-router-connect

Conversation

@keithmattix

@keithmattix Keith Mattix II (keithmattix) commented Aug 3, 2026

Copy link
Copy Markdown

Fixes #689, #265 and starts to address #484. Modifies the ate router to:

  1. Read :authority from extproc forwarded metadata instead of the requests authority (necessary for CONNECT since the inner http request may not have the proper host header).
  2. Always send to the port in the authority header (passed back to Envoy via metadata)
  3. Serve network extproc on a separate port (needed for TCP tunneled within CONNECT). Doesn't fully work though; see below.

#484 is currently blocked because Envoy's implementation of NetworkExternalProcessor doesn't support passing filter state to the extproc server. Dynamic metadata doesn't work because only Host (i.e. endpoint) and cluster metadata can propagate across internal listeners (a necessary implementation detail of doing CONNECT in Envoy). This PR still adds the xDS for it; the extproc calls just fail because there's no metadata.

Open questions:

  1. Do we need all combinations of [plaintext CONNECT, TLS CONNECT] x [http 1.1, h2]? I've implemented all 4 but wanted to confirm
  2. This PR still has extproc relying on :authority header to determine actor+atespace since potential changes to DNS (e.g. maybe we use headers instead) are under discussion. Need to resolve that at some point, but doesn't have to block this.
  3. We're assuming that non HTTP actors don't get suspended during the lifetime of a TCP connection; we only run Resume on the initial connection establishment. We could potentially do network ext authz instead (it actually on gets invoked once during conn establishment vs. we hit extproc on every client write), but wanted to discuss. I will say that I highly doubt that we'll need ANYTHING in the TCP body. Per Lior Lieberman (@LiorLieberman)'s comments, there's Envoy work being done on network ext_proc, so I assume that's what we're doing.
  4. Not sure if we've agreed if we want to do CONNECT (+ TLS?) to atunnel or not. This PR doesn't do that yet. I have no problem adding that once this merges.

Tested 6 scenarios locally:

  1. Plain HTTP ingress → hello from: 169.254.17.2 | preserved memory count: 14...
  2. Plain HTTPS ingress → hello from: 169.254.17.2 | preserved memory count: 15...
  3. CONNECT (plaintext) + HTTP/1.1 → port 9090 → hello from extra port 9090 on pod 169.254.17.2
  4. CONNECT (plaintext) + h2c → port 9090 → hello from extra port 9090 on pod 169.254.17.2
  5. CONNECT+TLS + HTTP/1.1 → port 9090 → hello from extra port 9090 on pod 169.254.17.2
  6. CONNECT+TLS + h2c → port 9090 → hello from extra port 9090 on pod 169.254.17.2

Once #652 merges, I can add the agentgateway implementation Added and tested agentgateway implementation as well

  • Tests pass
  • Appropriate changes to documentation are included in the PR

@keithmattix Keith Mattix II (keithmattix) changed the title Ate router connect Support CONNECT in atenet router Aug 3, 2026
@keithmattix
Keith Mattix II (keithmattix) marked this pull request as ready for review August 3, 2026 20:59
@maxsmythe Max Smythe (maxsmythe) added kind/feature An enhancement / feature request or implementation area/network labels Aug 4, 2026
@maxsmythe

Copy link
Copy Markdown
Collaborator

Bowei Du (@bowei) to route networking PR

@bowei

Copy link
Copy Markdown
Collaborator

Can you put the vendor change in its own commit (make it obvious)

@keithmattix

Copy link
Copy Markdown
Author

Bowei Du (@bowei) good call - done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need that network ext_proc? Can we resolve instead on the CONNECT itself. connect_terminate listener is already an HCM with no ext_proc on it - we can add the http ExtProc there.

What are the concerns with that?

Comment on lines +140 to +147
// TODO(router): authority is always empty today (see SubstrateMetadataNamespace's
// doc comment) -- dynamic metadata doesn't survive the connect_terminate ->
// main_internal internal-listener hop, and NetworkExternalProcessor has no
// filter-state-reading mechanism (no request_attributes) to fall back on the
// way the HTTP leg's handleRequestHeaders does. Until Envoy adds one, every
// CONNECT scenario that reaches this raw TCP leg -- non-HTTP payloads, and any
// TLS-wrapped payload per buildMainInternalListener's transport-protocol
// match -- fails here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I understand that. Can you elaborate?

AFAIU the metadata fails to cross the hop likely because passthrough_metadata only forwards static Host/Cluster metadata and never carried the value in the first place. I dont think its a NetworkExternalProcessor gap. Unless I am missing something

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I call it a NetworkExternalProcessor gap because its HTTP equivalent has a way to pass filter state to the extproc proc server and we use that to get the original CONNECT authority over there. Because request level metadata isn't passed through the internal listener transport socket; we can't use metadata to propagate it; we have to use filter state. So the requirements for the Envoy team is to be either allow internal listeners to propagate request metadata OR to allow network extproc to pass filter state via connection attributes. The same limitation applies to network ext auth btw

@keithmattix

Copy link
Copy Markdown
Author

Lior Lieberman (@LiorLieberman) at connect_terminate, we don't know whether the CONNECT is tunneling HTTP or not and we want different behavior depending on the inner protocol. If it's HTTP, we want to hit the ext proc on every request so that we Resume. If it's just TCP, we only want to hit the extproc once (on connection establishment). But we don't know which one we want until we get to main_internal and sniff the protocol of the CONNECT tunnel's contents.

@keithmattix
Keith Mattix II (keithmattix) force-pushed the ate-router-connect branch 2 times, most recently from 569a7a5 to 172f73d Compare August 6, 2026 15:10
@LiorLieberman

Lior Lieberman (LiorLieberman) commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Documenting some of the things Keith and I discussed just now;

  • This PR does implement CONNECT from the client to atenet and currently do not implement connect from atenet-to-atunnel. Rest of the bullets below focus on CONNECT between client to atenet.

can we do HTTP ext_proc at first CONNECT?

To answer this we need to agree on one thing -- we want/need extProc to call resumeActor on **every ** (inner) http request. With this as a given, we can use extProc on the outer connect, but we could loose the ability to reuse a persistent connection (likely has to be h2) between the harness and atenet-ingress and just stream new inner http requests within it as they come. We would loose it cause if we do extProc on outer CONNECT we wont trigger extProc on inner new http req. Long way to say that we DO need inner http ext procs.

If I try to put a visual representation, I think that this PR currently attempts to do

CONNECT -> HTTP ext_proc (for httptraffic)
_________ -> network ext_proc (for non-http traffic)

To avoid using NetworkExtProc I think we can do;

CONNECT ext_proc -> HTTP ext_proc (yes - we will have two extProc calls one when the CONNECT is established, the second one in the inner http - will return early though)
_________________-> no network ext_proc (for non-http traffic)

@LiorLieberman

Copy link
Copy Markdown
Collaborator

Also botengyao just added a PR (merged) for filter_state propagation yesterday..

Keith - take a look - envoyproxy/envoy#46551

@keithmattix

Keith Mattix II (keithmattix) commented Aug 6, 2026

Copy link
Copy Markdown
Author

Agree with the summary except for one thing (emphasis mine):

yes - we will have two extProc calls once when the CONNECT is established, the second one will return early though)

Not in the common case right? Again, assuming we want per request extproc triggers, the second extproc will be triggered onRequestHeaders like it does now. If anything, the first one would need to return early for HTTP, but we can't even do that because we don't know what's inside the CONNECT tunnel

@LiorLieberman

Copy link
Copy Markdown
Collaborator

Agree with the summary except for one thing (emphasis mine):

yes - we will have two extProc calls once when the CONNECT is established, the second one will return early though)

Not in the common case right? Again, assuming we want per request extproc triggers, the second extproc will be triggered onRequestHeaders like it does now. If anything, the first one would need to return early for HTTP, but we can't even do that because we don't know what's inside the CONNECT tunnel

I messed up my language earlier - fixed the language. one when the CONNECT is established, the second one in the inner http - will return early though).

If anything, the first one would need to return early for HTTP, but we can't even do that because we don't know what's inside the CONNECT tunnel

why? Becuase we dont know whats the inside in CONNECT my point was -- outer -- always extproc (since we need it both for TCP and HTTP to call resume), then for the inner, for TCP we dont need again (cause we know the outer did it), for HTTP we do need again, cause it may be the second http stream and not the first one. So the only "cost" we paid is that we did two extProcs for http request in the first CONNECT initiation.

@keithmattix

Keith Mattix II (keithmattix) commented Aug 6, 2026

Copy link
Copy Markdown
Author

I'm still not sure I understand: what does "return early" mean here practically? Does that mean the extproc hop is quicker than normal or that we won't call Resume() for the actor? If it's the latter, that still doesn't work if we assume that we want to call extproc for every inner http request; the second ext proc would need to be a fully fledged call to Resume so we can wake up the actor. Even if we just scope it down to the very first CONNECT establishment where we'll have double extproc, you'd need to have some sort of really small cache or other coordination mechanism if you wanted to return early (honestly may not be worth the hassle if ResumeActor() is idempotent)

@LiorLieberman

Lior Lieberman (LiorLieberman) commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

(honestly may not be worth the hassle if ResumeActor() is idempotent)

Exactly, its just a super minor optimizations I suggested (say to keep a 30 sec cache or something and if you just called resume actor dont call again for that actor). I agree its not necessary to have that optimization for now. We may be proved wrong if client harnesses wont maintain persistent connections too often as we think they will.

Consolidates the vendor/, go.mod, and go.sum changes needed for
arbitrary-port CONNECT ingress support into a single commit.
Adds arbitrary-port ingress via HTTP CONNECT: a connect_terminate
listener (plaintext and TLS variants) terminates the client's CONNECT
and reinjects the tunneled bytes into a main_internal internal
listener. There, a filter-chain matcher keyed on transport protocol
then application protocol splits the traffic:

  - HTTP-tunneled requests go through the normal HCM + ext_proc path,
    so each inner request still triggers ext_proc/Resume exactly like
    plain ingress traffic -- this is what lets a long-lived CONNECT
    tunnel keep working across an actor moving to a new worker pod.
  - Raw TCP (or anything else) falls through to a tcp_proxy filter
    chain fed by a new network (L4) ext_proc server, resolved once per
    connection rather than per request.

The ORIGINAL_DST cluster moves from header-mutation-based routing to
metadata-based (OriginalDstMetadataKey/OriginalDstAddressKey): a
header only works for HTTP traffic, and the network ext_proc leg has
none, so one metadata-based mechanism now serves both legs.

Also includes: the atunnel-side CONNECT ingress handling
(internal/atunnel/ingress.go), the e2e RouterClient CONNECT support
and arbitrary-port test suite, and the counter demo's extra listening
port used to prove traffic actually reached a non-default port.
Adds arbitrary-port ingress via CONNECT for the agentgateway
dataplane, mirroring the Envoy implementation's design:

  - Dedicated CONNECT binds (plaintext and TLS, matching Envoy's
    connect_terminate/connect_terminate_tls) terminate the tunnel in
    Tunnel mode and re-enter it into an internal wildcard bind.
  - The wildcard bind's route resolves the actor from the original
    CONNECT authority via source.connectHeaders, rather than its own
    (re-entered, unrelated) request Host -- the same per-inner-request
    ext_proc/Resume behavior as the Envoy main_internal path.
  - ext_proc header mutations set both Value and RawValue: Envoy and
    agentgateway differ on which field they read, so both dataplanes
    need both set.
Both dataplanes can resolve the worker atunnel address without any
header mutation at all, superseding the "set both Value and RawValue"
header-based approach from the previous commit:

  - Envoy's ORIGINAL_DST cluster already resolves via dynamic metadata
    (OriginalDstMetadataKey/OriginalDstAddressKey) -- the
    x-ate-original-dst header mutation and its UseHttpHeader-based
    reading were dead code once that migration landed.
  - Agentgateway's dynamic backend now supports a CEL target
    expression, evaluated with the same ext_proc dynamic metadata
    already surfaced as `extproc.*`. Its config backend becomes
    `dynamic: { target: extproc["envoy.filters.listener.original_dst"]["local"] }`
    instead of rewriting :authority and restoring it via
    X-Ate-Original-Host on the atunnel side.

:authority/Host is now never touched by ext_proc on either dataplane,
so atunnel authorizes the actor by its own, unmodified Host with no
restore step needed.
@keithmattix

Copy link
Copy Markdown
Author

I've done another pass to clean up the commits to make this a bit more reviewable. If we're all on the same page regarding the points in #715 (comment), this should be ready for review.

/cc Bowei Du (@bowei)

@bowei

Copy link
Copy Markdown
Collaborator

Re: caching of the resume -- we really need to discuss the semantics. Ideally, the server tells the consumer how long to cache. I want to avoid baking timeouts as constants into the client -- this presents problems for flexibility in how the caching works, including disabling it in certain cases. #593

@bowei

Copy link
Copy Markdown
Collaborator

I see that the code has the Network ext_proc, but there was a lot of discussion above how we don't need it at the moment.

Is it possible to get a conclusive comment here on whether or not we have settled on the approach? It's not clear at the moment from what I can tell from the discussion.

@keithmattix

Copy link
Copy Markdown
Author

Re: caching of the resume -- we really need to discuss the semantics

If we take the approach currently in the PR, we don't need to cache any resumes; we only call it once. Lior Lieberman (@LiorLieberman) was suggesting alternative approaches that would reduce the need for network extproc.

Is it possible to get a conclusive comment here on whether or not we have settled on the approach?

Lior Lieberman (@LiorLieberman) correct me if I'm wrong, but my understanding from our discussion (above and synchronously) is that we're aligned on using network ext proc for now for a couple of reasons:

  1. Removes the need for any consideration of caching the resume
  2. Reduces ext proc hops (it's loopback but still slower than in process) as the alternate architecture would require two hops in the common HTTP case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/network kind/feature An enhancement / feature request or implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support multiple, arbitrary actor ports

4 participants