test: enhance lease port management in server tests
Kim committed
May 20, 2026 at 09:58 UTC
6f7f75cd90fb4a8131b696fb664033c283701192
2 files changed
+86
-7
portal/server_test.go
+85
-6
@@ -12,11 +12,14 @@ import (
12
"encoding/pem"
13
"io"
14
"math/big"
15
+ "net"
16
"net/http"
17
"net/http/httptest"
18
"os"
19
"path/filepath"
20
+ "strconv"
21
"strings"
22
+ "sync"
23
"testing"
24
"time"
25
@@ -25,11 +28,85 @@ import (
28
"github.com/gosuda/portal-tunnel/v2/utils"
29
)
30
31
+var (
32
+ testLeasePortsMu sync.Mutex
33
+ testLeasePorts = make(map[int]struct{})
34
+)
35
+
36
func tempIdentityPath(t *testing.T) string {
37
t.Helper()
38
return t.TempDir()
39
}
40
41
+func tempLeasePort(t *testing.T) int {
42
+ t.Helper()
43
+
44
+ for attempt := 0; attempt < 100; attempt++ {
45
+ probe, err := net.Listen("tcp", "127.0.0.1:0")
46
+ if err != nil {
47
+ t.Fatalf("allocate probe port: %v", err)
48
+ }
49
+ _, portText, err := net.SplitHostPort(probe.Addr().String())
50
+ if closeErr := probe.Close(); closeErr != nil {
51
+ t.Fatalf("close probe port: %v", closeErr)
52
+ }
53
+ if err != nil {
54
+ t.Fatalf("parse probe port: %v", err)
55
+ }
56
+ start, err := strconv.Atoi(portText)
57
+ if err != nil {
58
+ t.Fatalf("parse probe port %q: %v", portText, err)
59
+ }
60
+ if start <= 0 || start > 65535 {
61
+ continue
62
+ }
63
+ if !reserveTestLeasePort(start) {
64
+ continue
65
+ }
66
+ if tempLeasePortAvailable(start) {
67
+ return start
68
+ }
69
+ releaseTestLeasePort(start)
70
+ }
71
+ t.Fatalf("could not find a free lease port")
72
+ return 0
73
+}
74
+
75
+func reserveTestLeasePort(port int) bool {
76
+ testLeasePortsMu.Lock()
77
+ defer testLeasePortsMu.Unlock()
78
+
79
+ if _, exists := testLeasePorts[port]; exists {
80
+ return false
81
+ }
82
+ testLeasePorts[port] = struct{}{}
83
+ return true
84
+}
85
+
86
+func releaseTestLeasePort(port int) {
87
+ testLeasePortsMu.Lock()
88
+ defer testLeasePortsMu.Unlock()
89
+
90
+ delete(testLeasePorts, port)
91
+}
92
+
93
+func tempLeasePortAvailable(port int) bool {
94
+ addr := ":" + strconv.Itoa(port)
95
+ tcpListener, err := net.Listen("tcp", addr)
96
+ if err != nil {
97
+ return false
98
+ }
99
+ defer tcpListener.Close()
100
+
101
+ udpListener, err := net.ListenPacket("udp", addr)
102
+ if err != nil {
103
+ return false
104
+ }
105
+ defer udpListener.Close()
106
+
107
+ return true
108
+}
109
+
110
func newTestClient(t *testing.T, cancel context.CancelFunc, server *Server) *http.Client {
111
t.Helper()
112
client := utils.NewHTTPClient(
@@ -266,12 +343,13 @@ func TestServerStartDomainReportsCompatibilityInfo(t *testing.T) {
343
func TestRegisterLeaseIncludesSNIPortForPublicIngress(t *testing.T) {
344
t.Parallel()
345
346
+ port := tempLeasePort(t)
347
server, err := NewServer(ServerConfig{
348
PortalURL: "https://portal.example.com:4017",
349
IdentityPath: tempIdentityPath(t),
350
SNIPort: 4443,
273
- MinPort: 40000,
274
- MaxPort: 40009,
351
+ MinPort: port,
352
+ MaxPort: port,
353
TCPEnabled: true,
354
})
355
if err != nil {
@@ -401,11 +479,12 @@ func TestRegisterLeaseDerivesFixedHostnameFromName(t *testing.T) {
479
func TestRegisterLeaseBuildsUDPEnabledRuntime(t *testing.T) {
480
t.Parallel()
481
482
+ port := tempLeasePort(t)
483
server, err := NewServer(ServerConfig{
484
PortalURL: "https://portal.example.com",
485
IdentityPath: tempIdentityPath(t),
407
- MinPort: 40000,
408
- MaxPort: 40009,
486
+ MinPort: port,
487
+ MaxPort: port,
488
UDPEnabled: true,
489
})
490
if err != nil {
@@ -433,8 +512,8 @@ func TestRegisterLeaseBuildsUDPEnabledRuntime(t *testing.T) {
512
if record.datagram == nil {
513
t.Fatal("datagram = nil, want datagram runtime")
514
}
436
- if got := record.datagram.UDPPort(); got < 40000 || got > 40009 {
437
- t.Fatalf("UDPPort() = %d, want port within %d-%d", got, 40000, 40009)
515
+ if got := record.datagram.UDPPort(); got != port {
516
+ t.Fatalf("UDPPort() = %d, want %d", got, port)
517
}
518
if resp.SNIPort != server.config().SNIPort {
519
t.Fatalf("RegisterResponse.SNIPort = %d, want %d", resp.SNIPort, server.config().SNIPort)
sdk/expose_test.go
+1
-1
@@ -131,7 +131,7 @@ func TestExposureReconcileRemovesStaleListener(t *testing.T) {
131
132
relayAClosed := make(chan struct{})
133
exposure := &Exposure{
134
- cfg: utils.NewSnapshot(ExposeConfig{RelayURLs: []string{relayA, relayB}}, ExposeConfig.snapshot),
134
+ cfg: utils.NewSnapshot(ExposeConfig{RelayURLs: []string{relayB}}, ExposeConfig.snapshot),
135
relaySet: mustRelaySet(t, relayA, relayB),
136
relayListeners: make(map[string]*listener, 2),
137
}