GEP-2644: TCPRoute
- Issue: #2644
- Status: Provisional
(See definitions in GEP States.)
TLDR
Gateway API needs a first-class Route type for TCP because a meaningful class of Kubernetes workloads cannot be represented by the standard Gateway API routing model without it. HTTPRoute only addresses a single class of TCP-based traffic (HTTP/1.1 and HTTP/2 over TCP), and TLSRoute handles routing via SNI, leaving the broader universe of TCP workloads that don’t work in either of those cases without a portable routing API, which prevents Gateway API from serving as a common and portable configuration model for them. TCPRoute standardizes the minimal interoperable API surface for exposing TCP workloads through Gateway API.
This GEP retroactively documents the rationale, scope, and design constraints of the existing TCPRoute resource.
Goals
- Define a standard Gateway API Route resource that matches TCP traffic based off the inbound port and forwards it to a TCP based backend.
- Standardize the minimal interoperable forwarding model for TCP traffic: listener attachment and backend forwarding.
- Support forwarding to one or more backends, including weighted backend selection where implemented.
- Provide a stable baseline for evaluating future TCPRoute enhancements.
- Promote TCPRoute to v1 to signal stability.
Non-Goals
- Define rich TCP-specific matching semantics such as address matching, payload inspection, idle timeouts, or client ip preservation.
- Define TLS termination behavior at the Gateway. TLS-aware L4 routing is covered by
TLSRoute. - Define SNI-based or hostname-based routing. Use
TLSRoutefor SNI routing. - Define HTTP or gRPC routing behavior. Use
HTTPRouteorGRPCRoutefor L7 routing.
Longer Term Goals
The following topics are out of scope for this GEP but are candidates for future GEPs:
- Client IP preservation (e.g. PROXY protocol)
- Connection draining and timeout semantics
- Extended routing options (e.g. 5-tuple or 3-tuple matching)
- Listener port range support for TCP
Introduction
This GEP proposes a standard resource for TCP traffic routing. Presently, the Gateway API lacks a way to describe how to route plain TCP traffic meaning that it’s hard (or impossible) to natively define such applications using the Gateway API:
- Databases (e.g. PostgreSQL, MySQL, Redis)
- Message brokers (e.g. Kafka, RabbitMQ, NATS)
- SMTP, IMAP, and other mail protocols
- Legacy or proprietary TCP-based services
Without TCPRoute, users must rely on implementation-specific extensions or fall back to traditional Kubernetes Service resources. TCPRoute allows for consistent network configuration management using the Gateway API. Another benefit is that organizations may consolidate their load balancing infrastructure under one Gateway, instead of having one physical load balancer per Service.
API
TCPRoute Resource
These resources follow the same pattern as other route types. Notably, as TCP doesn’t have traffic control options exposed at this layer, the route rule only includes backends to forward TCP traffic to.
// TCPRoute provides a way to route TCP traffic. When combined with a Gateway
// listener, it can be used to forward traffic on the port specified by the
// listener to a set of backends specified by the TCPRoute.
type TCPRoute struct {
metav1.TypeMeta `json:",inline"`
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
// Spec defines the desired state of TCPRoute.
// +required
Spec TCPRouteSpec `json:"spec"`
// Status defines the current state of TCPRoute.
// +optional
Status TCPRouteStatus `json:"status,omitempty"`
}
// TCPRouteSpec defines the desired state of TCPRoute.
type TCPRouteSpec struct {
CommonRouteSpec `json:",inline"`
// Rules are a list of TCP matchers and actions.
//
// +required
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=1
// <gateway:experimental:validation:XValidation:message="Rule name must be unique within the route",rule="self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name))">
Rules []TCPRouteRule `json:"rules"`
}
// TCPRouteRule is the configuration for a given rule.
type TCPRouteRule struct {
// Name is the name of the route rule. This name MUST be unique within a Route if it is set.
//
// Support: Extended
// +optional
Name *SectionName `json:"name,omitempty"`
// BackendRefs defines the backend(s) where matching requests should be
// sent. If unspecified or invalid (refers to a nonexistent resource or a
// Service with no endpoints), the underlying implementation MUST actively
// reject connection attempts to this backend. Connection rejections must
// respect weight; if an invalid backend is requested to have 80% of
// the connections, then 80% of connections must be rejected instead.
//
// Support: Core for Kubernetes Service
//
// Support: Extended for Kubernetes ServiceImport
//
// Support: Implementation-specific for any other resource
//
// Support for weight: Extended
//
// +required
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=16
BackendRefs []BackendRef `json:"backendRefs,omitempty"`
}
// TCPRouteStatus defines the observed state of TCPRoute.
type TCPRouteStatus struct {
RouteStatus `json:",inline"`
}
Request flow
Following are some of the request flows covered by TCPRoute, and the expected behavior:
Basic TCP Forwarding
In this workflow, TCP traffic arriving at a specific listener port is forwarded
directly to the backend service. This is the base use case for the TCPRoute
object, and is included in the core TCPRoute feature.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tcp-gateway
namespace: gateway-conformance-infra
spec:
gatewayClassName: example-gateway-class
listeners:
- name: postgres
protocol: TCP
port: 5432
allowedRoutes:
kinds:
- kind: TCPRoute
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: tcp-postgres
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: tcp-gateway
sectionName: postgres
rules:
- backendRefs:
- name: postgres
port: 5432
A typical north/south
request flow for a gateway implemented using a TCPRoute is:
- A client opens a TCP connection to the Gateway address on port 5432.
- The Gateway receives the connection on the
Listenermatching port 5432. - The Gateway identifies the
TCPRouteattached to thatListener. - The Gateway opens a new TCP connection to one of the backend
Serviceendpoints based on thebackendRefsof theTCPRoute. - The Gateway proxies the TCP stream bidirectionally between the client and the backend for the lifetime of the connection.
Multiple Weighted Backends
In this workflow, TCP connections are distributed across multiple backends according to configured weights. Weight support is Extended for TCPRoute.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tcp-gateway
namespace: gateway-conformance-infra
spec:
gatewayClassName: example-gateway-class
listeners:
- name: kafka
protocol: TCP
port: 9092
allowedRoutes:
kinds:
- kind: TCPRoute
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: kafka-route
namespace: gateway-conformance-infra
spec:
parentRefs:
- name: tcp-gateway
sectionName: kafka
rules:
- backendRefs:
- name: kafka-broker-1
port: 9092
weight: 70
- name: kafka-broker-2
port: 9092
weight: 30
- A client opens a TCP connection to the Gateway address on port 9092.
- The Gateway receives the connection on the
Listenermatching port 9092. - The Gateway identifies the
TCPRouteattached to thatListener. - The Gateway selects a backend based on the configured weights: approximately
70% of new connections are sent to
kafka-broker-1and 30% tokafka-broker-2. Each TCP SYN (new connection) is a single unit for weighting purposes. - The Gateway proxies the TCP stream bidirectionally between the client and the selected backend.
Mixing TCP and UDP Protocols
A common use case is to expose the same service over TCP and UDP (e.g. DNS). An implementation MAY listen for both TCP and UDP traffic utilizing the same Listener port. In this example, all TCP traffic MUST be routed to the TCPRoute and all UDP traffic MUST be routed to the UDPRoute.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: dns-gateway
namespace: default
spec:
gatewayClassName: example-gatewayclass
listeners:
- name: dns-tcp
protocol: TCP
port: 53
allowedRoutes:
kinds:
- kind: TCPRoute
group: gateway.networking.k8s.io
- name: dns-udp
protocol: UDP
port: 53
allowedRoutes:
kinds:
- kind: UDPRoute
group: gateway.networking.k8s.io
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: dns-tcp-route
namespace: default
spec:
parentRefs:
- name: dns-gateway
sectionName: dns-tcp
rules:
- backendRefs:
- name: dns-tcp-service
port: 53
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: UDPRoute
metadata:
name: dns-udp-route
namespace: default
spec:
parentRefs:
- name: dns-gateway
sectionName: dns-udp
rules:
- backendRefs:
- name: dns-udp-service
port: 53
- A client sends a DNS query over TCP to the Gateway address on port 53.
- The Gateway receives the connection on the
dns-tcpListener. - The Gateway identifies the
TCPRouteattached to thatListenerand forwards the TCP connection todns-tcp-service. - Separately, a client sends a DNS query over UDP to the same Gateway address on port 53.
- The Gateway receives the datagram on the
dns-udpListener. - The Gateway identifies the
UDPRouteattached to thatListenerand forwards the UDP datagram todns-udp-service.
Conflict management and precedences
A conflict can happen when two or more distinct listeners on a Gateway definition have conflicting behavior, or when multiple routes attempt to attach to the same listener.
Unlike TLSRoute (which uses SNI hostnames to disambiguate traffic on the same port),
TCPRoute traffic is classified only by protocol:port. This means that a TCP
listener’s only distinctness field is port — there is no further attribute
to discriminate between connections arriving on the same listener.
Two TCP listeners on the same port
Two TCP listeners on the same port are always indistinct, because there is no hostname or other field to differentiate them. The implementation MUST mark both listeners as conflicted:
spec:
listeners:
- name: listener1
port: 5432
protocol: TCP
- name: listener2
port: 5432
protocol: TCP
status:
listeners:
- name: listener1
conditions:
- reason: Accepted
status: "True"
type: Accepted
- reason: ProtocolConflict
status: "True"
type: Conflicted
- name: listener2
conditions:
- reason: Accepted
status: "True"
type: Accepted
- reason: ProtocolConflict
status: "True"
type: Conflicted
TCP listener on the same port as an HTTP, HTTPS, or TLS listener
When a TCP protocol listener shares a port with an HTTP, HTTPS, or TLS protocol listener, all listeners sharing that port are indistinct and MUST NOT be accepted. TCP operates at a lower layer and cannot coexist with protocols that require additional parsing (e.g. HTTP Host header or TLS SNI) on the same port:
spec:
listeners:
- name: tcp-listener
port: 443
protocol: TCP
- name: https-listener
port: 443
protocol: HTTPS
hostname: "app.example.com"
tls:
mode: Terminate
certificateRefs:
- name: app-cert
kind: Secret
status:
listeners:
- name: tcp-listener
conditions:
- reason: Accepted
status: "True"
type: Accepted
- reason: ProtocolConflict
status: "True"
type: Conflicted
- name: https-listener
conditions:
- reason: Accepted
status: "True"
type: Accepted
- reason: ProtocolConflict
status: "True"
type: Conflicted
Multiple TCPRoutes attaching to the same listener
Because a TCP listener has no mechanism to distinguish between connections (no hostname, no SNI, no path), attaching multiple TCPRoutes to the same listener results in only one route effectively receiving traffic.
When multiple TCPRoutes reference the same listener, the implementation MUST
follow the general Gateway API route precedence rules defined in AllowedRoutes:
- The oldest Route based on
metadata.creationTimestamp. - If timestamps are equal, the Route appearing first in alphabetical order
(
namespace/name).
All attached TCPRoutes are Accepted, consistent with how other route types
handle precedence in the Gateway API. Only the winning route’s backends receive
traffic.
Note: Accepting all conflicting routes without surfacing which one is actively receiving traffic is not optimal for user experience. A future GEP may introduce a dedicated route condition reason to explicitly signal that a route has been superseded by another route on the same listener.
# Two TCPRoutes targeting the same listener
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: tcp-route-1
creationTimestamp: "2026-01-01T00:00:00Z"
spec:
parentRefs:
- name: tcp-gateway
sectionName: postgres
rules:
- backendRefs:
- name: postgres-primary
port: 5432
---
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
name: tcp-route-2
creationTimestamp: "2026-01-02T00:00:00Z"
spec:
parentRefs:
- name: tcp-gateway
sectionName: postgres
rules:
- backendRefs:
- name: postgres-replica
port: 5432
Both routes are accepted, but only tcp-route-1 (the oldest) receives traffic:
# tcp-route-1 status
status:
parents:
- parentRef:
group: gateway.networking.k8s.io
kind: Gateway
name: tcp-gateway
sectionName: postgres
conditions:
- type: Accepted
status: "True"
reason: Accepted
# tcp-route-2 status (accepted, but does not receive traffic)
status:
parents:
- parentRef:
group: gateway.networking.k8s.io
kind: Gateway
name: tcp-gateway
sectionName: postgres
conditions:
- type: Accepted
status: "True"
reason: Accepted
TCP and UDP listeners on the same port
TCP and UDP are distinct transport protocols. An implementation MAY support listeners for both TCP and UDP on the same port without conflict. See the Mixing TCP and UDP Protocols request flow for an example.
Conformance Details
Feature Names
- TCPRoute
Conformance test scenarios
The following scenarios will be validated:
-
TCPRoute attaches to a TCP listener by port specified
-
A Gateway has a TCP listener on port 5432
-
A TCPRoute specifies a
parentRefwithport: 5432 -
The TCPRoute is accepted with the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway port: 5432 conditions: - type: Accepted status: "True" reason: Accepted -
TCP traffic sent to port 5432 is forwarded to the backend.
Features:
TCPRoute -
-
TCPRoute attaches to a TCP listener by sectionName
-
A Gateway has a TCP listener named
postgreson port 5432 -
A TCPRoute specifies a
parentRefwithsectionName: postgres -
The TCPRoute is accepted with the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: postgres conditions: - type: Accepted status: "True" reason: Accepted -
TCP traffic sent to the
postgreslistener is forwarded to the backend.
Features:
TCPRoute -
-
TCPRoute attaches to a TCP listener by sectionName and port
-
A Gateway has a TCP listener named
postgreson port 5432 -
A TCPRoute specifies a
parentRefwithsectionName: postgresandport: 5432 -
The TCPRoute is accepted with the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: postgres port: 5432 conditions: - type: Accepted status: "True" reason: Accepted -
TCP traffic sent to port 5432 on the
postgreslistener is forwarded to the backend.
Features:
TCPRoute -
-
TCPRoute attaches to all TCP listeners in a Gateway when sectionName and port are omitted
-
A Gateway has multiple TCP listeners:
postgreson port 5432 andkafkaon port 9092 -
A TCPRoute specifies a
parentRefwith only the Gateway name (nosectionNameorport) -
The TCPRoute is accepted with the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway conditions: - type: Accepted status: "True" reason: Accepted -
The TCPRoute attaches to all TCP listeners on the Gateway.
Features:
TCPRoute -
-
TCPRoute fails attachment to a non-TCP listener when port or sectionName is specified
-
A Gateway has a UDP listener named
udp-listeneron port 5432 and no TCP listeners on that port/name -
A TCPRoute specifies a
parentReftargeting the UDP listener viasectionName: udp-listener -
The TCPRoute is not accepted with the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: mixed-gateway sectionName: udp-listener conditions: - type: Accepted status: "False" reason: NotAllowedByListeners -
No TCP traffic is routed through the UDP listener.
Features:
TCPRoute -
-
TCPRoute references a backend Service that does not exist
-
A Gateway has a TCP listener named
postgreson port 5432 -
A TCPRoute specifies a
parentRefwithsectionName: postgresand abackendRefpointing to a Servicenonexistent-servicethat does not exist -
The TCPRoute has the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: postgres conditions: - type: Accepted status: "True" reason: Accepted - type: ResolvedRefs status: "False" reason: BackendNotFound -
TCP connections to this route MUST be rejected.
Features:
TCPRoute -
-
TCPRoute with a cross-namespace backendRef and no valid ReferenceGrant
-
A Gateway has a TCP listener named
postgreson port 5432 -
A TCPRoute in the
gateway-conformance-infranamespace specifies abackendRefpointing to a Service in another namespace without a valid ReferenceGrant -
The TCPRoute has the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: postgres conditions: - type: Accepted status: "True" reason: Accepted - type: ResolvedRefs status: "False" reason: RefNotPermitted -
TCP connections to this route MUST be rejected.
Features:
TCPRoute,ReferenceGrant -
-
TCPRoute with multiple weighted backends distributes connections according to configured weights
-
A Gateway has a TCP listener named
kafkaon port 9092 -
A TCPRoute specifies a
parentRefwithsectionName: kafkaand multiplebackendRefs:kafka-broker-1withweight: 70kafka-broker-2withweight: 30
-
The TCPRoute is accepted with the following status:
parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: kafka conditions: - type: Accepted status: "True" reason: Accepted - type: ResolvedRefs status: "True" reason: ResolvedRefs -
TCP connections sent to port 9092 are distributed across backends respecting the configured weights (approximately 70% to
kafka-broker-1and 30% tokafka-broker-2). In this situation, TCP SYN packets denote one connection for weighting purposes.
Features:
TCPRoute(weight support is Extended) -
-
Two TCP listeners on the same port are marked as conflicted
-
A Gateway has two TCP listeners on port 5432:
listener1andlistener2 -
Both listeners are marked as conflicted with the following status:
listeners: - name: listener1 conditions: - type: Accepted status: "True" reason: Accepted - type: Conflicted status: "True" reason: ProtocolConflict - name: listener2 conditions: - type: Accepted status: "True" reason: Accepted - type: Conflicted status: "True" reason: ProtocolConflict -
No traffic is routed through either listener.
Features:
TCPRoute -
-
TCP listener on the same port as an HTTPS listener causes conflict
-
A Gateway has a TCP listener on port 443 and an HTTPS listener on port 443
-
Both listeners are marked as conflicted with the following status:
listeners: - name: tcp-listener conditions: - type: Accepted status: "True" reason: Accepted - type: Conflicted status: "True" reason: ProtocolConflict - name: https-listener conditions: - type: Accepted status: "True" reason: Accepted - type: Conflicted status: "True" reason: ProtocolConflict -
No traffic is routed through either listener.
Features:
TCPRoute -
-
Multiple TCPRoutes attaching to the same listener results in only the oldest receiving traffic
-
A Gateway has a TCP listener named
postgreson port 5432 -
Two TCPRoutes (
tcp-route-1created first,tcp-route-2created second) both specify aparentRefwithsectionName: postgres -
Both routes are accepted:
# tcp-route-1 status parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: postgres conditions: - type: Accepted status: "True" reason: Accepted# tcp-route-2 status parents: - parentRef: group: gateway.networking.k8s.io kind: Gateway name: tcp-gateway sectionName: postgres conditions: - type: Accepted status: "True" reason: Accepted -
TCP traffic sent to port 5432 is forwarded only to the backends of
tcp-route-1(the oldest route).
Features:
TCPRoute -
Standard Graduation Criteria
The TCPRoute resource pre-dates the current GEP process and has existed in the
Gateway API since early releases as a v1alpha2 resource. This GEP retroactively
documents the rationale, scope, and design constraints of the existing resource.
Because TCPRoute has been available and implemented for a significant period of time, it is being grandfathered into the current process and is not subject to the standard probationary period requirement. The graduation criteria for Standard are:
- At least one Feature Name must be listed:
TCPRoute. - The Conformance Details must be filled out, with conformance test scenarios listed.
- Conformance tests must be implemented that test all the listed test scenarios.
- At least three (3) implementations must have submitted conformance reports that pass those conformance tests.
References
- TCPRoute and UDPRoute Specification
- TLSRoute Specification
- GEP-735: TCP and UDP addresses matching (Declined, but relevant context)
- Gateway API Use Cases