diff --git a/cmd/reload.go b/cmd/reload.go index edddaab..7ec5e04 100644 --- a/cmd/reload.go +++ b/cmd/reload.go @@ -121,24 +121,30 @@ func awaitConfigChange(ctx context.Context, path string, current *config.Config) } } -// waitForPorts waits until every address given can be bound, so the tunnel -// being started is not racing the one that just stopped for its own ports. +type listenerBinding struct { + network string + address string +} + +// waitForPorts waits until every address can be bound on the protocol the old +// transport used, so the tunnel being started is not racing the one that just +// stopped for its own ports. // // The listeners close as soon as their context ends, but nothing reports when // they have; the panel's HTTP server in particular is shut down gracefully and // takes a moment. Binding the address is the only direct evidence that it is // free, so that is what is checked — and if it never comes free, this gives up // and lets the listener report the real error rather than hanging here. -func waitForPorts(ctx context.Context, addrs []string) { - deadline := time.Now().Add(portSettleTimeout) - for _, addr := range addrs { - if addr == "" { +func waitForPorts(ctx context.Context, bindings []listenerBinding) { + for _, binding := range bindings { + if binding.address == "" { continue } + // Each listener gets the full settling budget. Sharing one deadline + // means a slow control port consumes all of the web port's wait too. + deadline := time.Now().Add(portSettleTimeout) for time.Now().Before(deadline) { - ln, err := net.Listen("tcp", addr) - if err == nil { - ln.Close() + if bindingAvailable(binding) { break } select { @@ -150,6 +156,27 @@ func waitForPorts(ctx context.Context, addrs []string) { } } +func bindingAvailable(binding listenerBinding) bool { + switch binding.network { + case "udp": + pc, err := net.ListenPacket("udp", binding.address) + if err != nil { + return false + } + pc.Close() + return true + case "tcp": + ln, err := net.Listen("tcp", binding.address) + if err != nil { + return false + } + ln.Close() + return true + default: + return false + } +} + // portsInUse names the addresses a run binds that can be known from the // configuration alone — the tunnel's own listener and the per-tunnel web page. // @@ -157,19 +184,35 @@ func waitForPorts(ctx context.Context, addrs []string) { // re-implementing the port-mapping parser, ranges and all, and they are the // listeners that close immediately anyway; it is the gracefully shut down HTTP // server that actually needs waiting for. -func portsInUse(cfg *config.Config) []string { - var addrs []string +func portsInUse(cfg *config.Config) []listenerBinding { + var bindings []listenerBinding if cfg.Server.BindAddr != "" { - addrs = append(addrs, cfg.Server.BindAddr) + network := tunnelNetwork(cfg.Server.Transport) + // XDI and spoof use raw sockets rather than a TCP/UDP listener that can + // be probed safely here. Their teardown is left to the transport. + if network != "" { + bindings = append(bindings, listenerBinding{network: network, address: cfg.Server.BindAddr}) + } if cfg.Server.WebPort > 0 { - addrs = append(addrs, net.JoinHostPort("", itoa(cfg.Server.WebPort))) + bindings = append(bindings, listenerBinding{network: "tcp", address: net.JoinHostPort("", itoa(cfg.Server.WebPort))}) } - return addrs + return bindings } if cfg.Client.WebPort > 0 { - addrs = append(addrs, net.JoinHostPort("", itoa(cfg.Client.WebPort))) + bindings = append(bindings, listenerBinding{network: "tcp", address: net.JoinHostPort("", itoa(cfg.Client.WebPort))}) + } + return bindings +} + +func tunnelNetwork(transport config.TransportType) string { + switch transport { + case config.UDP, config.KCP, config.QUIC: + return "udp" + case config.XDI, config.SPOOF: + return "" + default: + return "tcp" } - return addrs } func itoa(n int) string { diff --git a/cmd/reload_test.go b/cmd/reload_test.go index 9926dc0..c7d84b9 100644 --- a/cmd/reload_test.go +++ b/cmd/reload_test.go @@ -191,7 +191,7 @@ func TestPortsInUse(t *testing.T) { server.Server.BindAddr = "0.0.0.0:3080" server.Server.WebPort = 2060 got := portsInUse(server) - if len(got) != 2 || got[0] != "0.0.0.0:3080" || got[1] != ":2060" { + if len(got) != 2 || got[0] != (listenerBinding{network: "tcp", address: "0.0.0.0:3080"}) || got[1] != (listenerBinding{network: "tcp", address: ":2060"}) { t.Fatalf("server ports = %v, want the bind address and the web port", got) } @@ -199,7 +199,7 @@ func TestPortsInUse(t *testing.T) { client.Client.RemoteAddr = "example.com:3080" client.Client.WebPort = 2061 got = portsInUse(client) - if len(got) != 1 || got[0] != ":2061" { + if len(got) != 1 || got[0] != (listenerBinding{network: "tcp", address: ":2061"}) { t.Fatalf("client ports = %v, want only the web port", got) } @@ -211,6 +211,28 @@ func TestPortsInUse(t *testing.T) { } } +func TestTunnelNetwork(t *testing.T) { + tests := []struct { + transport config.TransportType + want string + }{ + {config.TCP, "tcp"}, + {config.TCPMUX, "tcp"}, + {config.WS, "tcp"}, + {config.WSMUX, "tcp"}, + {config.UDP, "udp"}, + {config.KCP, "udp"}, + {config.QUIC, "udp"}, + {config.XDI, ""}, + {config.SPOOF, ""}, + } + for _, tt := range tests { + if got := tunnelNetwork(tt.transport); got != tt.want { + t.Errorf("tunnelNetwork(%q) = %q, want %q", tt.transport, got, tt.want) + } + } +} + // waitForPorts must return once the address is free, and must give up rather // than hang when it never comes free. func TestWaitForPorts(t *testing.T) { @@ -222,7 +244,7 @@ func TestWaitForPorts(t *testing.T) { // Held open: this cannot succeed, so it has to give up on the timeout. start := time.Now() - waitForPorts(context.Background(), []string{addr}) + waitForPorts(context.Background(), []listenerBinding{{network: "tcp", address: addr}}) held := time.Since(start) ln.Close() if held < portSettleTimeout { @@ -231,8 +253,26 @@ func TestWaitForPorts(t *testing.T) { // Now free: this should return almost immediately. start = time.Now() - waitForPorts(context.Background(), []string{addr}) + waitForPorts(context.Background(), []listenerBinding{{network: "tcp", address: addr}}) if free := time.Since(start); free > 2*time.Second { t.Fatalf("took %v to notice a free port", free) } } + +func TestWaitForUDPPort(t *testing.T) { + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen UDP: %v", err) + } + binding := listenerBinding{network: "udp", address: pc.LocalAddr().String()} + + go func() { + time.Sleep(200 * time.Millisecond) + pc.Close() + }() + start := time.Now() + waitForPorts(context.Background(), []listenerBinding{binding}) + if waited := time.Since(start); waited < 150*time.Millisecond || waited > 2*time.Second { + t.Fatalf("waited %v for a UDP listener released after 200ms", waited) + } +}