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.
This commit is contained in:
MHSanaei
2026-10-24 12:23:24 +02:00
parent e66d18fe05
commit d103c0c012
2 changed files with 63 additions and 3 deletions
+30 -3
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
mrand "math/rand/v2"
"net"
"net/netip"
"sync"
@@ -23,6 +24,15 @@ import (
// first; generated amneziawg socks bridges dial whichever port it holds.
const EgressBasePort = 64900
// reservedLoopbackFloor is where the panel's own loopback listeners start (Xray API 62789, node
// egress, TUIC and AmneziaWG relays); a fallback egress port must stay below it.
const reservedLoopbackFloor = 62789
// listenTCP opens a loopback listener; a test stands in for the OS through it.
var listenTCP = func(ctx context.Context, addr string) (net.Listener, error) {
return (&net.ListenConfig{}).Listen(ctx, "tcp", addr)
}
// socks5EgressServer is a minimal loopback SOCKS5 server routing Xray's
// bridged amneziawg outbounds into their embedded devices' netstacks.
type socks5EgressServer struct {
@@ -114,11 +124,10 @@ func (s *socks5EgressServer) Listen() error {
if s.listener != nil {
return nil
}
lc := &net.ListenConfig{}
ln, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", EgressBasePort))
ln, err := listenTCP(context.Background(), fmt.Sprintf("127.0.0.1:%d", EgressBasePort))
if err != nil {
var fallbackErr error
if ln, fallbackErr = lc.Listen(context.Background(), "tcp", "127.0.0.1:0"); fallbackErr != nil {
if ln, fallbackErr = listenBelowReserved(context.Background()); fallbackErr != nil {
return fmt.Errorf("amneziawgnet: egress listen: %w", errors.Join(err, fallbackErr))
}
logger.Warningf("amneziawgnet: egress port %d unavailable, using a free port: %v", EgressBasePort, err)
@@ -131,6 +140,24 @@ func (s *socks5EgressServer) Listen() error {
return nil
}
// fallbackPortFloor keeps the fallback clear of the well-known and registered low ports.
const fallbackPortFloor = 40000
// listenBelowReserved binds a random loopback port under reservedLoopbackFloor, retrying taken or
// OS-reserved ones; an OS-chosen port could land inside the panel's own ranges.
func listenBelowReserved(ctx context.Context) (net.Listener, error) {
var errs []error
for range 64 {
port := fallbackPortFloor + mrand.IntN(reservedLoopbackFloor-fallbackPortFloor)
ln, err := listenTCP(ctx, fmt.Sprintf("127.0.0.1:%d", port))
if err == nil {
return ln, nil
}
errs = append(errs, err)
}
return nil, errors.Join(errs...)
}
// Port is the port generated socks bridges must dial: the bound one, or
// EgressBasePort while nothing is bound, since Listen tries it first.
func (s *socks5EgressServer) Port() int {
+33
View File
@@ -1,7 +1,9 @@
package amneziawgnet
import (
"context"
"encoding/json"
"errors"
"net"
"strconv"
"testing"
@@ -79,3 +81,34 @@ func TestBridgesStaleUntilRegeneratedForTheBoundPort(t *testing.T) {
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)
}
}