Repository: envoyproxy/gateway
Pull request #737 (2022-11-17 12:51:03)

feat: add support for TCP Route
Resolves: https://github.com/envoyproxy/gateway/issues/643

Progress:

- [x] Xds IR
- [x] Xds Translator
- [x] Gateway API Translator
- [x] Provider
- [x] Docs

Signed-off-by: bitliu <bitliu@tencent.com>

danehans: @Xunzhuo are you able to complete this PR and the additional work to add TCPRoute support for v0.3.0-rc.1?
Xunzhuo: Yes, it could be done possibly, I am quite busy this week, will come back to feature implementations this weekend.
codecov-commenter: # [Codecov](https://codecov.io/gh/envoyproxy/gateway/pull/737?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#737](https://codecov.io/gh/envoyproxy/gateway/pull/737?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (5ffa0e6) into [main](https://codecov.io/gh/envoyproxy/gateway/commit/22131aa1efaa48b414395c5de15097ff99793b7e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (22131aa) will **increase** coverage by `0.23%`.
> The diff coverage is `66.66%`.

```diff
@@            Coverage Diff             @@
##             main     #737      +/-   ##
==========================================
+ Coverage   63.91%   64.14%   +0.23%     
==========================================
  Files          50       50              
  Lines        6506     6521      +15     
==========================================
+ Hits         4158     4183      +25     
+ Misses       2087     2078       -9     
+ Partials      261      260       -1     
```


| [Impacted Files](https://codecov.io/gh/envoyproxy/gateway/pull/737?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [internal/xds/translator/translator.go](https://codecov.io/gh/envoyproxy/gateway/pull/737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvdHJhbnNsYXRvci5nbw==) | `74.59% <43.47%> (-7.53%)` | :arrow_down: |
| [internal/xds/translator/listener.go](https://codecov.io/gh/envoyproxy/gateway/pull/737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvbGlzdGVuZXIuZ28=) | `81.90% <100.00%> (+1.00%)` | :arrow_up: |
| [internal/provider/kubernetes/controller.go](https://codecov.io/gh/envoyproxy/gateway/pull/737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvcHJvdmlkZXIva3ViZXJuZXRlcy9jb250cm9sbGVyLmdv) | `49.08% <0.00%> (+1.54%)` | :arrow_up: |
| [internal/provider/kubernetes/helpers.go](https://codecov.io/gh/envoyproxy/gateway/pull/737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwvcHJvdmlkZXIva3ViZXJuZXRlcy9oZWxwZXJzLmdv) | `81.96% <0.00%> (+4.09%)` | :arrow_up: |
| [internal/xds/translator/cluster.go](https://codecov.io/gh/envoyproxy/gateway/pull/737/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-aW50ZXJuYWwveGRzL3RyYW5zbGF0b3IvY2x1c3Rlci5nbw==) | `100.00% <0.00%> (+5.55%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/examples/kubernetes/tcp-routing.yaml b/examples/kubernetes/tcp-routing.yaml
new file mode 100644
index 000000000..546b500c7
--- /dev/null
+++ b/examples/kubernetes/tcp-routing.yaml
@@ -0,0 +1,148 @@
+kind: GatewayClass
+apiVersion: gateway.networking.k8s.io/v1beta1
+metadata:
+  name: eg
+spec:
+  controllerName: gateway.envoyproxy.io/gatewayclass-controller
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: tcp-gateway
+spec:
+  gatewayClassName: eg
+  listeners:
+  - name: foo
+    protocol: TCP
+    port: 8088
+    allowedRoutes:
+      kinds:
+      - kind: TCPRoute
+  - name: bar
+    protocol: TCP
+    port: 8089
+    allowedRoutes:
+      kinds:
+      - kind: TCPRoute
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: foo
+  labels:
+    app: backend-1
+spec:
+  ports:
+  - name: http
+    port: 3001
+    targetPort: 3000
+  selector:
+    app: backend-1
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bar
+  labels:
+    app: backend-2
+spec:
+  ports:
+  - name: http
+    port: 3002
+    targetPort: 3000
+  selector:
+    app: backend-2
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend-1
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backend-1
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: backend-1
+        version: v1
+    spec:
+      containers:
+      - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e
+        imagePullPolicy: IfNotPresent
+        name: backend-1
+        ports:
+        - containerPort: 3000
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: SERVICE_NAME
+          value: foo
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend-2
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backend-2
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: backend-2
+        version: v1
+    spec:
+      containers:
+      - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e
+        imagePullPolicy: IfNotPresent
+        name: backend-2
+        ports:
+        - containerPort: 3000
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: SERVICE_NAME
+          value: bar
+---
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+  name: tcp-app-1
+spec:
+  parentRefs:
+  - name: tcp-gateway
+    sectionName: foo
+  rules:
+  - backendRefs:
+    - name: foo
+      port: 3001
+---
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+  name: tcp-app-2
+spec:
+  parentRefs:
+  - name: tcp-gateway
+    sectionName: bar
+  rules:
+  - backendRefs:
+    - name: bar
+      port: 3002
diff --git a/internal/gatewayapi/contexts.go b/internal/gatewayapi/contexts.go
index 8de55aa68..a7dcdf989 100644
--- a/internal/gatewayapi/contexts.go
+++ b/internal/gatewayapi/contexts.go
@@ -193,11 +193,6 @@ type RouteContext interface {
 	// TLSRoute, TCPRoute, UDPRoute etc.
 	GetRouteType() string
 
-	// TODO: [v1alpha2-v1beta1] This should not be required once all Route
-	// objects being implemented are of type v1beta1.
-	// GetHostnames returns the hosts targeted by the Route object.
-	GetHostnames() []string
-
 	// GetRouteStatus returns the RouteStatus object associated with the Route.
 	GetRouteStatus() *v1beta1.RouteStatus
 
@@ -387,11 +382,6 @@ func (u *UDPRouteContext) GetRouteType() string {
 	return KindUDPRoute
 }
 
-// GetHostnames return empty string array because UDPRoute has no hostnames
-func (u *UDPRouteContext) GetHostnames() []string {
-	return []string{""}
-}
-
 func (u *UDPRouteContext) GetParentReferences() []v1beta1.ParentReference {
 	parentReferences := make([]v1beta1.ParentReference, len(u.Spec.ParentRefs))
 	for idx, p := range u.Spec.ParentRefs {
@@ -461,6 +451,87 @@ func (u *UDPRouteContext) GetRouteParentContext(forParentRef v1beta1.ParentRefer
 	return ctx
 }
 
+// TCPRouteContext wraps a TCPRoute and provides helper methods for
+// accessing the route's parents.
+type TCPRouteContext struct {
+	*v1alpha2.TCPRoute
+
+	parentRefs map[v1beta1.ParentReference]*RouteParentContext
+}
+
+func (t *TCPRouteContext) GetRouteType() string {
+	return KindTCPRoute
+}
+
+func (t *TCPRouteContext) GetParentReferences() []v1beta1.ParentReference {
+	parentReferences := make([]v1beta1.ParentReference, len(t.Spec.ParentRefs))
+	for idx, p := range t.Spec.ParentRefs {
+		parentReferences[idx] = UpgradeParentReference(p)
+	}
+	return parentReferences
+}
+
+func (t *TCPRouteContext) GetRouteStatus() *v1beta1.RouteStatus {
+	return &t.Status.RouteStatus
+}
+
+func (t *TCPRouteContext) GetRouteParentContext(forParentRef v1beta1.ParentReference) *RouteParentContext {
+	if t.parentRefs == nil {
+		t.parentRefs = make(map[v1beta1.ParentReference]*RouteParentContext)
+	}
+
+	if ctx := t.parentRefs[forParentRef]; ctx != nil {
+		return ctx
+	}
+
+	var parentRef *v1beta1.ParentReference
+	for i, p := range t.Spec.ParentRefs {
+		p := UpgradeParentReference(p)
+		if reflect.DeepEqual(p, forParentRef) {
+			upgraded := UpgradeParentReference(t.Spec.ParentRefs[i])
+			parentRef = &upgraded
+			break
+		}
+	}
+	if parentRef == nil {
+		panic("parentRef not found")
+	}
+
+	routeParentStatusIdx := -1
+	for i := range t.Status.Parents {
+		p := UpgradeParentReference(t.Status.Parents[i].ParentRef)
+		defaultNamespace := v1beta1.Namespace(metav1.NamespaceDefault)
+		if forParentRef.Namespace == nil {
+			forParentRef.Namespace = &defaultNamespace
+		}
+		if p.Namespace == nil {
+			p.Namespace = &defaultNamespace
+		}
+		if reflect.DeepEqual(p, forParentRef) {
+			routeParentStatusIdx = i
+			break
+		}
+	}
+	if routeParentStatusIdx == -1 {
+		rParentStatus := v1alpha2.RouteParentStatus{
+			// TODO: get this value from the config
+			ControllerName: v1alpha2.GatewayController(egv1alpha1.GatewayControllerName),
+			ParentRef:      DowngradeParentReference(forParentRef),
+		}
+		t.Status.Parents = append(t.Status.Parents, rParentStatus)
+		routeParentStatusIdx = len(t.Status.Parents) - 1
+	}
+
+	ctx := &RouteParentContext{
+		ParentReference: parentRef,
+
+		tcpRoute:             t.TCPRoute,
+		routeParentStatusIdx: routeParentStatusIdx,
+	}
+	t.parentRefs[forParentRef] = ctx
+	return ctx
+}
+
 // RouteParentContext wraps a ParentReference and provides helper methods for
 // setting conditions and other status information on the associated
 // HTTPRoute, TLSRoute etc.
@@ -471,6 +542,7 @@ type RouteParentContext struct {
 	// a single field pointing to *v1beta1.RouteStatus.
 	httpRoute *v1beta1.HTTPRoute
 	tlsRoute  *v1alpha2.TLSRoute
+	tcpRoute  *v1alpha2.TCPRoute
 	udpRoute  *v1alpha2.UDPRoute
 
 	routeParentStatusIdx int
diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go
index 77b94123b..d405899f6 100644
--- a/internal/gatewayapi/helpers.go
+++ b/internal/gatewayapi/helpers.go
@@ -22,6 +22,9 @@ import (
 const (
 	TCPProtocol = "TCP"
 	UDPProtocol = "UDP"
+
+	L4Protocol = "L4"
+	L7Protocol = "L7"
 )
 
 type protocolPort struct {
@@ -305,19 +308,24 @@ func hostnameMatchesWildcardHostname(hostname, wildcardHostname string) bool {
 
 func containsPort(ports []*protocolPort, port *protocolPort) bool {
 	for _, protocolPort := range ports {
-		if protocolPort.port == port.port && layer4Protocol(protocolPort) == layer4Protocol(port) {
+		curProtocol, curLevel := layer4Protocol(protocolPort)
+		myProtocol, myLevel := layer4Protocol(port)
+		if protocolPort.port == port.port && (curProtocol == myProtocol && curLevel == myLevel) {
 			return true
 		}
 	}
 	return false
 }
 
-func layer4Protocol(protocolPort *protocolPort) string {
+// layer4Protocol returns listener L4 protocol and listen protocol level
+func layer4Protocol(protocolPort *protocolPort) (string, string) {
 	switch protocolPort.protocol {
-	case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.TLSProtocolType, v1beta1.TCPProtocolType:
-		return TCPProtocol
+	case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.TLSProtocolType:
+		return TCPProtocol, L7Protocol
+	case v1beta1.TCPProtocolType:
+		return TCPProtocol, L4Protocol
 	default:
-		return UDPProtocol
+		return UDPProtocol, L4Protocol
 	}
 }
 
@@ -342,10 +350,14 @@ func irHTTPListenerName(listener *ListenerContext) string {
 	return fmt.Sprintf("%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name)
 }
 
-func irTCPListenerName(listener *ListenerContext, tlsRoute *TLSRouteContext) string {
+func irTLSListenerName(listener *ListenerContext, tlsRoute *TLSRouteContext) string {
 	return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, tlsRoute.Name)
 }
 
+func irTCPListenerName(listener *ListenerContext, tcpRoute *TCPRouteContext) string {
+	return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, tcpRoute.Name)
+}
+
 func irUDPListenerName(listener *ListenerContext, udpRoute *UDPRouteContext) string {
 	return fmt.Sprintf("%s-%s-%s-%s", listener.gateway.Namespace, listener.gateway.Name, listener.Name, udpRoute.Name)
 }
diff --git a/internal/gatewayapi/listener.go b/internal/gatewayapi/listener.go
index 31415031b..5d47b6de0 100644
--- a/internal/gatewayapi/listener.go
+++ b/internal/gatewayapi/listener.go
@@ -53,6 +53,8 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap
 				t.validateAllowedRoutes(listener, KindTLSRoute)
 			case v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType:
 				t.validateAllowedRoutes(listener, KindHTTPRoute)
+			case v1beta1.TCPProtocolType:
+				t.validateAllowedRoutes(listener, KindTCPRoute)
 			case v1beta1.UDPProtocolType:
 				t.validateAllowedRoutes(listener, KindUDPRoute)
 			default:
@@ -60,8 +62,8 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap
 					v1beta1.ListenerConditionAccepted,
 					metav1.ConditionFalse,
 					v1beta1.ListenerReasonUnsupportedProtocol,
-					fmt.Sprintf("Protocol %s is unsupported, must be %s, %s, or %s.", listener.Protocol,
-						v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.UDPProtocolType),
+					fmt.Sprintf("Protocol %s is unsupported, must be %s, %s, %s or %s.", listener.Protocol,
+						v1beta1.HTTPProtocolType, v1beta1.HTTPSProtocolType, v1beta1.TCPProtocolType, v1beta1.UDPProtocolType),
 				)
 			}
 
@@ -114,6 +116,8 @@ func (t *Translator) ProcessListeners(gateways []*GatewayContext, xdsIR XdsIRMap
 					proto = ir.HTTPSProtocolType
 				case v1beta1.TLSProtocolType:
 					proto = ir.TLSProtocolType
+				case v1beta1.TCPProtocolType:
+					proto = ir.TCPProtocolType
 				case v1beta1.UDPProtocolType:
 					proto = ir.UDPProtocolType
 				}
diff --git a/internal/gatewayapi/resource.go b/internal/gatewayapi/resource.go
index c0a7b37f6..34aab828b 100644
--- a/internal/gatewayapi/resource.go
+++ b/internal/gatewayapi/resource.go
@@ -24,6 +24,7 @@ type Resources struct {
 	HTTPRoutes      []*v1beta1.HTTPRoute
 	GRPCRoutes      []*v1alpha2.GRPCRoute
 	TLSRoutes       []*v1alpha2.TLSRoute
+	TCPRoutes       []*v1alpha2.TCPRoute
 	UDPRoutes       []*v1alpha2.UDPRoute
 	ReferenceGrants []*v1alpha2.ReferenceGrant
 	Namespaces      []*v1.Namespace
diff --git a/internal/gatewayapi/route.go b/internal/gatewayapi/route.go
index 6cc2d47a1..3d5dc6398 100644
--- a/internal/gatewayapi/route.go
+++ b/internal/gatewayapi/route.go
@@ -20,6 +20,7 @@ var _ RoutesTranslator = (*Translator)(nil)
 type RoutesTranslator interface {
 	ProcessHTTPRoutes(httpRoutes []*v1beta1.HTTPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*HTTPRouteContext
 	ProcessTLSRoutes(tlsRoutes []*v1alpha2.TLSRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TLSRouteContext
+	ProcessTCPRoutes(tcpRoutes []*v1alpha2.TCPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*TCPRouteContext
 	ProcessUDPRoutes(udpRoutes []*v1alpha2.UDPRoute, gateways []*GatewayContext, resources *Resources, xdsIR XdsIRMap) []*UDPRouteContext
 }
 
@@ -375,7 +376,7 @@ func (t *Translator) processTLSRouteParentRefs(tlsRoute *TLSRouteContext, resour
 			// Create the TCP Listener while parsing the TLSRoute since
 			// the listener directly links to a routeDestination.
 			irListener := &ir.TCPListener{
-				Name:    irTCPListenerName(listener, tlsRoute),
+				Name:    irTLSListenerName(listener, tlsRoute),
 				Address: "0.0.0.0",
 				Port:    uint32(containerPort),
 				TLS: &ir.TLSInspectorConfig{
@@ -541,6 +542,131 @@ func (t *Translator) processUDPRouteParentRefs(udpRoute *UDPRouteContext, resour
 	}
 }
 
+func (t *Translator) ProcessTCPRoutes(tcpRoutes []*v1alpha2.TCPRoute, gateways []*GatewayContext, resources *Resources,
+	xdsIR XdsIRMap) []*TCPRouteContext {
+	var relevantTCPRoutes []*TCPRouteContext
+
+	for _, tcp := range tcpRoutes {
+		if tcp == nil {
+			panic("received nil tcproute")
+		}
+		tcpRoute := &TCPRouteContext{TCPRoute: tcp}
+
+		// Find out if this route attaches to one of our Gateway's listeners,
+		// and if so, get the list of listeners that allow it to attach for each
+		// parentRef.
+		relevantRoute := t.processAllowedListenersForParentRefs(tcpRoute, gateways, resources)
+		if !relevantRoute {
+			continue
+		}
+
+		relevantTCPRoutes = append(relevantTCPRoutes, tcpRoute)
+
+		t.processTCPRouteParentRefs(tcpRoute, resources, xdsIR)
+	}
+
+	return relevantTCPRoutes
+}
+
+func (t *Translator) processTCPRouteParentRefs(tcpRoute *TCPRouteContext, resources *Resources, xdsIR XdsIRMap) {
+	for _, parentRef := range tcpRoute.parentRefs {
+		// Skip parent refs that did not accept the route
+		if !parentRef.IsAccepted(tcpRoute) {
+			continue
+		}
+
+		// Need to compute Route rules within the parentRef loop because
+		// any conditions that come out of it have to go on each RouteParentStatus,
+		// not on the Route as a whole.
+		var routeDestinations []*ir.RouteDestination
+
+		// compute backends
+		if len(tcpRoute.Spec.Rules) != 1 {
+			parentRef.SetCondition(tcpRoute,
+				v1beta1.RouteConditionResolvedRefs,
+				metav1.ConditionFalse,
+				"InvalidRule",
+				"One and only one rule is supported",
+			)
+			continue
+		}
+		if len(tcpRoute.Spec.Rules[0].BackendRefs) != 1 {
+			parentRef.SetCondition(tcpRoute,
+				v1beta1.RouteConditionResolvedRefs,
+				metav1.ConditionFalse,
+				"InvalidBackend",
+				"One and only one backend is supported",
+			)
+			continue
+		}
+
+		backendRef := tcpRoute.Spec.Rules[0].BackendRefs[0]
+		// TODO: [v1alpha2-v1beta1] Replace with NamespaceDerefOr when TCPRoute graduates to v1beta1.
+		serviceNamespace := NamespaceDerefOrAlpha(backendRef.Namespace, tcpRoute.Namespace)
+		service := resources.GetService(serviceNamespace, string(backendRef.Name))
+
+		if !t.validateBackendRef(&backendRef, parentRef, tcpRoute, resources, serviceNamespace, KindTCPRoute) {
+			continue
+		}
+
+		// weight is not used in tcp route destinations
+		routeDestinations = append(routeDestinations, &ir.RouteDestination{
+			Host: service.Spec.ClusterIP,
+			Port: uint32(*backendRef.Port),
+		})
+
+		accepted := false
+		for _, listener := range parentRef.listeners {
+			// only one route is allowed for a TCP listener
+			if listener.AttachedRoutes() > 0 {
+				continue
+			}
+			if !listener.IsReady() {
+				continue
+			}
+			accepted = true
+			irKey := irStringKey(listener.gateway)
+			containerPort := servicePortToContainerPort(int32(listener.Port))
+			// Create the TCP Listener while parsing the TCPRoute since
+			// the listener directly links to a routeDestination.
+			irListener := &ir.TCPListener{
+				Name:         irTCPListenerName(listener, tcpRoute),
+				Address:      "0.0.0.0",
+				Port:         uint32(containerPort),
+				Destinations: routeDestinations,
+			}
+			gwXdsIR := xdsIR[irKey]
+			gwXdsIR.TCP = append(gwXdsIR.TCP, irListener)
+
+			// Theoretically there should only be one parent ref per
+			// Route that attaches to a given Listener, so fine to just increment here, but we
+			// might want to check to ensure we're not double-counting.
+			if len(routeDestinations) > 0 {
+				listener.IncrementAttachedRoutes()
+			}
+		}
+
+		// If no negative conditions have been set, the route is considered "Accepted=True".
+		if accepted && parentRef.tcpRoute != nil &&
+			len(parentRef.tcpRoute.Status.Parents[parentRef.routeParentStatusIdx].Conditions) == 0 {
+			parentRef.SetCondition(tcpRoute,
+				v1beta1.RouteConditionAccepted,
+				metav1.ConditionTrue,
+				v1beta1.RouteReasonAccepted,
+				"Route is accepted",
+			)
+		}
+		if !accepted {
+			parentRef.SetCondition(tcpRoute,
+				v1beta1.RouteConditionAccepted,
+				metav1.ConditionFalse,
+				v1beta1.RouteReasonUnsupportedValue,
+				"Multiple routes on the same TCP listener",
+			)
+		}
+	}
+}
+
 // processRuleRouteDestination takes a backendRef and translates it into a destination or sets error statuses and
 // returns the weight for the backend so that 500 error responses can be returned for invalid backends in
 // the same proportion as the backend would have otherwise received
diff --git a/internal/gatewayapi/runner/runner.go b/internal/gatewayapi/runner/runner.go
index 71c0edd00..ebe994b08 100644
--- a/internal/gatewayapi/runner/runner.go
+++ b/internal/gatewayapi/runner/runner.go
@@ -111,6 +111,10 @@ func (r *Runner) subscribeAndTranslate(ctx context.Context) {
 				key := utils.NamespacedName(tlsRoute)
 				r.ProviderResources.TLSRouteStatuses.Store(key, tlsRoute)
 			}
+			for _, tcpRoute := range result.TCPRoutes {
+				key := utils.NamespacedName(tcpRoute)
+				r.ProviderResources.TCPRouteStatuses.Store(key, tcpRoute)
+			}
 			for _, udpRoute := range result.UDPRoutes {
 				key := utils.NamespacedName(udpRoute)
 				r.ProviderResources.UDPRouteStatuses.Store(key, udpRoute)
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index 2d70eeb37..71a9b3aa8 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -15,6 +15,7 @@ const (
 	KindHTTPRoute = "HTTPRoute"
 	KindGRPCRoute = "GRPCRoute"
 	KindTLSRoute  = "TLSRoute"
+	KindTCPRoute  = "TCPRoute"
 	KindUDPRoute  = "UDPRoute"
 	KindService   = "Service"
 	KindSecret    = "Secret"
@@ -62,13 +63,14 @@ type TranslateResult struct {
 	Gateways   []*v1beta1.Gateway
 	HTTPRoutes []*v1beta1.HTTPRoute
 	TLSRoutes  []*v1alpha2.TLSRoute
+	TCPRoutes  []*v1alpha2.TCPRoute
 	UDPRoutes  []*v1alpha2.UDPRoute
 	XdsIR      XdsIRMap
 	InfraIR    InfraIRMap
 }
 
 func newTranslateResult(gateways []*GatewayContext,
-	httpRoutes []*HTTPRouteContext, tlsRoutes []*TLSRouteContext, udpRoutes []*UDPRouteContext, xdsIR XdsIRMap,
+	httpRoutes []*HTTPRouteContext, tlsRoutes []*TLSRouteContext, tcpRoutes []*TCPRouteContext, udpRoutes []*UDPRouteContext, xdsIR XdsIRMap,
 	infraIR InfraIRMap) *TranslateResult {
 	translateResult := &TranslateResult{
 		XdsIR:   xdsIR,
@@ -84,6 +86,9 @@ func newTranslateResult(gateways []*GatewayContext,
 	for _, tlsRoute := range tlsRoutes {
 		translateResult.TLSRoutes = append(translateResult.TLSRoutes, tlsRoute.TLSRoute)
 	}
+	for _, tcpRoute := range tcpRoutes {
+		translateResult.TCPRoutes = append(translateResult.TCPRoutes, tcpRoute.TCPRoute)
+	}
 	for _, udpRoute := range udpRoutes {
 		translateResult.UDPRoutes = append(translateResult.UDPRoutes, udpRoute.UDPRoute)
 	}
@@ -107,13 +112,16 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult {
 	// Process all relevant TLSRoutes.
 	tlsRoutes := t.ProcessTLSRoutes(resources.TLSRoutes, gateways, resources, xdsIR)
 
+	// Process all relevant TCPRoutes.
+	tcpRoutes := t.ProcessTCPRoutes(resources.TCPRoutes, gateways, resources, xdsIR)
+
 	// Process all relevant UDPRoutes.
 	udpRoutes := t.ProcessUDPRoutes(resources.UDPRoutes, gateways, resources, xdsIR)
 
 	// Sort xdsIR based on the Gateway API spec
 	sortXdsIRMap(xdsIR)
 
-	return newTranslateResult(gateways, httpRoutes, tlsRoutes, udpRoutes, xdsIR, infraIR)
+	return newTranslateResult(gateways, httpRoutes, tlsRoutes, tcpRoutes, udpRoutes, xdsIR, infraIR)
 }
 
 func (t *Translator) GetRelevantGateways(gateways []*v1beta1.Gateway) []*GatewayContext {
diff --git a/internal/gatewayapi/validate.go b/internal/gatewayapi/validate.go
index 33da36607..0bfabb51f 100644
--- a/internal/gatewayapi/validate.go
+++ b/internal/gatewayapi/validate.go
@@ -419,7 +419,7 @@ func (t *Translator) validateConflictedLayer7Listeners(gateways []*GatewayContex
 	for _, gateway := range gateways {
 		portListenerInfo := map[v1beta1.PortNumber]*portListeners{}
 		for _, listener := range gateway.listeners {
-			if listener.Protocol == v1beta1.UDPProtocolType {
+			if listener.Protocol == v1beta1.UDPProtocolType || listener.Protocol == v1beta1.TCPProtocolType {
 				continue
 			}
 			if portListenerInfo[listener.Port] == nil {
diff --git a/internal/gatewayapi/zz_generated.deepcopy.go b/internal/gatewayapi/zz_generated.deepcopy.go
index 288eeb96e..f6e3ace02 100644
--- a/internal/gatewayapi/zz_generated.deepcopy.go
+++ b/internal/gatewayapi/zz_generated.deepcopy.go
@@ -64,6 +64,17 @@ func (in *Resources) DeepCopyInto(out *Resources) {
 			}
 		}
 	}
+	if in.TCPRoutes != nil {
+		in, out := &in.TCPRoutes, &out.TCPRoutes
+		*out = make([]*v1alpha2.TCPRoute, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(v1alpha2.TCPRoute)
+				(*in).DeepCopyInto(*out)
+			}
+		}
+	}
 	if in.UDPRoutes != nil {
 		in, out := &in.UDPRoutes, &out.UDPRoutes
 		*out = make([]*v1alpha2.UDPRoute, len(*in))
diff --git a/internal/ir/infra.go b/internal/ir/infra.go
index 4209a2f4a..ef8d310d4 100644
--- a/internal/ir/infra.go
+++ b/internal/ir/infra.go
@@ -88,6 +88,9 @@ const (
 	// TLSProtocolType accepts TLS sessions over TCP.
 	TLSProtocolType ProtocolType = "TLS"
 
+	// TCPProtocolType accepts TCP connection.
+	TCPProtocolType ProtocolType = "TCP"
+
 	// UDPProtocolType accepts UDP connection.
 	UDPProtocolType ProtocolType = "UDP"
 )
diff --git a/internal/message/types.go b/internal/message/types.go
index 7b8542cc8..4efef215c 100644
--- a/internal/message/types.go
+++ b/internal/message/types.go
@@ -26,6 +26,7 @@ type ProviderResources struct {
 	HTTPRouteStatuses watchable.Map[types.NamespacedName, *gwapiv1b1.HTTPRoute]
 	GRPCRouteStatuses watchable.Map[types.NamespacedName, *gwapiv1a2.GRPCRoute]
 	TLSRouteStatuses  watchable.Map[types.NamespacedName, *gwapiv1a2.TLSRoute]
+	TCPRouteStatuses  watchable.Map[types.NamespacedName, *gwapiv1a2.TCPRoute]
 	UDPRouteStatuses  watchable.Map[types.NamespacedName, *gwapiv1a2.UDPRoute]
 }
 
@@ -54,6 +55,7 @@ func (p *ProviderResources) Close() {
 	p.GatewayStatuses.Close()
 	p.HTTPRouteStatuses.Close()
 	p.TLSRouteStatuses.Close()
+	p.TCPRouteStatuses.Close()
 	p.UDPRouteStatuses.Close()
 }
 
diff --git a/internal/provider/kubernetes/config/rbac/role.yaml b/internal/provider/kubernetes/config/rbac/role.yaml
index 5cb91cea5..51908e1b2 100644
--- a/internal/provider/kubernetes/config/rbac/role.yaml
+++ b/internal/provider/kubernetes/config/rbac/role.yaml
@@ -41,6 +41,7 @@ rules:
   - httproutes
   - referencegrants
   - referencepolicies
+  - tcproutes
   - tlsroutes
   - udproutes
   verbs:
@@ -55,6 +56,7 @@ rules:
   - gateways/status
   - grpcroutes/status
   - httproutes/status
+  - tcproutes/status
   - tlsroutes/status
   - udproutes/status
   verbs:
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index ce3fff30f..b80ddfb75 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -40,12 +40,14 @@ const (
 	gatewayTLSRouteIndex       = "gatewayTLSRouteIndex"
 	gatewayHTTPRouteIndex      = "gatewayHTTPRouteIndex"
 	gatewayGRPCRouteIndex      = "gatewayGRPCRouteIndex"
+	gatewayTCPRouteIndex       = "gatewayTCPRouteIndex"
 	gatewayUDPRouteIndex       = "gatewayUDPRouteIndex"
 	secretGatewayIndex         = "secretGatewayIndex"
 	targetRefGrantRouteIndex   = "targetRefGrantRouteIndex"
 	serviceHTTPRouteIndex      = "serviceHTTPRouteIndex"
 	serviceGRPCRouteIndex      = "serviceGRPCRouteIndex"
 	serviceTLSRouteIndex       = "serviceTLSRouteIndex"
+	serviceTCPRouteIndex       = "serviceTCPRouteIndex"
 	serviceUDPRouteIndex       = "serviceUDPRouteIndex"
 	authenFilterHTTPRouteIndex = "authenHTTPRouteIndex"
 )
@@ -82,117 +84,10 @@ func newGatewayAPIController(mgr manager.Manager, cfg *config.Server, su status.
 	// Subscribe to status updates
 	r.subscribeAndUpdateStatus(ctx)
 
-	// Only enqueue GatewayClass objects that match this Envoy Gateway's controller name.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1b1.GatewayClass{}},
-		&handler.EnqueueRequestForObject{},
-		predicate.NewPredicateFuncs(r.hasMatchingController),
-	); err != nil {
-		return err
-	}
-
-	// Watch Gateway CRUDs and reconcile affected GatewayClass.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1b1.Gateway{}},
-		&handler.EnqueueRequestForObject{},
-		predicate.NewPredicateFuncs(r.validateGatewayForReconcile),
-	); err != nil {
-		return err
-	}
-	if err := addGatewayIndexers(ctx, mgr); err != nil {
-		return err
-	}
-
-	// Watch HTTPRoute CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1b1.HTTPRoute{}},
-		&handler.EnqueueRequestForObject{},
-	); err != nil {
-		return err
-	}
-	if err := addHTTPRouteIndexers(ctx, mgr); err != nil {
-		return err
-	}
-
-	// Watch GRPCRoute CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1a2.GRPCRoute{}},
-		&handler.EnqueueRequestForObject{},
-	); err != nil {
-		return err
-	}
-	if err := addGRPCRouteIndexers(ctx, mgr); err != nil {
-		return err
-	}
-
-	// Watch TLSRoute CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1a2.TLSRoute{}},
-		&handler.EnqueueRequestForObject{},
-	); err != nil {
-		return err
-	}
-	if err := addTLSRouteIndexers(ctx, mgr); err != nil {
-		return err
-	}
-
-	// Watch UDPRoute CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1a2.UDPRoute{}},
-		&handler.EnqueueRequestForObject{},
-	); err != nil {
-		return err
-	}
-	if err := addUDPRouteIndexers(ctx, mgr); err != nil {
+	// watch gateway api resources
+	if err := r.watchResources(ctx, mgr, c); err != nil {
 		return err
 	}
-
-	// Watch Service CRUDs and process affected *Route objects.
-	if err := c.Watch(
-		&source.Kind{Type: &corev1.Service{}},
-		&handler.EnqueueRequestForObject{},
-		predicate.NewPredicateFuncs(r.validateServiceForReconcile)); err != nil {
-		return err
-	}
-
-	// Watch Secret CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &corev1.Secret{}},
-		&handler.EnqueueRequestForObject{},
-		predicate.NewPredicateFuncs(r.validateSecretForReconcile),
-	); err != nil {
-		return err
-	}
-
-	// Watch ReferenceGrant CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &gwapiv1a2.ReferenceGrant{}},
-		&handler.EnqueueRequestForObject{},
-	); err != nil {
-		return err
-	}
-	if err := addReferenceGrantIndexers(ctx, mgr); err != nil {
-		return err
-	}
-
-	// Watch Deployment CRUDs and process affected Gateways.
-	if err := c.Watch(
-		&source.Kind{Type: &appsv1.Deployment{}},
-		&handler.EnqueueRequestForObject{},
-		predicate.NewPredicateFuncs(r.validateDeploymentForReconcile),
-	); err != nil {
-		return err
-	}
-
-	// Watch AuthenticationFilter CRUDs and enqueue associated HTTPRoute objects.
-	if err := c.Watch(
-		&source.Kind{Type: &egv1a1.AuthenticationFilter{}},
-		&handler.EnqueueRequestForObject{},
-		predicate.NewPredicateFuncs(r.httpRoutesForAuthenticationFilter)); err != nil {
-		return err
-	}
-
-	r.log.Info("watching gatewayAPI related objects")
 	return nil
 }
 
@@ -253,34 +148,9 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile.
 		return reconcile.Result{}, nil
 	}
 
-	updater := func(gc *gwapiv1b1.GatewayClass, accepted bool) error {
-		if r.statusUpdater != nil {
-			r.statusUpdater.Send(status.Update{
-				NamespacedName: types.NamespacedName{Name: gc.Name},
-				Resource:       &gwapiv1b1.GatewayClass{},
-				Mutator: status.MutatorFunc(func(obj client.Object) client.Object {
-					gc, ok := obj.(*gwapiv1b1.GatewayClass)
-					if !ok {
-						panic(fmt.Sprintf("unsupported object type %T", obj))
-					}
-
-					return status.SetGatewayClassAccepted(gc.DeepCopy(), accepted)
-				}),
-			})
-		} else {
-			// this branch makes testing easier by not going through the status.Updater.
-			copy := status.SetGatewayClassAccepted(gc.DeepCopy(), accepted)
-
-			if err := r.client.Status().Update(ctx, copy); err != nil && !kerrors.IsNotFound(err) {
-				return fmt.Errorf("error updating status of gatewayclass %s: %w", copy.Name, err)
-			}
-		}
-		return nil
-	}
-
 	// Update status for all gateway classes
 	for _, gc := range cc.notAcceptedClasses() {
-		if err := updater(gc, false); err != nil {
+		if err := r.gatewayClassUpdater(ctx, gc, false); err != nil {
 			r.resources.GatewayAPIResources.Delete(acceptedGC.Name)
 			return reconcile.Result{}, err
 		}
@@ -290,98 +160,10 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile.
 	resourceTree := gatewayapi.NewResources()
 	resourceMap := newResourceMapping()
 
-	// Find gateways for the acceptedGC
-	// Find the Gateways that reference this Class.
-	gatewayList := &gwapiv1b1.GatewayList{}
-	if err := r.client.List(ctx, gatewayList, &client.ListOptions{
-		FieldSelector: fields.OneTermEqualSelector(classGatewayIndex, acceptedGC.Name),
-	}); err != nil {
-		r.log.Info("no associated Gateways found for GatewayClass", "name", acceptedGC.Name)
+	if err := r.processGateways(ctx, acceptedGC, resourceMap, resourceTree); err != nil {
 		return reconcile.Result{}, err
 	}
 
-	for _, gtw := range gatewayList.Items {
-		gtw := gtw
-		r.log.Info("processing Gateway", "namespace", gtw.Namespace, "name", gtw.Name)
-		resourceMap.allAssociatedNamespaces[gtw.Namespace] = struct{}{}
-
-		for _, listener := range gtw.Spec.Listeners {
-			listener := listener
-			// Get Secret for gateway if it exists.
-			if terminatesTLS(&listener) {
-				for _, certRef := range listener.TLS.CertificateRefs {
-					certRef := certRef
-					if refsSecret(&certRef) {
-						secret := new(corev1.Secret)
-						secretNamespace := gatewayapi.NamespaceDerefOr(certRef.Namespace, gtw.Namespace)
-						err := r.client.Get(ctx,
-							types.NamespacedName{Namespace: secretNamespace, Name: string(certRef.Name)},
-							secret,
-						)
-						if err != nil && !kerrors.IsNotFound(err) {
-							r.log.Error(err, "unable to find Secret")
-							return reconcile.Result{}, err
-						}
-
-						r.log.Info("processing Secret", "namespace", secretNamespace, "name", string(certRef.Name))
-
-						if secretNamespace != gtw.Namespace {
-							from := ObjectKindNamespacedName{
-								kind:      gatewayapi.KindGateway,
-								namespace: gtw.Namespace,
-								name:      gtw.Name,
-							}
-							to := ObjectKindNamespacedName{
-								kind:      gatewayapi.KindSecret,
-								namespace: secretNamespace,
-								name:      string(certRef.Name),
-							}
-							refGrant, err := r.findReferenceGrant(ctx, from, to)
-							switch {
-							case err != nil:
-								r.log.Error(err, "failed to find ReferenceGrant")
-							case refGrant == nil:
-								r.log.Info("no matching ReferenceGrants found", "from", from.kind,
-									"from namespace", from.namespace, "target", to.kind, "target namespace", to.namespace)
-							default:
-								// RefGrant found
-								resourceMap.allAssociatedRefGrants[utils.NamespacedName(refGrant)] = refGrant
-								r.log.Info("added ReferenceGrant to resource map", "namespace", refGrant.Namespace,
-									"name", refGrant.Name)
-							}
-						}
-
-						resourceMap.allAssociatedNamespaces[secretNamespace] = struct{}{}
-						resourceTree.Secrets = append(resourceTree.Secrets, secret)
-					}
-				}
-			}
-		}
-
-		// Route Processing
-		// Get TLSRoute objects and check if it exists.
-		if err := r.processTLSRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
-			return reconcile.Result{}, err
-		}
-
-		// Get HTTPRoute objects and check if it exists.
-		if err := r.processHTTPRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
-			return reconcile.Result{}, err
-		}
-
-		// Get GRPCRoute objects and check if it exists.
-		if err := r.processGRPCRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
-			return reconcile.Result{}, err
-		}
-
-		// Get UDPRoute objects and check if it exists.
-		if err := r.processUDPRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
-			return reconcile.Result{}, err
-		}
-
-		resourceTree.Gateways = append(resourceTree.Gateways, &gtw)
-	}
-
 	for serviceNamespaceName := range resourceMap.allAssociatedBackendRefs {
 		r.log.Info("processing Service", "namespace", serviceNamespaceName.Namespace,
 			"name", serviceNamespaceName.Name)
@@ -426,7 +208,7 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile.
 		}
 	}
 
-	if err := updater(acceptedGC, true); err != nil {
+	if err := r.gatewayClassUpdater(ctx, acceptedGC, true); err != nil {
 		r.log.Error(err, "unable to update GatewayClass status")
 		return reconcile.Result{}, err
 	}
@@ -459,6 +241,31 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, request reconcile.
 	return reconcile.Result{}, nil
 }
 
+func (r *gatewayAPIReconciler) gatewayClassUpdater(ctx context.Context, gc *gwapiv1b1.GatewayClass, accepted bool) error {
+	if r.statusUpdater != nil {
+		r.statusUpdater.Send(status.Update{
+			NamespacedName: types.NamespacedName{Name: gc.Name},
+			Resource:       &gwapiv1b1.GatewayClass{},
+			Mutator: status.MutatorFunc(func(obj client.Object) client.Object {
+				gc, ok := obj.(*gwapiv1b1.GatewayClass)
+				if !ok {
+					panic(fmt.Sprintf("unsupported object type %T", obj))
+				}
+
+				return status.SetGatewayClassAccepted(gc.DeepCopy(), accepted)
+			}),
+		})
+	} else {
+		// this branch makes testing easier by not going through the status.Updater.
+		copy := status.SetGatewayClassAccepted(gc.DeepCopy(), accepted)
+
+		if err := r.client.Status().Update(ctx, copy); err != nil && !kerrors.IsNotFound(err) {
+			return fmt.Errorf("error updating status of gatewayclass %s: %w", copy.Name, err)
+		}
+	}
+	return nil
+}
+
 func (r *gatewayAPIReconciler) getNamespace(ctx context.Context, name string) (*corev1.Namespace, error) {
 	nsKey := types.NamespacedName{Name: name}
 	ns := new(corev1.Namespace)
@@ -535,6 +342,106 @@ func (r *gatewayAPIReconciler) getAuthenticationFilter(ctx context.Context, ns,
 	return filter, nil
 }
 
+func (r *gatewayAPIReconciler) processGateways(ctx context.Context, acceptedGC *gwapiv1b1.GatewayClass, resourceMap *resourceMappings, resourceTree *gatewayapi.Resources) error {
+	// Find gateways for the acceptedGC
+	// Find the Gateways that reference this Class.
+	gatewayList := &gwapiv1b1.GatewayList{}
+	if err := r.client.List(ctx, gatewayList, &client.ListOptions{
+		FieldSelector: fields.OneTermEqualSelector(classGatewayIndex, acceptedGC.Name),
+	}); err != nil {
+		r.log.Info("no associated Gateways found for GatewayClass", "name", acceptedGC.Name)
+		return err
+	}
+
+	for _, gtw := range gatewayList.Items {
+		gtw := gtw
+		r.log.Info("processing Gateway", "namespace", gtw.Namespace, "name", gtw.Name)
+		resourceMap.allAssociatedNamespaces[gtw.Namespace] = struct{}{}
+
+		for _, listener := range gtw.Spec.Listeners {
+			listener := listener
+			// Get Secret for gateway if it exists.
+			if terminatesTLS(&listener) {
+				for _, certRef := range listener.TLS.CertificateRefs {
+					certRef := certRef
+					if refsSecret(&certRef) {
+						secret := new(corev1.Secret)
+						secretNamespace := gatewayapi.NamespaceDerefOr(certRef.Namespace, gtw.Namespace)
+						err := r.client.Get(ctx,
+							types.NamespacedName{Namespace: secretNamespace, Name: string(certRef.Name)},
+							secret,
+						)
+						if err != nil && !kerrors.IsNotFound(err) {
+							r.log.Error(err, "unable to find Secret")
+							return err
+						}
+
+						r.log.Info("processing Secret", "namespace", secretNamespace, "name", string(certRef.Name))
+
+						if secretNamespace != gtw.Namespace {
+							from := ObjectKindNamespacedName{
+								kind:      gatewayapi.KindGateway,
+								namespace: gtw.Namespace,
+								name:      gtw.Name,
+							}
+							to := ObjectKindNamespacedName{
+								kind:      gatewayapi.KindSecret,
+								namespace: secretNamespace,
+								name:      string(certRef.Name),
+							}
+							refGrant, err := r.findReferenceGrant(ctx, from, to)
+							switch {
+							case err != nil:
+								r.log.Error(err, "failed to find ReferenceGrant")
+							case refGrant == nil:
+								r.log.Info("no matching ReferenceGrants found", "from", from.kind,
+									"from namespace", from.namespace, "target", to.kind, "target namespace", to.namespace)
+							default:
+								// RefGrant found
+								resourceMap.allAssociatedRefGrants[utils.NamespacedName(refGrant)] = refGrant
+								r.log.Info("added ReferenceGrant to resource map", "namespace", refGrant.Namespace,
+									"name", refGrant.Name)
+							}
+						}
+
+						resourceMap.allAssociatedNamespaces[secretNamespace] = struct{}{}
+						resourceTree.Secrets = append(resourceTree.Secrets, secret)
+					}
+				}
+			}
+		}
+
+		// Route Processing
+		// Get TLSRoute objects and check if it exists.
+		if err := r.processTLSRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
+			return err
+		}
+
+		// Get HTTPRoute objects and check if it exists.
+		if err := r.processHTTPRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
+			return err
+		}
+
+		// Get GRPCRoute objects and check if it exists.
+		if err := r.processGRPCRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
+			return err
+		}
+
+		// Get TCPRoute objects and check if it exists.
+		if err := r.processTCPRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
+			return err
+		}
+
+		// Get UDPRoute objects and check if it exists.
+		if err := r.processUDPRoutes(ctx, utils.NamespacedName(&gtw).String(), resourceMap, resourceTree); err != nil {
+			return err
+		}
+
+		resourceTree.Gateways = append(resourceTree.Gateways, &gtw)
+	}
+	return nil
+}
+
 func addReferenceGrantIndexers(ctx context.Context, mgr manager.Manager) error {
 	if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.ReferenceGrant{}, targetRefGrantRouteIndex, func(rawObj client.Object) []string {
 		refGrant := rawObj.(*gwapiv1a2.ReferenceGrant)
@@ -731,6 +638,56 @@ func serviceTLSRouteIndexFunc(rawObj client.Object) []string {
 	return services
 }
 
+// addTCPRouteIndexers adds indexing on TCPRoute, for Service objects that are
+// referenced in TCPRoute objects via `.spec.rules.backendRefs`. This helps in
+// querying for TCPRoutes that are affected by a particular Service CRUD.
+func addTCPRouteIndexers(ctx context.Context, mgr manager.Manager) error {
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.TCPRoute{}, gatewayTCPRouteIndex, func(rawObj client.Object) []string {
+		tcpRoute := rawObj.(*gwapiv1a2.TCPRoute)
+		var gateways []string
+		for _, parent := range tcpRoute.Spec.ParentRefs {
+			if string(*parent.Kind) == gatewayapi.KindGateway {
+				// If an explicit Gateway namespace is not provided, use the TCPRoute namespace to
+				// lookup the provided Gateway Name.
+				gateways = append(gateways,
+					types.NamespacedName{
+						Namespace: gatewayapi.NamespaceDerefOrAlpha(parent.Namespace, tcpRoute.Namespace),
+						Name:      string(parent.Name),
+					}.String(),
+				)
+			}
+		}
+		return gateways
+	}); err != nil {
+		return err
+	}
+
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &gwapiv1a2.TCPRoute{}, serviceTCPRouteIndex, serviceTCPRouteIndexFunc); err != nil {
+		return err
+	}
+	return nil
+}
+
+func serviceTCPRouteIndexFunc(rawObj client.Object) []string {
+	tcpRoute := rawObj.(*gwapiv1a2.TCPRoute)
+	var services []string
+	for _, rule := range tcpRoute.Spec.Rules {
+		for _, backend := range rule.BackendRefs {
+			if backend.Kind == nil || string(*backend.Kind) == gatewayapi.KindService {
+				// If an explicit Service namespace is not provided, use the TCPRoute namespace to
+				// lookup the provided Gateway Name.
+				services = append(services,
+					types.NamespacedName{
+						Namespace: gatewayapi.NamespaceDerefOrAlpha(backend.Namespace, tcpRoute.Namespace),
+						Name:      string(backend.Name),
+					}.String(),
+				)
+			}
+		}
+	}
+	return services
+}
+
 // addUDPRouteIndexers adds indexing on UDPRoute, for Service objects that are
 // referenced in UDPRoute objects via `.spec.rules.backendRefs`. This helps in
 // querying for UDPRoutes that are affected by a particular Service CRUD.
@@ -986,6 +943,34 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context) {
 		r.log.Info("tlsRoute status subscriber shutting down")
 	}()
 
+	// TCPRoute object status updater
+	go func() {
+		message.HandleSubscription(r.resources.TCPRouteStatuses.Subscribe(ctx),
+			func(update message.Update[types.NamespacedName, *gwapiv1a2.TCPRoute]) {
+				// skip delete updates.
+				if update.Delete {
+					return
+				}
+				key := update.Key
+				val := update.Value
+				r.statusUpdater.Send(status.Update{
+					NamespacedName: key,
+					Resource:       new(gwapiv1a2.TCPRoute),
+					Mutator: status.MutatorFunc(func(obj client.Object) client.Object {
+						t, ok := obj.(*gwapiv1a2.TCPRoute)
+						if !ok {
+							panic(fmt.Sprintf("unsupported object type %T", obj))
+						}
+						tCopy := t.DeepCopy()
+						tCopy.Status.Parents = val.Status.Parents
+						return tCopy
+					}),
+				})
+			},
+		)
+		r.log.Info("tcpRoute status subscriber shutting down")
+	}()
+
 	// UDPRoute object status updater
 	go func() {
 		message.HandleSubscription(r.resources.UDPRouteStatuses.Subscribe(ctx),
@@ -1014,3 +999,130 @@ func (r *gatewayAPIReconciler) subscribeAndUpdateStatus(ctx context.Context) {
 		r.log.Info("udpRoute status subscriber shutting down")
 	}()
 }
+
+// watchResources watches gateway api resources.
+func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.Manager, c controller.Controller) error {
+	// Only enqueue GatewayClass objects that match this Envoy Gateway's controller name.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1b1.GatewayClass{}},
+		&handler.EnqueueRequestForObject{},
+		predicate.NewPredicateFuncs(r.hasMatchingController),
+	); err != nil {
+		return err
+	}
+
+	// Watch Gateway CRUDs and reconcile affected GatewayClass.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1b1.Gateway{}},
+		&handler.EnqueueRequestForObject{},
+		predicate.NewPredicateFuncs(r.validateGatewayForReconcile),
+	); err != nil {
+		return err
+	}
+	if err := addGatewayIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch HTTPRoute CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1b1.HTTPRoute{}},
+		&handler.EnqueueRequestForObject{},
+	); err != nil {
+		return err
+	}
+	if err := addHTTPRouteIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch GRPCRoute CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1a2.GRPCRoute{}},
+		&handler.EnqueueRequestForObject{},
+	); err != nil {
+		return err
+	}
+	if err := addGRPCRouteIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch TLSRoute CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1a2.TLSRoute{}},
+		&handler.EnqueueRequestForObject{},
+	); err != nil {
+		return err
+	}
+	if err := addTLSRouteIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch UDPRoute CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1a2.UDPRoute{}},
+		&handler.EnqueueRequestForObject{},
+	); err != nil {
+		return err
+	}
+	if err := addUDPRouteIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch TCPRoute CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1a2.TCPRoute{}},
+		&handler.EnqueueRequestForObject{},
+	); err != nil {
+		return err
+	}
+	if err := addTCPRouteIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch Service CRUDs and process affected *Route objects.
+	if err := c.Watch(
+		&source.Kind{Type: &corev1.Service{}},
+		&handler.EnqueueRequestForObject{},
+		predicate.NewPredicateFuncs(r.validateServiceForReconcile)); err != nil {
+		return err
+	}
+
+	// Watch Secret CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &corev1.Secret{}},
+		&handler.EnqueueRequestForObject{},
+		predicate.NewPredicateFuncs(r.validateSecretForReconcile),
+	); err != nil {
+		return err
+	}
+
+	// Watch ReferenceGrant CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &gwapiv1a2.ReferenceGrant{}},
+		&handler.EnqueueRequestForObject{},
+	); err != nil {
+		return err
+	}
+	if err := addReferenceGrantIndexers(ctx, mgr); err != nil {
+		return err
+	}
+
+	// Watch Deployment CRUDs and process affected Gateways.
+	if err := c.Watch(
+		&source.Kind{Type: &appsv1.Deployment{}},
+		&handler.EnqueueRequestForObject{},
+		predicate.NewPredicateFuncs(r.validateDeploymentForReconcile),
+	); err != nil {
+		return err
+	}
+
+	// Watch AuthenticationFilter CRUDs and enqueue associated HTTPRoute objects.
+	if err := c.Watch(
+		&source.Kind{Type: &egv1a1.AuthenticationFilter{}},
+		&handler.EnqueueRequestForObject{},
+		predicate.NewPredicateFuncs(r.httpRoutesForAuthenticationFilter)); err != nil {
+		return err
+	}
+
+	r.log.Info("watching gatewayAPI related objects")
+	return nil
+}
diff --git a/internal/provider/kubernetes/predicates.go b/internal/provider/kubernetes/predicates.go
index 6881321b5..85fcd0969 100644
--- a/internal/provider/kubernetes/predicates.go
+++ b/internal/provider/kubernetes/predicates.go
@@ -149,6 +149,14 @@ func (r *gatewayAPIReconciler) validateServiceForReconcile(obj client.Object) bo
 		return false
 	}
 
+	tcpRouteList := &gwapiv1a2.TCPRouteList{}
+	if err := r.client.List(ctx, tcpRouteList, &client.ListOptions{
+		FieldSelector: fields.OneTermEqualSelector(serviceTCPRouteIndex, utils.NamespacedName(svc).String()),
+	}); err != nil {
+		r.log.Error(err, "unable to find associated TCPRoutes")
+		return false
+	}
+
 	udpRouteList := &gwapiv1a2.UDPRouteList{}
 	if err := r.client.List(ctx, udpRouteList, &client.ListOptions{
 		FieldSelector: fields.OneTermEqualSelector(serviceUDPRouteIndex, utils.NamespacedName(svc).String()),
@@ -161,6 +169,7 @@ func (r *gatewayAPIReconciler) validateServiceForReconcile(obj client.Object) bo
 	allAssociatedRoutes := len(httpRouteList.Items) +
 		len(grpcRouteList.Items) +
 		len(tlsRouteList.Items) +
+		len(tcpRouteList.Items) +
 		len(udpRouteList.Items)
 
 	return allAssociatedRoutes != 0
diff --git a/internal/provider/kubernetes/rbac.go b/internal/provider/kubernetes/rbac.go
index c8f284085..a7ce8f5be 100644
--- a/internal/provider/kubernetes/rbac.go
+++ b/internal/provider/kubernetes/rbac.go
@@ -5,8 +5,8 @@
 
 package kubernetes
 
-// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;grpcroutes;tlsroutes;udproutes;referencepolicies;referencegrants,verbs=get;list;watch;update
-// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;grpcroutes/status;tlsroutes/status;udproutes/status,verbs=update
+// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses;gateways;httproutes;grpcroutes;tlsroutes;tcproutes;udproutes;referencepolicies;referencegrants,verbs=get;list;watch;update
+// +kubebuilder:rbac:groups="gateway.networking.k8s.io",resources=gatewayclasses/status;gateways/status;httproutes/status;grpcroutes/status;tlsroutes/status;tcproutes/status;udproutes/status,verbs=update
 // +kubebuilder:rbac:groups="gateway.envoyproxy.io",resources=authenticationfilters,verbs=get;list;watch;update
 
 // RBAC for watched resources of Gateway API controllers.
diff --git a/internal/provider/kubernetes/routes.go b/internal/provider/kubernetes/routes.go
index 8c36e7622..1cc480403 100644
--- a/internal/provider/kubernetes/routes.go
+++ b/internal/provider/kubernetes/routes.go
@@ -233,6 +233,63 @@ func (r *gatewayAPIReconciler) processHTTPRoutes(ctx context.Context, gatewayNam
 	return nil
 }
 
+// processTCPRoutes finds TCPRoutes corresponding to a gatewayNamespaceName, further checks for
+// the backend references and pushes the TCPRoutes to the resourceTree.
+func (r *gatewayAPIReconciler) processTCPRoutes(ctx context.Context, gatewayNamespaceName string,
+	resourceMap *resourceMappings, resourceTree *gatewayapi.Resources) error {
+	tcpRouteList := &gwapiv1a2.TCPRouteList{}
+	if err := r.client.List(ctx, tcpRouteList, &client.ListOptions{
+		FieldSelector: fields.OneTermEqualSelector(gatewayTCPRouteIndex, gatewayNamespaceName),
+	}); err != nil {
+		r.log.Error(err, "unable to find associated UDPRoutes")
+		return err
+	}
+
+	for _, tcpRoute := range tcpRouteList.Items {
+		tcpRoute := tcpRoute
+		r.log.Info("processing TCPRoute", "namespace", tcpRoute.Namespace, "name", tcpRoute.Name)
+
+		for _, rule := range tcpRoute.Spec.Rules {
+			for _, backendRef := range rule.BackendRefs {
+				backendRef := backendRef
+				ref := gatewayapi.UpgradeBackendRef(backendRef)
+				if err := validateBackendRef(&ref); err != nil {
+					r.log.Error(err, "invalid backendRef")
+					continue
+				}
+
+				backendNamespace := gatewayapi.NamespaceDerefOrAlpha(backendRef.Namespace, tcpRoute.Namespace)
+				resourceMap.allAssociatedBackendRefs[types.NamespacedName{
+					Namespace: backendNamespace,
+					Name:      string(backendRef.Name),
+				}] = struct{}{}
+
+				if backendNamespace != tcpRoute.Namespace {
+					from := ObjectKindNamespacedName{kind: gatewayapi.KindTCPRoute, namespace: tcpRoute.Namespace, name: tcpRoute.Name}
+					to := ObjectKindNamespacedName{kind: gatewayapi.KindService, namespace: backendNamespace, name: string(backendRef.Name)}
+					refGrant, err := r.findReferenceGrant(ctx, from, to)
+					switch {
+					case err != nil:
+						r.log.Error(err, "failed to find ReferenceGrant")
+					case refGrant == nil:
+						r.log.Info("no matching ReferenceGrants found", "from", from.kind,
+							"from namespace", from.namespace, "target", to.kind, "target namespace", to.namespace)
+					default:
+						resourceMap.allAssociatedRefGrants[utils.NamespacedName(refGrant)] = refGrant
+						r.log.Info("added ReferenceGrant to resource map", "namespace", refGrant.Namespace,
+							"name", refGrant.Name)
+					}
+				}
+			}
+		}
+
+		resourceMap.allAssociatedNamespaces[tcpRoute.Namespace] = struct{}{}
+		resourceTree.TCPRoutes = append(resourceTree.TCPRoutes, &tcpRoute)
+	}
+
+	return nil
+}
+
 // processUDPRoutes finds UDPRoutes corresponding to a gatewayNamespaceName, further checks for
 // the backend references and pushes the UDPRoutes to the resourceTree.
 func (r *gatewayAPIReconciler) processUDPRoutes(ctx context.Context, gatewayNamespaceName string,
diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go
index 957e94607..ce3d35e7b 100644
--- a/internal/xds/translator/listener.go
+++ b/internal/xds/translator/listener.go
@@ -19,6 +19,7 @@ import (
 	tcp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
 	udp "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3"
 	tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
+	"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
 	"github.com/envoyproxy/go-control-plane/pkg/wellknown"
 	"google.golang.org/protobuf/types/known/anypb"
 
@@ -359,3 +360,22 @@ func buildXdsUDPListener(clusterName string, udpListener *ir.UDPListener) (*list
 
 	return xdsListener, nil
 }
+
+// Point to xds cluster.
+func makeConfigSource() *core.ConfigSource {
+	source := &core.ConfigSource{}
+	source.ResourceApiVersion = resource.DefaultAPIVersion
+	source.ConfigSourceSpecifier = &core.ConfigSource_ApiConfigSource{
+		ApiConfigSource: &core.ApiConfigSource{
+			TransportApiVersion:       resource.DefaultAPIVersion,
+			ApiType:                   core.ApiConfigSource_DELTA_GRPC,
+			SetNodeOnFirstMessageOnly: true,
+			GrpcServices: []*core.GrpcService{{
+				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
+					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "xds_cluster"},
+				},
+			}},
+		},
+	}
+	return source
+}
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index 78f0b241f..a3bb2795d 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -26,7 +26,23 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) {
 
 	tCtx := new(types.ResourceVersionTable)
 
-	for _, httpListener := range ir.HTTP {
+	if err := processHTTPListenerXdsTranslation(tCtx, ir.HTTP); err != nil {
+		return nil, err
+	}
+
+	if err := processTCPListenerXdsTranslation(tCtx, ir.TCP); err != nil {
+		return nil, err
+	}
+
+	if err := processUDPListenerXdsTranslation(tCtx, ir.UDP); err != nil {
+		return nil, err
+	}
+
+	return tCtx, nil
+}
+
+func processHTTPListenerXdsTranslation(tCtx *types.ResourceVersionTable, httpListeners []*ir.HTTPListener) error {
+	for _, httpListener := range httpListeners {
 		addFilterChain := true
 		var xdsRouteCfg *route.RouteConfiguration
 
@@ -46,14 +62,14 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) {
 				addFilterChain = false
 				xdsRouteCfg = findXdsRouteConfig(tCtx, routeName)
 				if xdsRouteCfg == nil {
-					return nil, errors.New("unable to find xds route config")
+					return errors.New("unable to find xds route config")
 				}
 			}
 		}
 
 		if addFilterChain {
 			if err := addXdsHTTPFilterChain(xdsListener, httpListener); err != nil {
-				return nil, err
+				return err
 			}
 		}
 
@@ -89,13 +105,15 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) {
 			}
 			xdsCluster := buildXdsCluster(httpRoute.Name, httpRoute.Destinations, httpListener.IsHTTP2)
 			tCtx.AddXdsResource(resource.ClusterType, xdsCluster)
-
 		}
 
 		xdsRouteCfg.VirtualHosts = append(xdsRouteCfg.VirtualHosts, vHost)
 	}
