1919from rolo .proxy import Proxy
2020from rolo .routing import RuleAdapter , WithHost
2121from werkzeug .datastructures import Headers
22+ from twisted .internet import reactor
23+ from twisted .protocols .portforward import ProxyFactory
2224
2325LOG = logging .getLogger (__name__ )
2426
@@ -30,6 +32,9 @@ class ProxiedDockerContainerExtension(Extension):
3032
3133 Requests may potentially use HTTP2 with binary content as the protocol (e.g., gRPC over HTTP2).
3234 To ensure proper routing of requests, subclasses can define the `http2_ports`.
35+
36+ For services requiring raw TCP proxying (e.g., native database protocols), use the `tcp_ports`
37+ parameter to enable transparent TCP forwarding to the container.
3338 """
3439
3540 name : str
@@ -41,7 +46,7 @@ class ProxiedDockerContainerExtension(Extension):
4146 host : str | None
4247 """
4348 Optional host on which to expose the container endpoints.
44- Can be either a static hostname, or a pattern like `<regex("(.+\.)?"):subdomain>myext.<domain>`
49+ Can be either a static hostname, or a pattern like `<regex("(.+\\ .)?"):subdomain>myext.<domain>`
4550 """
4651 path : str | None
4752 """Optional path on which to expose the container endpoints."""
@@ -59,6 +64,22 @@ class ProxiedDockerContainerExtension(Extension):
5964 """Callable that returns the target port for a given request, for routing purposes"""
6065 http2_ports : list [int ] | None
6166 """List of ports for which HTTP2 proxy forwarding into the container should be enabled."""
67+ tcp_ports : list [int ] | None
68+ """
69+ List of container ports for raw TCP proxying through the gateway.
70+ Enables transparent TCP forwarding for protocols that don't use HTTP (e.g., native DB protocols).
71+
72+ When tcp_ports is set, the extension must implement tcp_connection_matcher() to identify
73+ its traffic by inspecting initial connection bytes.
74+ """
75+
76+ tcp_connection_matcher : Callable [[bytes ], bool ] | None
77+ """
78+ Optional function to identify TCP connections belonging to this extension.
79+
80+ Called with initial connection bytes (up to 512 bytes) to determine if this extension
81+ should handle the connection. Return True to claim the connection, False otherwise.
82+ """
6283
6384 def __init__ (
6485 self ,
@@ -71,6 +92,7 @@ def __init__(
7192 health_check_fn : Callable [[], None ] | None = None ,
7293 request_to_port_router : Callable [[Request ], int ] | None = None ,
7394 http2_ports : list [int ] | None = None ,
95+ tcp_ports : list [int ] | None = None ,
7496 ):
7597 self .image_name = image_name
7698 if not container_ports :
@@ -84,6 +106,7 @@ def __init__(
84106 self .health_check_fn = health_check_fn
85107 self .request_to_port_router = request_to_port_router
86108 self .http2_ports = http2_ports
109+ self .tcp_ports = tcp_ports
87110 self .main_port = self .container_ports [0 ]
88111 self .container_host = get_addressable_container_host ()
89112
@@ -106,6 +129,53 @@ def update_gateway_routes(self, router: http.Router[http.RouteHandler]):
106129 self .container_host , port , self .should_proxy_request
107130 )
108131
132+ # set up raw TCP proxies with protocol detection
133+ if self .tcp_ports :
134+ self ._setup_tcp_protocol_routing ()
135+
136+ def _setup_tcp_protocol_routing (self ):
137+ """
138+ Set up TCP routing on the LocalStack gateway for this extension.
139+
140+ This method patches the gateway's HTTP protocol handler to intercept TCP
141+ connections and allow this extension to claim them via tcp_connection_matcher().
142+ This enables multiple TCP protocols to share the main gateway port (4566).
143+
144+ Uses monkeypatching to intercept dataReceived() before HTTP processing.
145+ """
146+ from localstack_extensions .utils .tcp_protocol_router import (
147+ patch_gateway_for_tcp_routing ,
148+ register_tcp_extension ,
149+ )
150+
151+ # Get the connection matcher from the extension
152+ matcher = getattr (self , "tcp_connection_matcher" , None )
153+ if not matcher :
154+ LOG .warning (
155+ f"Extension { self .name } has tcp_ports but no tcp_connection_matcher(). "
156+ "TCP routing will not work without a matcher."
157+ )
158+ return
159+
160+ # Apply gateway patches (only happens once globally)
161+ patch_gateway_for_tcp_routing ()
162+
163+ # Register this extension for TCP routing
164+ # Use first port as the default target port
165+ target_port = self .tcp_ports [0 ] if self .tcp_ports else self .main_port
166+
167+ register_tcp_extension (
168+ extension_name = self .name ,
169+ matcher = matcher ,
170+ backend_host = self .container_host ,
171+ backend_port = target_port ,
172+ )
173+
174+ LOG .info (
175+ f"Registered TCP extension { self .name } -> "
176+ f"{ self .container_host } :{ target_port } on gateway"
177+ )
178+
109179 @abstractmethod
110180 def should_proxy_request (self , headers : Headers ) -> bool :
111181 """Define whether a request should be proxied, based on request headers."""
0 commit comments