mirror of
https://github.com/go-gost/x.git
synced 2026-10-21 05:21:35 +00:00
feat(handler): add configurable UDP port range for SOCKS5 UDP ASSOCIATE
Add udp.bindRange.min/max (udp.minPort/maxPort) metadata to constrain the client-facing UDP relay socket to a fixed port set, so NAT routers can forward a small range to the SOCKS server. listenPacketInRange scans ports from a random offset within [lo, hi] and retries on EADDRINUSE; with no range configured, the OS chooses (port 0) as before. RFC 1928 leaves relay-port selection to the server, so BND.PORT reports the actually-bound port.
This commit is contained in:
@@ -27,6 +27,8 @@ type metadata struct {
|
||||
enableUDP bool
|
||||
udpBufferSize int
|
||||
udpResolveDomain bool
|
||||
udpBindMin int
|
||||
udpBindMax int
|
||||
compatibilityMode bool
|
||||
hash string
|
||||
muxCfg *mux.Config
|
||||
@@ -60,6 +62,8 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) {
|
||||
h.md.enableUDP = mdutil.GetBool(md, "udp")
|
||||
h.md.udpBufferSize = mdutil.GetInt(md, "udp.bufferSize", "udpBufferSize")
|
||||
h.md.udpResolveDomain = mdutil.GetBool(md, "udp.resolveDomain", "udpResolveDomain")
|
||||
h.md.udpBindMin = mdutil.GetInt(md, "udp.bindRange.min", "udp.minPort")
|
||||
h.md.udpBindMax = mdutil.GetInt(md, "udp.bindRange.max", "udp.maxPort")
|
||||
|
||||
h.md.compatibilityMode = mdutil.GetBool(md, "comp")
|
||||
h.md.hash = mdutil.GetString(md, "hash")
|
||||
|
||||
+34
-1
@@ -6,8 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/hosts"
|
||||
@@ -53,7 +55,7 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st
|
||||
Netns: h.options.Netns,
|
||||
}
|
||||
|
||||
cc, err := lc.ListenPacket(ctx, network, net.JoinHostPort(host, "0"))
|
||||
cc, err := listenPacketInRange(ctx, &lc, network, host, h.md.udpBindMin, h.md.udpBindMax)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
reply := gosocks5.NewReply(gosocks5.Failure, nil)
|
||||
@@ -194,6 +196,37 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st
|
||||
return nil
|
||||
}
|
||||
|
||||
// listenPacketInRange binds the client-facing UDP relay socket. When lo..hi is
|
||||
// a valid range (lo > 0, hi >= lo) it scans ports from a random offset within
|
||||
// [lo, hi] and retries on EADDRINUSE, so a NAT router can forward a small,
|
||||
// fixed port set to the SOCKS server. The random offset avoids every
|
||||
// association hammering port lo under concurrency; the rotation visits distinct
|
||||
// ports, so a free port is always found when the range fits the attempt cap.
|
||||
// RFC 1928 leaves relay-port selection to the server; the caller still reports
|
||||
// the actually-bound port via BND.PORT. With no range, the OS chooses (port 0).
|
||||
// A non-EADDRINUSE error fails fast.
|
||||
func listenPacketInRange(ctx context.Context, lc *xnet.ListenConfig, network, host string, lo, hi int) (net.PacketConn, error) {
|
||||
if lo > 0 && hi >= lo {
|
||||
span := hi - lo + 1
|
||||
attempts := min(span, 64)
|
||||
offset := rand.IntN(span)
|
||||
var lastErr error
|
||||
for i := range attempts {
|
||||
port := lo + (offset+i)%span
|
||||
cc, err := lc.ListenPacket(ctx, network, net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
if err == nil {
|
||||
return cc, nil
|
||||
}
|
||||
lastErr = err
|
||||
if !errors.Is(err, syscall.EADDRINUSE) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
return lc.ListenPacket(ctx, network, net.JoinHostPort(host, "0"))
|
||||
}
|
||||
|
||||
// filteredPacketConn implements SOCKS5 RFC 1928 UDP relay security by filtering
|
||||
// incoming packets to only accept those from the client IP that established the TCP control connection.
|
||||
type filteredPacketConn struct {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package v5
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
)
|
||||
|
||||
func TestListenPacketInRange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// No range configured: OS picks (port 0), always succeeds.
|
||||
lc := &xnet.ListenConfig{}
|
||||
cc, err := listenPacketInRange(ctx, lc, "udp", "127.0.0.1", 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("port-0 bind: %v", err)
|
||||
}
|
||||
cc.Close()
|
||||
|
||||
// Range configured: bound port must fall within [lo, hi]. With 51
|
||||
// candidates and attempts = min(span, 64), only a fully-occupied range
|
||||
// fails — extremely unlikely on a dev/CI host.
|
||||
const lo, hi = 35000, 35050
|
||||
cc, err = listenPacketInRange(ctx, lc, "udp", "127.0.0.1", lo, hi)
|
||||
if err != nil {
|
||||
t.Fatalf("range bind [%d,%d]: %v", lo, hi, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
_, portStr, _ := net.SplitHostPort(cc.LocalAddr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
if port < lo || port > hi {
|
||||
t.Errorf("bound port %d outside [%d, %d]", port, lo, hi)
|
||||
}
|
||||
}
|
||||
|
||||
// A single-port range whose only port is already held must surface the
|
||||
// EADDRINUSE error (so the caller replies REP 0x01) — no hang, no silent miss.
|
||||
func TestListenPacketInRange_BusyPort(t *testing.T) {
|
||||
blocker, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("blocker bind: %v", err)
|
||||
}
|
||||
defer blocker.Close()
|
||||
_, portStr, _ := net.SplitHostPort(blocker.LocalAddr().String())
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
|
||||
lc := &xnet.ListenConfig{}
|
||||
if _, err := listenPacketInRange(context.Background(), lc, "udp", "127.0.0.1", port, port); err == nil {
|
||||
t.Fatalf("expected bind on held port %d to fail", port)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user