proxy

package
v0.0.0-...-7c0b2e9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: Apache-2.0 Imports: 72 Imported by: 0

Documentation

Overview

Package proxy provides functions to generate Envoy resource types (listeners, clusters, endpoints, routes, filter chains) from pod and service registry data. It encapsulates the details of building Envoy configurations for transparent traffic interception and service discovery.

Index

Constants

View Source
const (

	// ReporterSource/ReporterDestination tag an access log entry with the hop that
	// emitted it (egress client side vs per-pod inbound server side), mirroring the
	// aether_stats `reporter` label.
	ReporterSource      = "source"
	ReporterDestination = "destination"
)
View Source
const (
	// CaptureHTTPRoutePrefix prefixes the name of a per-port table:
	// cap_http_<P>.
	CaptureHTTPRoutePrefix = "cap_http_"
	// CaptureHTTPOtherRouteName is the per-port table of the port numbers no
	// service declares as HTTP.
	CaptureHTTPOtherRouteName = "cap_http_other"
)

The capture listener's HTTP chain (proposal 043, "The chains of a capture listener"): cap_http, for every port, the mesh's HTTP proxy port (18081) included, with ONE route table that branches on the port the client dialled (capturematcher.go, MergeCaptureRouteTables).

The agent still BUILDS one table per HTTP port number of the mesh, cap_http_<P>, and cap_http_other for every other port: they state the rule port by port, and the published cap_http is compiled from them. The proxy port's is cap_http_18081, the loopback table's virtual hosts (BuildProxyPortCaptureRouteConfiguration). None of those names is published. (Through chart 3.0.1 cap_http_18081 was, and a chain of its own served it.)

View Source
const (
	// CaptureHTTPChainName is the capture listener's one HTTP chain, for
	// every port, and the name of its route table.
	CaptureHTTPChainName = "cap_http"
	// DialledPortFilterStateKey holds, on cap_http, the destination port of
	// the connection: the port the client dialled. The route table reads it.
	// It is set by the chain, on the connection, and never shared upstream:
	// a client has no way to set it.
	DialledPortFilterStateKey = "aether.dialled_port"
)

The capture listener selects its chain with filter_chain_matcher, Envoy's generic matching tree, and not with each chain's filter_chain_match (proposal 043, "The chains of a capture listener").

The reason is backtracking. filter_chain_match resolves the destination port first, the address second and the server name third, and never goes back: a connection to a port that SOME chain names is matched among those chains alone, and one to an address that some chain names among that address's chains alone. One HTTP chain beside the per-address raw-TCP and TLSRoute chains was therefore unreachable for every port number any service declares raw TCP, and for every port of an address that has a TLSRoute: the request matched the address, failed the SNI and left through the passthrough. Chart 3.0.0 worked around it with one HTTP chain per HTTP port number of the mesh, each with its own route table.

The matching tree does go back: a branch that ends without a match hands the connection to the on_no_match of the map it came from (source/common/matcher/exact_map_matcher.h). So one HTTP chain serves every port, and the tree is:

destination address = a service's address
  destination port = one of its raw-TCP ports
    server name = a TLSRoute hostname of it  -> that TLSRoute's chain
    else                                     -> the port's raw-TCP chain
  any other port, unless the service serves HTTP on it
    server name = a TLSRoute hostname of it  -> that TLSRoute's chain
  (nothing matched: back to the root's on_no_match)
else (any address, or back from above)
  cleartext, sniffed HTTP, any port          -> cap_http
  (nothing matched: the default chain, the passthrough)

(Scoped capture matches on the port alone: the TCP mesh port to the blackhole chain, any other port to cap_http.)

The port that was dialled is no longer a property of the chain: cap_http stores it in filter state (DialledPortFilterStateKey) and its one route table branches on it (MergeCaptureRouteTables). That holds for the mesh's HTTP proxy port too: a dial to 18081 is one more branch of the table, where the port in Host decides (BuildProxyPortCaptureRouteConfiguration). Through chart 3.0.1 it had a chain of its own, cap_http_18081, with its own table.

TLS to a port the service itself serves as HTTP never reaches its TLSRoute: the chart-3.0.0 listener sent it to the passthrough, and so does this one (the owner's ruling for this change). So does TLS to 18081 when the service serves HTTP at all, which is where the proxy port carries its HTTP.

View Source
const (
	// SpireAgentSDSClusterName is the bootstrap-defined static cluster the edge
	// proxy's transport sockets reference for SDS. It points at the SPIRE Agent's
	// Workload API socket, which serves the Envoy SDS API natively (the edge's
	// own SVID + trust bundle), so the edge needs no agent-side SPIRE bridge.
	SpireAgentSDSClusterName = "spire_agent"

	// EdgeHTTPRouteName is the RDS route configuration name the edge listener
	// references; the edge agent publishes the exposed-service virtual hosts
	// under it.
	EdgeHTTPRouteName = "edge_http"

	// EdgeListenerName is the name of the edge proxy's plain-HTTP public listener.
	EdgeListenerName = "edge_http"

	// EdgeHTTPSListenerName is the name of the edge proxy's TLS-terminating public
	// listener. It MUST differ from EdgeListenerName: when TLS is enabled and no
	// Gateway opts into HTTP→HTTPS redirect, both the HTTPS (:443) and the plain-HTTP
	// (:80) routing listeners are emitted; a shared name collides in the snapshot/LDS
	// and one clobbers the other (regression that dropped the :443 listener).
	EdgeHTTPSListenerName = "edge_https"

	// EdgeRedirectListenerName is the :80 listener that 301-redirects to https
	// when TLS is enabled.
	EdgeRedirectListenerName = "edge_redirect"

	// EdgeReadinessListenerName is a dedicated, always-bound plain-HTTP listener
	// serving only the drain-aware /aether/readyz endpoint. It is independent of
	// any Gateway listener so the edge readiness probe has a stable target even
	// under proposal 021 Phase 2 (where the public listeners move to per-Gateway
	// internal ports and nothing binds the HTTPS port).
	EdgeReadinessListenerName = "edge_readiness"

	// DefaultEdgeHTTPPort is the port the edge plain-HTTP / redirect listener
	// binds for external (north-south) traffic. Privileged (the edge is an ingress
	// gateway); the pod is granted NET_BIND_SERVICE, so it binds unprivileged.
	DefaultEdgeHTTPPort = 80

	// DefaultEdgeHTTPSPort is the port the edge TLS-terminating listener binds
	// when downstream TLS is enabled.
	DefaultEdgeHTTPSPort = 443

	// DefaultEdgeReadinessPort is the (unprivileged, internal) port the dedicated
	// readiness listener binds. The kubelet readiness probe targets it over plain
	// HTTP; it is never exposed via a Service. In aether's 18xxx range — 15021 is
	// Istio's health port (proposal 030).
	DefaultEdgeReadinessPort = 18021

	// DefaultEastWestTunnelPort is the host-netns port the node proxy listens on
	// for cross-cluster east/west waypoint traffic, and the port cross-cluster
	// endpoints are dialed at (proposal 019). Host-network, so the proxy binds
	// the node address directly. Must match across the clusterset. In aether's
	// 18xxx range, out of Istio's reserved 15000-15090 band (proposal 030).
	//
	// ONE PORT NUMBER FOR BOTH TRANSPORTS. When east-west QUIC lands (038's
	// follow-on; client certs in QUIC's handshake are in the pinned Envoy since
	// envoyproxy/envoy#47076), it binds UDP on THIS number beside the TCP
	// tunnel -- not a new port. TCP and UDP are independent socket families, so
	// the two listeners coexist the way HTTP/3 runs TCP+QUIC on :443, and the
	// way the edge already does it: BuildEdgeGatewayHTTP3Listener binds UDP on
	// the same internalPort as the TCP HTTPS listener (http3.go), and alt-svc
	// advertises that one port (gatewayH3ExternalPort). The same principle is
	// stated for the capture port in common/constants/mesh (ProxyCapturePort).
	//
	// Why it is a constraint and not a preference: a second number would need a
	// second CNI rule, a second Service port, a second cross-cluster "must match"
	// agreement, and a second capture path -- and TCP and QUIC on DIFFERENT
	// capture mechanisms is the exact split that produced #916. Pinned by
	// TestEastWestPortIsSharedAcrossTransports, which fails the moment a
	// distinct QUIC/H3/UDP east-west port constant appears in this package.
	DefaultEastWestTunnelPort = 18009
)
View Source
const (
	L4StatKeyTCPPrefix = "tcp_"
	L4StatKeyUDPPrefix = "udp_"
)

L4 stat keys (aether#1023). Every L4 cluster reports under its OWN alt_stat_name, which the chart's aether.cluster stats tag lifts into the aether_cluster label:

tcp_<ns>/<svc>          the TCP floor            tcp:<fqdn>
tcp_<ns>/<svc>_<port>   a TCP per-port or primary-port alias cluster
                                                 tcp:<fqdn>:<port>
udp_<ns>/<svc>          the UDP floor            udp:<fqdn>

Until #1023 all of them passed the bare "<ns>/<svc>" service key, so the floor, the primary-port alias and every per-port cluster reported into ONE cluster.<ns>/<svc>.* tree -- and for a service with an HTTP port that tree was also the HTTP cluster's. A tick could not be assigned to a cluster kind: the #1007 attribution had to rule the HTTP side out from access logs.

The separator is "_", not the ":" the cluster NAMES use, on purpose: Envoy sanitizes every stat name and tag value (Stats::Utility::sanitizeStatsName rewrites ":" to "_"), so a "tcp:<ns>/<svc>:<port>" alt_stat_name would surface as aether_cluster="tcp_<ns>/<svc>_<port>" anyway, and a gate written against the "tcp:" spelling would match nothing, forever. Writing the sanitized form keeps the config and the label identical. It is unambiguous: namespaces and service names are DNS labels and cannot contain "_".

There is no "tls_" key: a TLSRoute SNI chain routes to its backends' PORT-QUALIFIED tcp: clusters, "tcp:<fqdn>:<port>" -- the per-port cluster, or the primary-port alias when the backendRef names the primary port -- because l4route resolves every backendRef through TCPPortClusterName, and Gateway API's CRD validation requires a port on a Service backendRef ("Must have port for Service reference"). So its connections report under tcp_<ns>/<svc>_<port>, not the floor's tcp_<ns>/<svc> (aether#1044; kind: upstream_cluster=tcp_aether-test/l4tls-a_9443). Only a portless ref, which that validation rejects, would fall back to the floor. The chain itself is told apart by the L4 access log's filter_chain_name (cap_tls_*).

Cardinality: one key per CLUSTER, never per endpoint or per source -- bounded by services x raw-TCP ports, the same set the cluster names already are. The keys carry no dot, so the chart's tag regex `^cluster\.(([^.]+)\.)` captures each one whole (pinned by //agent/test/envoy_validate against the chart's own regex, after Envoy's sanitization). The HTTP keys ("<ns>/<svc>", "<ns>/<svc>@<ns>/<sa>") are untouched, and a query selecting an HTTP service by exact key or by `<ns>/<svc>(@.*)?` cannot match an L4 key.

Each returns "" for an empty service key (Envoy then keys stats by the cluster's own, already unique, name).

View Source
const (
	GeoHeaderCountry = "x-geo-country"
	GeoHeaderCity    = "x-geo-city"
	GeoHeaderASN     = "x-geo-asn"
	GeoHeaderAnon    = "x-geo-anon"
)

Geo header names — the aether-RESERVED x-geo-* namespace (proposal 028). The edge strips these from every request BEFORE the geoip filter runs: Envoy's geoip filter only OVERWRITES a header on a successful lookup — on a miss (unknown/private IP, driver failure) a client-supplied value would pass through into the mesh.

View Source
const (

	// MeshLivePath is the liveness path answered locally by the inbound listener: a
	// 200 proves the proxy config is loaded and the listener is serving, and (since
	// the request arrived over the listener's mTLS) that mTLS termination is up.
	MeshLivePath = "/-/-/live"
	// MeshReadyPath is the readiness path: it returns 200 only when, in addition to
	// the liveness conditions, the pod's application health-probe cluster is healthy.
	MeshReadyPath = "/-/-/ready"
)
View Source
const (
	// OutboundHTTPRouteName is the name of the outbound HTTP route configuration
	OutboundHTTPRouteName = "out_http"

	// OutboundRouteInitialFetchTimeout bounds how long the egress listener stays
	// WARMING for the first out_http RDS delivery before Envoy gives up waiting
	// and activates it with an unresolved route table (issue #817).
	//
	// 15s is deliberate on both sides:
	//
	//   - It is Envoy's own default for ConfigSource.initial_fetch_timeout, so
	//     stating it changes no behaviour — it pins the value where a test can
	//     see it instead of leaving it to an upstream default that could drift.
	//   - It equals the agent's registryReadyTimeout (the budget PreListen
	//     spends trying to build a registry-backed initial snapshot before it
	//     falls back to local-only). A shorter warming budget would let a fresh
	//     Envoy epoch activate the egress listener while the agent that feeds it
	//     is still inside its own initial-snapshot budget — reopening exactly the
	//     404 NR window this constant exists for.
	//
	// Longer is not free: on a COLD proxy start there is no previous epoch
	// holding the listen socket, so extra warming is extra time with no egress
	// listener at all, and listeners_warming holds the proxy un-Ready for it.
	OutboundRouteInitialFetchTimeout = 15 * time.Second
)
View Source
const (
	// RefusalNoHTTPPort: the service serves no HTTP port at all (proposal 037).
	RefusalNoHTTPPort = "no-http-port"
	// RefusalPortMismatch: the Host header names a port other than the one
	// the client dialled. The dialled port decides; a request is never
	// answered by a port the client did not connect to.
	RefusalPortMismatch = "port-mismatch"
	// RefusalPortNotHTTP: the port named is a port of the service, and is
	// declared raw TCP or UDP.
	RefusalPortNotHTTP = "port-not-http"
	// RefusalPortNotDeclared: the service declares no such port.
	RefusalPortNotDeclared = "port-not-declared"
	// RefusalPortRequired: the address names no port (a port-less name, or
	// the mesh HTTP port) and the service's legacy target, the only thing
	// such an address ever meant, is not an HTTP port.
	RefusalPortRequired = "port-required"
)

The values of the x-aether-error response header: why the mesh refused an HTTP request for a service it knows (proposal 043, "The rule, per entry path"). Every refusal is an immediate 421 Misdirected Request with a body that lists the service's ports, and never a passthrough: passing an HTTP request for a known mesh service to the cluster's own routing fails open.

View Source
const (

	// AgentXDSClusterName is the bootstrap-defined static cluster the node proxy
	// reaches its agent on (the /run/aether/xds.sock pipe). It is the cluster
	// dynamic_resources.ads_config already names; it MUST stay spelled the way
	// charts/aether/templates/agent-proxy-configmap.yaml spells it.
	//
	// The on-demand certificate selector points at it through its OWN
	// api_config_source rather than through `ads: {}` — see
	// meshCertSelectorSDSSource, where the reason is the whole of issue #842's
	// rev228 outage.
	AgentXDSClusterName = "agent_xds"
)
View Source
const AuthzSidecarClusterName = "authz_sidecar"

AuthzSidecarClusterName is the static bootstrap cluster the chart provisions for the node-local authorization sidecar (proposal 027): a Unix-socket pipe to the authz container in the aether-proxy pod. Always present/warm when the sidecar is enabled — never demand-scoped.

View Source
const BlackholeClusterName = "blackhole"

BlackholeClusterName is the static, endpoint-less cluster the scoped-mode TCP blackhole chain forwards to (proposal 037).

View Source
const (
	// CallerNamespaceFilterStateKey holds the namespace of the pod a
	// connection came from, on that pod's HTTP chains.
	CallerNamespaceFilterStateKey = "aether.source.namespace"
)

A SHORT NAME is a Host with no namespace: "echo", "echo:8080". It means the service of that name in the CALLER's namespace, as the cluster's DNS search path does, and never a service of another namespace, whether or not the name is unique in the cluster.

The route tables are shared by every pod of the node, so the caller is not in the table: it is on the connection. Each pod's HTTP chains store the pod's namespace in filter state (CallerNamespaceFilterStateKey), which a client cannot set, and a short name's virtual host selects its routes by that value (the generic matching API on the virtual host):

virtual host "echo|short|:8080", domains ["echo:8080"]
  matcher, input = filter state aether.source.namespace
    "a" -> the routes "echo.a.<mesh>:8080" has in this table
    "b" -> the routes "echo.b.<mesh>:8080" has in this table
    another namespace -> what the table gives a Host that is no mesh
        name: "echo" is not a mesh name for that caller
    no namespace on the connection -> 421 caller-namespace-unknown

The arms are COPIES of what the full name gets in the same table, so a short name is routed, refused, parked and filtered exactly like the full name of the caller's namespace: per-port rules, HTTPRoute rules, refusals and the cold path are not written twice.

View Source
const ExtAuthzFilterName = "envoy.filters.http.ext_authz"

ExtAuthzFilterName is the Envoy HTTP filter this entry configures.

View Source
const (
	// HealthGatewayListenerName names the node-local health gateway listener.
	HealthGatewayListenerName = "health_gateway"
)
View Source
const InvalidBackendBody = "aether: the route's backendRef names no HTTP port of its Service (see the route's ResolvedRefs condition)\n"

InvalidBackendBody is the body of the 500 an invalid backendRef answers.

View Source
const L4AccessLogName = "aether_l4_access_logs"

L4AccessLogName is the log_name of the connection-level L4 access log (aether#1023): its own VictoriaLogs stream, `log_name=aether_l4_access_logs`, beside the HTTP stream `aether_access_logs`.

A separate stream because the field shapes differ: an L4 record has no method, path, authority, response code or request id, and every one of those would render "-" in the HTTP stream. And it deliberately carries NO `reporter` attribute: the collector's identity connectors (the platform's GitOps repository otel-collector values, aether#863/#842) key on `reporter` -- log_name is a RESOURCE attribute the transform cannot see -- so an L4 record carrying it would enter the HTTP request counters and their denominator. The L4 record is always the SOURCE side (the capture listener), which is what filter_chain_name and pod_name already say.

View Source
const MaxRefusalBody = 4096

MaxRefusalBody is the longest body of a refusal. Envoy rejects a route table whose direct response carries more than 4096 bytes (the default max_direct_response_body_size_bytes), and with it every other route of the table: one body that grows with the object it describes (a Service's ports) must not be able to do that.

View Source
const NoHTTPPortErrorHeader = "x-aether-error"

NoHTTPPortErrorHeader is the response header naming why a mesh HTTP request was refused with 421 (see the Refusal constants).

View Source
const OutcomeHeader = "x-aether-outcome"

The outcome header (aether#1641, aether#1646).

A destination proxy answers 503 in two very different situations:

  • it never reached its application: the connect was refused or timed out, no host was healthy, the pool overflowed, the app cluster was not there. Replaying the request on another endpoint is safe whatever its method, and is what keeps a pod roll hitless;
  • it had begun sending the request to the application, and the application's connection then closed or reset before any response. The application may have run the request. Replaying a POST runs it twice.

The status code is 503 in both, so a caller that retries "a 503" replays the second kind. For a gRPC request it is wrong the other way round: both kinds come back as HTTP 200 with grpc-status 14, and a caller that retries "a 503" retries neither.

So the destination says what happened, on EVERY response, in one response header, and the caller's retry policy reads that header and nothing else about the response (outboundRetryPolicy).

Grammar

OutcomeHeader's value is four fields separated by ";":

<code> ";" <flags> ";" <method> ";" <details>

code     the HTTP status of a response the APPLICATION sent (200, 503, …),
         or 0 for a reply this proxy generated itself on a route. That is
         what Envoy's %RESPONSE_CODE% holds when response headers are
         finalised: a local reply's code is recorded after that point.
         For a gRPC request the local reply goes out as HTTP 200 with a
         grpc-status; code is 0 for it all the same.
         A reply the connection manager generated BEFORE any route was
         chosen (see "Replies without a route" below) carries its own
         HTTP status here instead of 0
flags    Envoy's %RESPONSE_FLAGS%: "-" or a comma-separated set of short
         response flags (UF, UH, UC, …)
method   the request's :method, as received
details  Envoy's %RESPONSE_CODE_DETAILS%: "via_upstream" for an
         application's response, otherwise the reason of the local reply.
         Free text; it is last because it is the only field that may
         contain a ";"

Values, as the pinned proxy writes them (//agent/test/mtlspool TestBegunRequestIsNotReplayed asserts each):

200;-;GET;via_upstream
503;-;POST;via_upstream
0;UF;POST;upstream_reset_before_response_started{remote_connection_failure|delayed_connect_error:_Connection_refused}
0;UH;POST;no_healthy_upstream
0;NC;POST;cluster_not_found
0;UC;POST;upstream_reset_before_response_started{connection_termination}
0;UR;POST;upstream_reset_before_response_started{remote_reset}
0;UR;POST;upstream_reset_before_response_started{remote_refused_stream_reset}
0;-;POST;overload      (the proxy is overloaded; stamped by the route)
503;-;POST;overload    (the same reply when no route was chosen)

The order is for the reader, outcomeRetriableRegex: what decides first comes first, so that the regex shares its prefixes (see the size limit there).

This is a WIRE CONTRACT between agent versions: a caller of one release reads what a destination of another wrote. A caller retries nothing on a response whose header it does not recognise (or that has none), so a change must keep old values meaningful to old callers: add flags to the caller's sets, never repurpose a field, and use a new header name for a new grammar.

Replies without a route

A virtual host's response headers apply only to a stream that has a route. The connection manager also answers some requests itself before choosing one, and the one that matters is the overload manager's stop_accepting_requests: "503 envoy overloaded", which a caller must move to another endpoint (the chart's overload ladder relies on it). Measured on the pinned proxy, that reply reaches a caller in two shapes: stamped by the virtual host after all ("0;-;<method>;overload", over the mesh's HTTP/2 hop) or with no header at all (a plain HTTP/1.1 or HTTP/2 client). So the connection manager's local-reply mapper stamps any local reply that still has no outcome header (inboundOutcomeLocalReply), with the reply's own status as the code, and the caller's rule accepts both shapes. Every other reply of that kind (400 for a malformed request, 408, 431, …) gets a value no rule matches.

Who touches it

  • the inbound route stamps it, overwriting anything the application set, and removes a REQUEST header of that name before the application sees it (buildInboundRouteConfiguration); the inbound connection manager stamps the local replies the route did not (inboundOutcomeLocalReply);
  • every caller-side route to a mesh cluster matches it for retry and then removes it from the response it forwards (outboundRetryPolicy, stripOutcomeHeader), so it reaches neither the client application nor, at the edge, the outside. Envoy decides the retry before it finalises the response headers, so the removal does not change the decision;
  • the same routes, and every forwarding route of the edge, remove a REQUEST header of that name. The header is the mesh's; no client sends it legitimately. Behind a destination proxy a request header could do nothing anyway (the response's is overwritten). It matters where there is none: an edge rule that splits between a mesh service and a cleartext Kubernetes Service carries the mesh retry policy, and a cleartext backend that echoes request headers into its response would otherwise let an external client make the edge replay its own POST.
View Source
const PassthroughClusterName = "passthrough_original_dst"

PassthroughClusterName is the cluster name for the ORIGINAL_DST passthrough used by the redirect-all capture mode (proposal 022, M2a spike). Envoy resolves the upstream from the SO_ORIGINAL_DST recovered by the original_dst listener filter, so no endpoint configuration is needed.

View Source
const RefusalCallerUnknown = "caller-namespace-unknown"

RefusalCallerUnknown: a short name on a connection that carries no caller namespace. That is a defect of the proxy's own configuration (every pod's HTTP chains store it), never something a client can cause, and the one case a short name is refused in: without the caller nothing says whose name it is.

View Source
const RefusalFilterLevels = "filter-levels"

RefusalFilterLevels: a short-name route on which a service-wide filter and the route's own filter of the same kind could not be combined.

View Source
const RefusalReservedPortBody = "aether: ports 18081 and 18082 are the mesh's own (its HTTP proxy port and its L4 port) and are a port of no service. " +
	"Put the service's own port in the Host: <service>.<namespace>.<mesh domain>:<port>.\n"

RefusalReservedPortBody is the body of the refusal of a mesh authority that names one of the mesh's own two ports.

View Source
const RefusalShortNameCold = "short-name-cold"

RefusalShortNameCold: a short name of a service this node does not know the ports of yet, on a port it cannot tell is one of them.

View Source
const (

	// SourceIdentityCertMapperFilterStateKey carries the source pod's SPIFFE
	// ID, written as a literal string by every listener chain that can
	// originate mesh traffic, under the name Envoy's upstream certificate mapper reads, and with a HASHABLE
	// factory so it reaches the upstream connection-pool key (issue #842).
	//
	// THE NAME IS NOT OURS TO CHOOSE. The
	// envoy.tls.upstream_certificate_mappers.filter_state_override mapper
	// hardcodes the lookup — it walks
	// TransportSocketOptions::downstreamSharedFilterStateObjects() comparing
	// obj.name_ against the literal "envoy.tls.certificate_mappers.on_demand_secret"
	// (source/extensions/transport_sockets/tls/cert_mappers/filter_state_override/config.cc
	// at the pinned 1.40.0-dev.20260904.13144fb snapshot) and falls back to its
	// configured default_value on any miss. There is no configurable key, so a
	// namespaced "aether.*" name is not available here. It is an Envoy-owned
	// name by construction, which is why it is spelled in full rather than
	// built from a prefix: a typo is a SILENT fallback to the node identity,
	// never an error.
	//
	// THE FACTORY IS THE POINT. set_filter_state's default "envoy.string"
	// factory builds Router::StringAccessorImpl, which is NOT Envoy::Hashable;
	// CommonUpstreamTransportSocketFactory::hashKey folds a downstream shared
	// filter-state object into the upstream pool key only behind
	// dynamic_cast<const Hashable*>. "envoy.hashable_string" builds
	// HashableString — StringAccessorImpl PLUS Hashable, hash() =
	// xxHash64(value) — so the source identity finally contributes bytes to the
	// pool key and upstream pools partition per (host, source identity). That
	// is what let connection_pool_per_downstream_connection come off every mesh
	// cluster (#831/#841 established the flag was the only thing preventing a
	// cross-source client-certificate leak; #842 replaced it with the correct
	// partition).
	//
	// THE VALUE MUST STAY BOUNDED. Envoy exposes the hashable factory under an
	// explicitly named, documented-with-a-warning extension rather than a
	// boolean precisely because anything per-request here multiplies upstream
	// connections. A SPIFFE ID is bounded by ServiceAccounts-per-node, which is
	// exactly the partition the client certificate needs. Never put a
	// per-request or per-pod value in this key.
	//
	// IT IS THE ONLY SOURCE-IDENTITY KEY (since #1165). Readers:
	//   - the cluster's filter_state_override certificate mapper (above);
	//   - CommonUpstreamTransportSocketFactory::hashKey (the pool partition);
	//   - accesslog.go's `source_spiffe_id` (HTTP and L4 logs) — HashableString
	//     is a StringAccessor, so %FILTER_STATE(<key>:PLAIN)% renders it; the
	//     attribute NAME did not change when its key moved here;
	//   - the QUIC selection matcher (route.go, ApplyQUICClusterSelection),
	//     through envoy.matching.inputs.filter_state, which reads
	//     serializeAsString() — again the StringAccessor surface.
	//
	// EXPORTED so the out-of-tree runtime harness (//agent/test/mtlspool) stamps the
	// same key production does rather than re-spelling the literal.
	SourceIdentityCertMapperFilterStateKey = "envoy.tls.certificate_mappers.on_demand_secret"
)
View Source
const SourceMetadataNamespace = "aether.source"

SourceMetadataNamespace is the dynamic-metadata namespace carrying the calling workload's identity on the egress paths (proposal 027 follow-up): per-pod system facts, kept separate from the user-owned context_extensions.

View Source
const (
	// SubsetHeadersFilterName is both the outbound HCM filter name and the
	// ECDS resource name carrying its header_to_metadata config. One shared
	// resource serves every pod's outbound listener on the node: when the
	// subset-key vocabulary changes, the agent pushes a new extension config
	// and Envoy swaps it in place — no listener drain, no connection resets.
	SubsetHeadersFilterName = "aether.subset-headers"
)
View Source
const WaypointTunnelListenerName = "ew_tunnel"

WaypointTunnelListenerName is the node's host-netns east/west tunnel listener (proposal 019). One per node; it accepts cross-cluster mTLS dialed at node_ip:<tunnel-port> and SNI-forwards it to the destination local pod.

Variables

View Source
var ErrNoTrustDomain = errors.New("no SPIFFE trust domain known yet: refusing to build a mesh identity")

ErrNoTrustDomain is returned by every builder that would otherwise have to format a mesh identity out of an empty trust domain.

The trust domain is LATE-BOUND (agent/internal/identity.TrustDomain, #740), so "not known yet" is a real state on the startup path. Emitting anyway produces `spiffe:///ns/<ns>/sa/<sa>`: a syntactically valid, semantically dead SDS resource name that the agent never serves and Envoy therefore never resolves — the inbound listener comes up with NO certificate, every mesh connection to the pod fails, and nothing repairs it because the malformed name is already published. That is the node C outage of 2026-09-19 (issue #815). Refusing is always better: the caller keeps the config it has and retries on the next rebuild.

Functions

func AppClusterName

func AppClusterName(cniPod *cniv1.CNIPod, port uint16) string

AppClusterName returns the name of the per-pod, per-port application cluster that the inbound filter chain for that port forwards decrypted traffic to. It is unique per (pod, port) so each SNI-selected chain routes to its own loopback port.

func AppHealthPathFromPod

func AppHealthPathFromPod(cniPod *cniv1.CNIPod) string

AppHealthPathFromPod returns the HTTP path used to health-check the pod's application, taken from the endpoint.aether.io/health-path annotation, defaulting to "/" when unset.

func AppPortFromPod

func AppPortFromPod(cniPod *cniv1.CNIPod) uint16

AppPortFromPod returns the port the pod's application listens on, taken from the endpoint.aether.io/port annotation, defaulting to the standard endpoint port when the annotation is absent or invalid.

func AppPortProtocols

func AppPortProtocols(cniPod *cniv1.CNIPod) map[uint16]bool

AppPortProtocols returns which served ports the app speaks HTTP/2 (h2c) on, from the endpoint.aether.io/ports annotation entries of the form "<port>=h2" (or "=http2"). Ports without a suffix are HTTP/1.1. The loopback (Envoy->app) hop uses this; the mesh hop is always HTTP/2 mTLS regardless.

func AppPortsFromPod

func AppPortsFromPod(cniPod *cniv1.CNIPod) []uint16

AppPortsFromPod returns the full set of application ports the pod serves (multi-port routing, proposal 005): the endpoint.aether.io/ports annotation unioned with the default port (AppPortFromPod), sorted and de-duplicated. A pod without the annotation yields just {default port} — today's shape.

func ApplyEdgeHardening

ApplyEdgeHardening applies the Envoy edge best-practices to an edge HCM + listener from the effective EdgeConfigSpec (proposal 029). A nil spec (or any unset field) uses the compiled best-practice default. Edge-only — the node-proxy HCMs have a different (mesh-internal) threat model.

func ApplyQUICClusterSelection

func ApplyQUICClusterSelection(vh *routev3.VirtualHost, h2Cluster string, arms map[string]string) int

ApplyQUICClusterSelection rewrites, in place, every route on vh that sends traffic to h2Cluster so that it selects per SOURCE identity between that cluster and the source's own `quic:` cluster (proposal 038 Phase 4b). It returns how many routes it rewrote.

The mechanism is a route-level matcher cluster specifier keyed on the SourceIdentityCertMapperFilterStateKey filter state -- the source-identity stamp every mesh-originating chain already carries (BuildSourceFilterStates; the same key the cluster's certificate mapper reads, so the arm a request takes and the client certificate it presents come from one value). Until #1165 this read the retired aether.source.spiffe_id copy. Nothing else would work here: the route tables are node-shared (one cap_http / out_http per snapshot), so a route cannot NAME a per-source cluster; and a header written by request_headers_to_add is applied AFTER route matching, so cluster_header cannot read it. The filter-state input is evaluated at match time on the stream's filter state, whose parent is the connection's, where set_filter_state put the identity.

arms maps a local source SPIFFE ID to its quic: cluster name; a request from an identity with no arm (or with no stamp at all -- a chain built before the trust domain was known) takes on_no_match, the h2 cluster, which is byte-for-byte the route it had before.

An arm may name a twin the snapshot does NOT carry (issue #1020): the agent builds a twin only for a (source, destination) pair that has dialled. The matcher's action is a static cluster name (ClusterAction has nothing else), so the arm names the twin and the HCM's on_demand filter does the rest -- it resolves the route through this plugin, finds no such cluster, and asks for it by that name over ODCDS (on_demand_update.cc: entry->clusterName() on the plugin's route entry). The agent validates the name, records the pair and publishes the twin with its load assignment; the paused request resumes on it, over HTTP/3. If the name is refused or the fetch outlives the on_demand timeout, the router finds no cluster and answers 503 (NC) -- the same failure mode as the mesh catch-all's cold path; there is no per-route fallback to the h2 cluster in Envoy's API.

Which routes this reaches (aether#961): EVERY route on the vhost whose action names the h2 cluster -- the default route, and a GAMMA (HTTPRoute) rule whose single backendRef is the parent Service itself, since it renders as the same `cluster:` action. Such a rule rides QUIC with the caller's own twin exactly like the default route; nothing is lost. What stays h2 is a WeightedClusters action (a GAMMA split, or a rule to another Service's cluster): the matcher plugin's action names ONE cluster, so a per-source weighted split has no representation here -- not a policy choice.

func ApplyServiceChainFilter

func ApplyServiceChainFilter(vh *routev3.VirtualHost, ef *ExtensionFilter)

ApplyServiceChainFilter enables the service-wide ALWAYS-ON extension filter on a service's capture vhost via vhost-level typed_per_filter_config (proposal 025 M4 CHAIN scope). It applies to every route in the vhost — including the default route and routes added later — and a per-route ExtensionRef overrides it (Envoy resolves route-level typed_per_filter_config over vhost-level; most-specific wins). No-op on a nil filter or a non-allow-listed name.

func AuthzSidecarHTTPFilter

func AuthzSidecarHTTPFilter(timeout time.Duration, failureModeAllow bool) *http_connection_managerv3.HttpFilter

AuthzSidecarHTTPFilter builds the ext_authz HCM entry for the node-local authz sidecar (proposal 027 system half): FULL transport config — the per-route type (ExtAuthzPerRoute) cannot carry it — but `disabled: true`, so it has ZERO effect until a route/vhost opts in via typed_per_filter_config (the 025 enablement machinery). failureModeAllow=false is fail-closed: requests on opted-in routes are DENIED when the sidecar is unreachable.

func BuildCallerNamespaceFilterState

func BuildCallerNamespaceFilterState(namespace string) *listenerv3.Filter

BuildCallerNamespaceFilterState returns the network filter that stores a pod's namespace under CallerNamespaceFilterStateKey. It is not shared with the upstream connection: only the route tables read it.

func BuildCaptureCatchAllVirtualHost

func BuildCaptureCatchAllVirtualHost(meshDomain string, port uint32, portless, redirectAll bool) *routev3.VirtualHost

BuildCaptureCatchAllVirtualHost builds the "*" virtual host of ONE capture route table (proposal 043).

  • port is the table's dialled port, 0 for cap_http_other. A mesh authority that spells exactly that port may still name its cluster on demand (the cold path of a service this node holds no endpoint of yet).
  • portless additionally lets a port-less mesh authority name its cluster: the two legacy tables (the mesh HTTP port, and port 80), where a port-less name is the legacy spelling of the service's default cluster.
  • redirectAll selects the fallthrough: the ORIGINAL_DST passthrough, or a hard 404 in scoped capture.

cap_http_other gets no on-demand route: no cluster can be named by a port number that no service declares as HTTP.

func BuildCapturePassthroughFilterChain

func BuildCapturePassthroughFilterChain() *listenerv3.FilterChain

BuildCapturePassthroughFilterChain returns the DefaultFilterChain for the redirect-all capture listener (proposal 022, M2a spike). It is used as Listener.DefaultFilterChain — the fallback when no named filter chain matches.

It routes ALL unmatched TCP traffic (non-mesh destinations) to the passthrough_original_dst ORIGINAL_DST cluster, which connects to the original pre-REDIRECT destination in plain TCP. This is the passthrough "egress intact" proof path: HTTPS to github.com, DNS, kube-apiserver, non-mesh in-cluster services — all of these have NO matching per-ClusterIP TCP floor chain and (being non-HTTP or TLS-encrypted) also do not match the HCM chain's route table, so they fall here.

DefaultFilterChain is what the listener's matching tree falls back to when it selects no chain: the true "catch everything else" passthrough.

No buildNetworkNamespaceFilterState filter is added: the passthrough path needs the network namespace to resolve the ORIGINAL_DST address, but the namespace is already known to Envoy via the listener binding — the ORIGINAL_DST cluster type uses the SO_ORIGINAL_DST sockopt recovered by the original_dst listener filter, not filter state.

func BuildCaptureRouteConfiguration

func BuildCaptureRouteConfiguration(name string, vhosts []*routev3.VirtualHost, catchAll *routev3.VirtualHost, short *ShortNames) *routev3.RouteConfiguration

BuildCaptureRouteConfiguration builds ONE of the per-port capture tables (MergeCaptureRouteTables compiles them into the published cap_http): name is the table's (cap_http_<P> or cap_http_other), vhosts the virtual hosts the agent built for it from the mesh Services and the registry endpoints, and catchAll the table's own lowest-priority "*" virtual host (BuildCaptureCatchAllVirtualHost).

Like the outbound table it is emitted unconditionally, catch-all and all, and it owns its virtual_hosts slice rather than appending onto the caller's (see BuildOutboundRouteConfiguration).

short, when set, adds the short names of the table (ShortNameVirtualHosts), built from vhosts against catchAll: a short name that is no mesh name for its caller gets what catchAll gives any Host that is no mesh name.

func BuildCaptureTCPBlackholeFilterChain

func BuildCaptureTCPBlackholeFilterChain(sourceSpiffeID string) *listenerv3.FilterChain

BuildCaptureTCPBlackholeFilterChain returns the scoped-mode chain that catches raw TCP to the TCP mesh port on a VIP that claims no TCP port (proposal 037).

The matching tree selects it on the destination port alone, after every service address's branch: it takes only what none of those claimed (captureHTTPBranch).

Emitted in scoped capture only. In redirect-all mode the passthrough chain forwards to the original destination, the generated mesh Service has no backing endpoint for that port, and kube-proxy REJECTs — already deterministic and already attributable, so a blackhole there would only hide it.

func BuildCaptureTCPRouteFilterChain

func BuildCaptureTCPRouteFilterChain(svc CaptureTCPService, rules []L4ServiceRoute, sourceSpiffeID string) *listenerv3.FilterChain

BuildCaptureTCPRouteFilterChain builds a per-ClusterIP TCP floor filter chain for a service that has a TCPRoute. It matches the service's ClusterIP as the original destination (/32 prefix_ranges) and routes via tcp_proxy weighted_clusters to the rule's backends.

This replaces the passthrough floor chain (buildCaptureTCPFloorFilterChain) for services that have a TCPRoute: the single-cluster tcp_proxy becomes a weighted_clusters tcp_proxy routing to the rule's backends.

Returns nil for invalid inputs so callers can skip them cleanly.

func BuildCaptureTLSRouteFilterChains

func BuildCaptureTLSRouteFilterChains(svc CaptureTCPService, rules []L4ServiceRoute, sourceSpiffeID string) []*listenerv3.FilterChain

BuildCaptureTLSRouteFilterChains builds per-SNI filter chains for a TLSRoute attached to a service. Each rule's SNI hostnames become one filter chain with server_names match; the chain routes via tcp_proxy to the rule's weighted backends.

Placement: these chains are inserted AFTER the per-ClusterIP TCP floor chain (so the ClusterIP match takes precedence for non-TLSRoute traffic) but BEFORE the global HCM catch-all. The tls_inspector already installed on the capture listener reads the SNI so filter_chain_match.server_names works.

NOTE: the filter chains also match prefix_ranges (the service ClusterIP) to avoid matching SNI from other services, since a client can set any SNI. Without the IP match, a TLSRoute for service A could accidentally intercept TLS connections headed for service B if they happen to carry the same SNI.

func BuildDialledPortFilterState

func BuildDialledPortFilterState() *listenerv3.Filter

BuildDialledPortFilterState returns the network filter that stores the connection's destination port under DialledPortFilterStateKey. The capture listener restores the original destination before any filter runs, so the port is the one the client dialled. Not shared with the upstream.

func BuildEdgeDirectResponseRoute

func BuildEdgeDirectResponseRoute(prefix, exact string, headers []RouteHeaderMatch, method string, queryParams []RouteQueryParamMatch, status uint32) *routev3.Route

BuildEdgeDirectResponseRoute builds an Envoy route that matches on path + headers + method + query params and returns a fixed direct_response (no backend). Used when a rule's backendRef(s) are unresolvable; Gateway API mandates HTTP 500 in that case.

func BuildEdgeGatewayHTTP3Listener

func BuildEdgeGatewayHTTP3Listener(namespace, gatewayName string, internalPort uint32, tlsSecretNames []string, geoFilters []*http_connection_managerv3.HttpFilter, edgeCfg *configv1.EdgeConfigSpec) *listenerv3.Listener

BuildEdgeGatewayHTTP3Listener builds the UDP/QUIC listener that serves HTTP/3 traffic on the same internalPort as the Gateway's co-located HTTPS (TCP) listener. Returns nil when tlsSecretNames is empty — QUIC mandates TLS, so a plain-HTTP Gateway cannot use HTTP/3.

The listener:

  • Binds UDP on defaultEdgeAddress:internalPort with enable_reuse_port=true (required for QUIC).
  • Sets udp_listener_config.quic_options (empty = Envoy's built-in QUIC defaults).
  • Uses envoy.transport_sockets.quic wrapping the same SDS-backed DownstreamTlsContext as the HTTPS listener so cert rotation is automatic and no extra secret wiring is needed.
  • HCM: codec_type=HTTP3, Http3ProtocolOptions{} (empty = QUIC defaults), geo+readiness filter prefix, RDS pointing at the per-Gateway route name (EdgeGatewayRouteName), and ApplyEdgeHardening (use_remote_address, timeouts, h2 caps — same policy as HTTPS).

func BuildEdgeGatewayHTTPListener

func BuildEdgeGatewayHTTPListener(namespace, gatewayName string, internalPort uint32, httpRedirect bool, geoFilters []*http_connection_managerv3.HttpFilter, edgeCfg *configv1.EdgeConfigSpec) *listenerv3.Listener

BuildEdgeGatewayHTTPListener builds a per-Gateway plain-HTTP or HTTP→HTTPS redirect listener. Listener name and RDS route config name are unique per Gateway (proposal 021 Phase 2). When httpRedirect is true, the listener emits a 301 redirect to HTTPS (no RDS reference); otherwise it serves routes via RDS.

func BuildEdgeGatewayHTTPSListener

func BuildEdgeGatewayHTTPSListener(namespace, gatewayName string, internalPort uint32, tlsSecretNames []string, geoFilters []*http_connection_managerv3.HttpFilter, edgeCfg *configv1.EdgeConfigSpec) *listenerv3.Listener

BuildEdgeGatewayHTTPSListener builds a per-Gateway TLS-terminating HTTPS listener. It terminates downstream TLS using the named SDS cert(s) and routes via the per-Gateway RDS route config. The listener name is unique per (Gateway, port).

func BuildEdgeGatewayRouteConfiguration

func BuildEdgeGatewayRouteConfiguration(namespace, gatewayName string, vhosts []*routev3.VirtualHost, http3ExternalPort uint32) *routev3.RouteConfiguration

BuildEdgeGatewayRouteConfiguration builds the per-Gateway route table from the given virtual hosts. It has a catch-all 404 (no ODCDS — the edge serves only explicitly routed services). The route config name is unique per Gateway.

http3ExternalPort, when non-zero, adds a response-level alt-svc header ("h3=":<port>"; ma=86400") so HTTP/1.1 and H2 clients upgrade to HTTP/3. Pass 0 when HTTP/3 is not enabled for this Gateway.

func BuildEdgeK8sCluster

func BuildEdgeK8sCluster(name, fqdn string, port uint32) *clusterv3.Cluster

BuildEdgeK8sCluster builds a STRICT_DNS cleartext cluster that reaches a non-mesh HTTPRoute backend via the k8s Service FQDN. It has NO transport socket (cleartext — the backend is a plain k8s Service, not a mesh endpoint). HTTP/1.1 upstream (no h2 options), STRICT_DNS so CoreDNS resolves the FQDN on demand. fqdn must be the full cluster-local FQDN (e.g. "svc.ns.svc.cluster.local").

func BuildEdgeListener

func BuildEdgeListener(name string, port uint32, tlsSecretNames []string) *listenerv3.Listener

BuildEdgeListener builds the edge proxy's public-facing HTTP listener: an HCM on 0.0.0.0:<port> that routes external traffic to mesh services via RDS (EdgeHTTPRouteName). The readiness health_check filter answers ProxyReadinessPath ahead of the router (backs the pod readinessProbe and is drain-aware), and the router forwards to the per-service clusters, which dial destination pods directly over single-identity mTLS. No on_demand/subset filters: the edge's routable set is its explicit exposed services, not the whole mesh.

When tlsSecretNames is non-empty the filter chain terminates downstream TLS, presenting one of those SDS-served certs selected by SNI; otherwise the edge serves plain HTTP. Either way the upstream (edge -> pod) hop stays mTLS.

func BuildEdgeReadinessListener

func BuildEdgeReadinessListener(port uint32) *listenerv3.Listener

BuildEdgeReadinessListener builds the dedicated, always-bound plain-HTTP readiness listener. It carries the drain-aware readiness health_check filter (answers /aether/readyz with 200/503 from the server's drain state) ahead of a catch-all that 404s every other path — this listener exists ONLY to give the kubelet readiness probe a stable target that does not depend on which public listeners are bound (the public listeners move to per-Gateway internal ports under proposal 021 Phase 2, so probing :443 there fails).

func BuildEdgeRedirectListener

func BuildEdgeRedirectListener(port uint32) *listenerv3.Listener

BuildEdgeRedirectListener builds the :80 listener that 301-redirects every request to its https URL (used alongside the TLS listener when downstream TLS is enabled).

func BuildEdgeRoute

func BuildEdgeRoute(prefix, exact string, headers []RouteHeaderMatch, method string, queryParams []RouteQueryParamMatch, cluster string, mutation *GammaHeaderMutation, redirect *GammaRedirect, urlRewrite *GammaURLRewrite, timeout *durationpb.Duration) *routev3.Route

BuildEdgeRoute builds one Envoy route for the edge. Exactly one of prefix/exact is non-empty (exact takes precedence if both are set). Prefix "/" matches all. headers/method/queryParams add per-match predicates (ANDed with the path match). mutation applies RequestHeaderModifier / ResponseHeaderModifier at the route level. timeout sets the per-route request timeout (nil = no timeout / inherit default).

When redirect is non-nil the route emits a Route_Redirect (no cluster). When urlRewrite is non-nil the route rewrite fields are applied before forwarding to cluster. Both nil → plain forwarding route.

func BuildEdgeRouteConfiguration

func BuildEdgeRouteConfiguration(vhosts []*routev3.VirtualHost) *routev3.RouteConfiguration

BuildEdgeRouteConfiguration builds the edge listener's route table from the exposed-service virtual hosts. Unlike the node proxy's outbound route config, it has NO on-demand catch-all (the edge serves only its explicit exposed set); an unmatched authority gets an immediate 404 from the catch-all default vhost.

func BuildEdgeRouteWeighted

func BuildEdgeRouteWeighted(prefix, exact string, headers []RouteHeaderMatch, method string, queryParams []RouteQueryParamMatch, backends []WeightedRouteBackend, mutation *GammaHeaderMutation, redirect *GammaRedirect, urlRewrite *GammaURLRewrite, timeout *durationpb.Duration) *routev3.Route

BuildEdgeRouteWeighted builds one Envoy route for the edge with a weighted_clusters action when there are multiple backends, or a plain single-cluster action when there is exactly one (keeping the common path clean).

Gateway API weight semantics:

  • total_weight = sum of all entry weights; each entry's share = weight/total.
  • Weight 0 = the backend receives no traffic (remains valid per spec).
  • If all backends have weight 0 (total_weight = 0), Envoy treats this as "no healthy backend" and returns 500 — this is the expected behavior per the Gateway API spec for that edge case; we do not special-case it.

headers/method/queryParams add per-match predicates (ANDed with the path). redirect is applied ahead of backend resolution (no backends needed for a redirect). urlRewrite is applied on forwarding routes only. timeout sets the per-route request timeout (nil = no timeout / inherit cluster default).

func BuildEdgeTCPListener

func BuildEdgeTCPListener(port uint32, backends []L4Backend) *listenerv3.Listener

BuildEdgeTCPListener builds a 0.0.0.0:<port> TCP listener for an edge TCPRoute. It routes all inbound TCP to the named upstream cluster via tcp_proxy (no TLS termination — the upstream hop to the mesh pod is SPIRE mTLS with no ALPN). The cluster name is the "tcp:<svc>.<meshDomain>" cluster whose transport socket is injected at snapshot time via InjectUpstreamTCPMTLS (same path as the east-west TCP floor). When rules is empty or all backends are missing, the listener is omitted (returns nil) — a Gateway TCP listener with no TCPRoute attached should not produce an Envoy listener.

func BuildEdgeTLSPassthroughListener

func BuildEdgeTLSPassthroughListener(port uint32, rules []L4ServiceRoute) *listenerv3.Listener

BuildEdgeTLSPassthroughListener builds a 0.0.0.0:<port> TLS passthrough listener for an edge TLSRoute. It installs a tls_inspector listener filter so Envoy reads the client SNI; each rule's SNIHostnames become a filter_chain_match.server_names set routing via tcp_proxy to the rule's backends. No TLS termination: the SNI is read for routing only; the backend connection is SPIRE mTLS with no ALPN (TCP floor). Returns nil if rules is empty or none has both SNI hostnames and backends.

func BuildEdgeVirtualHost

func BuildEdgeVirtualHost(name string, domains []string, routes []*routev3.Route) *routev3.VirtualHost

BuildEdgeVirtualHost builds one edge virtual host: the given external domains with an ordered list of routes (first match wins, in HTTPRoute rule order).

func BuildFallthroughVirtualHost

func BuildFallthroughVirtualHost(name string, domains []string, redirectAll bool) *routev3.VirtualHost

BuildFallthroughVirtualHost builds a virtual host that does, for its domains, what a capture table's catch-all does for a name that is not the mesh's: the request leaves to the address that was dialled (redirect-all), or is answered 404 where nothing but mesh addresses is captured. It runs no HTTP filter of any service and reaches no mesh cluster.

It exists for the authorities a table must neither refuse nor leave to the catch-all's on-demand route (BuildCaptureCatchAllVirtualHost), which would fetch a mesh cluster for them without the service's filters.

func BuildHealthGatewayListener

func BuildHealthGatewayListener(socketPath string, probes []HealthGatewayProbe) *listenerv3.Listener

BuildHealthGatewayListener builds the agent-facing health gateway: a Unix domain socket listener whose HTTP filter chain holds one non-pass-through health_check filter per PROBE CLUSTER, matched on /healthz/<cluster-name> and gated on that cluster's active health check alone (cluster_min_healthy_percentages). The liveness loop probes these paths to reflect per-pod health into the registry — answered on worker threads from the same host-health state the load balancer uses, replacing the admin /clusters dump that was serialized on Envoy's main thread.

Paths, per pod:

/healthz/health_<namespace>_<pod>        the APPLICATION probe, exactly as before #815
/healthz/inboundready_<namespace>_<pod>  the pod's own mesh inbound listener completing
                             an mTLS handshake with its own certificate;
                             absent (404) when the pod is not gated

Response semantics per path: 200 = that cluster's host passes its active HC; 503 = it fails (which includes HC warm-up — hosts start failed until their first passing check); 404 (router catch-all) = not programmed. 404 is a first-class answer, not an error: it is how the agent learns a pod is UNGATED rather than unhealthy.

Filters are emitted in sorted cluster order so the listener config is deterministic and pod-set-equal snapshots do not produce spurious LDS updates.

func BuildNoHTTPPortVirtualHost

func BuildNoHTTPPortVirtualHost(name string, domains []string, tcpSpellings string) *routev3.VirtualHost

BuildNoHTTPPortVirtualHost returns a virtual host whose every route answers 421 Misdirected Request, for a service in scope that serves NO HTTP port (proposal 037).

Without it the request gets a 503 with cluster_not_found in %RESPONSE_CODE_DETAILS%: deterministic, but indistinguishable from a cluster that vanished mid-reload, and it never reaches ODCDS so the coordinator's expected 404 does not occur either. The caller asked for HTTP on a service that serves none, and nothing said so.

421 is the status whose definition is "the server is not able to produce a response for the combination of scheme and authority" — precisely this case. It is also a status no application behind the mesh produces, so it cannot be confused with an app's own 404 or 503. And it is immediate: no 2s ODCDS wait for a lookup that was never going to succeed.

Exact-domain vhosts outrank the wildcard catch-all, so this cannot be shadowed by the ODCDS vhost (route.go's "spike-verified" note on exact-domain precedence covers exactly this).

func BuildOnDemandServiceVirtualHost

func BuildOnDemandServiceVirtualHost(name string, domains []string, rules []GammaRoute) *routev3.VirtualHost

BuildOnDemandServiceVirtualHost builds the loopback table's virtual host of ONE mesh service the node holds no cluster of: its authorities, routed like the catch-all's on-demand route (the cluster the authority names, fetched by the on-demand filter).

It exists so that the service has a virtual host of its OWN before its cluster arrives. What a service-wide HTTP filter does is enabled by the service's virtual host; a request the catch-all parked resumes on the catch-all's route and was delivered with that filter skipped. The caller applies the filter to what this returns.

rules are the service's route rules that apply to these authorities. Their routes come first, as on the service's real virtual host, so a request that was parked is matched, filtered and sent to the rule's backends exactly as one that arrives later; a rule with no backendRef routes to the cluster the authority names.

func BuildOutboundClusterVirtualHost

func BuildOutboundClusterVirtualHost(clusterName string, domains []string) *routev3.VirtualHost

BuildOutboundClusterVirtualHost creates a virtual host routing the given authority domains to clusterName. The default-port cluster passes two domains (portless FQDN + FQDN:defaultPort) so both spellings reach it; a non-default port cluster passes the single FQDN:port. The authority port is meaningful (strip_any_host_port off), so exact-domain precedence keeps a ported authority from matching the portless default vhost (spike-verified).

func BuildOutboundRouteConfiguration

func BuildOutboundRouteConfiguration(vhosts []*routev3.VirtualHost, meshDomain string, known []string) *routev3.RouteConfiguration

BuildOutboundRouteConfiguration creates a route configuration for outbound traffic. It includes the provided virtual hosts plus the universal on-demand catch-all virtual host (mesh-shaped authorities → ODCDS, everything else → 404). The outbound listener never passes through (it only sees mesh-DNS authorities), so the catch-all keeps its hard-404 fallthrough.

The catch-all is the FLOOR of this table, not a decoration on a non-empty one: vhosts may legitimately be empty (an agent that started local-only and has no registry-derived service vhosts yet) and the result is still a complete, resolvable out_http — liveness 200 on MeshLivePath, mesh-shaped authorities to ODCDS, everything else an instant 404. Callers must therefore emit this UNCONDITIONALLY; see the RDS-resolution note in the agent snapshot builder (issue #817).

The result owns its virtual_hosts slice. Appending the catch-all straight onto the caller's slice would write it into that slice's spare capacity, handing a caller that reuses the backing array a table with someone else's catch-all in it.

known are the mesh names ("<svc>.<ns>.<meshDomain>") of the services with a generated Service: the short names are built here, from vhosts and those, against this table's floor (ShortNameVirtualHosts).

func BuildOutboundServiceVirtualHost

func BuildOutboundServiceVirtualHost(name string, domains []string, rules []GammaRoute) *routev3.VirtualHost

BuildOutboundServiceVirtualHost builds a service's outbound virtual host enriched with GAMMA rules: each rule's matches become Envoy routes to its weighted backend cluster(s), with the outbound retry policy, an optional timeout, and optional header mutations (RequestHeaderModifier / ResponseHeaderModifier filters). A trailing catch-all routes anything the rules don't match to the service's own cluster (name), so producer rules are additive. With no rules it is the passthrough vhost.

Rules with a Redirect field produce a redirect route (Route_Redirect) with no backend cluster. Rules with a URLRewrite field apply host/path rewriting on the RouteAction before forwarding to the backend.

func BuildProxyPortCaptureRouteConfiguration

func BuildProxyPortCaptureRouteConfiguration(name string, vhosts []*routev3.VirtualHost, meshDomain string, passthrough bool, known []string) *routev3.RouteConfiguration

BuildProxyPortCaptureRouteConfiguration builds the per-port capture table of the mesh's HTTP proxy port (a captured dial to "<anything>:18081"), cap_http_18081: the LOOPBACK table's virtual hosts under that name. It is one of the per-port tables MergeCaptureRouteTables compiles into cap_http, never published itself. 18081 is not a port of any service, so nothing was dialled that could decide: as on the loopback listener, the port in Host is the port, and a Host that names 18081 itself is a port the service does not declare. This is how a pod that is not on redirect-all, which has only the mesh ports diverted, reaches a service by name.

The floor differs from the loopback table's in one way: with passthrough (redirect-all), an authority that is not a mesh name leaves through the ORIGINAL_DST passthrough, as on every other capture table, instead of 404: all of the pod's egress is captured, and something else may listen on 18081.

func BuildRefusalVirtualHost

func BuildRefusalVirtualHost(name string, domains []string, reason, body string) *routev3.VirtualHost

BuildRefusalVirtualHost returns a virtual host whose every route answers 421 Misdirected Request with reason in the x-aether-error response header and body as the response body. name identifies the virtual host within its route table.

A direct response: it is answered by the proxy at once, it reaches no cluster, and it cannot be retried into a different answer.

func BuildServiceVirtualHost

func BuildServiceVirtualHost(name, cluster string, domains []string, rules []GammaRoute) *routev3.VirtualHost

BuildServiceVirtualHost is BuildOutboundServiceVirtualHost with the virtual host's name and its own cluster given separately: name identifies the virtual host within its route table, cluster is where a request that no rule sends elsewhere goes. A per-port table has several virtual hosts for one service (one per port and name family), and a service's port P is served by the cluster of that port, not by a cluster named after the virtual host.

func BuildSourceFilterStates

func BuildSourceFilterStates(sourceSpiffeID string) []*listenerv3.Filter

BuildSourceFilterStates returns the source-attribution network filters every mesh-originating filter chain carries, in a FIXED order (netns, then the certificate-mapper identity key): these land in the chain's repeated `filters` field, whose order is part of the listener's bytes and therefore of its delta-xDS hash.

Until #1165 a third entry — the retired "aether.source.spiffe_id" copy — sat between the two; see SourceIdentityCertMapperFilterStateKey.

sourceSpiffeID may be empty — before the trust domain is known there is no identity to stamp — in which case only the netns filter is emitted, which is byte-for-byte what this chain carried before issue #815. A chain built in that window selects no certificate by identity, so its egress presents the node identity (the mapper's default_value) until the next rebuild — the same degradation, for the same reason, as the pre-#842 OnNoMatch path.

func BuildSubsetHeadersExtension

func BuildSubsetHeadersExtension(keys []string) *corev3.TypedExtensionConfig

BuildSubsetHeadersExtension wraps the node's subset-header mapping as the ECDS TypedExtensionConfig resource. keys is the (sorted, validated) union of provider-defined subset keys across the node's dependency set: the provider's vocabulary travels to its consumers via the control plane, so consumers declare nothing.

func BuildWaypointIngressCluster

func BuildWaypointIngressCluster(fqdn string, podIPs []string) *clusterv3.Cluster

BuildWaypointIngressCluster builds the STATIC cluster of a service's local pods (podIP:defaultInboundPort) that the node tunnel forwards to. No transport socket: the bytes are already the source's end-to-end mTLS, forwarded raw; the destination pod's inbound listener terminates it. podIPs must be non-empty and pre-sorted by the caller for snapshot stability.

func BuildWaypointTunnelChain

func BuildWaypointTunnelChain(fqdn string) *listenerv3.FilterChain

BuildWaypointTunnelChain builds one local-service chain for the tunnel: it matches the structured waypoint SNI by suffix ("*.<fqdn>" — Envoy's leftmost wildcard is a suffix match, which is safe because aether owns the SNI and each service has a unique .<fqdn> suffix) and tcp_proxies the raw (still-mTLS) stream to that service's local pods at :18008.

func BuildWaypointTunnelListener

func BuildWaypointTunnelListener(tunnelPort uint32, chains []*listenerv3.FilterChain) *listenerv3.Listener

BuildWaypointTunnelListener builds the host-netns east/west tunnel listener: tls_inspector reads the ClientHello SNI and each chain (built by BuildWaypointTunnelChain) routes by the SNI suffix to a local service's pods. It binds 0.0.0.0:<tunnel-port> WITHOUT a network-namespace filepath, so it lives in the host netns (the proxy DaemonSet is host-network and can bind the node address). No TLS termination — the source's end-to-end mTLS passes through to the destination pod. Returns nil when there are no chains.

func CaptureHTTPRouteName

func CaptureHTTPRouteName(port uint32) string

CaptureHTTPRouteName returns the name of the per-port table of HTTP port number port: cap_http_<port>. No such table is published: cap_http is compiled from them.

func CaptureListenerName

func CaptureListenerName(cniPod *cniv1.CNIPod) string

CaptureListenerName returns the per-pod transparent-capture listener name.

func CaptureUDPListenerName

func CaptureUDPListenerName(cniPod *cniv1.CNIPod) string

CaptureUDPListenerName returns the per-pod UDP capture listener name.

func ClampRefusalBody

func ClampRefusalBody(body string) string

ClampRefusalBody returns body when Envoy accepts it, and otherwise its start, cut at a character boundary, ending in a mark that says so. The same input gives the same output on every node.

func ClusterNameOutsidePinGauge

func ClusterNameOutsidePinGauge(name string) bool

ClusterNameOutsidePinGauge reports whether a cluster of this name is, by its name alone, of a family the agent's pin gauges do not count (aether.agent.snapshot.tls_clusters, aether.agent.xds.acked_tls_clusters). Those gauges count mesh cluster entries, the clusters IsMeshEntryClusterName recognises; this is every other family the generators of this package build.

It says where a cluster is counted. It does NOT say the cluster has no TLS or no SAN pin:

  • the per-pod inbound-readiness cluster (NewInboundReadyProbeCluster) IS an mTLS cluster pinned to its pod's SPIFFE ID, and a QUIC twin is pinned exactly when its h2 base entry is. Neither is a cluster entry: the first is a node-local probe of the pod's own listener, the second is counted through its base;
  • the per-pod application and health-probe clusters, the UDP floor, the east/west waypoint ingress cluster, the edge's cleartext k8s-Service cluster, the ORIGINAL_DST passthrough and the blackhole have no upstream transport socket.

It is the single list. A cluster family added to this package is either one IsMeshEntryClusterName recognises or is added here; TestEveryClusterConstructorIsOfAClassifiedFamily fails until it is one or the other.

func CollectExtensionFilters

func CollectExtensionFilters(rules []GammaRoute, extra ...ExtensionFilter) []*http_connection_managerv3.HttpFilter

CollectExtensionFilters returns the default-disabled HCM http_filter entries for the union of allow-listed extension filters referenced by the given rules (deduped, stable order). Returns nil when none — the common case adds nothing to the chain. Non-allow-listed names are skipped (the webhook rejects them at admission). extra carries filters not referenced by any rule — the service-wide CHAIN-scope filters (025 M4), which are enabled at the vhost instead of a route but still need their default-disabled chain entry.

func DownstreamTransportSocket

func DownstreamTransportSocket(tlsCertificateSecretName, validationContextName, trustDomain string) *corev3.TransportSocket

DownstreamTransportSocket creates a TLS transport socket for downstream (inbound) connections. It requires mutual TLS and retrieves certificates and validation context via ADS-served SDS.

It also pins the CLIENT identity to a workload SVID SHAPE: the peer certificate must carry a URI SAN with the "spiffe://<trust-domain>/ns/" prefix (issue #843). RequireClientCertificate alone proves only that SOME certificate this trust bundle signs was presented; the inbound HCM then stamps that certificate's URI SAN into XFCC with SANITIZE_SET (ingress.go), and downstream RBAC/ext_authz rules key on that value. Requiring the SPIFFE path to be /ns/<ns>/sa/<sa> rejects a non-workload certificate and is what makes the XFCC value structurally trustworthy. It deliberately does NOT pin a specific ServiceAccount: in a mesh any workload may legitimately call, so identity-level authorization belongs in RBAC/ext_authz, which this pin is the precondition for.

trustDomain == "" emits NO matcher — byte-identical to the pre-#843 shape — rather than the unservable "spiffe:///ns/" (see WorkloadSANPrefix). That branch is defence in depth, not a live path: NewInboundListener already refuses with ErrNoTrustDomain before reaching here on the mTLS path, so the only reachable "no trust domain" outcome is a skipped pod with a WARN naming it, never a silently unpinned listener. //agent/test/envoy_validate asserts the shape over the generated bootstrap bytes so a future caller that bypasses that guard fails the build instead of shipping an unpinned inbound.

func EdgeGatewayH3ListenerName

func EdgeGatewayH3ListenerName(namespace, gatewayName string, internalPort uint32) string

EdgeGatewayH3ListenerName returns the unique name for a per-Gateway HTTP/3 (QUIC) UDP listener. The "_h3" suffix distinguishes it from the TCP HTTPS listener (edge_gw_<ns>_<gwname>_<port>) so both coexist in LDS without name collision.

func EdgeGatewayListenerName

func EdgeGatewayListenerName(namespace, gatewayName string, internalPort uint32) string

EdgeGatewayListenerName returns the UNIQUE name for a per-Gateway edge listener. Naming scheme: edge_gw_<ns>_<gwname>_<port> — distinct per (Gateway, port). This guarantees no LDS collision across Gateways (regression guard for #332, where all listeners shared "edge_http" and the :443 listener was silently dropped).

func EdgeGatewayRouteName

func EdgeGatewayRouteName(namespace, gatewayName string) string

EdgeGatewayRouteName returns the RDS route configuration name for a per-Gateway edge listener. Scheme: edge_rt_<ns>_<gwname>. Each Gateway gets an isolated route table so route leakage between Gateways is impossible.

func EdgeK8sClusterName

func EdgeK8sClusterName(namespace, service string, port uint32) string

EdgeK8sClusterName returns the data-plane name for a cleartext k8s-Service cluster that reaches a non-mesh (non-registry) HTTPRoute backend. The name is unique per (namespace, service, port) and DNS-safe. Scheme: edge_k8s_<namespace>_<service>_<port>

func EdgeTCPListenerName

func EdgeTCPListenerName(port uint32) string

EdgeTCPListenerName returns the name of an edge TCP listener for a given port. TCPRoute listeners are named edge_tcp_<port> so each Gateway TCP listener gets its own Envoy listener and stats prefix.

func EdgeTLSListenerName

func EdgeTLSListenerName(port uint32) string

EdgeTLSListenerName returns the name of an edge TLS passthrough listener for a given port. TLSRoute (Passthrough) listeners are named edge_tls_<port>.

func EdgeUpstreamTCPPortTransportSocket

func EdgeUpstreamTCPPortTransportSocket(tlsCertificateSecretName string, validationContextName string, sanURIs []string, sni string) *corev3.TransportSocket

EdgeUpstreamTCPPortTransportSocket is EdgeUpstreamTCPTransportSocket for a per-port raw-TCP cluster (tcp:<fqdn>:<port>): still no ALPN, and the port as SNI, which is how the destination inbound selects that port's tcp_proxy chain (proposal 037). An empty sni is the floor's shape.

func EdgeUpstreamTCPTransportSocket

func EdgeUpstreamTCPTransportSocket(tlsCertificateSecretName string, validationContextName string, sanURIs []string) *corev3.TransportSocket

EdgeUpstreamTCPTransportSocket is EdgeUpstreamTransportSocket for TCP floor clusters: it advertises NO ALPN (like the east-west MeshUpstreamTCPTransportSocket) and NO SNI so the destination inbound demuxes to the TCP floor's DEFAULT chain, while still fetching the edge SVID and trust bundle directly from the SPIRE Agent (not the agent's ADS stream). #304 removed the bespoke "aether-tcp" ALPN; #306 established that the SNI must be empty for the floor — a non-empty SNI would hit the destination's per-port HCM chain instead of the floor default.

func EdgeUpstreamTransportSocket

func EdgeUpstreamTransportSocket(tlsCertificateSecretName string, validationContextName string, sanURIs []string, sni string) *corev3.TransportSocket

EdgeUpstreamTransportSocket is UpstreamTransportSocket for the edge proxy: it fetches the (single) edge SVID and trust bundle over the SPIRE Agent's native SDS API (the static spire_agent cluster) instead of the agent's ADS stream. SAN pinning is unchanged — match_typed_subject_alt_names is inline static config; only the validation-context bundle comes over SDS.

func EndpointPriority

func EndpointPriority(localRegion, localZone string, endpoint *registryv1.ServiceEndpoint, wp WaypointRewrite) uint32

EndpointPriority maps an endpoint's locality distance from this node into an EDS priority (locality-aware failover): same zone 0, same region 1, elsewhere or unknown 2. Envoy keeps traffic at the lowest healthy priority and spills proportionally as it degrades (overprovisioning factor 1.4 default), so a zonal roll or outage drains to the region automatically — including DRAINING/UNHEALTHY EDS marks. An agent without locality labels expresses no preference (everything 0). This is deliberately NOT Envoy's zone_aware_lb_config, which needs the local proxy fleet's per-zone membership on every node — O(all nodes) state that demand-scoped distribution (004) exists to eliminate. With the waypoint enabled, a remote-cluster endpoint is shifted into a higher band so the local cluster is always preferred (same-cluster-first).

func ExtensionFilterAllowed

func ExtensionFilterAllowed(name string) bool

ExtensionFilterAllowed reports whether name is an allow-listed escape-hatch filter (one the proxy build compiles in). Delegates to the shared allow-list (common/extensionfilter) so the agent and the controller webhook agree. The builder skips non-allow-listed names defensively; the webhook (M2) rejects them at admission.

func GenerateCaptureListener

func GenerateCaptureListener(cniPod *cniv1.CNIPod, sourceSpiffeID string, capturePort uint32, meshDomain string, emitStatsPod bool, tcpServices []CaptureTCPService, withPassthrough bool, extensionFilters []*http_connection_managerv3.HttpFilter) (*listenerv3.Listener, error)

GenerateCaptureListener builds the per-pod transparent-capture listener (proposal 018, Phase 3a): bound to capturePort inside the pod netns, it accepts the outbound traffic the CNI redirects from a mesh ClusterIP:meshPort, recovers the original destination, and routes by cluster.local authority (the request Host) to the service's registry-backed EDS cluster — the same SPIRE-mTLS egress path the explicit :18081 listener serves.

TCP floor (Phase 3a): for a service's raw-TCP ports (tcpServices), per-address chains route via tcp_proxy over per-source mTLS to the service's EDS clusters, preserving SPIFFE SAN validation and registry health. The matching tree tries them, and the service's TLSRoutes, before the HTTP chains, and falls back to the HTTP chains when they do not match (captureChainMatcher).

withPassthrough (proposal 022, M2a spike): when true, the listener's default_filter_chain is the passthrough: what the matching tree does not select a chain for. It routes via the passthrough_original_dst ORIGINAL_DST cluster, forwarding the original destination in plain TCP.

Default off (no listener is generated unless transparent capture is on). sourceSpiffeID is the capturing pod's own SPIFFE ID (SourceIdentityForPod), stamped into filter state next to the netns on every chain that originates mesh traffic (see SourceIdentityCertMapperFilterStateKey). "" reproduces the pre-#815 shape exactly.

The listener has one HTTP chain whatever the mesh declares: cap_http, for every port the client dialled, the proxy port included. The caller publishes the route table of that name in the same snapshot (#817). Its chains are selected by filter_chain_matcher (captureChainMatcher, which says why); no chain carries a filter_chain_match.

func GenerateListenersFromRegistryPod

func GenerateListenersFromRegistryPod(cniPod *cniv1.CNIPod, trustDomain string, meshDomain string, emitStatsPod bool, cleartext bool, extensionFilters []*http_connection_managerv3.HttpFilter, inboundFilter *ExtensionFilter, udsSocketPath string) (inbound *listenerv3.Listener, outbound *listenerv3.Listener, appClusters []*clusterv3.Cluster, healthCluster *clusterv3.Cluster, err error)

GenerateListenersFromRegistryPod generates the per-pod inbound and outbound HTTP listeners and the per-pod application and health-probe clusters for a pod. The inbound listener (netns-bound, mTLS) accepts mesh traffic at <pod_ip>:18008 and forwards it to the pod's application on loopback; the outbound listener routes the pod's traffic to other services. The trustDomain names the pod's SVID and the SDS validation context for the inbound listener's mTLS. appClusters is one per served port (the SNI-selected inbound chains forward to these); healthCluster is the single delegated-liveness probe on the primary port. cleartext (SPIRE off) builds the inbound listener without a downstream mTLS transport socket — symmetric with the cleartext outbound clusters — so the mesh data path is routable without SPIRE. udsSocketPath, when non-empty, is the host path of the pod's Unix socket (already resolved and validated by the caller, proposal 034 Phase 1): every app cluster and the health-probe cluster dial that pipe instead of loopback. Empty — the default — is TCP delivery. Nothing else changes: the inbound listener stays netns-bound TCP on :18008 and registry/EDS endpoints stay pod_ip:18008, so a UDS pod is indistinguishable from a TCP pod to clients.

func GenerateOutboundHTTPListener

func GenerateOutboundHTTPListener(cniPod *cniv1.CNIPod, sourceSpiffeID, meshDomain string, emitStatsPod bool, extensionFilters []*http_connection_managerv3.HttpFilter) (*listenerv3.Listener, error)

GenerateOutboundHTTPListener builds the pod's explicit-egress listener. sourceSpiffeID is the pod's own SPIFFE ID (SourceIdentityForPod), stamped into filter state next to the netns for the cluster transport-socket matcher; "" is accepted and reproduces the pre-#815 shape exactly.

func GenerateUDPCaptureListener

func GenerateUDPCaptureListener(cniPod *cniv1.CNIPod, captureUDPPort uint32, udpRoutes map[string][]L4Backend, clusterIPs map[string]string) (*listenerv3.Listener, error)

GenerateUDPCaptureListener builds a per-pod UDP capture listener for UDPRoute routing (proposal 018 Phase 3b, transparent capture per proposal 038).

It binds captureUDPPort inside the pod netns with IP_TRANSPARENT and routes via a udp_proxy MATCHER keyed on the datagram's ORIGINAL destination IP: one arm per UDPRoute parent whose ClusterIP is known, to that route's selected backend cluster. Under TPROXY-style capture the IP header reaches the socket intact (cni/internal/plugin/capture.go, programCaptureDivert), so DestinationIPInput reads the VIP the pod dialled -- the thing the nat REDIRECT this replaces used to destroy, which is why the listener used to carry ONE cluster for the whole node (#873).

PORT. The listener binds the port the client dialled (ProxyL4OutboundPort, 18082 -- the L4 mesh spelling, shared with raw TCP; 038 D1), not a private capture port: Envoy sends a datagram reply from the listener socket's bound port, and a connected client drops a reply whose source port differs from the one it sent to. The CNI diverts udp dport 18082 with NO port rewrite. The TCP capture listener can keep a private port because a TCP reply's source is the accepted socket's local endpoint, which tproxy leaves as the original.

clusterIPs maps "<ns>/<svc>" to the Service's ClusterIP; the cache joins it from the same source the TCP floor chains use (SetCaptureTCPServices). A route whose parent has no known VIP yet produces no arm and a reason, and the cache's per-push reconcile picks it up when the VIP arrives.

WHAT udp_proxy CAN AND CANNOT EXPRESS (#873). udp_proxy's only route action is envoy.extensions.filters.udp.udp_proxy.v3.Route -- a message with exactly one field, `cluster`. tcp_proxy's weighted_clusters has no udp_proxy counterpart, so a UDPRoute traffic SPLIT cannot be represented by any route specifier; each arm binds ONE cluster. What it honours: weight 0 is an explicit DRAIN (Gateway API, #492) and is never bound; among the rest the HEAVIEST backend wins, ties broken by name, not backendRef order.

Returns nil (no listener) when no arm can be built.

SECURITY NOTE: datagrams forwarded via this listener are NOT protected by mesh mTLS. mTLS is a TCP/TLS construct; DTLS is not implemented. Backend clusters are plain, with no transport socket (proposal 018 Phase 3b).

func GeoRouteCacheClearHTTPFilter

func GeoRouteCacheClearHTTPFilter() *http_connection_managerv3.HttpFilter

GeoRouteCacheClearHTTPFilter clears Envoy's cached route AFTER geoip runs, so an HTTPRoute header match can route on the x-geo-* headers (proposal 028). Necessary because the HCM caches the route at decodeHeaders — before the filter chain — and the stock geoip filter sets its headers via an async lookup without clearing the cache, so the router would otherwise use the pre-geoip route. Emitted right after geoip on the edge routing chains. Inert when no route matches on a geo header (clearRouteCache just recomputes the same route). Lua is compiled into the proxy build; this is the documented "modify a routing header then re-route" pattern.

func GeoStripHTTPFilter

func GeoStripHTTPFilter() *http_connection_managerv3.HttpFilter

GeoStripHTTPFilter removes every reserved x-geo-* header from incoming requests. Emitted on the edge routing chains UNCONDITIONALLY (geoip enabled or not) so an internet client can never inject geolocation the mesh would trust.

func GeoipHTTPFilter

GeoipHTTPFilter builds the geoip filter with the MaxMind provider. The header→ field mapping only includes the requested headers; the strip entry has already removed all reserved names, so an un-emitted header is simply absent.

func HasExtAuthz

func HasExtAuthz(filters []*http_connection_managerv3.HttpFilter) bool

HasExtAuthz reports whether the extension union carries the ext_authz system entry — the signal that egress chains need the source-metadata entry too.

func HasWaypointEndpoint

func HasWaypointEndpoint(cla *endpointv3.ClusterLoadAssignment) bool

HasWaypointEndpoint reports whether any endpoint of the load assignment is dialed through a remote cluster's east/west waypoint tunnel (proposal 019). The east-west QUIC fan-out (proposal 038 Phase 4b) uses it to keep such a service on h2: a `quic:` twin's load assignment is the h2 cluster's (an alias under the twin's own EDS name, aether#1008) but it carries ONE QUIC socket and no transport-socket matcher, so it would dial the waypoint's tunnel address over UDP with the direct-path SNI -- and the waypoint tunnel has no QUIC leg (cross-cluster QUIC is the last step of 038 Phase 4, not built).

func HealthGatewayPath

func HealthGatewayPath(probeClusterName string) string

HealthGatewayPath returns the gateway path that reflects the active health of the given per-pod health-probe cluster (health_<namespace>_<pod>).

func HealthProbeClusterName

func HealthProbeClusterName(cniPod *cniv1.CNIPod) string

HealthProbeClusterName returns the name of the per-pod health-probe cluster.

func InboundListenerName

func InboundListenerName(cniPod *cniv1.CNIPod) string

InboundListenerName returns the name of a pod's inbound listener.

func InboundQUICListenerName

func InboundQUICListenerName(cniPod *cniv1.CNIPod) string

InboundQUICListenerName returns the name of a pod's HTTP/3 inbound listener. The "_h3" suffix is the edge's convention (EdgeGatewayH3ListenerName): the UDP listener shares the TCP inbound's port NUMBER, never its name.

func InboundQUICTransportSocket

func InboundQUICTransportSocket(tlsCertificateSecretName, validationContextName, trustDomain string) *corev3.TransportSocket

InboundQUICTransportSocket is the QUIC twin of DownstreamTransportSocket for the per-pod HTTP/3 inbound (proposal 038 Phase 4, R2/R4): the SAME bare DownstreamTlsContext -- this pod's SVID, client certificate required, the workload SAN pin -- wrapped in envoy.transport_sockets.quic, with three QUIC-specific settings that are each load-bearing:

  • ALPN ["h3"] and nothing else. QUIC carries HTTP/3 only; offering "h2" here makes an h3 client's handshake fail with TLS alert 120 no_application_protocol (found by the edge in 029 M4).
  • enable_resumption: false, EXPLICITLY. QUIC never re-verifies the client certificate on a resumed session, so a resumed session would carry a peer identity the destination never checked -- the #829/#831 shape, on a transport with no cx-level re-handshake. envoyproxy/envoy#47219 made off the default and the pin now carries it; explicit anyway, because an absent field is whatever the current default is, and defaults move. R4.
  • enable_early_data: false, EXPLICITLY. 0-RTT data arrives before the client certificate is validated at all. R4.

//agent/test/envoy_validate asserts both falses are PRESENT on every QUIC chain that requires a client certificate; an absent field is Envoy's default, and a default is not a contract.

func InboundQUICUDPListenerConfig

func InboundQUICUDPListenerConfig() *listenerv3.UdpListenerConfig

InboundQUICUDPListenerConfig is the udp_listener_config of the per-pod HTTP/3 inbound (aether#1021): QUIC with default transport options, UDP GRO on the receive path, and Envoy's automatic GSO writer on the send path.

RECEIVE: downstream_socket_config.prefer_gro = true. Envoy defaults GRO OFF for listener sockets (UdpListenerImpl, "Default prefer_gro to false for downstream server traffic"), while its QUIC CLIENT sockets always ask for it (envoy_quic_utils.cc createConnectionSocket), so before this the upstream half of every mesh HTTP/3 connection read with GRO and the inbound half did not. //agent/test/mtlspool TestQUICRequestCPU, 10k paced requests x 2 repetitions against the pinned proxy: destination-side CPU per request 0.757 -> 0.675 ms (-11 %), both proxies 1.692 -> 1.602 ms (-5 %). Falls back to non-GRO reads where the kernel lacks UDP_GRO (Linux < 5.0), with a warning.

SEND: udp_packet_packet_writer_config stays UNSET on purpose. For a QUIC listener Envoy already installs UdpGsoBatchWriterFactory when the kernel supports UDP_SEGMENT and the plain sendmsg writer when it does not (listener_impl.cc); naming envoy.udp_packet_writer.gso explicitly would drop that runtime check. The same harness measured the automatic writer, an explicit GSO writer and the explicit non-GSO default writer at 1.696 / 1.686 / 1.717 ms per request -- within noise of each other: a mesh RPC's response is one or two packets, and GSO only pays when a flush carries several.

The upstream half has no knob to turn: QUIC client sockets already read with GRO, and the only client packet writer at the pin (envoy.quic.packet_writer.default) has no GSO.

func InboundReadyClusterName

func InboundReadyClusterName(cniPod *cniv1.CNIPod) string

InboundReadyClusterName returns the name of the per-pod inbound-readiness probe cluster (inboundready_<namespace>_<pod>).

func InboundReadyProbeTransportSocket

func InboundReadyProbeTransportSocket(nodeSpiffeID, validationContextName, podSpiffeID string) *corev3.TransportSocket

InboundReadyProbeTransportSocket builds the upstream TLS context for the per-pod inbound-readiness probe cluster (issue #815) and for NOTHING else.

THE INVARIANT: nothing this cluster does may be observable by application traffic (issue #836). The probe dials each local pod's mesh inbound INSIDE that pod's network namespace every 5 s, so its TLS peer is always a pod on this node presenting that pod's own SVID — the one certificate that must never turn up in an unrelated cluster's handshake. In #829 a probe-sourced TLS session did exactly that: mesh clusters resumed from a session the probe had created and were handed a local pod's certificate.

That is why this context is written out here in full rather than calling the mesh helper (UpstreamTransportSocket): the probe carries its OWN MaxSessionKeys: 0, so it can never deposit resumable session state whatever the mesh path does. #834 set max_session_keys to 0 on the mesh builders too and that should stay — but it is a global switch someone may reverse for a good reason (upstream envoy#45982 scopes the cache by SNI, which would make resumption defensible again), and this boundary has to hold when they do.

Everything else is load-bearing and explained at NewInboundReadyProbeCluster: ALPN "h2" selects the inbound listener's always-present no-SNI HCM chain, the SAN pin (the pod's own SPIFFE ID) is what makes a pass mean "THIS pod answered" rather than "something answered on :18008", and the SNI stays EMPTY because a non-empty SNI is a destination PORT and would select a per-port chain instead.

func InjectUpstreamMTLS

func InjectUpstreamMTLS(cluster *clusterv3.Cluster, nodeSpiffeID, validationContextName string, sanURIs []string, sni, waypointSNI string)

InjectUpstreamMTLS sets the mesh upstream mTLS transport socket on a service cluster.

SINCE ISSUE #842 THIS IS ONE SOCKET, NOT A PER-IDENTITY LIST. The socket's custom_tls_certificate_selector resolves the client certificate per connection from the source SPIFFE ID the originating listener chain stamped into shared filter state; the certificate is fetched over SDS on demand. What it replaced was a `transport_socket_matches` entry per local ServiceAccount plus a `transport_socket_matcher` whose exact_match_map named them — a structure that had to be rebuilt, and re-pushed, whenever the node's identity set changed.

WHY THAT MATTERS BEYOND TIDINESS: the cluster proto is now invariant under the local identity set entirely. #815 release two made it invariant under pod churn within a ServiceAccount, but the FIRST pod of a new ServiceAccount landing on a node (and the LAST one leaving) still rewrote every mesh cluster on that node, and each rewritten EDS cluster re-warmed for the full 15 s EDS initial_fetch_timeout before its warming→active swap drained every upstream pool. Those last two events cost nothing now: nothing about a mesh cluster depends on which workloads are running.

NODE IDENTITY IS THE DEFAULT, and that is deliberate — it is the same choice, for the same connections, that the removed matcher's on_no_match made. Some upstream connections legitimately have no source pod: paths the node proxy originates on its own behalf, and — during a config transition — a downstream connection accepted by a listener chain that did not stamp the key. The node SVID is a real, attested identity in the same trust domain, so those connections still complete mTLS; the destination simply sees the node rather than a workload. A node identity arriving where a WORKLOAD identity is expected remains the failure signature to watch (never a connection drop) — see docs/runbook.md, "#842".

sanURIs (the service's expected server SPIFFE IDs) pin the upstream peer identity; empty disables pinning (bundle-only). sni is the local (intra-cluster) SNI — the destination port. When waypointSNI is non-empty (proposal 019 waypoint enabled) the cluster carries TWO sockets instead of one, differing only in SNI, selected by endpoint metadata — SNI is a property of the socket, not of the certificate, so that split survives #842 while the identity split does not.

func InjectUpstreamTCPMTLS

func InjectUpstreamTCPMTLS(cluster *clusterv3.Cluster, nodeSpiffeID, validationContextName string, sanURIs []string, sni string)

InjectUpstreamTCPMTLS is InjectUpstreamMTLS for TCP floor clusters: the same per-connection certificate selection, on a socket that advertises no ALPN so the destination inbound demuxes it to the TCP floor's default chain.

func IsMeshEntryClusterName

func IsMeshEntryClusterName(name, meshDomain string) bool

IsMeshEntryClusterName reports whether name is one a mesh cluster entry is published under: a service's HTTP cluster, per-port cluster or port alias ("<svc>.<ns>.<meshDomain>[:<port>]"), or its TCP-floor clusters (the same behind "tcp:"). Those are the clusters meant to be mTLS with a server-identity pin, on the node agent and on the edge alike.

A name of a family outside the pin gauges is not one, whatever it ends in: the waypoint ingress cluster is named by a service FQDN behind its prefix.

It reads the form of the name only. It does not say that a cluster of that name exists, nor that the one published under it carries a pin at this moment (cache.clusterEntry.pinState decides that).

func IsPerPodClusterName

func IsPerPodClusterName(name string) bool

IsPerPodClusterName reports whether the cluster name belongs to a per-pod cluster (app_<namespace>_<pod> delivery or health_<namespace>_<pod> probe), as opposed to a registry-derived service cluster.

func IsQUICClusterName

func IsQUICClusterName(name string) bool

IsQUICClusterName reports whether name is shaped like a `quic:` twin name. It does not validate the name; ParseQUICClusterName does.

func LoadAssignmentAlias

LoadAssignmentAlias returns base's load assignment re-published under name: the EDS resource of a cluster that has the SAME membership as base but must not subscribe to base's resource name. Every such cluster points its eds_cluster_config.service_name at its own cluster name and the cache publishes this copy in the same snapshot pass that emits the cluster:

  • the QUIC twins (QUICClusterFrom, aether#1008);
  • the HTTP port-alias clusters "<fqdn>:<port>" (aether#1013);
  • the TCP floor "tcp:<fqdn>" and its primary-port alias "tcp:<fqdn>:<port>" (aether#1013).

Why a copy instead of a shared name: Envoy's delta-ADS WatchMap deduplicates subscription interest per (type_url, resource name). A cluster added in a LATER CDS update than a sibling already subscribed to the same EDS name adds nothing to resource_names_subscribe, no request goes out, the control plane has nothing to answer (the resource did not change), and the new cluster warms for the full initial_fetch_timeout (15 s). The same mechanism as the SDS outage of #842. A delta-ADS subscriber must never share a resource name with an already-subscribed sibling.

Endpoints, named endpoints and policy are the base's -- including the health status the agent writes itself and every locality/metadata field -- so the alias sees exactly the membership the base cluster does.

The endpoint slice elements and the policy are SHARED with base, not cloned: load assignments are never mutated after they are built (a changed endpoint set builds a new one, see SnapshotCache.RemoveEndpoint), and the copy is rebuilt from the base on every snapshot, so identical inputs marshal to identical bytes. Every ClusterLoadAssignment field is carried; the proxy tests pin the field set so a new upstream field cannot be dropped silently.

func LoopbackShortNameFloor

func LoopbackShortNameFloor(fqdn, suffix string) []*routev3.Route

LoopbackShortNameFloor is the ShortNameFloor of the tables whose floor takes the port from the authority (the loopback table, and the proxy port's capture table, which is given the same virtual hosts): a mesh name with a port is offered on demand as "<fqdn>:<port>", except the mesh's own two ports; without a port it names nothing.

func MergeCaptureRouteTables

func MergeCaptureRouteTables(name string, perPort map[uint32]*routev3.RouteConfiguration, other *routev3.RouteConfiguration) (*routev3.RouteConfiguration, error)

MergeCaptureRouteTables compiles the per-port capture tables (proposal 043, "The addressing rule") into the ONE table the capture listener's cap_http chain serves.

The per-port tables are the rule: perPort[D] is what a request dialled to port D gets, for every HTTP port number D of the mesh and for the mesh's HTTP proxy port (18081, whose table is the loopback table's virtual hosts, BuildProxyPortCaptureRouteConfiguration: the port in Host decides there), and other is what a request dialled to any other port gets. The compiled table must give every request the answer the table of its dialled port gives it, and does so by construction:

  • its domains are every domain of every per-port table, so the proxy selects, for any Host, a virtual host holding the domain that is the most specific match of that Host in ANY per-port table;
  • that virtual host is a plain, ordered route list (no matcher): for each dialled port whose answers differ from the other ports', the routes of that port's answers, each route also matching the dialled port (RouteMatch.filter_state on DialledPortFilterStateKey, which the route's stream reads from the connection) and, when the port answers some spellings of the name differently from others, the Host's port suffix (an :authority regex: no port, ":<D>", exactly one port); then the other ports' answers, matching no dialled port.

Every answer's route list ends in a route every request takes (a service's fallback, a refusal), so a request a block's conditions take never reaches a later block, and first-match gives each request its dialled port's answer. "<name>:8080:8080" has no suffix the regex accepts: a mismatch, as it was.

The order of the dialled-port blocks is therefore free: their conditions are disjoint (one dialled port each), and each ends in a route every request of its port takes. Only the other ports' block, which matches no dialled port, must come after all of them. The proxy port's block is placed after the declared ports' (mergePortOrder), so that a request to a service's own port, the common case, is matched against fewer routes first.

An answer is the per-port virtual host's routes, or, for a short name, its caller-namespace matcher flattened: each namespace's routes under a filter-state match on the caller's namespace, then what a caller of no such namespace gets, then what a connection with no namespace gets.

A per-port virtual host's typed_per_filter_config (a service-wide HTTP filter is enabled there) stays a VIRTUAL-HOST config: the compiled virtual host of a service carries it, so the proxy combines it with a route's own config exactly as it does on the per-port table (and on the proxy port). The routes of that compiled virtual host that come from an answer without it (a refusal) disable each of its filters (route-level FilterConfig disabled: the route's own answer to "is this filter disabled" wins over the virtual host's, source/common/router/config_impl.cc, RouteEntryImplBase::filterDisabled, and a disabled filter is not created for the stream, source/common/http/filter_chain_helper.cc), so a refusal runs none of them, as on the per-port table. Only when that cannot be said (two answers of one compiled virtual host with different virtual-host configs, or a route without the virtual-host config that sets one of its filters itself) is the virtual-host config folded into each route instead (extensionfilter.CombineLevels, which refuses a route it cannot fold exactly); no table the agent builds does that today.

Domains are grouped into virtual hosts by the name of the per-port virtual host that first claims them: "<mesh name>|<what>" groups under the mesh name, so a service has one virtual host whatever spelling or port names it, and "<short name>|short|<suffix>" under the short name.

When the dialled-port filter state is missing (a chain that does not set it), every request gets other's answer: a mesh name the node knows is refused, never served on a port it was not dialled on.

An error means the per-port tables use a construct this compiler does not model (a domain wildcard other than "*" and "<name>:*", a virtual-host field other than its routes, matcher and per-filter config, names of one virtual host that one table answers differently for one suffix, a route list a request can fall out of). The caller must not publish the result then; nothing the agent builds today does that.

func MeshAuthorityRegex

func MeshAuthorityRegex(meshDomain, portSuffix string) string

MeshAuthorityRegex is the RE2 pattern of a mesh authority, <svc>.<ns>.<meshDomain> (two labels before the mesh domain, 020 Part 1), followed by portSuffix: a pattern for what may follow the name, e.g. "(:[0-9]+)?" for any port or none, ":8080" for exactly that port.

func MeshUpstreamTCPTransportSocket

func MeshUpstreamTCPTransportSocket(nodeSpiffeID string, validationContextName string, sanURIs []string, sni string) *corev3.TransportSocket

MeshUpstreamTCPTransportSocket is MeshUpstreamTransportSocket for TCP-proxy upstream connections (proposal 018, Phase 3a TCP floor): the same per-connection certificate selector, but it advertises NO ALPN (vs HTTP's "h2"). The destination inbound listener's HTTP chains match application_protocols:["h2"], so a no-ALPN mTLS connection falls through to the inbound TCP floor's DEFAULT chain — demultiplexing TCP from HTTP with the standard h2 ALPN instead of a bespoke token. sanURIs and sni semantics are unchanged.

func MeshUpstreamTransportSocket

func MeshUpstreamTransportSocket(nodeSpiffeID string, validationContextName string, sanURIs []string, sni string) *corev3.TransportSocket

MeshUpstreamTransportSocket is the ONE upstream socket a node-proxy mesh service cluster carries since issue #842. It is UpstreamTransportSocket with the statically named client certificate replaced by the per-connection on-demand certificate selector (upstreamCertSelector): the certificate is chosen at handshake time from the source pod's SPIFFE ID in filter state, instead of by a per-identity transport_socket_matches list the control plane had to rebuild whenever the node's identity SET changed.

nodeSpiffeID is the selector's default_value — the certificate a connection carrying no source identity presents, which is what the pre-#842 matcher's on_no_match presented for exactly the same connections. Everything else (ALPN h2, the SAN pin, the SNI, MaxSessionKeys 0) is byte-identical to UpstreamTransportSocket.

func NewAppCluster

func NewAppCluster(name string, addr AppAddress, port uint16, http2 bool) *clusterv3.Cluster

NewAppCluster builds a STATIC cluster that forwards decrypted inbound traffic to the pod's own application, at 127.0.0.1:<port> bound into the pod's network namespace or — for a UDS workload (addr.Pipe) — at the socket's host path with no netns bind. This is the only cleartext hop in the mesh: it is intra-pod (Envoy -> app), never pod-to-pod. When http2 is set the hop speaks HTTP/2 (h2c) to the app — a per-port app protocol (multi-port pods may serve h1 on one port and h2/gRPC on another); otherwise HTTP/1.1 is assumed. port names the app's TCP port and is ignored for a pipe upstream (all of a UDS pod's declared ports dial the same socket).

func NewAppDeliveryClusters

func NewAppDeliveryClusters(cniPod *cniv1.CNIPod, udsSocketPath string) (appClusters []*clusterv3.Cluster, healthCluster *clusterv3.Cluster)

NewAppDeliveryClusters builds a pod's delivery clusters: one app cluster per served port plus the health-probe cluster. udsSocketPath is the resolved host path of the pod's Unix socket, or "" for TCP loopback delivery.

Split out from GenerateListenersFromRegistryPod because delivery can change without the pod changing — a service-scoped EndpointPolicy (proposal 034 Phase 1b) is authored independently of the workload — so these clusters are rebuilt on their own, without regenerating the pod's listeners.

func NewAppHealthProbeCluster

func NewAppHealthProbeCluster(name string, addr AppAddress, port uint16, healthPath string, protocol registryv1.Service_Protocol) *clusterv3.Cluster

NewAppHealthProbeCluster builds a per-pod cluster that only exists to actively health-check the pod's application at the delivery address (delegated liveness). It is NOT referenced by any route — the agent scrapes its host health from the proxy admin and reflects it into the registry. The active HC must live on this separate cluster, not on app_<namespace>_<pod>: an active HC removes failing/pending hosts from load balancing, which would gate (break) the real delivery path through app_<namespace>_<pod> at startup and whenever the probe fails.

func NewBlackholeCluster

func NewBlackholeCluster() *clusterv3.Cluster

NewBlackholeCluster builds a STATIC cluster with no endpoints.

A connection routed here is closed immediately and counted in cluster.blackhole.upstream_cx_none_healthy — a deliberate, attributable close rather than a fake HTTP response.

It exists for scoped capture only. The CNI redirects :18082 unconditionally there (it cannot know which VIPs have TCP ports), and the scoped HCM chain is a catch-all with no match criteria — so raw TCP to :18082 on a service with no TCP port would hit the HCM and get a fabricated `HTTP/1.1 400`, which is the #460 shape: an error that describes the proxy's confusion rather than the caller's mistake.

func NewClusterLoadAssignment

func NewClusterLoadAssignment(serviceName string) *endpointv3.ClusterLoadAssignment

NewClusterLoadAssignment creates an empty cluster load assignment for a service. Endpoints should be added to the Endpoints field.

func NewDownstreamTLSSecret

func NewDownstreamTLSSecret(name string, cert, key []byte) *transport_sockets_v3.Secret

NewDownstreamTLSSecret builds an Envoy SDS Secret carrying an inline certificate/key (read by the edge from a provider-backed source). name is the provider-prefixed SDS name the edge listener references.

func NewInboundListener

func NewInboundListener(cniPod *cniv1.CNIPod, trustDomain string, emitStatsPod bool, cleartext bool, extensionFilters []*http_connection_managerv3.HttpFilter, inboundFilter *ExtensionFilter) (*listenerv3.Listener, error)

NewInboundListener builds a pod's inbound listener. It is bound into the pod's network namespace at :defaultInboundPort so the pod is reachable at <pod_ip>:defaultInboundPort, terminates mTLS presenting the pod's own SVID (so callers cryptographically verify they reached this pod, not just the node), requires and validates the caller's workload SVID, sets XFCC natively from the verified peer (SANITIZE_SET), and forwards the request to the pod's application on loopback (app_<namespace>_<pod>). Because the listener lives in the pod's netns, it follows the pod's lifecycle (drains on removal) and pod-scoped network policy applies to it.

When cleartext is true (SPIRE disabled) the listener accepts CLEARTEXT instead of terminating mTLS: no SVID is issued, the SPIRE bridge delivers no SDS secrets, and the outbound clusters already dial cleartext (h2c) — so the inbound side must accept cleartext to keep the mesh hop routable. In that mode the listener carries a single default HCM chain (no transport socket, AUTO codec so h2c and HTTP/1 are both detected, no ALPN/SNI demux, no XFCC since there is no verified peer). This is for SPIRE-off testing/conformance (the suite asserts routing correctness, not mTLS); the production posture keeps per-pod mTLS.

func NewInboundQUICListener

func NewInboundQUICListener(cniPod *cniv1.CNIPod, trustDomain, meshDomain string, emitStatsPod bool, cleartext bool, extensionFilters []*http_connection_managerv3.HttpFilter, inboundFilter *ExtensionFilter) (*listenerv3.Listener, error)

NewInboundQUICListener builds a pod's HTTP/3 inbound listener (proposal 038 Phase 4, R2/R3): UDP on the SAME port as the TCP inbound (defaultInboundPort, 18008 -- R3, the shared-port rule the port-role gate pins), bound into the pod's netns, terminating mTLS over QUIC with the pod's own SVID and the same client-certificate requirement and workload SAN pin as the TCP inbound (InboundQUICTransportSocket wraps the same bare context), and routing to the same app_<namespace>_<pod>_<port> clusters through the same inbound route. Callers that reach it are the per-source `quic:` clusters of Phase 4b; until those exist it is inert -- one UDP socket per pod, no client -- which is why it ships unconditionally rather than behind a flag: it soaks under ordinary churn from the first deploy that carries it.

Chains, and why they differ from the TCP inbound's:

  • QUIC carries HTTP/3 ONLY, so there is no tcp_proxy floor and no chain for a port declared raw TCP (proposal 037); those stay TCP-inbound-only.
  • The PRIMARY port is the DEFAULT chain (no match): there is no "h2" ALPN to key on, and every QUIC connection is HTTP/3.
  • Every other HTTP port is a server_names chain on QUICServerName(port, <pod's mesh authority>) = "<port>.<sa>.<ns>.<meshDomain>" -- NOT the bare port the TCP inbound matches, because the QUIC client insists on a DNS hostname SNI (aether#957; see QUICServerName). The match works on a QUIC listener because Envoy's QUIC proof source passes the CHLO's hostname into the filter-chain lookup (source/common/quic/envoy_quic_proof_source.cc, verified at the pinned commit). The primary port's clients send "<primary>.<authority>", which no chain names, so they land on the default chain exactly as an h2 primary-port client does.

No inbound divert is involved: a QUIC datagram from another node's proxy arrives on the pod's eth0 addressed to the pod IP and is delivered to this socket by ordinary routing. The CNI's mark-and-divert is for the pod's OWN outbound only, and outbound QUIC originates from the proxy in the host netns.

Returns (nil, nil) when cleartext (SPIRE off): QUIC mandates TLS, there is no SVID to present, and the TCP inbound's cleartext chain keeps the mesh hop routable on its own. Returns ErrNoTrustDomain in the same case the TCP inbound does, for the same reason.

func NewInboundReadyProbeCluster

func NewInboundReadyProbeCluster(name, netns, nodeSpiffeID, validationContextName, podSpiffeID string) *clusterv3.Cluster

NewInboundReadyProbeCluster builds the per-pod cluster whose only job is to actively prove that the pod's OWN mesh inbound listener is serving mTLS with the pod's own certificate (issue #815, the promotion hole).

WHY IT EXISTS. Until now an endpoint was promoted to HEALTHY on the app probe alone (health_<namespace>_<pod>), which dials the application in cleartext and has no SDS dependency whatsoever. The inbound listener, meanwhile, is published at CNI ADD while the pod's SVID arrives asynchronously 6-8 s later; while the listener warms, Envoy has BOUND its socket but not called listen(), so a peer gets ECONNREFUSED. Measured promotion p50 was 5.8 s against a 6.1-8.4 s SVID, and one pod promoted in <=0.5 s — i.e. the mesh advertised an endpoint whose mesh port refused connections.

WHAT "HEALTHY" MEANS HERE. The health check is TCP with an EMPTY send payload (connect-only) over a TLS transport socket, which is a deliberate composition: Envoy's TCP health-check session succeeds on the Connected event, and a connection with a TLS transport socket does not raise Connected until the handshake has completed (SslSocket::doHandshake raises it; RawBufferSocket raises it on connect). So a pass means, in one signal:

the inbound listener is listening (not warming, not absent)
AND it presented a certificate
AND that certificate verifies against the mesh trust bundle
AND its URI SAN is exactly THIS pod's SPIFFE ID.

No bytes are sent and no request is made, so nothing downstream of the filter chain (route table, authz/RBAC, the app) participates.

ADDRESSING. 127.0.0.1:defaultInboundPort bound into the pod's network namespace — the same netns hop the app clusters use. The inbound listener binds 0.0.0.0:defaultInboundPort in that netns, including for UDS-delivery pods (UDS changes app delivery, never the mesh inbound), so this always addresses it.

CHAIN SELECTION. ALPN "h2" and NO SNI, which lands on the inbound listener's no-SNI HCM chain (filter_chain_match application_protocols:["h2"], see buildInboundFilterChains). That chain always exists on an mTLS inbound listener and is the one real mesh HTTP traffic uses. The alternative — no ALPN — would land on the TCP floor default chain, whose tcp_proxy opens an upstream connection to the application on every single probe.

CLIENT IDENTITY. nodeSpiffeID: the node's own SVID, the same secret the service clusters' transport-socket matcher presents on its no-match path for node-originated connections (active health checks carry no source-pod filter state). The inbound DownstreamTlsContext requires a client certificate and validates it against the trust-domain bundle with no SAN restriction, so the node identity is accepted; nothing about inbound validation is loosened here.

podSpiffeID pins the SERVER side: the probe must not pass because SOMETHING answered on that address, only because THIS pod did.

func NewPassthroughOriginalDstCluster

func NewPassthroughOriginalDstCluster() *clusterv3.Cluster

NewPassthroughOriginalDstCluster builds an ORIGINAL_DST cluster for the redirect-all capture passthrough path (proposal 022, M2a spike).

Envoy's ORIGINAL_DST cluster type uses the SO_ORIGINAL_DST socket option (set by the netfilter REDIRECT target) to recover the pre-NAT destination address and connects directly to it, bypassing mesh mTLS. This is the correct mechanism for forwarding non-mesh egress in plaintext from within the capture listener: Envoy learned the original destination from the listener filter (original_dst), and the ORIGINAL_DST cluster type tells Envoy to use exactly that address/port as the upstream. No load assignment or EDS resource is needed — each connection resolves its own upstream from the socket metadata.

Security: traffic forwarded via this cluster is PLAINTEXT. It is intentionally used only for NON-mesh destinations (ones the capture listener does not recognize via its HCM route table or TCP floor chains). All mesh traffic continues to use per-source mTLS clusters as before.

func NewServiceCluster

func NewServiceCluster(name, edsServiceName, altStatName string, subsetKeys []string) *clusterv3.Cluster

NewServiceCluster builds an EDS service cluster.

IT NO LONGER SETS connection_pool_per_downstream_connection, and that is the second half of issue #842. The flag gave every downstream connection its own upstream pool, which is what kept a node proxy hosting many workload identities from handing source B a pooled connection carrying source A's client certificate (#831, reproduced by //agent/test/mtlspool's negative control). It bought that correctness by making upstream h2 multiplexing across sources impossible: one upstream connection and one full mTLS handshake per downstream connection, forever.

The pool key now carries the source identity directly. The listener stamps the source SPIFFE ID with the HASHABLE string factory (SourceIdentityCertMapperFilterStateKey) and shares it with the upstream, so CommonUpstreamTransportSocketFactory::hashKey folds it into the pool hash and pools partition per (host, source identity) — the correct granularity, and strictly finer than what the certificate requires. Pods sharing a ServiceAccount can now share an upstream connection; pods that do not, cannot.

THE TWO ARE A PAIR. Removing the flag without the hashable filter state re-opens the leak, and it fails OPEN: XFCC reports the wrong caller and nothing errors. //agent/test/mtlspool asserts both directions.

func NewTCPServiceCluster

func NewTCPServiceCluster(name, edsServiceName, altStatName string) *clusterv3.Cluster

NewTCPServiceCluster builds an EDS service cluster for a non-HTTP service's TCP floor path (proposal 018, Phase 3a). It is identical to NewServiceCluster except:

  • It omits TypedExtensionProtocolOptions (no HTTP/2 upstream protocol — the tcp_proxy sends raw bytes after mTLS termination, so h2 negotiation is wrong).
  • The cluster's transport socket (injected at snapshot time by InjectUpstreamTCPMTLS) uses ALPN "aether-tcp" instead of "h2", so the destination inbound's tls_inspector routes to the TCP floor filter chain, not the HCM.
  • subset_selectors and retry circuit-breakers are omitted — TCP proxy doesn't use subset routing or the HTTP retry mechanism.

Like NewServiceCluster it no longer sets connection_pool_per_downstream_connection (issue #842); the source identity is in the pool key instead. tcp_proxy takes a whole upstream connection per downstream connection anyway, so the flag bought this cluster type nothing but a distinct pool per connection — but it was still the thing that made certificate selection safe, and it comes off for the same reason and at the same time as the HTTP one.

func NewUDPServiceCluster

func NewUDPServiceCluster(name, altStatName string, loadAssignment *endpointv3.ClusterLoadAssignment) *clusterv3.Cluster

NewUDPServiceCluster builds a plain EDS cluster for a UDPRoute's backend (proposal 018, Phase 3b). It is intentionally transport-socket-free — UDP datagrams are not wrapped in mTLS (mTLS is a TCP/TLS construct; DTLS is not implemented in this mesh). Traffic forwarded via this cluster is PLAINTEXT.

SECURITY NOTE: mesh mTLS does NOT cover the UDP floor. All datagrams forwarded to backends via this cluster travel without encryption or mutual authentication. This is a known limitation; see proposal 018 Phase 3b.

The cluster is STATIC with an inline load assignment rather than EDS: the UDP floor has no inbound mTLS hop, so udp_proxy must reach the backend's application UDP port directly — not the mesh inbound :18008 that the shared bare-name EDS carries. Callers pass a load assignment from UDPLoadAssignment (app-port endpoints).

func OnDemandHTTPFilter

func OnDemandHTTPFilter() *http_connection_managerv3.HttpFilter

OnDemandHTTPFilter is production's on_demand HTTP filter (ODCDS over ADS, bounded by onDemandClusterTimeout), exported so the live harnesses in //test run the exact filter every node-proxy HCM carries. The east-west QUIC selection depends on it (issue #1020): a matcher arm that names a `quic:` twin the snapshot does not carry yet is fetched by this filter on first use.

func OnDuplicateCaptureAddress

func OnDuplicateCaptureAddress(f func(listener, address, kept, dropped string))

OnDuplicateCaptureAddress sets the function told of every service address two capture services claimed on a capture listener being built: the second service's chains are unreachable. The cache sets it to log and count.

func OnDuplicateDomain

func OnDuplicateDomain(f func(table, domain, kept, dropped string))

OnDuplicateDomain sets the function that is told of every authority two virtual hosts of one route table claimed (oneClaimPerDomain). The cache sets it to log: a duplicate is always a defect upstream of the table builders.

func OutboundListenerName

func OutboundListenerName(cniPod *cniv1.CNIPod) string

OutboundListenerName returns the name of the per-pod outbound HTTP listener, used by the CNI server to await Envoy's delta-xDS ACK of the listener.

func ParseQUICClusterName

func ParseQUICClusterName(name, meshDomain string) (service, sourceSAKey string, ok bool)

ParseQUICClusterName is the inverse of QUICClusterName: it splits a twin name into the destination's "<ns>/<svc>" service key and the source's "<ns>/<sa>" key. ok is false unless name is exactly what QUICClusterName produces for them. The on-demand CDS path (issue #1020) validates a requested name with it before building a twin, so a name with a port, a foreign domain, an extra label or an extra path segment is refused.

func PodResourceKey

func PodResourceKey(cniPod *cniv1.CNIPod) string

PodResourceKey returns "<namespace>_<pod>": the part of every per-pod xDS resource name that says WHICH pod it belongs to.

It is the only place that decides how a pod is spelled in a name. Every per-pod listener (inbound_, inbound_…_h3, outbound_http_, capture_, capture_udp_), every per-pod cluster (app_…_<port>, health_, inboundready_), and every per-pod filter chain name (in_, in_tcp_, in_h3_, out_http_, capture_) is built from it. //agent/internal/xds/proxy:proxy_test (TestPerPodResourceNamesCarryTheNamespace) is the inventory.

The pod NAME alone is not a key (issue #1584). Two pods of one name in two namespaces on one node (two StatefulSets called "web", each with a "web-0") then shared every one of those names. go-control-plane keeps one resource per name, and which pod's it kept was decided by map order on every snapshot build, separately for listeners and for clusters: one pod lost its listeners, and the other's inbound listener could route to an app cluster that dials the first pod's loopback.

STAT names are not built from it directly but from PodStatKey, which differs for a pod name that contains a dot. The chart extracts the aether.namespace and aether.pod stats tags from the listener stat prefixes (charts/aether/templates/agent-proxy-configmap.yaml), and its stats exclusions match the health_ and inboundready_ clusters' stat names; a change of either shape has to be made there too.

func PodStatKey

func PodStatKey(cniPod *cniv1.CNIPod) string

PodStatKey returns how a pod is spelled in a STAT name: PodResourceKey with each dot of the pod name replaced by '~' ("team-a_web~external-0"). For a pod name without a dot, which is every name a workload controller generates, it IS PodResourceKey.

Only stat names use it: the listeners' stat_prefix, the tcp_proxy and udp_proxy stat prefixes, and the probe clusters' alt_stat_name (podStatName). No resource is named by it. Envoy and go-control-plane key listeners and clusters by PodResourceKey, the health gateway's health_check filters and paths name the probe clusters by it, and the agent's liveness loop requests those paths.

The value of the chart's aether.pod stats tag is therefore the pod name with '~' for each dot (docs/runbook.md, "Chart 2.5.2").

func PortClusterName

func PortClusterName(serviceName, meshDomain string, port uint32) string

PortClusterName returns the data-plane name of a service's per-port cluster: <svc>.<ns>.<meshDomain>:<port>. Equals the FQDN:port authority a client addresses, so the catch-all cluster_header resolves it directly. Returns "" when the service key is not namespace-qualified (see ServiceClusterName).

func QUICAltStatName

func QUICAltStatName(h2AltStatName, sourceSAKey string) string

QUICAltStatName is the stats name of a per-source HTTP/3 twin: "<h2 alt_stat_name>@<ns>/<sa>" -- the h2 cluster's stat key with the same "@<source>" suffix QUICClusterName carries (aether#960). A plain clone kept the h2 cluster's alt_stat_name, so every twin and the h2 cluster reported into ONE cluster.<ns>/<svc>.* tree and nothing in Prometheus could tell HTTP/3 from HTTP/2, or one source's twin from another's -- a wrong-arm selection (#831's shape) was invisible in stats. The suffix has no dots, so the cluster_name tag extraction still captures the whole key, and the h2 series keep their exact name: dashboards that want both match `aether_cluster=~"<ns>/<svc>(@.*)?"`. An empty h2 alt_stat_name yields "" (Envoy then keys stats by the twin's own, already unique, name).

func QUICClusterFrom

func QUICClusterFrom(base *clusterv3.Cluster, name, clientSpiffeID, validationContextName string, sanURIs []string, sni string, idleTimeout time.Duration) *clusterv3.Cluster

QUICClusterFrom derives a source's HTTP/3 cluster from the destination's h2 service cluster: the same endpoints, the same subset selectors, the same outlier detection and circuit breakers -- a clone -- renamed, with HTTP/3 protocol options and the QUIC upstream socket presenting clientSpiffeID. base is the entry's BARE cluster (no transport socket); the caller passes the same sanURIs it would give InjectUpstreamMTLS for the h2 twin, so the two transports pin the same server identity, and sni = QUICServerName(<the h2 twin's port SNI>, <authority>), so the destination demuxes to the same inbound chain.

The twin subscribes to its OWN EDS resource, named after the twin (the control plane publishes the base's load assignment under that name too: LoadAssignmentAlias). It must not share the base's EDS name (aether#1008). Envoy's delta-ADS WatchMap deduplicates subscription interest per (type_url, resource name): a twin added AFTER its base is subscribed -- a new ServiceAccount's first pod on the node -- adds nothing to resource_names_subscribe, no request goes out, the control plane has no reason to answer (the resource did not change), and the twin sits in warming until its 15 s initial_fetch_timeout while every request from that source is 503/NC. The same mechanism as the SDS outage of #842.

idleTimeout is the twin pool's idle timeout (the agent's --east-west-quic-idle-timeout; <= 0 means config.DefaultQUICTwinIdleTimeout). It is deliberately shorter than the h2 base's config.UpstreamIdleTimeout so no idle h3 connection outlives a destination's hot-restart parent (aether#1054).

func QUICClusterName

func QUICClusterName(serviceName, meshDomain, sourceSAKey string) string

ServiceFromClusterName maps a data-plane cluster name (a mesh authority, <svc>.<ns>.<meshDomain>) back to the namespace-qualified "<ns>/<svc>" service key (proposal 020 Part 1). ok is false when the name is not under the mesh domain or the remainder is not exactly two DNS labels (<svc>.<ns> — service names are ServiceAccount names and namespaces are both single lowercase QUICClusterName returns the per-source HTTP/3 cluster for a destination service as dialled by ONE local ServiceAccount (proposal 038 Phase 4b): "quic:<svc>.<meshDomain>@<ns>/<sa>". The "@<source>" suffix is what makes the name unique per source identity; the prefix keeps it out of every ODCDS/authority namespace (a client can never dial a "quic:" authority).

func QUICSelectionArms

func QUICSelectionArms(r *routev3.Route) (arms map[string]string, noMatch string, ok bool)

QUICSelectionArms reads back the (source SPIFFE ID -> cluster) arms and the on_no_match cluster of a route rewritten by ApplyQUICClusterSelection, or ok=false when the route carries no such plugin. Test and gate helper.

func QUICServerName

func QUICServerName(port, fqdn string) string

QUICServerName is the SNI a `quic:` cluster presents and the server_names the destination's HTTP/3 inbound matches for a non-primary port: "<port>.<svc>.<ns>.<meshDomain>" -- the h2 path's bare "<port>" SNI with the destination's mesh authority appended (aether#957).

Why not the bare port, as on TCP: Envoy's QUIC client verifies the leaf against the SNI as a DNS hostname AFTER any configured validator succeeds (source/common/quic/envoy_quic_proof_verifier.cc, verified at the pin), and nothing but accept_untrusted skips it; a bare "8080" matches no DNS SAN, so every HTTP/3 handshake would fail with a verified peer. The SVID SPIRE issues to a mesh workload therefore carries two DNS SANs -- "<sa>.<ns>.<mesh domain>" and "*.<sa>.<ns>.<meshDomain>" (dnsNameTemplates on the workloads' ClusterSPIFFEID; mesh Service names ARE ServiceAccount names) -- and this shape lands on the wildcard for every port. The SPIFFE URI SAN pin (sanURIs) stays the identity check; the DNS match is only what the QUIC client insists on.

func QUICUpstreamTransportSocket

func QUICUpstreamTransportSocket(clientSpiffeID, validationContextName string, sanURIs []string, sni string) *corev3.TransportSocket

QUICUpstreamTransportSocket is the upstream side of proposal 038 Phase 4b: the envoy.transport_sockets.quic socket a per-source `quic:` cluster carries.

It names ONE client certificate statically -- clientSpiffeID, the source ServiceAccount's SVID over ADS-served SDS -- because QuicClientTransportSocketFactory::create REJECTS a certificate selector (quic_client_transport_socket_factory.cc, verified at the pin): the per-connection selection the TCP path uses since #842 does not exist for QUIC, so the per-source identity has to be the cluster's own. That is why there is one such cluster per (destination, local ServiceAccount) and why the route selects between them by the source's filter-state identity.

The rest is the TCP upstream's contract, kept on purpose: the server SAN pin (sanURIs), the SNI (in QUICServerName's hostname form, never the bare port: aether#957) and MaxSessionKeys: 0. R4 on the CLIENT side is that last field: QuicUpstreamTransport carries only an UpstreamTlsContext (no enable_resumption), and MaxSessionKeys: 0 is what disables client-side session resumption -- the same precondition the TCP cert selector already had. ALPN is h3 only.

func RefuseCaptureMeshNames

func RefuseCaptureMeshNames(name string, perPort map[uint32]*routev3.RouteConfiguration, other *routev3.RouteConfiguration) *routev3.RouteConfiguration

RefuseCaptureMeshNames is the table published in place of cap_http when the per-port tables cannot be compiled: every domain any of them claims is refused (421 port-not-declared), whatever was dialled, and every other Host gets the floor of the table of a port no service declares. No mesh name the node knows is delivered, and none leaves through the passthrough.

func ServiceClusterName

func ServiceClusterName(serviceName, meshDomain string) string

ServiceClusterName returns the data-plane name of a service's outbound cluster: <svc>.<ns>.<meshDomain> (proposal 020 Part 1). The control-plane key (registry, watch filter, dependency set, EDS, stats) is the namespace-qualified "<ns>/<svc>" serviceref key; this renders it as the FQDN authority, so the warm path (service vhost) and the cold path (catch-all cluster_header: ":authority" + ODCDS) resolve the same resource deterministically.

The cutover is complete (GAMMA #397, l4route + edge L4): every caller passes a namespace-qualified key. A bare (non-namespaced) name therefore can't occur in production; rather than render a flat <name>.<meshDomain> that would silently mis-route, it returns "" — an empty cluster name matches no cluster, so a stray bare key degrades to a clean no-route instead of a wrong route.

func ServiceFromClusterName

func ServiceFromClusterName(clusterName, meshDomain string) (string, bool)

labels), so nested or foreign authorities are rejected deterministically.

func ServiceLocalityLbEndpointFromRegistryEndpoint

func ServiceLocalityLbEndpointFromRegistryEndpoint(endpoint *registryv1.ServiceEndpoint, localRegion, localZone string, wp WaypointRewrite) *endpointv3.LocalityLbEndpoints

ServiceLocalityLbEndpointFromRegistryEndpoint builds an endpoint for the plain transport: its socket address is the destination pod's mesh inbound (<pod_ip>:defaultInboundPort), reached by the pod's own netns-bound inbound listener, and its host metadata carries the envoy.lb subset keys for affinity. The endpoint health reflects the registry's delegated-liveness status, and its priority the locality distance from this node (EndpointPriority).

func SetAccessLogConfig

func SetAccessLogConfig(c AccessLogConfig)

SetAccessLogConfig sets the global access-log configuration. Call once before the manager starts.

func SetTracingConfig

func SetTracingConfig(c TracingConfig)

SetTracingConfig sets the global proxy tracing configuration. Call once before the manager starts.

func ShortNameVirtualHostName

func ShortNameVirtualHostName(svc, suffix string) string

ShortNameVirtualHostName names the virtual host of a short domain.

func ShortNameVirtualHosts

func ShortNameVirtualHosts(vhosts []*routev3.VirtualHost, meshDomain string, known []string, floor ShortNameFloor, notMesh []*routev3.Route) []*routev3.VirtualHost

ShortNameVirtualHosts returns the virtual hosts of the short names of a route table, built from the table's own virtual hosts (see the comment at the top of the file). vhosts is not modified.

For every mesh authority "<svc>.<ns>.<meshDomain><suffix>" some virtual host claims, the short domain "<svc><suffix>" gets one virtual host whose matcher has an arm per namespace. The arm of a namespace is the route list of the virtual host that the FULL name of that namespace would be matched by for the same suffix: the one that claims it exactly, or, for a suffix with a port, the one that claims "<full name>:*". A namespace that has neither has no arm, and a request from it is refused like one from a namespace that has no such service.

known lists the mesh names ("<svc>.<ns>.<meshDomain>") the table answers for, whether or not a virtual host claims them. One that NO virtual host claims (a service the node knows by name only) is answered, under its full name, by the table's floor; floor says how, and its short name gets an arm that does the same. Without it the caller's namespace HAS the service and its short name was refused "name-not-in-namespace" as soon as another namespace's service of the same name had a virtual host, and fell to the catch-all, which no short authority matches, otherwise.

A virtual host's own typed_per_filter_config (a service-wide HTTP filter is enabled there) goes onto every route of its arm: inside a virtual host shared by several namespaces, the route is the only place left that belongs to one service. When a route names the SAME filter (an HTTPRoute rule's ExtensionRef of the same kind), the proxy had two levels under the service's own virtual host and has one place here, so the route carries what the two levels amount to for that filter (extensionfilter.CombineLevels): the route's alone for a filter whose most specific level wins, both for one that applies or merges every level. Keeping only the route's entry ran the request without the service-wide filter.

A route that names its cluster by the request's authority (the on-demand route of a service the node holds no cluster of) cannot be copied as it is: under a short name the authority is "echo:8080", which is no cluster's name and which the agent does not answer, so the request waited out the on-demand timeout. In an arm it names the cluster the FULL name spells, "<svc>.<ns>.<meshDomain>:<port>", when the short domain has a port; a wildcard domain has none to spell, and the request is refused at once (RefusalShortNameCold).

notMesh are the routes of the table's floor: what a Host that is no mesh name gets in this table. A caller whose namespace has no mesh service of the short name gets exactly those (see shortNameVirtualHost).

func SortLocalityLbEndpoints

func SortLocalityLbEndpoints(endpoints []*endpointv3.LocalityLbEndpoints)

SortLocalityLbEndpoints orders a load assignment's endpoints by their first endpoint's address. Endpoint order is part of the EDS resource's bytes, which the delta-xDS cache hashes to decide whether the resource changed — callers that rebuild assignments from maps (or from registry listings with unstable order) must sort so an unchanged endpoint set never hashes as changed.

func SortSubsetKeys

func SortSubsetKeys(keys map[string]struct{}) []string

SortSubsetKeys sorts and dedupes a subset key set. Selector and rule order is part of the CDS/ECDS resource bytes, and the delta-xDS cache decides "changed" by hashing those bytes — unsorted map-iteration order would make every snapshot hash as a full config change (see the transport-socket-match ordering incident).

func SourceIdentityForPod

func SourceIdentityForPod(cniPod *cniv1.CNIPod, trustDomain string) string

SourceIdentityForPod returns the SPIFFE ID that the pod's mesh-originating listener chains stamp into filter state (SourceIdentityCertMapperFilterStateKey, networkfilter.go) so the cluster's certificate mapper can select the pod's client certificate by identity rather than by netns path (issues #815, #842).

An unknown trust domain yields "" (SpiffeIDFromPod), and the chains then carry only the netns key — byte-for-byte the pre-#815 shape — picking the identity up on the next rebuild once the trust domain is known. The trust domain is late-bound (agent/internal/identity), so this is a real, if brief, startup state.

The cluster's certificate mapper READS this key, so a chain built in that window falls back to the mapper's default_value and its egress presents the NODE identity until the next rebuild. That is a degradation, not a failure: the node SVID is a real attested identity in the same trust domain, so mTLS still completes — unlike the inbound SERVER certificate, where an empty trust domain yields a name the agent never serves and the listener comes up with no certificate at all (the node C outage).

func SourceMetadataHTTPFilter

func SourceMetadataHTTPFilter(cniPod *cniv1.CNIPod, trustDomain string) *http_connection_managerv3.HttpFilter

SourceMetadataHTTPFilter builds a per-pod set_metadata entry writing the pod's identity (SPIFFE ID, namespace, ServiceAccount, pod name) into the aether.source namespace. Emitted on the pod's egress (capture + outbound) chains when the chain carries ext_authz, so egress policy can discriminate WHICH workload is calling — the node proxy is shared, but the listeners (and thus these entries) are per-pod.

func SourceSAKeyFromSpiffeID

func SourceSAKeyFromSpiffeID(spiffeID string) string

SourceSAKeyFromSpiffeID reduces a workload SPIFFE ID (spiffe://<td>/ns/<ns>/sa/<sa>) to the "<ns>/<sa>" key QUICClusterName takes; "" for anything else (a node identity, an edge identity, garbage).

func SpiffeIDFromPod

func SpiffeIDFromPod(cniPod *cniv1.CNIPod, trustDomain string) string

SpiffeIDFromPod returns the mesh SPIFFE ID of a local pod, derived — and only ever derived — from the mesh trust domain and the pod's own namespace and ServiceAccount, following the SPIRE convention spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>. It matches the identity SPIRE issues for the pod's k8s selectors, so the SDS secret it names is the one the SPIRE bridge serves for that pod.

This value is trusted input to the data plane: it names the SDS secret the pod's inbound listener presents, the client certificate the pod's egress presents (the per-netns transport-socket matcher), and the identity stamped into the aether.source authz metadata. It is therefore derived from API-server facts only.

It used to honour the aether.io/spiffe-id pod annotation as an override (#669). Annotations reach the agent verbatim over the CNI ADD path, so any principal able to create a pod could choose the identity that pod's proxy config presents — and because the bridge's SDS secrets are node-wide, a pod naming a co-located workload's SPIFFE ID had that workload's SVID presented on its behalf. The override is gone; SpiffeIDOverrideAnnotation reports the annotation so its presence can be logged and counted.

It returns "" — never "spiffe:///ns/…" — when the trust domain is not known yet. See ErrNoTrustDomain: every builder that needs a real identity refuses on the empty string, so the malformed form is unrepresentable rather than merely discouraged.

func SpiffeIDOverrideAnnotation

func SpiffeIDOverrideAnnotation(cniPod *cniv1.CNIPod) (string, bool)

SpiffeIDOverrideAnnotation returns the pod's aether.io/spiffe-id annotation value and whether a non-empty one is present. The value is never honoured (see SpiffeIDFromPod); callers use it to surface the rejected override — WARN log plus a counter — so an attempted identity override is never silent.

func TCPBackendClusterNamer

func TCPBackendClusterNamer(meshDomain string) l4project.ClusterNameFunc

TCPBackendClusterNamer is the cluster namer of a TCPRoute or TLSRoute backend, for common/l4project.Backends: the one copy for the node agent's L4 reconciler and the edge gateway reconciler, so the two cannot name the same backendRef differently.

Port-qualified since proposal 037 Phase 3: a backendRef naming a specific raw-TCP port resolves to that port's own cluster. A ref with no port yields 0 and the service's default floor cluster -- what every route written before Phase 3 gets.

The cache publishes tcp:<fqdn>:<port> for EVERY TCP port including the primary (an alias sharing the floor's EDS), so a port-qualified name always resolves; naming a port the service does not serve as TCP is what does not, and that is caught by the chain-to-CDS gate rather than silently routed somewhere else.

func TCPClusterName

func TCPClusterName(serviceName, meshDomain string) string

TCPClusterName returns the data-plane name of a service's TCP-floor cluster: "tcp:<svc>.<ns>.<meshDomain>". This cluster is identical to the HTTP cluster (<svc>.<ns>.<meshDomain>) except it uses ALPN "aether-tcp" on its upstream transport socket so the destination inbound demuxes to the TCP floor chain. The capture TCP floor filter chains reference this cluster by this name. Returns "" when the service key is not namespace-qualified.

func TCPPortClusterName

func TCPPortClusterName(tcpClusterName string, port uint32) string

TCPPortClusterName returns the cluster name for ONE non-primary raw-TCP port of a service: "tcp:<fqdn>:<port>" (proposal 037).

It takes the service's ALREADY-QUALIFIED floor cluster name (what TCPClusterName returns, and what CaptureTCPService.ClusterName carries) rather than the bare service, so the two cannot disagree about the mesh domain.

Distinct from PortClusterName, which names the HTTP per-port cluster "<fqdn>:<port>". A port is one or the other, so the two never collide -- and the "tcp:" prefix is what keeps a per-port TCP cluster out of the h2 cluster namespace even when the numbers match.

func TCPPortStatKey

func TCPPortStatKey(serviceKey string, port uint32) string

TCPPortStatKey is the alt_stat_name of a service's port-qualified TCP cluster (per-port, or the primary-port alias). Port 0 is the floor, mirroring TCPPortClusterName.

func TCPStatKey

func TCPStatKey(serviceKey string) string

TCPStatKey is the alt_stat_name of a service's TCP floor cluster.

func UDPCaptureArmsKey

func UDPCaptureArmsKey(arms []UDPCaptureArm) string

UDPCaptureArmsKey canonicalises an arm set for change detection: the same arms in any order give the same key, and any change to a VIP, a cluster or the membership gives a different one. (#873's one-string compare could only see the first service move; a second service or a late VIP was invisible.)

func UDPClusterName

func UDPClusterName(serviceName, meshDomain string) string

UDPClusterName returns the data-plane name of a service's UDP-floor cluster: "udp:<svc>.<ns>.<meshDomain>". This cluster carries the same backend endpoints as the HTTP cluster but is a plain EDS cluster with no transport socket (UDP datagrams are not wrapped in mTLS — mesh mTLS is a TCP/TLS construct and DTLS is not supported). The udp_proxy capture listener references this cluster. Returns "" when the service key is not namespace-qualified.

func UDPLoadAssignment

func UDPLoadAssignment(src *endpointv3.ClusterLoadAssignment, clusterName string, port uint32) *endpointv3.ClusterLoadAssignment

UDPLoadAssignment derives an inline UDP load assignment from a service's existing (TCP-inbound, :18008) load assignment: it clones it and rewrites every endpoint to the backend's application UDP port and UDP protocol. The UDP floor has no inbound mTLS proxy, so udp_proxy reaches the application port directly. Returns nil if src is nil.

func UDPStatKey

func UDPStatKey(serviceKey string) string

UDPStatKey is the alt_stat_name of a service's UDP floor cluster.

func UnsupportedUDPRouteShapes

func UnsupportedUDPRouteShapes(udpRoutes map[string][]L4Backend, clusterIPs map[string]string) []string

UnsupportedUDPRouteShapes reports the UDPRoute inputs that GenerateUDPCaptureListener discards, as human-readable reasons. Empty means everything in udpRoutes is faithfully represented.

It exists because the discard is otherwise invisible from both ends (#873): the UDPRoute is accepted by the API, common/l4project resolves and weights its backends correctly, and then the UDP path keeps ONE cluster per service. There is no NACK and no stat, so the first symptom is datagrams arriving somewhere unintended.

It reads the SAME selection the generator uses -- one call to selectUDPCaptureArms, not a second copy of the rules -- so the detector and the generator cannot drift (#874).

This is a pure function by design: the proxy package builds config and does not log. The caller decides what to do with the reasons.

func UpstreamTransportSocket

func UpstreamTransportSocket(tlsCertificateSecretName string, validationContextName string, sanURIs []string, sni string) *corev3.TransportSocket

UpstreamTransportSocket creates a TLS transport socket for upstream (outbound) connections. It uses mTLS with both a client certificate and validation context fetched via ADS-served SDS. sanURIs, when non-empty, pins the SERVER identity: the presented SVID's URI SAN must exactly match one of the expected per-service SPIFFE IDs (spiffe://<td>/ns/<ns>/sa/<service>), layered over the SDS-rotated trust bundle via a combined validation context. Without it the handshake only proves trust-domain membership — any mesh workload — which leaves registry poisoning able to impersonate a service (an attacker- registered endpoint would present a valid but WRONG identity).

func UpstreamsFromPod

func UpstreamsFromPod(cniPod *cniv1.CNIPod) []string

UpstreamsFromPod returns the upstream services the pod declares it consumes, parsed from the config.aether.io/upstreams annotation (comma-separated service names; surrounding whitespace is trimmed, empty items are dropped). Returns nil when the annotation is absent or empty — such a pod relies entirely on the on-demand cold path for its upstreams.

Each value is normalized to a namespace-qualified "<ns>/<svc>" serviceref key (proposal 020 Part 1) so it matches the registry / dependency-set keys (cache.setPodDependencies keys the pod's own service via serviceref too) — otherwise a bare upstream name like "echo" never matches the registry's "<ns>/echo" key, so the demand-scoped cluster (and its cap_http route) never resolves:

  • A bare name (no "/") is qualified with the POD's own namespace, so the common same-namespace case keeps the ergonomic bare-name annotation.
  • An already-qualified "<ns>/<svc>" value (containing "/") passes through unchanged, for cross-namespace upstreams.

Trimming, empty-item dropping, and dedup operate on the normalized keys so a bare name and its explicit "<pod-ns>/<name>" form collapse to one entry.

func ValidSubsetKey

func ValidSubsetKey(key string) bool

ValidSubsetKey reports whether a provider-defined metadata key may be used as a subset routing dimension (well-formed and not reserved).

func ValidationContextName

func ValidationContextName(trustDomain string) string

ValidationContextName is the SDS name of the trust bundle for a trust domain ("spiffe://<trust-domain>"), or "" when the trust domain is unknown — never the bare "spiffe://" that formatting an empty domain would yield.

func WaypointIngressClusterName

func WaypointIngressClusterName(fqdn string) string

WaypointIngressClusterName is the STATIC cluster of a service's LOCAL pods (at the mesh inbound port) that the node tunnel forwards a cross-cluster connection to. Keyed by the service FQDN, so it is unique per service on the node.

func WaypointTransportSocketMatcher

func WaypointTransportSocketMatcher() *matcherv3.Matcher

WaypointTransportSocketMatcher branches on the CHOSEN endpoint's envoy.lb "waypoint" metadata (proposal 019 Design A): a waypoint-tagged (cross-cluster) endpoint selects the waypoint socket's structured SNI, every other endpoint the local socket's port SNI.

Before #842 this was a two-LEVEL matcher — waypoint metadata, then source identity — because the second level chose the certificate. The certificate selector does that now, so only the first level survives and the matcher is a constant.

func WaypointTransportSocketMatches

func WaypointTransportSocketMatches(nodeSpiffeID, validationContextName string, sanURIs []string, sni, waypointSNI string) []*clusterv3.Cluster_TransportSocketMatch

WaypointTransportSocketMatches returns the two transport sockets a waypoint-enabled mesh cluster carries: the LOCAL one (port SNI) and the WAYPOINT one (structured SNI). Both resolve the client certificate through the same per-connection certificate selector, so they differ in exactly one field — the SNI — which is the whole reason two sockets are still needed.

The list is a fixed two entries whatever the node hosts, so it contributes nothing to cluster churn.

func WithoutSourceMetadata

func WithoutSourceMetadata(filters []*http_connection_managerv3.HttpFilter) []*http_connection_managerv3.HttpFilter

WithoutSourceMetadata returns the union minus any aether.source set_metadata entries (the inbound chains take this form).

func WorkloadSANPrefix

func WorkloadSANPrefix(trustDomain string) string

WorkloadSANPrefix is the URI-SAN prefix every mesh WORKLOAD SVID shares — "spiffe://<trust-domain>/ns/" — or "" when the trust domain is unknown.

It is the client-side pin the inbound listener applies (issue #843). The destination cannot know WHICH ServiceAccount may call it — in a mesh any workload legitimately may — but it can require that the peer certificate is STRUCTURALLY a workload SVID: a SPIFFE ID in this trust domain whose path is /ns/<ns>/sa/<sa>. That rejects anything issued under a different path (SPIRE's own /spire/agent/… node identities, or any non-workload infrastructure identity the bundle happens to sign) and is what makes the XFCC value the inbound HCM stamps SANITIZE_SET structurally trustworthy — the property every XFCC-derived RBAC/ext_authz rule already assumes.

It returns "" rather than "spiffe:///ns/" on an empty trust domain, the same refusal ValidationContextName and SpiffeIDFromPod make. A prefix built from an empty trust domain matches NO real certificate, so it would fail every inbound handshake — the #815/#819 failure mode with the blast radius of a whole node. Callers emit NO matcher on "" (see DownstreamTransportSocket).

The trust domain is the LOCAL one, which makes this pin exactly as restrictive as the upstream pin the mesh already ships (cache/mtls.go renders spiffe://<local-td>/ns/<ns>/sa/<svc> exact matchers). Cross-trust-domain traffic is therefore already impossible in the client direction; proposal 019 states a shared trust domain as a precondition and defers SPIRE federation. If federation is ever enabled, BOTH pins have to be widened together — neither one alone is the gate.

Types

type AccessLogConfig

type AccessLogConfig struct {
	// Enabled turns the OTel access logger on for every HCM.
	Enabled bool
	// CollectorCluster is the OTLP gRPC cluster the logs are pushed to (the proxy
	// bootstrap's "otel_collector"). Empty defaults to defaultCollectorName.
	CollectorCluster string
	// SuccessSampleRate is the percent (0-100) of *successful* requests logged.
	// Failures (any response flag, or status >= 500) are always logged regardless.
	SuccessSampleRate uint32
}

AccessLogConfig is the global access-log configuration. It is set ONCE at agent startup (SetAccessLogConfig) before the snapshot cache builds any listener, then read without locking — the same set-once pattern as SnapshotCache.emitStatsPod.

type AppAddress

type AppAddress struct {
	// Netns is the pod's network-namespace path. The upstream connection is
	// bound into it so 127.0.0.1 reaches the application container and not the
	// agent. Unused when Pipe is set.
	Netns string
	// Pipe, when non-empty, is the socket's HOST path (resolved from the
	// endpoint.aether.io/uds-socket annotation or an EndpointPolicy onto the
	// pod's csi.aether.io tmpfs, <uds-csi-root>/<pod-UID>/<file>,
	// common/udspath). Pathname sockets are mount-namespace-scoped,
	// so the proxy dials it from its own mount namespace; no netns bind exists
	// or is needed. Every declared port of a UDS pod dials this one socket.
	Pipe string
}

AppAddress locates a pod's application from the node proxy. It is one of two shapes: the default TCP loopback inside the pod's network namespace (Netns set, Pipe empty), or a Unix domain socket the app serves on (Pipe set, proposal 034 Phase 1).

type CaptureTCPService

type CaptureTCPService struct {
	// ClusterName is the EDS cluster the tcp_proxy forwards to
	// (<service>.<meshDomain>, same as the HTTP route target).
	ClusterName string
	// ClusterIP is the k8s Service ClusterIP matched by the filter chain's
	// prefix_ranges (/32 host match).
	ClusterIP string
	// TCPRouteRules are the L4ServiceRoutes from a TCPRoute parentRef=Service
	// (proposal 018, Phase 3b). When non-empty, the passthrough floor chain is
	// replaced with a weighted_clusters tcp_proxy routing to these backends.
	// Empty = passthrough (Phase 3a behaviour).
	TCPRouteRules []L4ServiceRoute
	// TLSRouteRules are the L4ServiceRoutes from a TLSRoute parentRef=Service.
	// When non-empty, per-SNI filter chains (server_names match) are inserted
	// BEFORE the per-ClusterIP TCP floor chain on the capture listener.
	TLSRouteRules []L4ServiceRoute
	// HTTPPorts are the port numbers the service declares as HTTP. No chain is
	// built from them; on these ports (and on the proxy port, when there is
	// one) TLS to the service's address is not matched against its TLSRoutes,
	// so that it leaves through the passthrough as cleartext HTTP's sibling
	// (captureAddressMatcher).
	HTTPPorts []uint32
	// TCPPorts are the service's raw-TCP application ports other than its
	// legacy target, derived from its endpoints' port_protocols (proposals 037
	// and 043). Each gets its own destination_port-qualified chain to its own
	// cluster, tcp:<fqdn>:<p>, so a client dialing <VIP>:<p> reaches that port.
	TCPPorts []uint32
	// LegacyTCPPort is the service's legacy target (the port its pods declare
	// in endpoint.aether.io/port) when that port is raw TCP, and zero
	// otherwise. It is one more declared raw-TCP port: it gets a chain on its
	// own number, and on the TCP mesh port (:18082), which is the legacy
	// spelling of it (proposal 043, "The legacy spellings during Phases 1 and
	// 2"). Both forward to the floor cluster, tcp:<fqdn>.
	//
	// Those two are the only ports a raw-TCP legacy target is reached on.
	// There is no port-less "any port of the address" chain any more (it was
	// cap_tcp_anyport_<cluster>, removed in chart 3.0.0): a connection to a
	// port the service does not declare is not the service's.
	//
	// No service-level protocol is read to set it: the cache derives it from
	// the registry endpoints, never from the mesh Service's annotation.
	LegacyTCPPort uint32
}

CaptureTCPService describes a non-HTTP mesh service that requires a per-ClusterIP TCP-proxy floor chain on the transparent-capture listener. When the capture listener sees outbound traffic whose original-dst ClusterIP matches one of these services, it routes via tcp_proxy over per-source mTLS to the service's EDS cluster — preserving SPIFFE SAN validation and registry health without demux through the HCM.

type EdgeL4TCPRoute

type EdgeL4TCPRoute struct {
	// Port is the Gateway listener port (TCP protocol) this route is bound to.
	Port uint32
	// Backends is the flat weighted backend list from all attached TCPRoute rules.
	Backends []L4Backend
}

EdgeL4TCPRoute describes one edge TCP listener: a Gateway TCP listener on port with all backends consolidated from the attached TCPRoute rules.

type EdgeL4TLSRoute

type EdgeL4TLSRoute struct {
	// Port is the Gateway listener port (TLS protocol, Passthrough mode).
	Port uint32
	// Rules are the ordered per-SNI rules from all attached TLSRoutes; each
	// rule's SNIHostnames become a filter_chain_match.server_names set.
	Rules []L4ServiceRoute
}

EdgeL4TLSRoute describes one edge TLS passthrough listener: a Gateway TLS listener (mode: Passthrough) on port with per-SNI route rules.

type ExtensionFilter

type ExtensionFilter struct {
	Name   string
	Config *anypb.Any
}

ExtensionFilter is one proxy-extension escape-hatch filter (proposal 025): a named Envoy HTTP filter configured opaquely via an HTTPFilter CRD. Name is the Envoy filter name (allow-listed — the proxy build must compile it in); Config is the opaque per-route typed_config (validated by the admission webhook, M2). aether adds the filter to the HCM chain default-disabled; the route's typed_per_filter_config (Config) re-enables + configures it for that route.

type GammaBackend

type GammaBackend struct {
	Service string
	Cluster string
	Weight  uint32
	Port    uint32
	// Invalid marks a backendRef that names no HTTP port of its Service (the
	// Service has no such port, or the port is not HTTP). Its share of the
	// rule's traffic is answered 500, as the Gateway API specifies for an
	// invalid backendRef; it is never delivered to another port. Set by the
	// snapshot cache when it resolves the backend's port, not by a projector.
	Invalid bool
}

GammaBackend is one weighted backend of a route: the resolved data-plane Cluster the route forwards to, plus the bare Service name (for the dependency set).

Port is the backendRef's port, a port of the backend SERVICE, or 0 when the producer of the rule did not carry one (a rule imported from a peer cluster, proposal 026). The cache resolves it through the backend's port map to the cluster of that pod port before the route is built; Cluster as projected is the backend's default cluster and is what a backend with no usable port keeps.

type GammaHeaderKV

type GammaHeaderKV struct {
	Name  string
	Value string
}

GammaHeaderKV is one header name+value pair for a header mutation action.

type GammaHeaderMatch

type GammaHeaderMatch struct {
	Name  string
	Value string
}

GammaHeaderMatch is an exact request-header match.

type GammaHeaderMutation

type GammaHeaderMutation struct {
	// SetRequest are key/value pairs to set (overwrite) on the request.
	SetRequest []GammaHeaderKV
	// AddRequest are key/value pairs to append to the request.
	AddRequest []GammaHeaderKV
	// RemoveRequest are header names to strip from the request.
	RemoveRequest []string
	// SetResponse are key/value pairs to set (overwrite) on the response.
	SetResponse []GammaHeaderKV
	// AddResponse are key/value pairs to append to the response.
	AddResponse []GammaHeaderKV
	// RemoveResponse are header names to strip from the response.
	RemoveResponse []string
}

GammaHeaderMutation describes the header modifications carried by one RequestHeaderModifier or ResponseHeaderModifier filter on a route rule. Multiple filters merge (later set/add entries win for set, remove is additive).

type GammaMatch

type GammaMatch struct {
	Prefix string
	Exact  string
	// Regex is an RE2 safe_regex path match, used for gRPC RegularExpression method
	// matches: the reconciler builds a /<serviceRegex>/<methodRegex> pattern and
	// stores it here. Empty means no regex path match.
	Regex   string
	Headers []GammaHeaderMatch
}

GammaMatch is one HTTPRoute match: a path (prefix OR exact OR regex; empty = prefix "/") and zero or more exact header matches (ANDed). Exactly one of Prefix, Exact, or Regex should be set; Exact takes precedence over Prefix, and Regex is only used when neither Prefix nor Exact is set.

type GammaRedirect

type GammaRedirect struct {
	// Scheme is the target scheme ("http" or "https"). Empty = unchanged.
	Scheme string
	// Hostname is the target hostname. Empty = unchanged.
	Hostname string
	// Port is the target port (0 = unchanged / preserve original).
	Port uint32
	// StatusCode is the HTTP redirect code (301 or 302; default 301).
	StatusCode int
	// PathType selects how to rewrite the path (empty = no path rewrite).
	// "ReplaceFullPath" → Envoy PathRedirect; "ReplacePrefixMatch" → PrefixRewrite.
	PathType  string
	PathValue string
	// ListenerPort is the external port of the Gateway listener serving this route
	// (e.g. 8080 when the Gateway declares port: 8080 and the LB Service maps
	// 8080 → some internal port). It is set by the per-Gateway route builder and
	// ONLY used when Port == 0 and Scheme == "" (i.e. the redirect preserves the
	// original port). Without this, Envoy's RedirectAction with port_redirect=0
	// emits the listener's bound (internal) port in the Location header — not the
	// external port the client connected to. For default HTTP ports (80, 443) this
	// field is left 0 so the port is omitted from Location (correct for default
	// ports). For non-default ports it is set to ExternalPort so Envoy encodes the
	// correct port in Location.
	ListenerPort uint32
}

GammaRedirect describes a RequestRedirect filter: it replaces the route action with an Envoy RedirectAction instead of forwarding to a backend cluster.

type GammaRoute

type GammaRoute struct {
	// DroppedBackends reports that the rule had backendRefs the projection
	// dropped: another kind than a core Service, or a Service in another
	// namespace that no ReferenceGrant permits. They are invalid backendRefs
	// and the Gateway API gives them the same answer as any other, 500 for
	// their share of the rule's weight (DroppedBackendWeight; weight 0 takes
	// nothing). Until chart 3.0.0 they were dropped silently: their share went
	// to the rule's other backends, and a rule left with none routed to its
	// parent Service. Set by the GAMMA reconciler, not by a projector.
	DroppedBackends      bool
	DroppedBackendWeight uint32
	// ParentPort is the Service port the rule's parentRef names, or 0 when the
	// parentRef names none (proposal 043). A rule without a parent port
	// applies on every HTTP port of the parent Service; with one, on that
	// port only. It is the PARENT SERVICE's port: for a Service the registrar
	// generated that is the pod's port, for a user Service it is resolved
	// through the Service's port map before the rule is placed.
	ParentPort uint32
	// Imported marks a rule that came from a peer cluster's projection
	// (proposal 026) and not from a route of this cluster. Such a rule carries
	// no parent port and no backend ports (the projection has no field for
	// them yet), so it is kept on the virtual hosts of the service's legacy
	// target and never spread over the service's other ports. Set by the
	// cache when it takes the imported rules in.
	Imported       bool
	Matches        []GammaMatch
	Backends       []GammaBackend
	Timeout        *durationpb.Duration
	HeaderMutation *GammaHeaderMutation
	// Redirect, when non-nil, makes this route return a redirect response instead
	// of forwarding to a backend. Coexists with Matches; takes precedence over
	// Backends per the Gateway API spec.
	Redirect *GammaRedirect
	// URLRewrite, when non-nil, rewrites the request URL (host and/or path) before
	// forwarding. Applied on top of the normal RouteAction (Backends still used).
	URLRewrite *GammaURLRewrite
	// ExtensionFilters are proxy-extension escape-hatch filters (proposal 025)
	// attached to this route via an HTTPRoute/GRPCRoute `ExtensionRef` → HTTPFilter
	// CRD. Each is emitted into the route's typed_per_filter_config (enabling +
	// configuring an allow-listed Envoy HTTP filter that aether adds to the HCM chain
	// default-disabled). Empty for the common case.
	ExtensionFilters []ExtensionFilter
}

GAMMA east-west L7 routing (proposal 018, Phase 2). A GammaRoute is one resolved HTTPRoute rule: ordered matches forwarding to one or more weighted backend clusters, with an optional timeout, header mutations, redirect, and URL rewrite. Backends are already resolved to data-plane cluster names (<backend>.<meshDomain>) by the reconciler.

func FromConfigProjection

func FromConfigProjection(p *registryv1.ServiceConfigProjection) []GammaRoute

FromConfigProjection converts an imported projection back to in-memory GAMMA rules the consumer feeds into SetServiceRoutes (it then builds vhosts exactly as the local path does). nil-safe.

func GammaRouteFromProto

func GammaRouteFromProto(r *registryv1.GammaRoute) GammaRoute

GammaRouteFromProto converts a registryv1.GammaRoute proto (produced by the shared gammaproject projector, locally or imported from a peer) into the agent's in-memory GammaRoute the cache consumes.

type GammaURLRewrite

type GammaURLRewrite struct {
	// Hostname is the Host header to rewrite to. Empty = unchanged.
	Hostname string
	// PathType selects how to rewrite the path (empty = no path rewrite).
	// "ReplaceFullPath" → Envoy RegexRewrite (.*) → value.
	// "ReplacePrefixMatch" → Envoy PrefixRewrite.
	PathType  string
	PathValue string
}

GammaURLRewrite describes a URLRewrite filter: rewrites the request host and/or path before forwarding to the backend. Applied within the RouteAction.

type GeoipConfig

type GeoipConfig struct {
	// CityDBPath is the MaxMind city-type mmdb (serves country + city).
	CityDBPath string
	// Headers selects which x-geo-* headers to emit ("country", "city").
	Headers []string
	// XffNumTrustedHops mirrors the HCM value (edge.xffNumTrustedHops): when >0 the
	// client address is derived from X-Forwarded-For.
	XffNumTrustedHops uint32
}

GeoipConfig is the edge geoip configuration (proposal 028).

type HealthGatewayProbe

type HealthGatewayProbe struct {
	AppCluster          string
	InboundReadyCluster string
}

HealthGatewayProbe is one pod's entry on the health gateway.

AppCluster is the pod's application health-probe cluster (health_<namespace>_<pod>) and InboundReadyCluster its inbound-readiness probe (inboundready_<namespace>_<pod>, issue #815), or "" when that probe is not programmed for this pod.

EACH CLUSTER GETS ITS OWN PATH — /healthz/<cluster-name> — reflecting THAT cluster alone. #819 instead ANDed both clusters behind the single /healthz/health_<namespace>_<pod> path, which cost node C four endpoints on 2026-09-19: the agent could see only the conjunction, so "the app is fine but the mesh inbound never came up" was indistinguishable from "the app died", and the liveness loop demoted the endpoints permanently with no signal. Two paths let the loop weigh the two facts differently, which is the whole of the can't-tell rule in agent/internal/cni/server/liveness.go.

func NewHealthGatewayProbe

func NewHealthGatewayProbe(appProbeCluster, inboundReadyCluster string) HealthGatewayProbe

NewHealthGatewayProbe builds a probe entry for one pod. inboundReadyCluster may be "" (pod not gated).

type L4Backend

type L4Backend = l4project.Backend

L4Backend is one weighted backend of an L4 route: the namespace-qualified "<ns>/<svc>" key, the resolved data-plane cluster name, and the weight.

It is an alias for the shared projection type so the node agent's L4 reconciler and the edge gateway reconciler can both feed this layer from one projector (common/l4project.Backends). An explicit weight 0 means DRAIN and is omitted from the weighted-cluster set here rather than normalised to 1 (#492).

type L4ServiceRoute

type L4ServiceRoute struct {
	// SNIHostnames are the TLS SNI names matched by this rule (TLSRoute only).
	// Empty means this is a TCPRoute rule (matches all connections to the VIP).
	SNIHostnames []string
	// Backends are the weighted backend clusters this rule forwards to. All
	// backends must already be resolved to "tcp:<svc>.<meshDomain>" cluster names.
	Backends []L4Backend
}

L4ServiceRoute holds the L4 routing rules for one mesh service derived from a TCPRoute or TLSRoute parentRef=Service. It is the L4 parallel to GammaRoute.

For TCPRoute: one or more rules with weighted backends; no SNI match. For TLSRoute: one or more rules, each associated with a set of SNI hostnames; a connection's SNI must match one of the rule's hostnames to be routed.

type RouteHeaderMatch

type RouteHeaderMatch struct {
	Name  string
	Value string
	Regex bool
}

RouteHeaderMatch describes one HTTP header predicate for an HTTPRoute match. When Regex is false the header value is matched exactly (case-sensitive per HTTP/1.1 header value semantics); when true it is a RE2 SafeRegex pattern.

type RouteQueryParamMatch

type RouteQueryParamMatch struct {
	Name  string
	Value string
	Regex bool
}

RouteQueryParamMatch describes one query-parameter predicate for an HTTPRoute match. When Regex is false the parameter value is matched exactly; when true it is a RE2 SafeRegex pattern.

type ShortNameFloor

type ShortNameFloor func(fqdn, suffix string) []*routev3.Route

ShortNameFloor says what a table's floor (its catch-all) does with the FULL name fqdn followed by suffix ("", ":<port>" or ":*"), as routes, for a mesh name no virtual host of the table claims. Nil routes mean no arm: the short name is then refused for that caller like a name its namespace does not have.

type ShortNames

type ShortNames struct {
	MeshDomain string
	// MeshOnly are the mesh names this table answers for under their mesh
	// name alone: their short name is not the table's (shortNameVirtualHosts).
	MeshOnly []string
}

ShortNames is what a capture table's builder needs to add its short names.

type TracingConfig

type TracingConfig struct {
	// Enabled adds an OpenTelemetry tracer to every HCM so the proxy generates and
	// propagates W3C trace context (traceparent) and exports spans.
	Enabled bool
	// CollectorCluster is the OTLP gRPC cluster spans are exported to (the proxy
	// bootstrap's "otel_collector"). Empty defaults to defaultCollectorName.
	CollectorCluster string
	// SampleRate is the fraction (0.0-1.0) of requests traced (Envoy random
	// sampling). At high data-plane QPS keep this low.
	SampleRate float64
}

TracingConfig is the global proxy data-plane tracing configuration. Set ONCE at agent startup (SetTracingConfig) before the snapshot cache builds any listener, then read without locking — same set-once pattern as AccessLogConfig.

type UDPCaptureArm

type UDPCaptureArm struct {
	// Service is the "<ns>/<svc>" UDPRoute parent.
	Service string
	// ClusterIP is the parent's mesh-Service VIP, the matcher key.
	ClusterIP string
	// Cluster is the bound backend's udp: cluster.
	Cluster string
}

UDPCaptureArm is one matcher arm of a pod's UDP capture listener: datagrams the pod addressed to ClusterIP (the UDPRoute parent Service's VIP) go to Cluster (the heaviest live backend of that route).

func UDPCaptureArms

func UDPCaptureArms(udpRoutes map[string][]L4Backend, clusterIPs map[string]string) []UDPCaptureArm

UDPCaptureArms returns the arms GenerateUDPCaptureListener would build for these routes, sorted by service; empty when it would build no listener.

It exists so a caller can tell whether a rebuild would change anything WITHOUT building the listener (and without re-emitting the warnings that come with it). The arm set is node-global, so one answer serves every pod.

type WaypointRewrite

type WaypointRewrite struct {
	Enabled      bool
	TunnelPort   uint32
	LocalCluster string
}

WaypointRewrite controls the split-horizon EDS rewrite (proposal 019). When Enabled, an endpoint belonging to a cluster other than LocalCluster is dialed at its node's routable IP + TunnelPort (the per-node east/west waypoint) instead of its (cross-cluster, possibly non-routable) pod IP, and is tagged with waypoint metadata so the cluster's transport-socket matcher can present the structured SNI. When disabled (the default) every endpoint is dialed at pod_ip:18008 — the flat pod-to-pod path (proposal 018 flat-network mode), unchanged. LocalCluster is this agent's own cluster name.

type WeightedRouteBackend

type WeightedRouteBackend struct {
	Cluster string
	Weight  uint32
}

WeightedRouteBackend is one entry in a weighted_clusters route action. Cluster is the resolved Envoy cluster name; Weight is the Gateway API backendRef weight (default 1; 0 = receives no traffic but is a valid backend per spec).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL