Skip to content

Commit aa17647

Browse files
authoredMay 17, 2024
get-free-port: prevent duplicate ports on Linux (#1478)
1 parent 3b68c6d commit aa17647

File tree

1 file changed

+55
-39
lines changed

1 file changed

+55
-39
lines changed
 

‎testsuite/freeport.go

+55-39
Original file line numberDiff line numberDiff line change
@@ -25,57 +25,73 @@ package testsuite
2525
import (
2626
"fmt"
2727
"net"
28+
"runtime"
2829
)
2930

30-
// Modified from Temporalite which itself modified from
31-
// https://github.com/phayes/freeport/blob/95f893ade6f232a5f1511d61735d89b1ae2df543/freeport.go
31+
// Copied and adapted from
32+
// https://github.com/temporalio/cli/blob/350cb2f9dca55e5063b39ffbdaa2739fdeab4399/temporalcli/devserver/freeport.go
3233

33-
func newPortProvider() *portProvider {
34-
return &portProvider{}
35-
}
36-
37-
type portProvider struct {
38-
listeners []*net.TCPListener
39-
}
40-
41-
// GetFreePort asks the kernel for a free open port that is ready to use.
42-
// Returns the interface's IP and the free port.
43-
func (p *portProvider) GetFreePort() (string, int, error) {
44-
addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:0")
45-
if err != nil {
46-
if addr, err = net.ResolveTCPAddr("tcp6", "[::1]:0"); err != nil {
47-
return "", 0, fmt.Errorf("failed to get free port: %w", err)
48-
}
49-
}
50-
51-
l, err := net.ListenTCP("tcp", addr)
34+
// Returns a TCP port that is available to listen on, for the given (local) host.
35+
//
36+
// This works by binding a new TCP socket on port 0, which requests the OS to
37+
// allocate a free port. There is no strict guarantee that the port will remain
38+
// available after this function returns, but it should be safe to assume that
39+
// a given port will not be allocated again to any process on this machine
40+
// within a few seconds.
41+
//
42+
// On Unix-based systems, binding to the port returned by this function requires
43+
// setting the `SO_REUSEADDR` socket option (Go already does that by default,
44+
// but other languages may not); otherwise, the OS may fail with a message such
45+
// as "address already in use". Windows default behavior is already appropriate
46+
// in this regard; on that platform, `SO_REUSEADDR` has a different meaning and
47+
// should not be set (setting it may have unpredictable consequences).
48+
func getFreePort(host string) (string, int, error) {
49+
l, err := net.Listen("tcp", host+":0")
5250
if err != nil {
53-
return "", 0, err
51+
return "", 0, fmt.Errorf("failed to assign a free port: %w", err)
5452
}
53+
defer func() { _ = l.Close() }()
54+
port := l.Addr().(*net.TCPAddr).Port
5555

56-
p.listeners = append(p.listeners, l)
57-
tcpAddr := l.Addr().(*net.TCPAddr)
58-
59-
return tcpAddr.IP.String(), tcpAddr.Port, nil
60-
}
61-
62-
func (p *portProvider) Close() error {
63-
for _, l := range p.listeners {
64-
if err := l.Close(); err != nil {
65-
return err
56+
// On Linux and some BSD variants, ephemeral ports are randomized, and may
57+
// consequently repeat within a short time frame after the listenning end
58+
// has been closed. To avoid this, we make a connection to the port, then
59+
// close that connection from the server's side (this is very important),
60+
// which puts the connection in TIME_WAIT state for some time (by default,
61+
// 60s on Linux). While it remains in that state, the OS will not reallocate
62+
// that port number for bind(:0) syscalls, yet we are not prevented from
63+
// explicitly binding to it (thanks to SO_REUSEADDR).
64+
//
65+
// On macOS and Windows, the above technique is not necessary, as the OS
66+
// allocates ephemeral ports sequentially, meaning a port number will only
67+
// be reused after the entire range has been exhausted. Quite the opposite,
68+
// given that these OSes use a significantly smaller range for ephemeral
69+
// ports, making an extra connection just to reserve a port might actually
70+
// be harmful (by hastening ephemeral port exhaustion).
71+
if runtime.GOOS != "darwin" && runtime.GOOS != "windows" {
72+
r, err := net.DialTCP("tcp", nil, l.Addr().(*net.TCPAddr))
73+
if err != nil {
74+
return "", 0, fmt.Errorf("failed to assign a free port: %w", err)
75+
}
76+
c, err := l.Accept()
77+
if err != nil {
78+
return "", 0, fmt.Errorf("failed to assign a free port: %w", err)
6679
}
80+
// Closing the socket from the server side
81+
_ = c.Close()
82+
defer func() { _ = r.Close() }()
6783
}
68-
return nil
84+
85+
return host, port, nil
6986
}
7087

7188
func getFreeHostPort() (string, error) {
72-
pp := newPortProvider()
73-
host, port, err := pp.GetFreePort()
74-
closeErr := pp.Close()
89+
host, port, err := getFreePort("127.0.0.1")
7590
if err != nil {
76-
return "", err
77-
} else if closeErr != nil {
78-
return "", fmt.Errorf("failed to close TCP listener: %w", closeErr)
91+
host, port, err = getFreePort("[::1]")
92+
if err != nil {
93+
return "", err
94+
}
7995
}
8096
return fmt.Sprintf("%v:%v", host, port), nil
8197
}

0 commit comments

Comments
 (0)