@@ -135,7 +135,7 @@ func TestTransparentBridge(t *testing.T) {
135135 // side believes it captured a connection whose SO_ORIGINAL_DST is the
136136 // origin's host:port.
137137 clientSide , serverSide := net .Pipe ()
138- defer clientSide .Close ()
138+ defer func () { _ = clientSide .Close () } ()
139139
140140 done := make (chan struct {})
141141 go func () {
@@ -223,6 +223,119 @@ func TestTransparentBridge(t *testing.T) {
223223 }
224224}
225225
226+ // TestTransparentBridge_CustomPort proves the configurable-ports path: the
227+ // origin runs on a RANDOM ephemeral port (NOT in shouldSniff's hardcoded
228+ // {80,443,8080,8443} set), and the bridge is configured (Decision.Ports) to
229+ // intercept exactly that port. Before the shouldSniff↔Decision.Ports unification
230+ // the transparent path would not sniff this port, so the bridge could never peek
231+ // and the call would tunnel (agent handshake would fail against the origin's real
232+ // cert). With the unification, HandlesPort(port) widens the sniff and the bridge
233+ // engages. Asserting decryption here is the regression test for that wiring.
234+ func TestTransparentBridge_CustomPort (t * testing.T ) {
235+ var gotOriginPath string
236+ origin := httptest .NewTLSServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
237+ gotOriginPath = r .URL .Path
238+ if r .URL .Path == "/secret" {
239+ _ , _ = w .Write ([]byte ("OK-SECRET" ))
240+ return
241+ }
242+ w .WriteHeader (http .StatusNotFound )
243+ }))
244+ defer origin .Close ()
245+
246+ originCAPEM := pem .EncodeToMemory (& pem.Block {Type : "CERTIFICATE" , Bytes : origin .Certificate ().Raw })
247+ originURL , err := url .Parse (origin .URL )
248+ if err != nil {
249+ t .Fatalf ("parse origin URL: %v" , err )
250+ }
251+ originHostPort := originURL .Host // 127.0.0.1:<random ephemeral port>
252+ customPort := portOf (originHostPort )
253+ if customPort == 443 || customPort == 8443 || customPort == 80 || customPort == 8080 {
254+ t .Skipf ("random origin port %d collided with a default sniff port; rerun" , customPort )
255+ }
256+
257+ src , err := tlsbridge .NewEphemeralSource ()
258+ if err != nil {
259+ t .Fatalf ("NewEphemeralSource: %v" , err )
260+ }
261+ minter := tlsbridge .NewMinter (src , tlsbridge.MinterOpts {})
262+ up , err := tlsbridge .NewUpstreamClient (originCAPEM )
263+ if err != nil {
264+ t .Fatalf ("NewUpstreamClient: %v" , err )
265+ }
266+ engine := & tlsbridge.Engine {
267+ // Only the custom port is configured — proves both that it IS bridged
268+ // (despite shouldSniff not knowing it) and that the default set is replaced.
269+ Decision : tlsbridge .NewDecision (tlsbridge.DecisionOpts {
270+ Scope : tlsbridge .ScopeAll ,
271+ Ports : map [int ]bool {customPort : true },
272+ }),
273+ Term : tlsbridge .NewTerminator (minter ),
274+ Skip : tlsbridge .NewSkipSet (),
275+ Upstream : up ,
276+ CAPEM : src .CACertPEM (),
277+ }
278+
279+ probe := & bridgeProbePlugin {}
280+ p , err := plugintesting .BuildPipeline ([]pipeline.Plugin {probe })
281+ if err != nil {
282+ t .Fatalf ("BuildPipeline: %v" , err )
283+ }
284+ srv := & Server {OutboundPipeline : pipeline .NewHolder (p ), Client : http .DefaultClient , TLSBridge : engine }
285+
286+ clientSide , serverSide := net .Pipe ()
287+ defer func () { _ = clientSide .Close () }()
288+ done := make (chan struct {})
289+ go func () {
290+ defer close (done )
291+ srv .HandleTransparentConn (serverSide , originHostPort )
292+ }()
293+
294+ pool := x509 .NewCertPool ()
295+ if ! pool .AppendCertsFromPEM (engine .CAPEM ) {
296+ t .Fatalf ("append bridge CA" )
297+ }
298+ host := hostOnly (originHostPort )
299+ tconn := tls .Client (clientSide , & tls.Config {ServerName : host , RootCAs : pool , NextProtos : []string {"http/1.1" }})
300+ _ = tconn .SetDeadline (time .Now ().Add (10 * time .Second ))
301+ if err := tconn .Handshake (); err != nil {
302+ t .Fatalf ("agent handshake through bridge on custom port failed: %v" , err )
303+ }
304+ req , err := http .NewRequest (http .MethodGet , "https://" + host + "/secret" , nil )
305+ if err != nil {
306+ t .Fatalf ("new request: %v" , err )
307+ }
308+ writeErr := make (chan error , 1 )
309+ go func () { writeErr <- req .Write (tconn ) }()
310+ resp , err := http .ReadResponse (bufio .NewReader (tconn ), req )
311+ if err != nil {
312+ t .Fatalf ("read response on custom-port bridge: %v" , err )
313+ }
314+ defer resp .Body .Close ()
315+ body , _ := io .ReadAll (resp .Body )
316+ if werr := <- writeErr ; werr != nil {
317+ t .Fatalf ("write request: %v" , werr )
318+ }
319+
320+ if probe .gotPath != "/secret" {
321+ t .Errorf ("probe path = %q, want /secret (bridge did not engage on custom port)" , probe .gotPath )
322+ }
323+ if gotOriginPath != "/secret" {
324+ t .Errorf ("origin path = %q, want /secret" , gotOriginPath )
325+ }
326+ if got := strings .TrimSpace (string (body )); got != "OK-SECRET" {
327+ t .Errorf ("body = %q, want OK-SECRET" , got )
328+ }
329+
330+ _ = clientSide .Close ()
331+ _ = serverSide .Close ()
332+ select {
333+ case <- done :
334+ case <- time .After (5 * time .Second ):
335+ t .Fatalf ("HandleTransparentConn did not return" )
336+ }
337+ }
338+
226339// TestConnectBridge drives the full CONNECT → 200 → agent-TLS → decrypt →
227340// pipeline → re-originate loop through the PUBLIC forward-proxy HTTP handler
228341// (so the real net/http Hijack path runs). An httptest TLS origin stands in
0 commit comments