istio/istio
xDS server
Active contributors: howardjohn, hzxuzhonghu, costinm, ramaraochavali, stevenctl
Purpose
The xDS server is the gRPC endpoint that streams configuration to Envoy sidecars, gateways, ztunnels, and waypoints. It is the largest single subsystem inside istiod and the place where Istio's translation pipeline produces its visible output.
The implementation lives entirely under pilot/pkg/xds/. The architecture document at architecture/networking/pilot.md is the canonical narrative; this page is a code-level companion.
Directory layout
pilot/pkg/xds/
├── discovery.go # DiscoveryServer struct: state, push pipeline, run loop
├── ads.go # ADS bidirectional gRPC handler (state-of-the-world)
├── delta.go # Delta xDS handler
├── pushqueue.go # Per-proxy work queue with coalescing
├── proxy_dependencies.go # ProxyNeedsPush filter
├── eventhandler.go # Adapter from controllers to push triggers
├── monitoring.go # Metrics
├── debug.go # Debug HTTP server
├── debuggen.go # Generator that returns debug-only types
├── auth.go # Stream authentication (caCertChain → SAN)
├── cds.go / eds.go / lds.go / rds.go / sds.go / ecds.go / nds.go / pcds.go
│ # Per-type generators (typically thin wrappers around networking/core)
├── statusgen.go # Generator for proxy status
├── workload.go # Workload API generator (ztunnel)
├── krtxds.go # krt-driven generator dispatch
├── adstest.go / deltaadstest.go / adstest.go
│ # Test helpers (start a fake server, connect a fake client)
├── filters/ # gRPC stream filters
├── endpoints/ # EDS endpoint slice computation
└── v3/ # xDS v3 type-URL constantsKey abstractions
| Symbol | File | Role |
|---|---|---|
DiscoveryServer |
discovery.go |
The owner of the push pipeline; one instance per istiod |
Connection |
ads.go |
Per-stream context: proxy, watched types, ACK state |
model.Proxy |
pilot/pkg/model/context.go |
The xDS client representation (identity, labels, sidecar scope) |
model.PushContext |
pilot/pkg/model/push_context.go |
Immutable snapshot used for one or more pushes |
PushRequest |
pilot/pkg/model/push_context.go |
A single push: configsUpdated, full vs partial, reason |
PushQueue |
pushqueue.go |
Coalescing per-proxy queue |
XdsResourceGenerator |
pilot/pkg/model/xds.go |
The interface every type-URL handler implements |
proxy_dependencies.ProxyNeedsPush |
proxy_dependencies.go |
Skip pushes a proxy doesn't need |
WatchedResource |
pilot/pkg/model/xds.go |
Per-stream subscription state |
How it works
graph TD
sources[Service Registry / ConfigStore / CA] -->|events| EH[EventHandler]
EH --> deb[Debouncer<br/>DebounceAfter / debounceMax]
deb --> pcb[PushContext.Build]
pcb -->|configsUpdated| dispatch[Dispatch]
dispatch --> filt[ProxyNeedsPush]
filt --> queue[PushQueue<br/>coalesce per proxy]
queue --> handler[ADS Push handler]
handler --> gen[Generators]
gen --> cache[XDS cache lookup]
cache --> resp[DiscoveryResponse]
resp --> stream[gRPC stream]Push triggers
Every config or service event eventually calls DiscoveryServer.ConfigUpdate(req *PushRequest). The request carries a set of (GroupVersionKind, Namespace, Name) keys describing what changed. The debouncer collapses bursts (default PILOT_DEBOUNCE_AFTER=100ms, PILOT_DEBOUNCE_MAX=10s) and then rebuilds the PushContext.
Some events bypass the debouncer:
- EDS-only events (when
enableEDSDebounce=falseper type). - Targeted "send to this single proxy" requests (e.g. workload re-registration).
PushContext
PushContext is the input to translation. It is immutable per push: once built, it can be read concurrently lock-free. It contains:
Services— the merged service catalog.ServiceIndex— per-namespace and per-host indexes.AuthnPolicies,AuthzPolicies— security state.Telemetry— telemetry attachments.SidecarIndex— per-namespace sidecar scopes.gatewayIndex— gateway resources.EnvoyFilters— escape-hatch patches.Mesh— the merged MeshConfig.
Building it is the most expensive step in the pipeline. Caching is the single biggest performance lever.
Generators and the XDS cache
A generator is a function that, given (Proxy, PushContext, ConfigsUpdated), returns a list of *discovery.Resource. There is one per top-level type:
CdsGenerator(cds.go) — clusters; delegates tonetworking/core/cluster*.go.EdsGenerator(eds.go) — endpoints.LdsGenerator(lds.go) — listeners; delegates tonetworking/core/listener*.go.RdsGenerator(rds.go) — routes; delegates tonetworking/core/httproute*.go.SdsGenerator(sds.go) — secrets, when istiod serves SDS directly (gateways).NdsGenerator(nds.go) — Istio name table for in-pod DNS.EcdsGenerator(ecds.go) — extension config (Wasm).WorkloadGenerator(workload.go) — Istio workload API (ztunnel).DebugGen(debuggen.go) — debug-only types served at/debug/....StatusGen(statusgen.go) — proxy status reflection.
The xDS cache, located in pilot/pkg/networking/core/cluster_cache.go and pilot/pkg/xds/eds.go, stores already-encoded protobuf.Any payloads keyed by their inputs. The first generation pays the encoding cost; subsequent pushes are cheap. The cache key must include every input the generator reads — incorrect keys have caused multiple CVEs (e.g. CVE-2022-21679, CVE-2024-...) and are why -tags=assert enables a strict-mode that panics on key collisions.
Per-stream lifecycle
sequenceDiagram
participant Envoy
participant ADS as DiscoveryServer
participant Auth as auth.go
participant Gen as Generators
Envoy->>ADS: gRPC StreamAggregatedResources
ADS->>Auth: extract identity (mTLS SAN or JWT)
ADS->>ADS: build model.Proxy
Envoy->>ADS: DiscoveryRequest type=CDS
ADS->>Gen: CdsGenerator.Generate(proxy, push)
Gen-->>ADS: resources, version
ADS-->>Envoy: DiscoveryResponse
Envoy->>ADS: ACK / NACK with response_nonce
Note over ADS: Connection persists; further pushes triggered<br/>by ConfigUpdate or proxy NACK rebuild
Envoy-xADS: graceful shutdown / disconnectADS is implemented in ads.go (state-of-the-world) and delta.go (delta xDS — incremental). Most Envoy clients today negotiate delta if available; legacy clients still use SoTW.
NACK handling
NACKs are recorded in the Connection.NonceAcked / NonceNAcked map and surfaced via /debug/syncz. A NACK does not trigger a re-push by default — istiod assumes the proxy has the same state until the next event. proxy-status uses this to flag stuck proxies.
Authentication
Stream auth is in pilot/pkg/xds/auth.go. Identities come from:
- mTLS SANs — the workload identity in the client cert chain (the production path on port 15012).
- JWT — for cross-trust-domain or legacy plain-text scenarios.
- Kubernetes Service Account — verified by the kube-apiserver TokenReview.
The accepted authenticators are configured per-port. Port 15012 typically allows mTLS or JWT; the legacy 15010 plaintext port allows JWT only.
ProxyDependencies
proxy_dependencies.ProxyNeedsPush is the filter that decides "does this push affect this proxy?". It is conservative: it must not return false for a proxy that does need an update, but it can return true unnecessarily. The function takes the (Proxy, PushRequest) and looks at configsUpdated to make decisions like:
- A namespace-only update for namespace
foodoesn't affect proxies in namespacebarunless the proxy's sidecar scope explicitly importsfoo. - A
ServiceEntrychange in any namespace might affect every sidecar (because SE has cluster-wide visibility by default).
This optimization is the difference between "every change pushes to every proxy" and "every change pushes to the few proxies that care".
Integration points
- Inputs: every controller in istiod calls
ConfigUpdatewhen its data changes — the registry controllers (pilot/pkg/serviceregistry/), the config store (pilot/pkg/config/), the CA (when root cert changes), the trust bundle, etc. - Outputs: gRPC streams to Envoy sidecars, gateways, the ingress/egress gateway, ztunnels (via the workload API), and waypoint Envoys.
- Authentication:
auth.gointegrates withpkg/security/authentication/and the kube TokenReview path. - Metrics: dozens of
pilot_xds_*metrics inmonitoring.go(push time, NACKs, connections, queue depth).
Tunables
The headline runtime knobs (set via env vars; see reference/configuration.md):
| Variable | Default | What it does |
|---|---|---|
PILOT_DEBOUNCE_AFTER |
100ms |
Wait this long after an event before pushing |
PILOT_DEBOUNCE_MAX |
10s |
Maximum debounce time when events keep arriving |
PILOT_PUSH_THROTTLE |
100 |
Max concurrent pushes |
PILOT_ENABLE_EDS_DEBOUNCE |
true |
Debounce EDS-only changes too |
UNSAFE_PILOT_ENABLE_RUNTIME_ASSERTIONS |
false |
Cache key collision panics — turn on in tests |
PILOT_FILTER_GATEWAY_CLUSTER_CONFIG |
false |
Trim gateway cluster config to only what the gateway needs |
PILOT_ENABLE_DELTA_XDS |
true |
Allow Delta xDS streams |
Entry points for modification
- New xDS type → register a
Generatorindiscovery.go's constructor (NewDiscoveryServer) and add a debug endpoint indebug.go. - New cache key dependency → audit every place the cache is written. Add an entry to the key struct and an integration test that mutates the new input.
- New stream-level metadata → most metadata flows through
model.Proxy— extend the proxy build inads.goand propagate through generators. - New authenticator →
pkg/security/authentication/and wire it viaauth.go.
Key source files
| File | Purpose |
|---|---|
pilot/pkg/xds/discovery.go |
The DiscoveryServer struct + push pipeline |
pilot/pkg/xds/ads.go |
SoTW ADS handler |
pilot/pkg/xds/delta.go |
Delta ADS handler |
pilot/pkg/xds/pushqueue.go |
Per-proxy queue |
pilot/pkg/xds/proxy_dependencies.go |
Push-filter |
pilot/pkg/model/push_context.go |
The snapshot type |
pilot/pkg/model/context.go |
The Proxy type |
pilot/pkg/networking/core/cluster_cache.go |
The cache (cluster side; similar files for routes, listeners) |
pilot/pkg/xds/debug.go |
All /debug/* endpoints |
See also
- istiod — the binary this lives inside.
- systems/networking-core — what generators delegate to.
- systems/service-discovery — where ServiceRegistry feeds in from.
architecture/networking/pilot.md— the canonical design doc.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.