Files
3x-ui/internal/amneziawgnet/egress_port_test.go
T
MHSanaei d103c0c012 fix(amneziawg): keep the fallback egress port below the panel's reserved ports
When 64900 was taken, the egress asked the OS for any free port. Where the
ephemeral range reaches above 62789 (Windows), that could be the Xray API port
or a node-egress, TUIC or AmneziaWG relay port, which then failed to bind. The
fallback now picks a random free loopback port below 62789.
2026-10-24 12:23:24 +02:00

115 lines
3.5 KiB
Go

package amneziawgnet
import (
"context"
"encoding/json"
"errors"
"net"
"strconv"
"testing"
)
// holdEgressBasePort occupies EgressBasePort the way another service would; a
// port the OS already refuses, such as a Windows reservation, needs no holder.
func holdEgressBasePort(t *testing.T) {
t.Helper()
ln, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(EgressBasePort)))
if err != nil {
return
}
t.Cleanup(func() { ln.Close() })
}
// bridgePort is the port a socks bridge generated right now dials.
func bridgePort(t *testing.T) int {
t.Helper()
out, ok := BuildSocksBridge([]byte(`{"protocol":"amneziawg","tag":"awg-hop","settings":{}}`))
if !ok {
t.Fatal("bridge rejected")
}
var got struct {
Settings struct {
Port int `json:"port"`
} `json:"settings"`
}
if err := json.Unmarshal(out, &got); err != nil {
t.Fatal(err)
}
return got.Settings.Port
}
// Windows can reserve a port range covering EgressBasePort, and any host can run
// another service on it; the egress must still come up and report where.
func TestEgressListenFallsBackWhenBasePortIsTaken(t *testing.T) {
srv := GetEgressServer()
srv.Close()
t.Cleanup(srv.Close)
holdEgressBasePort(t)
if err := srv.Listen(); err != nil {
t.Fatalf("Listen with EgressBasePort taken: %v", err)
}
port := srv.Port()
if port == EgressBasePort {
t.Fatalf("Port() = %d, the taken EgressBasePort", port)
}
conn, err := net.Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
if err != nil {
t.Fatalf("egress not accepting on Port() %d: %v", port, err)
}
conn.Close()
}
// Xray's bridges are generated apart from the listener, so a listener that came
// up elsewhere must be reported until the bridges are regenerated for it.
func TestBridgesStaleUntilRegeneratedForTheBoundPort(t *testing.T) {
srv := GetEgressServer()
srv.Close()
t.Cleanup(srv.Close)
holdEgressBasePort(t)
if got := bridgePort(t); got != EgressBasePort {
t.Fatalf("bridge generated before Listen dials %d, want %d", got, EgressBasePort)
}
if err := srv.Listen(); err != nil {
t.Fatal(err)
}
if !BridgesStale() {
t.Fatalf("bridges dial %d while the egress listens on %d, but BridgesStale() = false", EgressBasePort, srv.Port())
}
if got := bridgePort(t); got != srv.Port() || BridgesStale() {
t.Fatalf("regenerated bridge dials %d with BridgesStale() = %v, want %d and false", got, BridgesStale(), srv.Port())
}
}
// An OS-chosen port can land on the Xray API port or another panel relay range on hosts whose
// ephemeral range reaches that high (Windows), so the fallback must pick below that range.
func TestEgressFallbackStaysBelowThePanelRanges(t *testing.T) {
srv := GetEgressServer()
srv.Close()
t.Cleanup(srv.Close)
var asked []string
orig := listenTCP
listenTCP = func(ctx context.Context, addr string) (net.Listener, error) {
asked = append(asked, addr)
if addr == net.JoinHostPort("127.0.0.1", strconv.Itoa(EgressBasePort)) {
return nil, errors.New("base port taken")
}
return orig(ctx, addr)
}
t.Cleanup(func() { listenTCP = orig })
if err := srv.Listen(); err != nil {
t.Fatalf("Listen: %v", err)
}
for _, addr := range asked[1:] {
_, p, _ := net.SplitHostPort(addr)
if port, _ := strconv.Atoi(p); port == 0 || port >= reservedLoopbackFloor {
t.Fatalf("fallback asked for %s, want a fixed port below %d", addr, reservedLoopbackFloor)
}
}
if port := srv.Port(); port >= reservedLoopbackFloor {
t.Fatalf("Port() = %d, inside the panel's reserved range", port)
}
}