Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 59 additions & 16 deletions cmd/reload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -150,26 +156,63 @@ 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.
//
// The forwarded ports are deliberately not included. Working them out means
// 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 {
Expand Down
48 changes: 44 additions & 4 deletions cmd/reload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,15 @@ 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)
}

client := &config.Config{}
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)
}

Expand All @@ -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) {
Expand All @@ -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 {
Expand All @@ -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)
}
}