Files
3x-ui/internal/tuic/socks_bridge.go
T
MHSanaei e51a32b045 fix(tuic): bound one client's connections and UDP relays
The UDP association cap applied per QUIC connection, and nothing limited
how many connections one credential could authenticate, so a single
client could hold enough UDP relays (each a host port) to exhaust the
host's ephemeral range. A client now gets at most 32 connections and 512
open UDP relays across all of them; past that a connection is refused at
authentication and a relay is not opened.
2026-10-31 14:59:20 +01:00

619 lines
16 KiB
Go

package tuic
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/netip"
"sync"
"sync/atomic"
"time"
)
var ErrUdpPayloadTooLarge = errors.New("tuic socks: UDP packet exceeds the maximum SOCKS datagram size")
const maxSocksUdpDatagramSize = 65507
func ValidateRelayEmail(email string) error {
if len(email) == 0 || len(email) > 255 {
return errors.New("TUIC client email must contain 1 to 255 UTF-8 bytes")
}
return nil
}
// SocksRelay describes the loopback SOCKS5 endpoint where decrypted TUIC traffic is forwarded.
type SocksRelay struct {
Gate *RelayGate
InboundID int
Addr string // e.g. "127.0.0.1:63201"
Password string // internal shared password for the SOCKS inbound
}
// CountingConn wraps a net.Conn and tracks bytes read and written atomically.
type CountingConn struct {
net.Conn
bytesRead *atomic.Int64
bytesWritten *atomic.Int64
}
func (c *CountingConn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
if n > 0 && c.bytesRead != nil {
c.bytesRead.Add(int64(n))
}
return n, err
}
func (c *CountingConn) Write(p []byte) (int, error) {
n, err := c.Conn.Write(p)
if n > 0 && c.bytesWritten != nil {
c.bytesWritten.Add(int64(n))
}
return n, err
}
// DialTCP establishes a SOCKS5 CONNECT tunnel to the target address on behalf of user.
func (r *SocksRelay) DialTCP(ctx context.Context, user string, target *Address) (net.Conn, error) {
conn, err := r.dialAuthenticated(ctx, user)
if err != nil {
return nil, err
}
stop := context.AfterFunc(ctx, func() { _ = conn.Close() })
defer stop()
// Send SOCKS5 CONNECT request
req := buildSocks5ConnectRequest(target)
if req == nil {
conn.Close()
return nil, ErrInvalidAddr
}
if _, err := conn.Write(req); err != nil {
conn.Close()
return nil, fmt.Errorf("tuic socks: send CONNECT request: %w", err)
}
if _, err := readSocks5Reply(conn); err != nil {
conn.Close()
return nil, err
}
_ = conn.SetDeadline(time.Time{})
if err := ctx.Err(); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
func (r *SocksRelay) dialAuthenticated(ctx context.Context, user string) (net.Conn, error) {
if r.Gate != nil {
waiting, cancel := context.WithTimeout(ctx, relayOpenTimeout)
release, err := r.Gate.acquire(waiting, r.InboundID, user)
cancel()
if err != nil {
return nil, err
}
defer release()
}
dialer := net.Dialer{Timeout: 10 * time.Second}
conn, err := dialer.DialContext(ctx, "tcp", r.Addr)
if err != nil {
return nil, fmt.Errorf("tuic socks: dial relay %s: %w", r.Addr, err)
}
stop := context.AfterFunc(ctx, func() { _ = conn.Close() })
defer stop()
_ = conn.SetDeadline(time.Now().Add(10 * time.Second))
if err := socks5Handshake(conn, user, r.Password); err != nil {
_ = conn.Close()
return nil, err
}
if err := ctx.Err(); err != nil {
_ = conn.Close()
return nil, err
}
return conn, nil
}
func buildSocks5ConnectRequest(target *Address) []byte {
if target == nil {
return nil
}
var req []byte
switch target.Type {
case AddrTypeIPv4:
ip4 := target.IP.To4()
if len(ip4) != 4 {
return nil
}
req = make([]byte, 4+4+2)
req[0] = 0x05 // SOCKS5
req[1] = 0x01 // CONNECT
req[2] = 0x00 // RSV
req[3] = 0x01 // ATYP IPv4
copy(req[4:8], ip4)
binary.BigEndian.PutUint16(req[8:10], target.Port)
case AddrTypeIPv6:
ip16 := target.IP.To16()
if len(ip16) != 16 {
return nil
}
req = make([]byte, 4+16+2)
req[0] = 0x05
req[1] = 0x01
req[2] = 0x00
req[3] = 0x04 // ATYP IPv6
copy(req[4:20], ip16)
binary.BigEndian.PutUint16(req[20:22], target.Port)
case AddrTypeDomain:
dLen := len(target.Host)
if dLen == 0 || dLen > 255 {
return nil
}
req = make([]byte, 4+1+dLen+2)
req[0] = 0x05
req[1] = 0x01
req[2] = 0x00
req[3] = 0x03 // ATYP Domain
req[4] = byte(dLen)
copy(req[5:5+dLen], []byte(target.Host))
binary.BigEndian.PutUint16(req[5+dLen:7+dLen], target.Port)
default:
return nil
}
return req
}
// SocksUDPSession manages a SOCKS5 UDP ASSOCIATE tunnel to Xray.
type SocksUDPSession struct {
ctrl net.Conn
udpConn *net.UDPConn
targetEP net.Addr
user string
closed atomic.Bool
// onClose runs once when the session closes, releasing its place in a budget.
onClose func()
}
// DialUDP establishes a SOCKS5 UDP ASSOCIATE tunnel to Xray.
func (r *SocksRelay) DialUDP(ctx context.Context, user string) (*SocksUDPSession, error) {
ctrl, err := r.dialAuthenticated(ctx, user)
if err != nil {
return nil, err
}
stop := context.AfterFunc(ctx, func() { _ = ctrl.Close() })
defer stop()
// SOCKS5 UDP ASSOCIATE (0x03), dst 0.0.0.0:0
if _, err := ctrl.Write([]byte{0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
ctrl.Close()
return nil, fmt.Errorf("tuic socks: send UDP ASSOCIATE request: %w", err)
}
bind, err := readSocks5Reply(ctrl)
if err != nil {
ctrl.Close()
return nil, err
}
_ = ctrl.SetDeadline(time.Time{})
udpConn, err := net.DialUDP("udp", nil, net.UDPAddrFromAddrPort(bind))
if err != nil {
ctrl.Close()
return nil, fmt.Errorf("tuic socks: dial UDP relay endpoint %s: %w", bind, err)
}
if err := ctx.Err(); err != nil {
_ = ctrl.Close()
_ = udpConn.Close()
return nil, err
}
return &SocksUDPSession{
ctrl: ctrl,
udpConn: udpConn,
targetEP: udpConn.RemoteAddr(),
user: user,
}, nil
}
// Send sends a UDP payload to target via the SOCKS5 UDP ASSOCIATE relay.
func (s *SocksUDPSession) Send(target *Address, payload []byte) (int, error) {
if s.closed.Load() {
return 0, net.ErrClosed
}
packet, err := buildSocks5UDPRequest(target, payload)
if err != nil {
return 0, err
}
return s.udpConn.Write(packet)
}
func buildSocks5UDPRequest(target *Address, payload []byte) ([]byte, error) {
hdr := buildSocks5UDPHeader(target)
if hdr == nil {
return nil, ErrInvalidAddr
}
if len(hdr)+len(payload) > maxSocksUdpDatagramSize {
return nil, ErrUdpPayloadTooLarge
}
packet := make([]byte, len(hdr)+len(payload))
copy(packet, hdr)
copy(packet[len(hdr):], payload)
return packet, nil
}
// Receive reads a relayed UDP payload and extracts its original source address.
func (s *SocksUDPSession) Receive(buf []byte) (*Address, []byte, error) {
if s.closed.Load() {
return nil, nil, net.ErrClosed
}
n, err := s.udpConn.Read(buf)
if err != nil {
return nil, nil, err
}
if n < 4 {
return nil, nil, fmt.Errorf("tuic socks: UDP packet too short (%d bytes)", n)
}
// SOCKS5 UDP header: [RSV(2)][FRAG(1)][ATYP(1)]
atyp := buf[3]
var addr *Address
var offset int
switch atyp {
case 0x01: // IPv4
if n < 10 {
return nil, nil, fmt.Errorf("tuic socks: truncated IPv4 UDP reply")
}
ip := net.IP(buf[4:8])
port := binary.BigEndian.Uint16(buf[8:10])
addr = &Address{Type: AddrTypeIPv4, IP: ip, Host: ip.String(), Port: port}
offset = 10
case 0x04: // IPv6
if n < 22 {
return nil, nil, fmt.Errorf("tuic socks: truncated IPv6 UDP reply")
}
ip := net.IP(buf[4:20])
port := binary.BigEndian.Uint16(buf[20:22])
addr = &Address{Type: AddrTypeIPv6, IP: ip, Host: ip.String(), Port: port}
offset = 22
case 0x03: // Domain
dLen := int(buf[4])
if n < 5+dLen+2 {
return nil, nil, fmt.Errorf("tuic socks: truncated domain UDP reply")
}
host := string(buf[5 : 5+dLen])
port := binary.BigEndian.Uint16(buf[5+dLen : 7+dLen])
addr = &Address{Type: AddrTypeDomain, Host: host, Port: port}
offset = 7 + dLen
default:
return nil, nil, fmt.Errorf("tuic socks: unsupported reply ATYP 0x%02x", atyp)
}
return addr, buf[offset:n], nil
}
// Close closes the SOCKS5 UDP session.
func (s *SocksUDPSession) Close() error {
if s.closed.Swap(true) {
return nil
}
if s.onClose != nil {
s.onClose()
}
_ = s.udpConn.Close()
return s.ctrl.Close()
}
func buildSocks5UDPHeader(target *Address) []byte {
if target == nil {
return nil
}
var hdr []byte
switch target.Type {
case AddrTypeIPv4:
ip4 := target.IP.To4()
if len(ip4) != 4 {
return nil
}
hdr = make([]byte, 10)
hdr[0] = 0x00 // RSV
hdr[1] = 0x00 // RSV
hdr[2] = 0x00 // FRAG
hdr[3] = 0x01 // ATYP IPv4
copy(hdr[4:8], ip4)
binary.BigEndian.PutUint16(hdr[8:10], target.Port)
case AddrTypeIPv6:
ip16 := target.IP.To16()
if len(ip16) != 16 {
return nil
}
hdr = make([]byte, 22)
hdr[0] = 0x00
hdr[1] = 0x00
hdr[2] = 0x00
hdr[3] = 0x04 // ATYP IPv6
copy(hdr[4:20], ip16)
binary.BigEndian.PutUint16(hdr[20:22], target.Port)
case AddrTypeDomain:
dLen := len(target.Host)
if dLen == 0 || dLen > 255 {
return nil
}
hdr = make([]byte, 4+1+dLen+2)
hdr[0] = 0x00
hdr[1] = 0x00
hdr[2] = 0x00
hdr[3] = 0x03 // ATYP Domain
hdr[4] = byte(dLen)
copy(hdr[5:5+dLen], []byte(target.Host))
binary.BigEndian.PutUint16(hdr[5+dLen:7+dLen], target.Port)
default:
return nil
}
return hdr
}
func socks5Handshake(conn net.Conn, user, password string) error {
if _, err := conn.Write([]byte{0x05, 0x02, 0x00, 0x02}); err != nil {
return fmt.Errorf("tuic socks: send greeting: %w", err)
}
var resp [2]byte
if _, err := io.ReadFull(conn, resp[:]); err != nil {
return fmt.Errorf("tuic socks: read greeting reply: %w", err)
}
if resp[0] != 0x05 {
return fmt.Errorf("tuic socks: unexpected SOCKS version %d", resp[0])
}
switch resp[1] {
case 0x00: // no auth
return nil
case 0x02: // username/password
req := make([]byte, 0, 3+len(user)+len(password))
req = append(req, 0x01, byte(len(user)))
req = append(req, user...)
req = append(req, byte(len(password)))
req = append(req, password...)
if _, err := conn.Write(req); err != nil {
return fmt.Errorf("tuic socks: send auth: %w", err)
}
var authResp [2]byte
if _, err := io.ReadFull(conn, authResp[:]); err != nil {
return fmt.Errorf("tuic socks: read auth reply: %w", err)
}
if authResp[1] != 0x00 {
return fmt.Errorf("tuic socks: auth rejected (code %d)", authResp[1])
}
return nil
default:
return fmt.Errorf("tuic socks: unsupported auth method %d", resp[1])
}
}
func readSocks5Reply(r io.Reader) (netip.AddrPort, error) {
var hdr [4]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return netip.AddrPort{}, fmt.Errorf("tuic socks: read reply header: %w", err)
}
if hdr[0] != 0x05 {
return netip.AddrPort{}, fmt.Errorf("tuic socks: unexpected SOCKS version %d", hdr[0])
}
if hdr[1] != 0x00 {
return netip.AddrPort{}, fmt.Errorf("tuic socks: request rejected (code %d)", hdr[1])
}
addr, err := readSocks5Addr(r, hdr[3])
if err != nil {
return netip.AddrPort{}, err
}
var portBytes [2]byte
if _, err := io.ReadFull(r, portBytes[:]); err != nil {
return netip.AddrPort{}, fmt.Errorf("tuic socks: read reply port: %w", err)
}
return netip.AddrPortFrom(addr, binary.BigEndian.Uint16(portBytes[:])), nil
}
func readSocks5Addr(r io.Reader, atyp byte) (netip.Addr, error) {
switch atyp {
case 0x01:
var b [4]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return netip.Addr{}, err
}
return netip.AddrFrom4(b), nil
case 0x04:
var b [16]byte
if _, err := io.ReadFull(r, b[:]); err != nil {
return netip.Addr{}, err
}
return netip.AddrFrom16(b), nil
case 0x03:
var l [1]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return netip.Addr{}, err
}
name := make([]byte, l[0])
if _, err := io.ReadFull(r, name); err != nil {
return netip.Addr{}, err
}
resolved, err := net.ResolveIPAddr("ip", string(name))
if err != nil {
return netip.Addr{}, fmt.Errorf("tuic socks: resolve domain reply %q: %w", name, err)
}
addr, ok := netip.AddrFromSlice(resolved.IP)
if !ok {
return netip.Addr{}, fmt.Errorf("tuic socks: unparseable domain reply address")
}
return addr, nil
default:
return netip.Addr{}, fmt.Errorf("tuic socks: unsupported SOCKS5 address type %d", atyp)
}
}
// halfCloseIdle bounds how long the surviving direction of a half-closed pair
// may sit idle, so a peer that vanished mid-transfer cannot pin it forever.
const halfCloseIdle = 2 * time.Minute
type closeWriter interface {
CloseWrite() error
}
type readDeadliner interface {
SetReadDeadline(t time.Time) error
}
type guardedReader struct {
r io.Reader
dl readDeadliner
armed atomic.Bool
}
func newGuardedReader(r io.Reader) *guardedReader {
gr := &guardedReader{r: r}
if dl, ok := r.(readDeadliner); ok {
gr.dl = dl
}
return gr
}
func (g *guardedReader) Read(p []byte) (int, error) {
if g.armed.Load() && g.dl != nil {
_ = g.dl.SetReadDeadline(time.Now().Add(halfCloseIdle))
}
return g.r.Read(p)
}
func (g *guardedReader) arm() {
g.armed.Store(true)
if g.dl != nil {
_ = g.dl.SetReadDeadline(time.Now().Add(halfCloseIdle))
}
}
// PipeBiDirectional pipes data between two connections and tracks byte counts in each direction.
func PipeBiDirectional(a, b io.ReadWriteCloser, upCounter, downCounter *atomic.Int64) {
PipeBiDirectionalContext(context.Background(), a, b, upCounter, downCounter)
}
func PipeBiDirectionalContext(ctx context.Context, a, b io.ReadWriteCloser, upCounter, downCounter *atomic.Int64) {
closeBoth := func() { _ = a.Close(); _ = b.Close() }
stop := context.AfterFunc(ctx, closeBoth)
defer stop()
ga := newGuardedReader(a)
gb := newGuardedReader(b)
var wg sync.WaitGroup
wg.Add(2)
pipe := func(dst io.Writer, dstGuard *guardedReader, src *guardedReader, counter *atomic.Int64) {
defer wg.Done()
buf := make([]byte, 32*1024)
for {
n, err := src.Read(buf)
if n > 0 {
if counter != nil {
counter.Add(int64(n))
}
if _, werr := dst.Write(buf[:n]); werr != nil {
closeBoth()
break
}
}
if err != nil {
if !errors.Is(err, io.EOF) {
closeBoth()
}
break
}
}
if cw, ok := dst.(closeWriter); ok {
_ = cw.CloseWrite()
} else if closer, ok := dst.(io.Closer); ok {
_ = closer.Close()
}
dstGuard.arm()
}
// a -> b (upload: client to upstream)
go pipe(b, gb, ga, upCounter)
// b -> a (download: upstream to client)
go pipe(a, ga, gb, downCounter)
wg.Wait()
_ = a.Close()
_ = b.Close()
}
// SOCKSBasePort is the first loopback port used for a TUIC inbound's
// internal Xray SOCKS5 relay inbound.
const SOCKSBasePort = 64000
// relayPortSlots is how many ids fit in the TUIC relay port window (64001..65000).
const relayPortSlots = 1000
// SOCKSPortForInbound derives one inbound's loopback SOCKS5 relay port from
// its id, bounded within the dedicated range 64001..65000 so it never collides
// with AmneziaWG (65101..65535), API (62789), or node egress (62800..63800).
func SOCKSPortForInbound(inboundID int) int {
if inboundID <= 0 {
return SOCKSBasePort + 1
}
return SOCKSBasePort + 1 + (inboundID-1)%relayPortSlots
}
var (
socksPasswordOnce sync.Once
socksPassword string
)
// SocksPassword returns the process-wide password used to authenticate into
// every TUIC SOCKS5 relay inbound.
func SocksPassword() string {
socksPasswordOnce.Do(func() {
var b [24]byte
if _, err := rand.Read(b[:]); err != nil {
socksPassword = fmt.Sprintf("tuic-fallback-%x", b)
return
}
socksPassword = base64.RawURLEncoding.EncodeToString(b[:])
})
return socksPassword
}
// SocksInboundSettings preserves each client email as the Xray routing identity.
// The caller selects a policy level so relay traffic is not billed twice.
func SocksInboundSettings(emails []string, password string, userLevel uint32) ([]byte, error) {
type account struct {
User string `json:"user"`
Pass string `json:"pass"`
}
settings := struct {
Auth string `json:"auth"`
UDP bool `json:"udp"`
UserLevel uint32 `json:"userLevel"`
Accounts []account `json:"accounts"`
}{Auth: "password", UDP: true, UserLevel: userLevel}
for _, email := range emails {
settings.Accounts = append(settings.Accounts, account{User: email, Pass: password})
}
return json.Marshal(settings)
}