+	return nil
+}
 
-	for _, tcpListener := range ir.TCP {
+func processTCPListenerXdsTranslation(tCtx *types.ResourceVersionTable, tcpListeners []*ir.TCPListener) error {
+	for _, tcpListener := range tcpListeners {
 		// 1:1 between IR TCPListener and xDS Cluster
 		xdsCluster := buildXdsCluster(tcpListener.Name, tcpListener.Destinations, false /*isHTTP2 */)
 		tCtx.AddXdsResource(resource.ClusterType, xdsCluster)
@@ -108,11 +126,14 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) {
 		}
 
 		if err := addXdsTCPFilterChain(xdsListener, tcpListener, xdsCluster.Name); err != nil {
-			return nil, err
+			return err
 		}
 	}
+	return nil
+}
 
-	for _, udpListener := range ir.UDP {
+func processUDPListenerXdsTranslation(tCtx *types.ResourceVersionTable, udpListeners []*ir.UDPListener) error {
+	for _, udpListener := range udpListeners {
 		// 1:1 between IR UDPListener and xDS Cluster
 		xdsCluster := buildXdsCluster(udpListener.Name, udpListener.Destinations, false /*isHTTP2 */)
 		tCtx.AddXdsResource(resource.ClusterType, xdsCluster)
@@ -121,11 +142,11 @@ func Translate(ir *ir.Xds) (*types.ResourceVersionTable, error) {
 		// translator
 		xdsListener, err := buildXdsUDPListener(xdsCluster.Name, udpListener)
 		if err != nil {
-			return nil, multierror.Append(err, errors.New("error building xds cluster"))
+			return multierror.Append(err, errors.New("error building xds cluster"))
 		}
 		tCtx.AddXdsResource(resource.ListenerType, xdsListener)
 	}
-	return tCtx, nil
+	return nil
 }
 
 // findXdsListener finds a xds listener with the same address, port and protocol, and returns nil if there is no match.
@@ -162,22 +183,3 @@ func findXdsRouteConfig(tCtx *types.ResourceVersionTable, name string) *route.Ro
 
 	return nil
 }
-
-// Point to xds cluster.
-func makeConfigSource() *core.ConfigSource {
-	source := &core.ConfigSource{}
-	source.ResourceApiVersion = resource.DefaultAPIVersion
-	source.ConfigSourceSpecifier = &core.ConfigSource_ApiConfigSource{
-		ApiConfigSource: &core.ApiConfigSource{
-			TransportApiVersion:       resource.DefaultAPIVersion,
-			ApiType:                   core.ApiConfigSource_DELTA_GRPC,
-			SetNodeOnFirstMessageOnly: true,
-			GrpcServices: []*core.GrpcService{{
-				TargetSpecifier: &core.GrpcService_EnvoyGrpc_{
-					EnvoyGrpc: &core.GrpcService_EnvoyGrpc{ClusterName: "xds_cluster"},
-				},
-			}},
-		},
-	}
-	return source
-}


Test patch:
diff --git a/docs/latest/design/egctl.md b/docs/latest/design/egctl.md
new file mode 100644
index 000000000..53a5e7b99
--- /dev/null
+++ b/docs/latest/design/egctl.md
@@ -0,0 +1,56 @@
+# Introduce egctl
+
+## Motivation
+
+EG should provide a command line tool with following capabilities:
+
+- Collect configuration from envoy proxy and gateway
+- Analyse system configuration to diagnose any issues in envoy gateway
+
+This tool is named `egctl`.
+
+## Syntax
+
+Use the following syntax to run `egctl` commands from your terminal window:
+
+```console
+egctl [command] [entity] [name] [flags]
+```
+
+where `command`, `name`, and `flags` are:
+
+* `command`: Specifies the operation that you want to perform on one or more resources,
+  for example `config`, `version`.
+
+* `entity`: Specifies the entity the operation is being performed on such as `envoy-proxy` or `envoy-gateway`.
+
+* `name`: Specifies the name of the specified instance. 
+
+* `flags`: Specifies optional flags. For example, you can use the `-c` or `--config` flags to specify the values for installing.
+
+If you need help, run `egctl help` from the terminal window.
+
+## Operation
+
+The following table includes short descriptions and the general syntax for all the `egctl` operations:
+
+| Operation | Syntax                           | Description                                                                 |
+| --------- | -------------------------------- | --------------------------------------------------------------------------- |
+| `version` | `egctl version`                  | Prints out build version information.                                       |
+| `config`  | `egctl config ENTITY`            | Retrieve information about proxy configuration from envoy proxy and gateway |
+| `analyze` | `egctl analyze`                  | Analyze EG configuration and print validation messages                      |
+
+## Examples
+
+Use the following set of examples to help you familiarize yourself with running the commonly used `egctl` operations:
+
+```console
+# Retrieve all information about proxy configuration from envoy
+egctl config envoy-proxy all <instance_name>
+
+# Retrieve listener information about proxy configuration from envoy 
+egctl config envoy-proxy listener <instance_name>
+
+# Retrieve information about envoy gateway
+egctl config envoy-gateway
+```
diff --git a/docs/latest/user/tcp-routing.md b/docs/latest/user/tcp-routing.md
new file mode 100644
index 000000000..dbf8c145c
--- /dev/null
+++ b/docs/latest/user/tcp-routing.md
@@ -0,0 +1,283 @@
+# TCP Routing
+
+[TCPRoute][] provides a way to route TCP requests. When combined with a Gateway listener, it can be used to forward connections on the port specified by the listener to a set of backends specified by the TCPRoute. To learn more about HTTP routing, refer to the [Gateway API documentation][].
+
+Follow the steps to install Envoy Gateway and the example manifest.
+
+In this example, we have one Gateway resource and two TCPRoute resources that distribute the traffic with the following rules:
+
+All TCP streams on port `8088` of the Gateway are forwarded to port 3001 of `foo` Kubernetes Service.
+All TCP streams on port `8089` of the Gateway are forwarded to port 3002 of `bar` Kubernetes Service.
+In this example two TCP listeners will be applied to the Gateway in order to route them to two separate backend TCPRoutes, note that the protocol set for the listeners on the Gateway is TCP:
+
+Install the GatewayClass and a `tcp-gateway` Gateway first.
+
+```shell
+cat <<EOF | kubectl apply -f -
+kind: GatewayClass
+apiVersion: gateway.networking.k8s.io/v1beta1
+metadata:
+  name: eg
+spec:
+  controllerName: gateway.envoyproxy.io/gatewayclass-controller
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: tcp-gateway
+spec:
+  gatewayClassName: eg
+  listeners:
+  - name: foo
+    protocol: TCP
+    port: 8088
+    allowedRoutes:
+      kinds:
+      - kind: TCPRoute
+  - name: bar
+    protocol: TCP
+    port: 8089
+    allowedRoutes:
+      kinds:
+      - kind: TCPRoute
+EOF
+```
+
+Install two services `foo` and `bar`, which are binded to `backend-1` and `backend-2`.
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: v1
+kind: Service
+metadata:
+  name: foo
+  labels:
+    app: backend-1
+spec:
+  ports:
+    - name: http
+      port: 3001
+      targetPort: 3000
+  selector:
+    app: backend-1
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: bar
+  labels:
+    app: backend-2
+spec:
+  ports:
+    - name: http
+      port: 3002
+      targetPort: 3000
+  selector:
+    app: backend-2
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend-1
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backend-1
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: backend-1
+        version: v1
+    spec:
+      containers:
+        - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e
+          imagePullPolicy: IfNotPresent
+          name: backend-1
+          ports:
+            - containerPort: 3000
+          env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+            - name: SERVICE_NAME
+              value: foo
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend-2
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backend-2
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: backend-2
+        version: v1
+    spec:
+      containers:
+        - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e
+          imagePullPolicy: IfNotPresent
+          name: backend-2
+          ports:
+            - containerPort: 3000
+          env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            - name: NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+            - name: SERVICE_NAME
+              value: bar
+EOF
+```
+
+Install two TCPRoutes `tcp-app-1` and `tcp-app-2` with different `sectionName`:
+
+```shell
+cat <<EOF | kubectl apply -f -
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+  name: tcp-app-1
+spec:
+  parentRefs:
+  - name: tcp-gateway
+    sectionName: foo
+  rules:
+  - backendRefs:
+    - name: foo
+      port: 3001
+---
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: TCPRoute
+metadata:
+  name: tcp-app-2
+spec:
+  parentRefs:
+  - name: tcp-gateway
+    sectionName: bar
+  rules:
+  - backendRefs:
+    - name: bar
+      port: 3002
+EOF
+```
+
+In the above example we separate the traffic for the two separate backend TCP Services by using the sectionName field in the parentRefs:
+
+``` yaml
+spec:
+  parentRefs:
+  - name: tcp-gateway
+    sectionName: foo
+```
+
+This corresponds directly with the name in the listeners in the Gateway:
+
+``` yaml
+  listeners:
+  - name: foo
+    protocol: TCP
+    port: 8088
+  - name: bar
+    protocol: TCP
+    port: 8089
+```
+
+In this way each TCPRoute "attaches" itself to a different port on the Gateway so that the `foo` service
+is taking traffic for port `8088` from outside the cluster and `bar` service takes the port `8089` traffic.
+
+Before testing, please get the tcp-gateway Gateway's address first:
+
+```shell
+export GATEWAY_HOST=$(kubectl get gateway/tcp-gateway -o jsonpath='{.status.addresses[0].value}')
+```
+
+You can try to use nc to test the TCP connections of envoy gateway with different ports, and you can see them succeeded:
+
+```shell
+nc -zv ${GATEWAY_HOST} 8088
+
+nc -zv ${GATEWAY_HOST} 8089
+```
+
+You can also try to send requests to envoy gateway and get responses as shown below:
+
+```shell
+curl -i "http://${GATEWAY_HOST}:8088"
+
+HTTP/1.1 200 OK
+Content-Type: application/json
+X-Content-Type-Options: nosniff
+Date: Tue, 03 Jan 2023 10:18:36 GMT
+Content-Length: 267
+
+{
+ "path": "/",
+ "host": "xxx.xxx.xxx.xxx:8088",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+  "Accept": [
+   "*/*"
+  ],
+  "User-Agent": [
+   "curl/7.85.0"
+  ]
+ },
+ "namespace": "default",
+ "ingress": "",
+ "service": "foo",
+ "pod": "backend-1-c6c5fb958-dl8vl"
+}
+```
+
+You can see that the traffic routing to `foo` service when sending request to `8088` port.
+
+```shell
+curl -i "http://${GATEWAY_HOST}:8089"
+
+HTTP/1.1 200 OK
+Content-Type: application/json
+X-Content-Type-Options: nosniff
+Date: Tue, 03 Jan 2023 10:19:28 GMT
+Content-Length: 267
+
+{
+ "path": "/",
+ "host": "xxx.xxx.xxx.xxx:8089",
+ "method": "GET",
+ "proto": "HTTP/1.1",
+ "headers": {
+  "Accept": [
+   "*/*"
+  ],
+  "User-Agent": [
+   "curl/7.85.0"
+  ]
+ },
+ "namespace": "default",
+ "ingress": "",
+ "service": "bar",
+ "pod": "backend-2-98fcff498-hcmgb"
+}                                            
+```
+
+You can see that the traffic routing to `bar` service when sending request to `8089` port.
+
+[TCPRoute]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1alpha2.TCPRoute
+[Gateway API documentation]: https://gateway-api.sigs.k8s.io/
diff --git a/docs/latest/user_docs.rst b/docs/latest/user_docs.rst
index a1bec495a..c735a8c44 100644
--- a/docs/latest/user_docs.rst
+++ b/docs/latest/user_docs.rst
@@ -15,3 +15,4 @@ Learn how to deploy, use, and operate Envoy Gateway.
   user/http-response-headers
   user/secure-gateways
   user/tls-passthrough
+  user/tcp-routing
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcp-with-hostname.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcp-with-hostname.in.yaml
new file mode 100644
index 000000000..1be8e3b69
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcp-with-hostname.in.yaml
@@ -0,0 +1,16 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          hostname: foo.com
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcp-with-hostname.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcp-with-hostname.out.yaml
new file mode 100644
index 000000000..83469a9ff
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcp-with-hostname.out.yaml
@@ -0,0 +1,40 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          hostname: foo.com
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          conditions:
+            - type: Programmed
+              status: "False"
+              reason: Invalid
+              message: Listener must not have hostname set when protocol is TCP.
+xdsIR:
+  envoy-gateway-gateway-1: {}
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-mismatch-port-protocol.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-mismatch-port-protocol.in.yaml
new file mode 100644
index 000000000..049bb0ea0
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-mismatch-port-protocol.in.yaml
@@ -0,0 +1,29 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8081
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-mismatch-port-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-mismatch-port-protocol.out.yaml
new file mode 100644
index 000000000..f9afdc091
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-mismatch-port-protocol.out.yaml
@@ -0,0 +1,75 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 0
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8081
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+            - type: ResolvedRefs
+              status: "False"
+              reason: PortNotFound
+              message: TCP Port 8081 not found on service default/service-1
+xdsIR:
+  envoy-gateway-gateway-1: {}
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp
+              protocol: "TCP"
+              servicePort: 162
+              containerPort: 10162
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-backends.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-backends.in.yaml
new file mode 100644
index 000000000..3e5271710
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-backends.in.yaml
@@ -0,0 +1,33 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
+              weight: 50
+            - name: service-2
+              port: 8080
+              weight: 50
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-backends.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-backends.out.yaml
new file mode 100644
index 000000000..3cb1726e2
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-backends.out.yaml
@@ -0,0 +1,79 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 0
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
+              weight: 50
+            - name: service-2
+              port: 8080
+              weight: 50
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+            - type: ResolvedRefs
+              status: "False"
+              reason: InvalidBackend
+              message: One and only one backend is supported
+xdsIR:
+  envoy-gateway-gateway-1: {}
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp
+              protocol: "TCP"
+              servicePort: 80
+              containerPort: 10080
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-rules.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-rules.in.yaml
new file mode 100644
index 000000000..52cfed696
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-rules.in.yaml
@@ -0,0 +1,34 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
+              weight: 50
+        - backendRefs:
+            - name: service-2
+              port: 8080
+              weight: 50
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-rules.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-rules.out.yaml
new file mode 100644
index 000000000..3f1717874
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-tcproute-with-multiple-rules.out.yaml
@@ -0,0 +1,80 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 0
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
+              weight: 50
+        - backendRefs:
+            - name: service-2
+              port: 8080
+              weight: 50
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+            - type: ResolvedRefs
+              status: "False"
+              reason: InvalidRule
+              message: One and only one rule is supported
+xdsIR:
+  envoy-gateway-gateway-1: {}
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp
+              protocol: "TCP"
+              servicePort: 80
+              containerPort: 10080
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-unmatched-tcproute.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-unmatched-tcproute.in.yaml
new file mode 100644
index 000000000..dc4f8e805
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-unmatched-tcproute.in.yaml
@@ -0,0 +1,29 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-2
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8080
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-unmatched-tcproute.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-unmatched-tcproute.out.yaml
new file mode 100644
index 000000000..8144ea49b
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-unmatched-tcproute.out.yaml
@@ -0,0 +1,47 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 0
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+
+xdsIR:
+  envoy-gateway-gateway-1: {}
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp
+              protocol: "TCP"
+              servicePort: 80
+              containerPort: 10080
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.in.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.in.yaml
index cef4eb2b2..7fe1283b9 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.in.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.in.yaml
@@ -8,7 +8,7 @@ gateways:
       gatewayClassName: envoy-gateway-class
       listeners:
         - name: unsupported
-          protocol: TCP
+          protocol: GRPC
           port: 80
           allowedRoutes:
             namespaces:
diff --git a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml
index 2cbd7035f..0a7e99ecf 100644
--- a/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml
+++ b/internal/gatewayapi/testdata/gateway-with-listener-with-unsupported-protocol.out.yaml
@@ -8,7 +8,7 @@ gateways:
       gatewayClassName: envoy-gateway-class
       listeners:
         - name: unsupported
-          protocol: TCP
+          protocol: GRPC
           port: 80
           allowedRoutes:
             namespaces:
@@ -21,7 +21,7 @@ gateways:
             - type: Accepted
               status: "False"
               reason: UnsupportedProtocol
-              message: Protocol TCP is unsupported, must be HTTP, HTTPS, or UDP.
+              message: Protocol GRPC is unsupported, must be HTTP, HTTPS, TCP or UDP.
             - type: Programmed
               status: "False"
               reason: Invalid
diff --git a/internal/gatewayapi/testdata/gateway-with-single-listener-with-multiple-tcproutes.in.yaml b/internal/gatewayapi/testdata/gateway-with-single-listener-with-multiple-tcproutes.in.yaml
new file mode 100644
index 000000000..131957291
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-single-listener-with-multiple-tcproutes.in.yaml
@@ -0,0 +1,42 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-2
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-2
+              port: 8163
diff --git a/internal/gatewayapi/testdata/gateway-with-single-listener-with-multiple-tcproutes.out.yaml b/internal/gatewayapi/testdata/gateway-with-single-listener-with-multiple-tcproutes.out.yaml
new file mode 100644
index 000000000..8348d196a
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-single-listener-with-multiple-tcproutes.out.yaml
@@ -0,0 +1,103 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 1
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-2
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-2
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "False"
+              reason: UnsupportedValue
+              message: Multiple routes on the same TCP listener
+xdsIR:
+  envoy-gateway-gateway-1:
+    tcp:
+      - name: "envoy-gateway-gateway-1-tcp-tcproute-1"
+        address: "0.0.0.0"
+        port: 10162
+        destinations:
+          - host: "7.7.7.7"
+            port: 8163
+            weight: 0
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp
+              protocol: "TCP"
+              servicePort: 162
+              containerPort: 10162
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-on-same-tcp-port.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-on-same-tcp-port.in.yaml
new file mode 100644
index 000000000..a53076da0
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-on-same-tcp-port.in.yaml
@@ -0,0 +1,35 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp1
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp2
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-on-same-tcp-port.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-on-same-tcp-port.out.yaml
new file mode 100644
index 000000000..676829906
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-on-same-tcp-port.out.yaml
@@ -0,0 +1,98 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp1
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp2
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp1
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 1
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+        - name: tcp2
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 0
+          conditions:
+            - type: Conflicted
+              status: "True"
+              reason: ProtocolConflict
+              message: Only one TCP listener is allowed in a given port
+            - type: Programmed
+              status: "False"
+              reason: Invalid
+              message: Listener is invalid, see other Conditions for details.
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+xdsIR:
+  envoy-gateway-gateway-1:
+    tcp:
+      - name: "envoy-gateway-gateway-1-tcp1-tcproute-1"
+        address: "0.0.0.0"
+        port: 10162
+        destinations:
+          - host: "7.7.7.7"
+            port: 8163
+            weight: 0
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp1
+              protocol: "TCP"
+              servicePort: 162
+              containerPort: 10162
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.in.yaml
new file mode 100644
index 000000000..a72962050
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.in.yaml
@@ -0,0 +1,53 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: http
+          protocol: HTTP
+          port: 80
+          hostname: "*"
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp
+          protocol: TCP
+          port: 80
+          allowedRoutes:
+            namespaces:
+              from: All
+httpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: HTTPRoute
+    metadata:
+      namespace: default
+      name: httproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - matches:
+            - path:
+                value: "/"
+          backendRefs:
+            - name: service-1
+              port: 8080
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml
new file mode 100644
index 000000000..1e85bd353
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-same-port-http-tcp-protocol.out.yaml
@@ -0,0 +1,142 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1beta1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      hostname: "*"
+      allowedRoutes:
+        namespaces:
+          from: All
+    - name: tcp
+      protocol: TCP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+  status:
+    listeners:
+    - name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      AttachedRoutes: 1
+      conditions:
+      - type: Programmed
+        status: "True"
+        reason: Programmed
+        message: Listener is ready
+    - name: tcp
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: TCPRoute
+      AttachedRoutes: 1
+      conditions:
+      - type: Programmed
+        status: "True"
+        reason: Programmed
+        message: Listener is ready
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1beta1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+    rules:
+    - matches:
+      - path:
+          value: "/"
+      backendRefs:
+      - name: service-1
+        port: 8080
+  status:
+    parents:
+    - parentRef:
+        namespace: envoy-gateway
+        name: gateway-1
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      conditions:
+      - type: Accepted
+        status: "True"
+        reason: Accepted
+        message: Route is accepted
+tcpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: TCPRoute
+  metadata:
+    namespace: default
+    name: tcproute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8163
+  status:
+    parents:
+    - parentRef:
+        namespace: envoy-gateway
+        name: gateway-1
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      conditions:
+      - type: Accepted
+        status: "True"
+        reason: Accepted
+        message: Route is accepted
+xdsIR:
+  envoy-gateway-gateway-1:
+    http:
+    - name: envoy-gateway-gateway-1-http
+      address: 0.0.0.0
+      port: 10080
+      hostnames:
+      - "*"
+      routes:
+      - name: default-httproute-1-rule-0-match-0-*
+        pathMatch:
+          prefix: "/"
+        destinations:
+        - host: 7.7.7.7
+          port: 8080
+          weight: 1
+    tcp:
+    - name: "envoy-gateway-gateway-1-tcp-tcproute-1"
+      address: "0.0.0.0"
+      port: 10080
+      destinations:
+      - host: "7.7.7.7"
+        port: 8163
+        weight: 0
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+      - address: ""
+        ports:
+        - name: http
+          protocol: "HTTP"
+          servicePort: 80
+          containerPort: 10080
+        - name: tcp
+          protocol: "TCP"
+          servicePort: 80
+          containerPort: 10080
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-with-sectionname.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-with-sectionname.in.yaml
new file mode 100644
index 000000000..dbdd26a3d
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-with-sectionname.in.yaml
@@ -0,0 +1,50 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp1
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp2
+          protocol: TCP
+          port: 163
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+          sectionName: tcp1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-2
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+          sectionName: tcp2
+      rules:
+        - backendRefs:
+            - name: service-2
+              port: 8163
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-with-sectionname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-with-sectionname.out.yaml
new file mode 100644
index 000000000..7974ccc61
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-with-sectionname.out.yaml
@@ -0,0 +1,134 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp1
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp2
+          protocol: TCP
+          port: 163
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp1
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 1
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+        - name: tcp2
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 1
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+          sectionName: tcp1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+            sectionName: tcp1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-2
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+          sectionName: tcp2
+      rules:
+        - backendRefs:
+            - name: service-2
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+            sectionName: tcp2
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+xdsIR:
+  envoy-gateway-gateway-1:
+    tcp:
+      - name: "envoy-gateway-gateway-1-tcp1-tcproute-1"
+        address: "0.0.0.0"
+        port: 10162
+        destinations:
+          - host: "7.7.7.7"
+            port: 8163
+            weight: 0
+      - name: "envoy-gateway-gateway-1-tcp2-tcproute-2"
+        address: "0.0.0.0"
+        port: 10163
+        destinations:
+          - host: "7.7.7.7"
+            port: 8163
+            weight: 0
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp1
+              protocol: "TCP"
+              servicePort: 162
+              containerPort: 10162
+            - name: tcp2
+              protocol: "TCP"
+              servicePort: 163
+              containerPort: 10163
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-without-sectionname.in.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-without-sectionname.in.yaml
new file mode 100644
index 000000000..773c92623
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-without-sectionname.in.yaml
@@ -0,0 +1,48 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp1
+          protocol: TCP
+          port: 161
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp2
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-2
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-2
+              port: 8163
diff --git a/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-without-sectionname.out.yaml b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-without-sectionname.out.yaml
new file mode 100644
index 000000000..9e933fadc
--- /dev/null
+++ b/internal/gatewayapi/testdata/gateway-with-two-listeners-with-tcproutes-without-sectionname.out.yaml
@@ -0,0 +1,130 @@
+gateways:
+  - apiVersion: gateway.networking.k8s.io/v1beta1
+    kind: Gateway
+    metadata:
+      namespace: envoy-gateway
+      name: gateway-1
+    spec:
+      gatewayClassName: envoy-gateway-class
+      listeners:
+        - name: tcp1
+          protocol: TCP
+          port: 161
+          allowedRoutes:
+            namespaces:
+              from: All
+        - name: tcp2
+          protocol: TCP
+          port: 162
+          allowedRoutes:
+            namespaces:
+              from: All
+    status:
+      listeners:
+        - name: tcp1
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 1
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+        - name: tcp2
+          supportedKinds:
+            - group: gateway.networking.k8s.io
+              kind: TCPRoute
+          AttachedRoutes: 1
+          conditions:
+            - type: Programmed
+              status: "True"
+              reason: Programmed
+              message: Listener is ready
+tcpRoutes:
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-1
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-1
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "True"
+              reason: Accepted
+              message: Route is accepted
+  - apiVersion: gateway.networking.k8s.io/v1alpha2
+    kind: TCPRoute
+    metadata:
+      namespace: default
+      name: tcproute-2
+    spec:
+      parentRefs:
+        - namespace: envoy-gateway
+          name: gateway-1
+      rules:
+        - backendRefs:
+            - name: service-2
+              port: 8163
+    status:
+      parents:
+        - parentRef:
+            namespace: envoy-gateway
+            name: gateway-1
+          controllerName: gateway.envoyproxy.io/gatewayclass-controller
+          conditions:
+            - type: Accepted
+              status: "False"
+              reason: UnsupportedValue
+              message: Multiple routes on the same TCP listener
+xdsIR:
+  envoy-gateway-gateway-1:
+    tcp:
+      - name: "envoy-gateway-gateway-1-tcp1-tcproute-1"
+        address: "0.0.0.0"
+        port: 10161
+        destinations:
+          - host: "7.7.7.7"
+            port: 8163
+            weight: 0
+      - name: "envoy-gateway-gateway-1-tcp2-tcproute-1"
+        address: "0.0.0.0"
+        port: 10162
+        destinations:
+          - host: "7.7.7.7"
+            port: 8163
+            weight: 0
+
+infraIR:
+  envoy-gateway-gateway-1:
+    proxy:
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+      name: envoy-gateway-gateway-1
+      image: envoyproxy/envoy:translator-tests
+      listeners:
+        - address: ""
+          ports:
+            - name: tcp1
+              protocol: "TCP"
+              servicePort: 161
+              containerPort: 10161
+            - name: tcp2
+              protocol: "TCP"
+              servicePort: 162
+              containerPort: 10162
diff --git a/internal/gatewayapi/translator_test.go b/internal/gatewayapi/translator_test.go
index 0b0ca2b1d..7e9739ed8 100644
--- a/internal/gatewayapi/translator_test.go
+++ b/internal/gatewayapi/translator_test.go
@@ -64,6 +64,7 @@ func TestTranslate(t *testing.T) {
 							Ports: []v1.ServicePort{
 								{Port: 8080, Protocol: v1.ProtocolTCP},
 								{Port: 8443, Protocol: v1.ProtocolTCP},
+								{Port: 8163, Protocol: v1.ProtocolTCP},
 								{Port: 8162, Protocol: v1.ProtocolUDP},
 							},
 						},
diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go
index 7e10a7779..3e7756223 100644
--- a/internal/ir/xds_test.go
+++ b/internal/ir/xds_test.go
@@ -69,6 +69,12 @@ var (
 		TLS:          &TLSInspectorConfig{SNIs: []string{"example.com"}},
 		Destinations: []*RouteDestination{&happyRouteDestination},
 	}
+	emptySNITCPListenerTLSPassthrough = TCPListener{
+		Name:         "empty-sni",
+		Address:      "0.0.0.0",
+		Port:         80,
+		Destinations: []*RouteDestination{&happyRouteDestination},
+	}
 	invalidNameTCPListenerTLSPassthrough = TCPListener{
 		Address:      "0.0.0.0",
 		Port:         80,
@@ -525,6 +531,11 @@ func TestValidateTCPListener(t *testing.T) {
 			input: happyTCPListenerTLSPassthrough,
 			want:  nil,
 		},
+		{
+			name:  "tcp empty SNIs",
+			input: emptySNITCPListenerTLSPassthrough,
+			want:  nil,
+		},
 		{
 			name:  "tls passthrough invalid name",
 			input: invalidNameTCPListenerTLSPassthrough,
@@ -794,7 +805,14 @@ func TestValidateRouteDestination(t *testing.T) {
 			want: ErrRouteDestinationHostInvalid,
 		},
 		{
-			name: "invalid port",
+			name: "missing ip",
+			input: RouteDestination{
+				Port: 8080,
+			},
+			want: ErrRouteDestinationHostInvalid,
+		},
+		{
+			name: "missing port",
 			input: RouteDestination{
 				Host: "10.11.12.13",
 			},
diff --git a/internal/provider/kubernetes/predicates_test.go b/internal/provider/kubernetes/predicates_test.go
index 9b2fadb20..4864a03e5 100644
--- a/internal/provider/kubernetes/predicates_test.go
+++ b/internal/provider/kubernetes/predicates_test.go
@@ -266,6 +266,17 @@ func TestValidateServiceForReconcile(t *testing.T) {
 			service: test.GetService(types.NamespacedName{Name: "service"}, nil, nil),
 			expect:  true,
 		},
+		{
+			name: "tcp route service routes exist",
+			configs: []client.Object{
+				test.GetGatewayClass("test-gc", v1alpha1.GatewayControllerName),
+				sampleGateway,
+				test.GetTCPRoute(types.NamespacedName{Name: "tcproute-test"}, "scheduled-status-test",
+					types.NamespacedName{Name: "service"}),
+			},
+			service: test.GetService(types.NamespacedName{Name: "service"}, nil, nil),
+			expect:  true,
+		},
 	}
 
 	// Create the reconciler.
@@ -284,6 +295,7 @@ func TestValidateServiceForReconcile(t *testing.T) {
 			WithIndex(&gwapiv1b1.HTTPRoute{}, serviceHTTPRouteIndex, serviceHTTPRouteIndexFunc).
 			WithIndex(&gwapiv1a2.GRPCRoute{}, serviceGRPCRouteIndex, serviceGRPCRouteIndexFunc).
 			WithIndex(&gwapiv1a2.TLSRoute{}, serviceTLSRouteIndex, serviceTLSRouteIndexFunc).
+			WithIndex(&gwapiv1a2.TCPRoute{}, serviceTCPRouteIndex, serviceTCPRouteIndexFunc).
 			WithIndex(&gwapiv1a2.UDPRoute{}, serviceUDPRouteIndex, serviceUDPRouteIndexFunc).
 			Build()
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/internal/provider/kubernetes/test/utils.go b/internal/provider/kubernetes/test/utils.go
index 15a8f91f5..2f4610df0 100644
--- a/internal/provider/kubernetes/test/utils.go
+++ b/internal/provider/kubernetes/test/utils.go
@@ -168,6 +168,34 @@ func GetTLSRoute(nsName types.NamespacedName, parent string, serviceName types.N
 	}
 }
 
+// GetTCPRoute returns a sample TCPRoute with a parent reference.
+func GetTCPRoute(nsName types.NamespacedName, parent string, serviceName types.NamespacedName) *gwapiv1a2.TCPRoute {
+	return &gwapiv1a2.TCPRoute{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: nsName.Namespace,
+			Name:      nsName.Name,
+		},
+		Spec: gwapiv1a2.TCPRouteSpec{
+			CommonRouteSpec: gwapiv1a2.CommonRouteSpec{
+				ParentRefs: []gwapiv1a2.ParentReference{
+					{Name: gwapiv1a2.ObjectName(parent)},
+				},
+			},
+			Rules: []gwapiv1a2.TCPRouteRule{
+				{
+					BackendRefs: []gwapiv1a2.BackendRef{
+						{
+							BackendObjectReference: gwapiv1a2.BackendObjectReference{
+								Name: gwapiv1a2.ObjectName(serviceName.Name),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
 // GetUDPRoute returns a sample UDPRoute with a parent reference.
 func GetUDPRoute(nsName types.NamespacedName, parent string, serviceName types.NamespacedName) *gwapiv1a2.UDPRoute {
 	return &gwapiv1a2.UDPRoute{
diff --git a/internal/provider/kubernetes/testdata/in/tcproute-experimental-crd.yaml b/internal/provider/kubernetes/testdata/in/tcproute-experimental-crd.yaml
new file mode 100644
index 000000000..9924e0ab3
--- /dev/null
+++ b/internal/provider/kubernetes/testdata/in/tcproute-experimental-crd.yaml
@@ -0,0 +1,518 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    api-approved.kubernetes.io: https://github.com/kubernetes-sigs/gateway-api/pull/1538
+    gateway.networking.k8s.io/bundle-version: v0.6.0
+    gateway.networking.k8s.io/channel: experimental
+  creationTimestamp: null
+  name: tcproutes.gateway.networking.k8s.io
+spec:
+  group: gateway.networking.k8s.io
+  names:
+    categories:
+    - gateway-api
+    kind: TCPRoute
+    listKind: TCPRouteList
+    plural: tcproutes
+    singular: tcproute
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1alpha2
+    schema:
+      openAPIV3Schema:
+        description: TCPRoute provides a way to route TCP requests. When combined
+          with a Gateway listener, it can be used to forward connections on the port
+          specified by the listener to a set of backends specified by the TCPRoute.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: Spec defines the desired state of TCPRoute.
+            properties:
+              parentRefs:
+                description: "ParentRefs references the resources (usually Gateways)
+                  that a Route wants to be attached to. Note that the referenced parent
+                  resource needs to allow this for the attachment to be complete.
+                  For Gateways, that means the Gateway needs to allow attachment from
+                  Routes of this kind and namespace. \n The only kind of parent resource
+                  with \"Core\" support is Gateway. This API may be extended in the
+                  future to support additional kinds of parent resources such as one
+                  of the route kinds. \n It is invalid to reference an identical parent
+                  more than once. It is valid to reference multiple distinct sections
+                  within the same parent resource, such as 2 Listeners within a Gateway.
+                  \n It is possible to separately reference multiple distinct objects
+                  that may be collapsed by an implementation. For example, some implementations
+                  may choose to merge compatible Gateway Listeners together. If that
+                  is the case, the list of routes attached to those resources should
+                  also be merged. \n Note that for ParentRefs that cross namespace
+                  boundaries, there are specific rules. Cross-namespace references
+                  are only valid if they are explicitly allowed by something in the
+                  namespace they are referring to. For example, Gateway has the AllowedRoutes
+                  field, and ReferenceGrant provides a generic way to enable any other
+                  kind of cross-namespace reference."
+                items:
+                  description: "ParentReference identifies an API object (usually
+                    a Gateway) that can be considered a parent of this resource (usually
+                    a route). The only kind of parent resource with \"Core\" support
+                    is Gateway. This API may be extended in the future to support
+                    additional kinds of parent resources, such as HTTPRoute. \n The
+                    API object must be valid in the cluster; the Group and Kind must
+                    be registered in the cluster for this reference to be valid."
+                  properties:
+                    group:
+                      default: gateway.networking.k8s.io
+                      description: "Group is the group of the referent. When unspecified,
+                        \"gateway.networking.k8s.io\" is inferred. To set the core
+                        API group (such as for a \"Service\" kind referent), Group
+                        must be explicitly set to \"\" (empty string). \n Support:
+                        Core"
+                      maxLength: 253
+                      pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                    kind:
+                      default: Gateway
+                      description: "Kind is kind of the referent. \n Support: Core
+                        (Gateway) \n Support: Implementation-specific (Other Resources)"
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                      type: string
+                    name:
+                      description: "Name is the name of the referent. \n Support:
+                        Core"
+                      maxLength: 253
+                      minLength: 1
+                      type: string
+                    namespace:
+                      description: "Namespace is the namespace of the referent. When
+                        unspecified, this refers to the local namespace of the Route.
+                        \n Note that there are specific rules for ParentRefs which
+                        cross namespace boundaries. Cross-namespace references are
+                        only valid if they are explicitly allowed by something in
+                        the namespace they are referring to. For example: Gateway
+                        has the AllowedRoutes field, and ReferenceGrant provides a
+                        generic way to enable any other kind of cross-namespace reference.
+                        \n Support: Core"
+                      maxLength: 63
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                      type: string
+                    port:
+                      description: "Port is the network port this Route targets. It
+                        can be interpreted differently based on the type of parent
+                        resource. \n When the parent resource is a Gateway, this targets
+                        all listeners listening on the specified port that also support
+                        this kind of Route(and select this Route). It's not recommended
+                        to set `Port` unless the networking behaviors specified in
+                        a Route must apply to a specific port as opposed to a listener(s)
+                        whose port(s) may be changed. When both Port and SectionName
+                        are specified, the name and port of the selected listener
+                        must match both specified values. \n Implementations MAY choose
+                        to support other parent resources. Implementations supporting
+                        other types of parent resources MUST clearly document how/if
+                        Port is interpreted. \n For the purpose of status, an attachment
+                        is considered successful as long as the parent resource accepts
+                        it partially. For example, Gateway listeners can restrict
+                        which Routes can attach to them by Route kind, namespace,
+                        or hostname. If 1 of 2 Gateway listeners accept attachment
+                        from the referencing Route, the Route MUST be considered successfully
+                        attached. If no Gateway listeners accept attachment from this
+                        Route, the Route MUST be considered detached from the Gateway.
+                        \n Support: Extended \n <gateway:experimental>"
+                      format: int32
+                      maximum: 65535
+                      minimum: 1
+                      type: integer
+                    sectionName:
+                      description: "SectionName is the name of a section within the
+                        target resource. In the following resources, SectionName is
+                        interpreted as the following: \n * Gateway: Listener Name.
+                        When both Port (experimental) and SectionName are specified,
+                        the name and port of the selected listener must match both
+                        specified values. \n Implementations MAY choose to support
+                        attaching Routes to other resources. If that is the case,
+                        they MUST clearly document how SectionName is interpreted.
+                        \n When unspecified (empty string), this will reference the
+                        entire resource. For the purpose of status, an attachment
+                        is considered successful if at least one section in the parent
+                        resource accepts it. For example, Gateway listeners can restrict
+                        which Routes can attach to them by Route kind, namespace,
+                        or hostname. If 1 of 2 Gateway listeners accept attachment
+                        from the referencing Route, the Route MUST be considered successfully
+                        attached. If no Gateway listeners accept attachment from this
+                        Route, the Route MUST be considered detached from the Gateway.
+                        \n Support: Core"
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                      type: string
+                  required:
+                  - name
+                  type: object
+                maxItems: 32
+                type: array
+              rules:
+                description: Rules are a list of TCP matchers and actions.
+                items:
+                  description: TCPRouteRule is the configuration for a given rule.
+                  properties:
+                    backendRefs:
+                      description: "BackendRefs defines the backend(s) where matching
+                        requests should be sent. If unspecified or invalid (refers
+                        to a non-existent 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
+                        connections, then 80% of connections must be rejected instead.
+                        \n Support: Core for Kubernetes Service \n Support: Implementation-specific
+                        for any other resource \n Support for weight: Extended"
+                      items:
+                        description: "BackendRef defines how a Route should forward
+                          a request to a Kubernetes resource. \n Note that when a
+                          namespace is specified, a ReferenceGrant object is required
+                          in the referent namespace to allow that namespace's owner
+                          to accept the reference. See the ReferenceGrant documentation
+                          for details."
+                        properties:
+                          group:
+                            default: ""
+                            description: Group is the group of the referent. For example,
+                              "gateway.networking.k8s.io". When unspecified or empty
+                              string, core API group is inferred.
+                            maxLength: 253
+                            pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                            type: string
+                          kind:
+                            default: Service
+                            description: Kind is kind of the referent. For example
+                              "HTTPRoute" or "Service". Defaults to "Service" when
+                              not specified.
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                            type: string
+                          name:
+                            description: Name is the name of the referent.
+                            maxLength: 253
+                            minLength: 1
+                            type: string
+                          namespace:
+                            description: "Namespace is the namespace of the backend.
+                              When unspecified, the local namespace is inferred. \n
+                              Note that when a namespace is specified, a ReferenceGrant
+                              object is required in the referent namespace to allow
+                              that namespace's owner to accept the reference. See
+                              the ReferenceGrant documentation for details. \n Support:
+                              Core"
+                            maxLength: 63
+                            minLength: 1
+                            pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                            type: string
+                          port:
+                            description: Port specifies the destination port number
+                              to use for this resource. Port is required when the
+                              referent is a Kubernetes Service. In this case, the
+                              port number is the service port number, not the target
+                              port. For other resources, destination port might be
+                              derived from the referent resource or this field.
+                            format: int32
+                            maximum: 65535
+                            minimum: 1
+                            type: integer
+                          weight:
+                            default: 1
+                            description: "Weight specifies the proportion of requests
+                              forwarded to the referenced backend. This is computed
+                              as weight/(sum of all weights in this BackendRefs list).
+                              For non-zero values, there may be some epsilon from
+                              the exact proportion defined here depending on the precision
+                              an implementation supports. Weight is not a percentage
+                              and the sum of weights does not need to equal 100. \n
+                              If only one backend is specified and it has a weight
+                              greater than 0, 100% of the traffic is forwarded to
+                              that backend. If weight is set to 0, no traffic should
+                              be forwarded for this entry. If unspecified, weight
+                              defaults to 1. \n Support for this field varies based
+                              on the context where used."
+                            format: int32
+                            maximum: 1000000
+                            minimum: 0
+                            type: integer
+                        required:
+                        - name
+                        type: object
+                      maxItems: 16
+                      minItems: 1
+                      type: array
+                  type: object
+                maxItems: 16
+                minItems: 1
+                type: array
+            required:
+            - rules
+            type: object
+          status:
+            description: Status defines the current state of TCPRoute.
+            properties:
+              parents:
+                description: "Parents is a list of parent resources (usually Gateways)
+                  that are associated with the route, and the status of the route
+                  with respect to each parent. When this route attaches to a parent,
+                  the controller that manages the parent must add an entry to this
+                  list when the controller first sees the route and should update
+                  the entry as appropriate when the route or gateway is modified.
+                  \n Note that parent references that cannot be resolved by an implementation
+                  of this API will not be added to this list. Implementations of this
+                  API can only populate Route status for the Gateways/parent resources
+                  they are responsible for. \n A maximum of 32 Gateways will be represented
+                  in this list. An empty list means the route has not been attached
+                  to any Gateway."
+                items:
+                  description: RouteParentStatus describes the status of a route with
+                    respect to an associated Parent.
+                  properties:
+                    conditions:
+                      description: "Conditions describes the status of the route with
+                        respect to the Gateway. Note that the route's availability
+                        is also subject to the Gateway's own status conditions and
+                        listener status. \n If the Route's ParentRef specifies an
+                        existing Gateway that supports Routes of this kind AND that
+                        Gateway's controller has sufficient access, then that Gateway's
+                        controller MUST set the \"Accepted\" condition on the Route,
+                        to indicate whether the route has been accepted or rejected
+                        by the Gateway, and why. \n A Route MUST be considered \"Accepted\"
+                        if at least one of the Route's rules is implemented by the
+                        Gateway. \n There are a number of cases where the \"Accepted\"
+                        condition may not be set due to lack of controller visibility,
+                        that includes when: \n * The Route refers to a non-existent
+                        parent. * The Route is of a type that the controller does
+                        not support. * The Route is in a namespace the controller
+                        does not have access to."
+                      items:
+                        description: "Condition contains details for one aspect of
+                          the current state of this API Resource. --- This struct
+                          is intended for direct use as an array at the field path
+                          .status.conditions.  For example, \n \ttype FooStatus struct{
+                          \t    // Represents the observations of a foo's current
+                          state. \t    // Known .status.conditions.type are: \"Available\",
+                          \"Progressing\", and \"Degraded\" \t    // +patchMergeKey=type
+                          \t    // +patchStrategy=merge \t    // +listType=map \t
+                          \   // +listMapKey=type \t    Conditions []metav1.Condition
+                          `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
+                          protobuf:\"bytes,1,rep,name=conditions\"` \n \t    // other
+                          fields \t}"
+                        properties:
+                          lastTransitionTime:
+                            description: lastTransitionTime is the last time the condition
+                              transitioned from one status to another. This should
+                              be when the underlying condition changed.  If that is
+                              not known, then using the time when the API field changed
+                              is acceptable.
+                            format: date-time
+                            type: string
+                          message:
+                            description: message is a human readable message indicating
+                              details about the transition. This may be an empty string.
+                            maxLength: 32768
+                            type: string
+                          observedGeneration:
+                            description: observedGeneration represents the .metadata.generation
+                              that the condition was set based upon. For instance,
+                              if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration
+                              is 9, the condition is out of date with respect to the
+                              current state of the instance.
+                            format: int64
+                            minimum: 0
+                            type: integer
+                          reason:
+                            description: reason contains a programmatic identifier
+                              indicating the reason for the condition's last transition.
+                              Producers of specific condition types may define expected
+                              values and meanings for this field, and whether the
+                              values are considered a guaranteed API. The value should
+                              be a CamelCase string. This field may not be empty.
+                            maxLength: 1024
+                            minLength: 1
+                            pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                            type: string
+                          status:
+                            description: status of the condition, one of True, False,
+                              Unknown.
+                            enum:
+                            - "True"
+                            - "False"
+                            - Unknown
+                            type: string
+                          type:
+                            description: type of condition in CamelCase or in foo.example.com/CamelCase.
+                              --- Many .condition.type values are consistent across
+                              resources like Available, but because arbitrary conditions
+                              can be useful (see .node.status.conditions), the ability
+                              to deconflict is important. The regex it matches is
+                              (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                            maxLength: 316
+                            pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                            type: string
+                        required:
+                        - lastTransitionTime
+                        - message
+                        - reason
+                        - status
+                        - type
+                        type: object
+                      maxItems: 8
+                      minItems: 1
+                      type: array
+                      x-kubernetes-list-map-keys:
+                      - type
+                      x-kubernetes-list-type: map
+                    controllerName:
+                      description: "ControllerName is a domain/path string that indicates
+                        the name of the controller that wrote this status. This corresponds
+                        with the controllerName field on GatewayClass. \n Example:
+                        \"example.net/gateway-controller\". \n The format of this
+                        field is DOMAIN \"/\" PATH, where DOMAIN and PATH are valid
+                        Kubernetes names (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).
+                        \n Controllers MUST populate this field when writing status.
+                        Controllers should ensure that entries to status populated
+                        with their ControllerName are cleaned up when they are no
+                        longer necessary."
+                      maxLength: 253
+                      minLength: 1
+                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$
+                      type: string
+                    parentRef:
+                      description: ParentRef corresponds with a ParentRef in the spec
+                        that this RouteParentStatus struct describes the status of.
+                      properties:
+                        group:
+                          default: gateway.networking.k8s.io
+                          description: "Group is the group of the referent. When unspecified,
+                            \"gateway.networking.k8s.io\" is inferred. To set the
+                            core API group (such as for a \"Service\" kind referent),
+                            Group must be explicitly set to \"\" (empty string). \n
+                            Support: Core"
+                          maxLength: 253
+                          pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                          type: string
+                        kind:
+                          default: Gateway
+                          description: "Kind is kind of the referent. \n Support:
+                            Core (Gateway) \n Support: Implementation-specific (Other
+                            Resources)"
+                          maxLength: 63
+                          minLength: 1
+                          pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                          type: string
+                        name:
+                          description: "Name is the name of the referent. \n Support:
+                            Core"
+                          maxLength: 253
+                          minLength: 1
+                          type: string
+                        namespace:
+                          description: "Namespace is the namespace of the referent.
+                            When unspecified, this refers to the local namespace of
+                            the Route. \n Note that there are specific rules for ParentRefs
+                            which cross namespace boundaries. Cross-namespace references
+                            are only valid if they are explicitly allowed by something
+                            in the namespace they are referring to. For example: Gateway
+                            has the AllowedRoutes field, and ReferenceGrant provides
+                            a generic way to enable any other kind of cross-namespace
+                            reference. \n Support: Core"
+                          maxLength: 63
+                          minLength: 1
+                          pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                          type: string
+                        port:
+                          description: "Port is the network port this Route targets.
+                            It can be interpreted differently based on the type of
+                            parent resource. \n When the parent resource is a Gateway,
+                            this targets all listeners listening on the specified
+                            port that also support this kind of Route(and select this
+                            Route). It's not recommended to set `Port` unless the
+                            networking behaviors specified in a Route must apply to
+                            a specific port as opposed to a listener(s) whose port(s)
+                            may be changed. When both Port and SectionName are specified,
+                            the name and port of the selected listener must match
+                            both specified values. \n Implementations MAY choose to
+                            support other parent resources. Implementations supporting
+                            other types of parent resources MUST clearly document
+                            how/if Port is interpreted. \n For the purpose of status,
+                            an attachment is considered successful as long as the
+                            parent resource accepts it partially. For example, Gateway
+                            listeners can restrict which Routes can attach to them
+                            by Route kind, namespace, or hostname. If 1 of 2 Gateway
+                            listeners accept attachment from the referencing Route,
+                            the Route MUST be considered successfully attached. If
+                            no Gateway listeners accept attachment from this Route,
+                            the Route MUST be considered detached from the Gateway.
+                            \n Support: Extended \n <gateway:experimental>"
+                          format: int32
+                          maximum: 65535
+                          minimum: 1
+                          type: integer
+                        sectionName:
+                          description: "SectionName is the name of a section within
+                            the target resource. In the following resources, SectionName
+                            is interpreted as the following: \n * Gateway: Listener
+                            Name. When both Port (experimental) and SectionName are
+                            specified, the name and port of the selected listener
+                            must match both specified values. \n Implementations MAY
+                            choose to support attaching Routes to other resources.
+                            If that is the case, they MUST clearly document how SectionName
+                            is interpreted. \n When unspecified (empty string), this
+                            will reference the entire resource. For the purpose of
+                            status, an attachment is considered successful if at least
+                            one section in the parent resource accepts it. For example,
+                            Gateway listeners can restrict which Routes can attach
+                            to them by Route kind, namespace, or hostname. If 1 of
+                            2 Gateway listeners accept attachment from the referencing
+                            Route, the Route MUST be considered successfully attached.
+                            If no Gateway listeners accept attachment from this Route,
+                            the Route MUST be considered detached from the Gateway.
+                            \n Support: Core"
+                          maxLength: 253
+                          minLength: 1
+                          pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                          type: string
+                      required:
+                      - name
+                      type: object
+                  required:
+                  - controllerName
+                  - parentRef
+                  type: object
+                maxItems: 32
+                type: array
+            required:
+            - parents
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/internal/xds/translator/testdata/in/xds-ir/http-route-weighted-backend.yaml b/internal/xds/translator/testdata/in/xds-ir/http-route-weighted-backend.yaml
new file mode 100644
index 000000000..e6d314d1f
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/http-route-weighted-backend.yaml
@@ -0,0 +1,21 @@
+http:
+- name: "first-listener"
+  address: "0.0.0.0"
+  port: 10080
+  hostnames:
+  - "*"
+  routes:
+  - name: "first-route" 
+    destinations:
+    - host: "1.1.1.1"
+      port: 50001
+      weight: 20
+    - host: "2.2.2.2"
+      port: 50002
+      weight: 40
+    - host: "3.3.3.3"
+      port: 50003
+      weight: 20
+    - host: "4.4.4.4"
+      port: 50004
+      weight: 20
diff --git a/internal/xds/translator/testdata/in/xds-ir/multiple-simple-tcp-route-same-port.yaml b/internal/xds/translator/testdata/in/xds-ir/multiple-simple-tcp-route-same-port.yaml
new file mode 100644
index 000000000..324de2128
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/multiple-simple-tcp-route-same-port.yaml
@@ -0,0 +1,41 @@
+tcp:
+- name: "tcp-route-simple"
+  address: "0.0.0.0"
+  port: 10080
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
+- name: "tcp-route-simple-1"
+  address: "0.0.0.0"
+  port: 10080
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
+- name: "tcp-route-simple-2"
+  address: "0.0.0.0"
+  port: 10080
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
+- name: "tcp-route-simple-3"
+  address: "0.0.0.0"
+  port: 10080
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
+- name: "tcp-route-simple-4"
+  address: "0.0.0.0"
+  port: 10080
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
diff --git a/internal/xds/translator/testdata/in/xds-ir/tcp-route-complex.yaml b/internal/xds/translator/testdata/in/xds-ir/tcp-route-complex.yaml
new file mode 100644
index 000000000..1de28530c
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/tcp-route-complex.yaml
@@ -0,0 +1,14 @@
+tcp:
+- name: "tcp-route-complex"
+  address: "0.0.0.0"
+  port: 10080
+  tls:
+    snis:
+    - foo.com
+    - bar.com
+    - example.com
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
diff --git a/internal/xds/translator/testdata/in/xds-ir/tcp-route-simple.yaml b/internal/xds/translator/testdata/in/xds-ir/tcp-route-simple.yaml
new file mode 100644
index 000000000..b88d57dd3
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/tcp-route-simple.yaml
@@ -0,0 +1,9 @@
+tcp:
+- name: "tcp-route-simple"
+  address: "0.0.0.0"
+  port: 10080
+  destinations:
+  - host: "1.2.3.4"
+    port: 50000
+  - host: "5.6.7.8"
+    port: 50001
diff --git a/internal/xds/translator/testdata/in/xds-ir/tcp-route-weighted-backend.yaml b/internal/xds/translator/testdata/in/xds-ir/tcp-route-weighted-backend.yaml
new file mode 100644
index 000000000..fe02dec82
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/tcp-route-weighted-backend.yaml
@@ -0,0 +1,22 @@
+tcp:
+- name: "tcp-route-weighted-backend"
+  address: "0.0.0.0"
+  port: 10080
+  tls:
+    snis:
+    - foo.com
+    - bar.com
+    - example.com
+  destinations:
+  - host: "1.1.1.1"
+    port: 50001
+    weight: 20
+  - host: "2.2.2.2"
+    port: 50002
+    weight: 40
+  - host: "3.3.3.3"
+    port: 50003
+    weight: 20
+  - host: "4.4.4.4"
+    port: 50004
+    weight: 20
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.clusters.yaml
new file mode 100644
index 000000000..bb307d770
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.clusters.yaml
@@ -0,0 +1,37 @@
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: first-route
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.1.1.1
+              portValue: 50001
+        loadBalancingWeight: 20
+      - endpoint:
+          address:
+            socketAddress:
+              address: 2.2.2.2
+              portValue: 50002
+        loadBalancingWeight: 40
+      - endpoint:
+          address:
+            socketAddress:
+              address: 3.3.3.3
+              portValue: 50003
+        loadBalancingWeight: 20
+      - endpoint:
+          address:
+            socketAddress:
+              address: 4.4.4.4
+              portValue: 50004
+        loadBalancingWeight: 20
+      loadBalancingWeight: 1
+      locality: {}
+  name: first-route
+  outlierDetection: {}
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.listeners.yaml
new file mode 100644
index 000000000..6c5555739
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.listeners.yaml
@@ -0,0 +1,40 @@
+- accessLog:
+  - filter:
+      responseFlagFilter:
+        flags:
+        - NR
+    name: envoy.access_loggers.file
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+      path: /dev/stdout
+  address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  defaultFilterChain:
+    filters:
+    - name: envoy.filters.network.http_connection_manager
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        httpFilters:
+        - name: envoy.filters.http.router
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+        rds:
+          configSource:
+            apiConfigSource:
+              apiType: DELTA_GRPC
+              grpcServices:
+              - envoyGrpc:
+                  clusterName: xds_cluster
+              setNodeOnFirstMessageOnly: true
+              transportApiVersion: V3
+            resourceApiVersion: V3
+          routeConfigName: first-listener
+        statPrefix: http
+  name: first-listener
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.routes.yaml
new file mode 100644
index 000000000..ed122e552
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.routes.yaml
@@ -0,0 +1,10 @@
+- name: first-listener
+  virtualHosts:
+  - domains:
+    - '*'
+    name: first-listener
+    routes:
+    - match:
+        prefix: /
+      route:
+        cluster: first-route
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.clusters.yaml
new file mode 100644
index 000000000..9b23924af
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.clusters.yaml
@@ -0,0 +1,115 @@
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-simple
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-simple
+  outlierDetection: {}
+  type: STATIC
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-simple-1
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-simple-1
+  outlierDetection: {}
+  type: STATIC
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-simple-2
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-simple-2
+  outlierDetection: {}
+  type: STATIC
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-simple-3
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-simple-3
+  outlierDetection: {}
+  type: STATIC
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-simple-4
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-simple-4
+  outlierDetection: {}
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.listeners.yaml
new file mode 100644
index 000000000..128fd616f
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.listeners.yaml
@@ -0,0 +1,70 @@
+- accessLog:
+  - filter:
+      responseFlagFilter:
+        flags:
+        - NR
+    name: envoy.access_loggers.file
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+      path: /dev/stdout
+  address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  filterChains:
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-simple
+        statPrefix: tcp
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-simple-1
+        statPrefix: tcp
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-simple-2
+        statPrefix: tcp
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-simple-3
+        statPrefix: tcp
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-simple-4
+        statPrefix: tcp
+  name: tcp-route-simple
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.clusters.yaml
new file mode 100644
index 000000000..eb1b767d8
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.clusters.yaml
@@ -0,0 +1,23 @@
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-complex
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-complex
+  outlierDetection: {}
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.listeners.yaml
new file mode 100644
index 000000000..dfb2a30ac
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.listeners.yaml
@@ -0,0 +1,35 @@
+- accessLog:
+  - filter:
+      responseFlagFilter:
+        flags:
+        - NR
+    name: envoy.access_loggers.file
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+      path: /dev/stdout
+  address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  filterChains:
+  - filterChainMatch:
+      serverNames:
+      - foo.com
+      - bar.com
+      - example.com
+    filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-complex
+        statPrefix: passthrough
+  listenerFilters:
+  - name: envoy.filters.listener.tls_inspector
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
+  name: tcp-route-complex
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.clusters.yaml
new file mode 100644
index 000000000..54c0cd9b4
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.clusters.yaml
@@ -0,0 +1,23 @@
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-simple
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.2.3.4
+              portValue: 50000
+      - endpoint:
+          address:
+            socketAddress:
+              address: 5.6.7.8
+              portValue: 50001
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-simple
+  outlierDetection: {}
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.listeners.yaml
new file mode 100644
index 000000000..05e159719
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.listeners.yaml
@@ -0,0 +1,26 @@
+- accessLog:
+  - filter:
+      responseFlagFilter:
+        flags:
+        - NR
+    name: envoy.access_loggers.file
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+      path: /dev/stdout
+  address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  filterChains:
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-simple
+        statPrefix: tcp
+  name: tcp-route-simple
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.clusters.yaml
new file mode 100644
index 000000000..c24165c14
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.clusters.yaml
@@ -0,0 +1,37 @@
+- commonLbConfig:
+    localityWeightedLbConfig: {}
+  connectTimeout: 5s
+  dnsLookupFamily: V4_ONLY
+  loadAssignment:
+    clusterName: tcp-route-weighted-backend
+    endpoints:
+    - lbEndpoints:
+      - endpoint:
+          address:
+            socketAddress:
+              address: 1.1.1.1
+              portValue: 50001
+        loadBalancingWeight: 20
+      - endpoint:
+          address:
+            socketAddress:
+              address: 2.2.2.2
+              portValue: 50002
+        loadBalancingWeight: 40
+      - endpoint:
+          address:
+            socketAddress:
+              address: 3.3.3.3
+              portValue: 50003
+        loadBalancingWeight: 20
+      - endpoint:
+          address:
+            socketAddress:
+              address: 4.4.4.4
+              portValue: 50004
+        loadBalancingWeight: 20
+      loadBalancingWeight: 1
+      locality: {}
+  name: tcp-route-weighted-backend
+  outlierDetection: {}
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.listeners.yaml
new file mode 100644
index 000000000..e16c3a238
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.listeners.yaml
@@ -0,0 +1,35 @@
+- accessLog:
+  - filter:
+      responseFlagFilter:
+        flags:
+        - NR
+    name: envoy.access_loggers.file
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+      path: /dev/stdout
+  address:
+    socketAddress:
+      address: 0.0.0.0
+      portValue: 10080
+  filterChains:
+  - filterChainMatch:
+      serverNames:
+      - foo.com
+      - bar.com
+      - example.com
+    filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        accessLog:
+        - name: envoy.access_loggers.file
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
+            path: /dev/stdout
+        cluster: tcp-route-weighted-backend
+        statPrefix: passthrough
+  listenerFilters:
+  - name: envoy.filters.listener.tls_inspector
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
+  name: tcp-route-weighted-backend
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go
index a785ee4e6..3d8df2245 100644
--- a/internal/xds/translator/translator_test.go
+++ b/internal/xds/translator/translator_test.go
@@ -67,6 +67,21 @@ func TestTranslate(t *testing.T) {
 		{
 			name: "tls-route-passthrough",
 		},
+		{
+			name: "tcp-route-simple",
+		},
+		{
+			name: "tcp-route-complex",
+		},
+		{
+			name: "multiple-simple-tcp-route-same-port",
+		},
+		{
+			name: "http-route-weighted-backend",
+		},
+		{
+			name: "tcp-route-weighted-backend",
+		},
 		{
 			name:           "multiple-listeners-same-port",
 			requireSecrets: true,