A minimal implementation of the Gateway API.
The goal of this project is to create a simple, pure Go implementation of the Gateway API.
- Reference Implementation: This project aims to be a reference implementation rather than a high-performance one. It prioritizes clarity and correctness over speed.
- Full Feature Support: We want to support all Gateway API features.
- Fallback Model: We are exploring a model where this reference implementation can serve as a fallback for specialized implementations for configurations they cannot accelerate. See docs/accelerated-operations.md.
- Pure Go: The implementation should be written in pure Go.
GARI supports serving HTTP/3 over QUIC on HTTPS listeners. Gateway API models HTTPS listeners, and GARI can serve the same hostnames, TLS certificates, and HTTPRoutes over both TCP (HTTP/1.1 and HTTP/2) and QUIC (HTTP/3).
HTTP/3 is opt-in and disabled by default.
-
CLI Flags:
--proxy-http3-bind-address: UDP address to bind for HTTP/3 (e.g.:8443). If empty, HTTP/3 is disabled.--proxy-http3-advertised-port: Port advertised in theAlt-Svcheader on HTTPS (TCP) responses (e.g.443). If0, the port is inferred from the bind address.
-
Programmatic Options (
pkg/gari.Options):ProxyHTTP3Addr: The UDP address string to bind for HTTP/3.ProxyHTTP3AdvertisedPort: The port number advertised inAlt-Svcheaders.HTTP3PacketConn: A customnet.PacketConnto use for the HTTP/3 server (overridingProxyHTTP3Addr).HTTP3QUICConfig: An optional*quic.Configfor fine-grained QUIC tuning (e.g., MTU / initial packet size).
When HTTP/3 is enabled, HTTPS (TCP) responses automatically include an Alt-Svc: h3=":<advertised-port>"; ma=86400 header so compatible clients can discover and upgrade to HTTP/3.
- Slow and Idle Client Bounds:
ReadHeaderTimeout(default: 10s, configurable via--read-header-timeout): Bounds the time allowed to read HTTP request headers and bounds TLS handshakes. Protects against slowloris attacks and stalled TLS handshakes.IdleTimeout(default: 60s, configurable via--idle-timeout): Maximum amount of time to wait for the next request when keep-alives are enabled. Also sets QUICMaxIdleTimeoutfor HTTP/3 when not explicitly specified inHTTP3QUICConfig.ReadTimeout/WriteTimeout(default: 0 / disabled, configurable via--read-timeout/--write-timeout): Unset by default to preserve long-lived streams and slow-progressing response bodies.
- Forwarded Headers and Trust Policy:
Outbound requests across all HTTP forwarding paths (standard forwarding, WebSocket upgrades, and request mirrors) are stamped with consistent forwarded headers:
X-Forwarded-Proto: Set tohttpswhen the client connection used TLS or HTTP/3, andhttpfor plaintext requests. Any client-supplied value is untrusted and replaced.X-Forwarded-Host: When a route applies aURLRewritethat modifies the hostname,X-Forwarded-Hostcarries the original pre-rewriteHostrequested by the client, while the outboundHostheader carries the rewritten hostname. When no hostname rewrite occurs,X-Forwarded-Hostis omitted (and any client-suppliedX-Forwarded-Hostis stripped) because the outboundHostheader already carries the authoritative clientHost.X-Forwarded-For:appendmode (default): Appends the immediate client IP to any existingX-Forwarded-Forheader. In edge deployments facing untrusted clients, only the rightmost entry added by GARI is trustworthy; earlier entries may have been forged unless GARI is behind an upstream trusted proxy.replacemode (--forwarded-for-mode=replace): Drops any client-suppliedX-Forwarded-Forheader and setsX-Forwarded-Forsolely to the immediate client IP. This ensures backends only receive verified client IPs when GARI is the edge proxy (first hop).
- Backend TLS Verification:
When reaching a backend over TLS:
- With a
BackendTLSPolicy: The backend TLS certificate is validated against the configuredcaCertificateRefs(or system roots whenwellKnownCACertificates: System), validatinghostnameand any configuredsubjectAltNames. - Without a
BackendTLSPolicy(appProtocol: https): GARI verifies backend certificates against system trust roots by default, settingServerNameto the backend Service's cluster DNS name (<service>.<namespace>.svc.cluster.local). In-cluster backends using self-signed certificates will fail verification until aBackendTLSPolicynames their CA. The route'sResolvedRefscondition status message informs the user that system trust roots are being used.
- With a
- All-in-One Mode Limitation: In all-in-one mode where multiple Gateways share a single controller and proxy process, two Gateways cannot share the same port and hostname for HTTP, HTTPS, or TLS listeners. Hostnames are first-come-first-served across Gateways (by creation timestamp precedence): the first Gateway wins, and any subsequent Gateway listener requesting the same port and hostname reports
Programmed=Falsewith reasonHostnameConflict(orPortUnavailablefor hostname-less listeners). Per-Gateway data plane mode (singlepod) is the recommended and conformance path.
GARI supports TCPRoute routing raw TCP traffic on dedicated listener ports.
- Per-Port Listeners & Splicing: TCP listeners open individual accept sockets per distinct listener port and splice bidirectional TCP traffic to resolved backends immediately upon connection establishment without waiting for client data (safe for backends that speak first).
- Connection Lifecycle & Limits: Connections can optionally expire after an idle timeout on the splice (default off / 0s, configurable via
--tcp-idle-timeout) and are capped per listener (default 10000, configurable via--tcp-max-connections). New connections exceeding the cap are refused and closed immediately without evicting active connections. - Weighted Routing: Traffic is distributed across configured backendRefs according to weight; backends with weight
0receive no traffic. - Per-Gateway Data Plane (Default / Conformance): In standard deployment (
singlepod), each Gateway runs its own data plane Deployment and LoadBalancer Service mapping TCP listener ports directly to matching container ports without collapsing onto shared proxy ports. - Multiple Attached TCPRoutes: Because raw TCP has no hostnames or paths to demultiplex on, when multiple TCPRoutes attach to the same TCP listener, only the oldest route's backends receive traffic. Subsequent routes targeting the same listener report
Accepted=Trueand contribute to the listener'sattachedRoutescount, but carry no traffic. - All-in-One Mode Limitation: In all-in-one mode where multiple Gateways share a single controller and proxy process, two Gateways cannot both bind the same TCP port. The first Gateway (by creation timestamp precedence) wins, and any subsequent Gateway listener requesting the same port reports
Programmed=Falsewith reasonPortUnavailable. Per-Gateway data plane mode (singlepod) is the recommended and conformance path.
GARI supports UDPRoute routing UDP datagram traffic on dedicated listener ports.
- Per-Port Listeners & Client-Address Sessions: UDP listeners open a
net.PacketConnper distinct listener port. Sessions are keyed by client address (IP:port); the first datagram from a client selects a backend according to configured weights, establishes a connected UDP socket, and forwards subsequent datagrams from that client to the same backend. - Weighted Routing: Traffic is distributed across configured backendRefs according to weight; backends with weight
0receive no traffic. - Session Lifecycle & Limits: Sessions expire after an idle timeout (default 60s, configurable via
--udp-idle-timeout) and are capped per listener (default 10000, configurable via--udp-max-sessions). New clients exceeding the cap are dropped without evicting active sessions. - Per-Gateway Data Plane (Default / Conformance): In standard deployment (
singlepod), each Gateway runs its own data plane Deployment and LoadBalancer Service mapping UDP listener ports directly to matching container ports. - Reserved Ports: Container ports 8080, 8443, and 8999 are reserved inside the data-plane pod; raw TCP or UDP listeners specifying these ports report
Accepted=Falsewith reasonPortUnavailable. - All-in-One Mode Limitation: In all-in-one mode, the first Gateway wins a UDP port; subsequent Gateways requesting the same port report
Programmed=Falsewith reasonPortUnavailable.
In singlepod per-Gateway data plane mode, pods listen on the following container ports:
| Purpose | Port | Protocol | Description |
|---|---|---|---|
| HTTP proxy | 8080 | TCP | Plain HTTP listeners (all HTTP listener ports collapse here behind the Service) |
| HTTPS / TLS proxy | 8443 | TCP | TLS and HTTPS listeners |
| HTTP/3 proxy | 8443 | UDP | HTTP/3 over QUIC |
| Metrics and Health | 8999 | TCP | /metrics, /healthz, and /readyz |
Ports 8080, 8443, and 8999 are reserved container ports inside the data-plane pod. A raw TCP or UDP listener requesting any of these ports reports Accepted=False with reason PortUnavailable.
Provisioned Services use named target ports (http, https, http3) to decouple Gateway listener ports from internal container ports and ensure rollout-safe upgrades.
Note for operators fronting data planes with standalone NEGs: update firewall rules and health check ports from 8000/8081 to 8080/8999.
In standard per-Gateway data plane mode (singlepod), the provisioned Service type, traffic policy, and Service annotations can be configured using a parametersRef pointing to a ConfigMap on GatewayClass (spec.parametersRef) or Gateway (spec.infrastructure.parametersRef):
serviceType:LoadBalancer(default) orClusterIP.externalTrafficPolicy:Cluster(default) orLocal(meaningful forLoadBalancer).serviceAnnotations: YAML/JSON map applied to the Service only (in addition tospec.infrastructure.annotations).- With
ClusterIP, the address comes fromspec.addresses(reported once the data plane is ready).
See docs/singlepod-parameters.md for details and a complete GKE Standalone NEG example.
| Flag | Default | Description |
|---|---|---|
--metrics-bind-address |
:8999 |
Address the metric and health probe endpoint binds to. |
--health-probe-bind-address |
:8999 |
Address the health probe endpoint binds to (if configured to the same port as --metrics-bind-address, probes and metrics share a single listener). |
--read-header-timeout |
10s |
Maximum duration allowed to read request headers and complete TLS handshakes on HTTP and HTTPS servers. |
--idle-timeout |
60s |
Maximum duration to wait for the next request on keep-alive connections (and QUIC MaxIdleTimeout for HTTP/3). |
--read-timeout |
0s (disabled) |
Maximum duration for reading the entire request, including body. |
--write-timeout |
0s (disabled) |
Maximum duration before timing out response writes. |
--tcp-max-connections |
10000 |
Maximum number of concurrent TCP connections per listener. |
--tcp-idle-timeout |
0s (disabled) |
Idle timeout for spliced TCP connections. |
--udp-max-sessions |
10000 |
Maximum number of concurrent UDP sessions per listener. |
--udp-idle-timeout |
60s |
Idle timeout for UDP sessions. |
--forwarded-for-mode |
append |
How to handle X-Forwarded-For: append (default) or replace. |
--proxy-bind-address |
:8080 |
Address the proxy binds to for HTTP. |
--proxy-https-bind-address |
:8443 |
Address the proxy binds to for HTTPS. |
--proxy-http3-bind-address |
"" (disabled) |
UDP address the proxy binds to for HTTP/3. |
--proxy-http3-advertised-port |
0 (inferred) |
Port advertised in the Alt-Svc header for HTTP/3. |
GARI tolerates partial Gateway API CRD installations (for example, on GKE standard channel clusters where only core Gateway API CRDs are installed). At startup, GARI checks the cluster for served Gateway API CRDs using the controller REST mapper:
GatewayClass,Gateway, andHTTPRouteare required. If any of these CRDs is absent, the controller fails fast at startup with an error naming the missing CRD.- Optional Gateway API CRDs (
TLSRoute,GRPCRoute,TCPRoute,UDPRoute,BackendTLSPolicy,ListenerSet, andReferenceGrant) are dynamically disabled when not served by the cluster. - Informers, event handlers, and reconcilers are only registered for served CRDs.
- Listener
supportedKindsexcludes disabled kinds. A listener restricted (explicitly or by default) to only disabled route kinds reportsResolvedRefs=Falsewith reasonInvalidRouteKinds. - If a CRD is installed after the controller has started, restarting the controller is required to discover and enable support for the newly installed CRD.
This project is licensed under the Apache 2.0 License.
We welcome contributions! Please see docs/contributing.md for more information.
We follow Google's Open Source Community Guidelines.
This is not an officially supported Google product.
This project is not eligible for the Google Open Source Software Vulnerability Rewards Program.