diff --git a/go.mod b/go.mod index 165a737..081d2f9 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ module proxyforward go 1.26.10 require ( - github.com/Microsoft/go-winio v0.6.2 + github.com/Microsoft/go-winio v0.6.4 github.com/energye/systray v1.0.3 github.com/hashicorp/yamux v0.1.2 github.com/kardianos/service v1.3.0 @@ -21,7 +21,7 @@ require ( github.com/wailsapp/wails/v2 v2.13.0 go.uber.org/goleak v1.3.0 golang.org/x/crypto v0.57.0 - golang.org/x/sys v0.48.0 + golang.org/x/sys v0.49.0 golang.org/x/time v0.15.0 modernc.org/sqlite v1.53.0 ) diff --git a/go.sum b/go.sum index 89141d8..1f674f6 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA= git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g= +github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -112,8 +112,8 @@ golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= @@ -121,8 +121,8 @@ golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/e2e/e2e_test.go b/internal/e2e/e2e_test.go index 171f0b5..1545ce8 100644 --- a/internal/e2e/e2e_test.go +++ b/internal/e2e/e2e_test.go @@ -135,12 +135,7 @@ func newHarnessWith(t *testing.T, localAddr string, opts harnessOpts) *harness { opts.tweakGateway(gwCfg) } - gwCtx, gwCancel := context.WithCancel(context.Background()) - gw := gateway.New(gwCfg, t.TempDir(), logger.With("side", "gateway")) - if err := gw.Start(gwCtx); err != nil { - gwCancel() - t.Fatal(err) - } + gw, gwCancel := startGateway(t, gwCfg, logger.With("side", "gateway")) t.Cleanup(func() { gwCancel() gw.Shutdown() @@ -199,6 +194,35 @@ func newHarnessWith(t *testing.T, localAddr string, opts harnessOpts) *harness { return h } +// startGateway starts a gateway on an ephemeral control port, retrying on a +// fresh port while QUIC is enabled but its UDP bind failed. The gateway binds +// UDP on whatever number the OS handed out for TCP, and nothing makes that +// number free for UDP: Windows runners reserve UDP ranges and system services +// hold UDP ports exclusively, so the bind sporadically fails with WSAEACCES. +// The gateway then (by design) serves TCP only, and every QUIC test would wait +// out its deadline on a tunnel that can never come up. +func startGateway(t *testing.T, cfg *config.Config, logger *slog.Logger) (*gateway.Gateway, context.CancelFunc) { + t.Helper() + dir := t.TempDir() + const attempts = 5 + for i := 1; ; i++ { + ctx, cancel := context.WithCancel(context.Background()) + gw := gateway.New(cfg, dir, logger) + if err := gw.Start(ctx); err != nil { + cancel() + t.Fatal(err) + } + if !cfg.Gateway.QUICEnabled || gw.QUICAddr() != nil { + return gw, cancel + } + cancel() + gw.Shutdown() + if i == attempts { + t.Fatalf("gateway QUIC listener failed to bind on %d ephemeral ports", attempts) + } + } +} + func (h *harness) startAgent() { h.t.Helper() ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go index 214fcc2..1fea89b 100644 --- a/internal/gateway/gateway.go +++ b/internal/gateway/gateway.go @@ -269,6 +269,16 @@ func (g *Gateway) ControlAddr() net.Addr { } return g.controlLn.Addr() } + +// QUICAddr is the UDP address the QUIC control listener is bound to, or nil +// when QUIC is off or its bind failed (Start logs that and serves TCP only). +func (g *Gateway) QUICAddr() net.Addr { + if g.quicLn == nil { + return nil + } + return g.quicLn.Addr() +} + func (g *Gateway) Fingerprint() string { return g.fingerprint } // GatewayID returns this gateway's derived gw_ display identity (empty before Start). diff --git a/internal/ipc/server_windows.go b/internal/ipc/server_windows.go index f51a0e4..d0d89c4 100644 --- a/internal/ipc/server_windows.go +++ b/internal/ipc/server_windows.go @@ -34,4 +34,6 @@ func listen() (net.Listener, error) { }) } -func isListenerClosed(err error) bool { return errors.Is(err, winio.ErrPipeListenerClosed) } +// winio reports a closed listener as net.ErrClosed (its ErrPipeListenerClosed is +// a deprecated alias of it). +func isListenerClosed(err error) bool { return errors.Is(err, net.ErrClosed) }