From 8503230891d7f8d4abed4d5e2c26d822fffd9f84 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Sun, 17 May 2026 03:00:49 +0800 Subject: [PATCH 01/17] Socks5 UDP with standard RFC behavior --- proxy/socks/protocol.go | 37 ++++++++++++++++++++++------------ proxy/socks/server.go | 37 ++++++++++++---------------------- proxy/socks/temp_udp_listen.go | 36 +++++++++++++++++++++++++++++++++ proxy/socks/udpfilter.go | 31 ---------------------------- 4 files changed, 73 insertions(+), 68 deletions(-) create mode 100644 proxy/socks/temp_udp_listen.go delete mode 100644 proxy/socks/udpfilter.go diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 76ceb47c0820..a52335084795 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -137,13 +137,13 @@ func (s *ServerSession) auth5(nMethod byte, reader io.Reader, writer io.Writer) return "", nil } -func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { +func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, *TempUDPConn, error) { var ( username string err error ) if username, err = s.auth5(nMethod, reader, writer); err != nil { - return nil, err + return nil, nil, err } var cmd byte @@ -151,7 +151,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri buffer := buf.StackNew() if _, err := buffer.ReadFullFrom(reader, 3); err != nil { buffer.Release() - return nil, errors.New("failed to read request").Base(err) + return nil, nil, errors.New("failed to read request").Base(err) } cmd = buffer.Byte(1) buffer.Release() @@ -168,28 +168,29 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri case cmdUDPAssociate: if !s.config.UdpEnabled { writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) - return nil, errors.New("UDP is not enabled.") + return nil, nil, errors.New("UDP is not enabled.") } request.Command = protocol.RequestCommandUDP case cmdTCPBind: writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) - return nil, errors.New("TCP bind is not supported.") + return nil, nil, errors.New("TCP bind is not supported.") default: writeSocks5Response(writer, statusCmdNotSupport, net.AnyIP, net.Port(0)) - return nil, errors.New("unknown command ", cmd) + return nil, nil, errors.New("unknown command ", cmd) } request.Version = socks5Version addr, port, err := addrParser.ReadAddressPort(nil, reader) if err != nil { - return nil, errors.New("failed to read address").Base(err) + return nil, nil, errors.New("failed to read address").Base(err) } request.Address = addr request.Port = port responseAddress := s.address responsePort := s.port + var tempUDPConn *TempUDPConn //nolint:gocritic // Use if else chain for clarity if request.Command == protocol.RequestCommandUDP { if s.config.Address != nil { @@ -199,20 +200,29 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri // Use conn.LocalAddr() IP as remote address in the response by default responseAddress = s.localAddress } + udpHub, err := net.ListenUDP("udp", &net.UDPAddr{IP: responseAddress.IP()}) + if err != nil { + return nil, nil, errors.New("failed to create UDP listener").Base(err) + } + responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) + tempUDPConn = &TempUDPConn{ + UDPConn: udpHub, + } } if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { - return nil, err + common.CloseIfExists(tempUDPConn) + return nil, nil, err } - return request, nil + return request, tempUDPConn, nil } // Handshake performs a Socks4/4a/5 handshake. -func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol.RequestHeader, error) { +func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol.RequestHeader, *TempUDPConn, error) { buffer := buf.StackNew() if _, err := buffer.ReadFullFrom(reader, 2); err != nil { buffer.Release() - return nil, errors.New("insufficient header").Base(err) + return nil, nil, errors.New("insufficient header").Base(err) } version := buffer.Byte(0) @@ -221,11 +231,12 @@ func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol switch version { case socks4Version: - return s.handshake4(cmd, reader, writer) + header, err := s.handshake4(cmd, reader, writer) + return header, nil, err case socks5Version: return s.handshake5(cmd, reader, writer) default: - return nil, errors.New("unknown Socks version: ", version) + return nil, nil, errors.New("unknown Socks version: ", version) } } diff --git a/proxy/socks/server.go b/proxy/socks/server.go index 478410f35735..8c4dc6949991 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -29,7 +29,6 @@ type Server struct { config *ServerConfig policyManager policy.Manager cone bool - udpFilter *UDPFilter httpServer *http.Server } @@ -46,7 +45,6 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { } if config.AuthType == AuthType_PASSWORD { httpConfig.Accounts = config.Accounts - s.udpFilter = new(UDPFilter) // We only use this when auth is enabled } s.httpServer, _ = http.NewServer(ctx, httpConfig) return s, nil @@ -60,11 +58,7 @@ func (s *Server) policy() policy.Session { // Network implements proxy.Inbound. func (s *Server) Network() []net.Network { - list := []net.Network{net.Network_TCP} - if s.config.UdpEnabled { - list = append(list, net.Network_UDP) - } - return list + return []net.Network{net.Network_TCP} } // Process implements proxy.Inbound. @@ -94,8 +88,6 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con return s.httpServer.ProcessWithFirstbyte(ctx, network, conn, dispatcher, firstbyte...) } return s.processTCP(ctx, conn, dispatcher, firstbyte) - case net.Network_UDP: - return s.handleUDPPayload(ctx, conn, dispatcher) default: return errors.New("unknown network: ", network) } @@ -126,7 +118,8 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche Reader: buf.NewReader(conn), Buffer: buf.MultiBuffer{buf.FromBytes(firstbyte)}, } - request, err := svrSession.Handshake(reader, conn) + request, tempUDPConn, err := svrSession.Handshake(reader, conn) + defer common.CloseIfExists(tempUDPConn) if err != nil { if inbound.Source.IsValid() { log.Record(&log.AccessMessage{ @@ -170,26 +163,22 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche } if request.Command == protocol.RequestCommandUDP { - if s.udpFilter != nil { - s.udpFilter.Add(conn.RemoteAddr()) + if tempUDPConn == nil { + return errors.New("UDP associate with listen port failed") } - return s.handleUDP(conn) + ctx, cancel := context.WithCancel(ctx) + errCh := make(chan error, 1) + go func() { + errCh <- s.handleUDPPayload(ctx, tempUDPConn, dispatcher) + }() + io.Copy(buf.DiscardBytes, conn) + cancel() + return <-errCh } - return nil } -func (*Server) handleUDP(c io.Reader) error { - // The TCP connection closes after this method returns. We need to wait until - // the client closes it. - return common.Error2(io.Copy(buf.DiscardBytes, c)) -} - func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error { - if s.udpFilter != nil && !s.udpFilter.Check(conn.RemoteAddr()) { - errors.LogDebug(ctx, "Unauthorized UDP access from ", conn.RemoteAddr().String()) - return nil - } udpServer := udp.NewDispatcher(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { payload := packet.Payload errors.LogDebug(ctx, "writing back UDP response with ", payload.Len(), " bytes") diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go new file mode 100644 index 000000000000..5fec57f39b12 --- /dev/null +++ b/proxy/socks/temp_udp_listen.go @@ -0,0 +1,36 @@ +package socks + +import ( + "net" + sync "sync" + + "github.com/xtls/xray-core/common/errors" +) + +type TempUDPConn struct { + *net.UDPConn + once sync.Once + remote net.Addr +} + +func (c *TempUDPConn) Read(b []byte) (n int, err error) { + n, addr, err := c.ReadFrom(b) + if err != nil { + return 0, err + } + c.once.Do(func() { + c.remote = addr + }) + return n, nil +} + +func (c *TempUDPConn) Write(b []byte) (n int, err error) { + if c.remote == nil { + return 0, errors.New("remote address not determined yet") + } + return c.UDPConn.WriteTo(b, c.remote) +} + +func (c *TempUDPConn) RemoteAddr() net.Addr { + return c.remote +} diff --git a/proxy/socks/udpfilter.go b/proxy/socks/udpfilter.go deleted file mode 100644 index 9ae3e69716d8..000000000000 --- a/proxy/socks/udpfilter.go +++ /dev/null @@ -1,31 +0,0 @@ -package socks - -import ( - "net" - "sync" -) - -/* -In the sock implementation of * ray, UDP authentication is flawed and can be bypassed. -Tracking a UDP connection may be a bit troublesome. -Here is a simple solution. -We create a filter, add remote IP to the pool when it try to establish a UDP connection with auth. -And drop UDP packets from unauthorized IP. -After discussion, we believe it is not necessary to add a timeout mechanism to this filter. -*/ - -type UDPFilter struct { - ips sync.Map -} - -func (f *UDPFilter) Add(addr net.Addr) bool { - ip, _, _ := net.SplitHostPort(addr.String()) - f.ips.Store(ip, true) - return true -} - -func (f *UDPFilter) Check(addr net.Addr) bool { - ip, _, _ := net.SplitHostPort(addr.String()) - _, ok := f.ips.Load(ip) - return ok -} From eaa9316c7b8807ee0b88cc4a3875f26c6878ecaa Mon Sep 17 00:00:00 2001 From: Fangliding Date: Sun, 17 May 2026 19:49:42 +0800 Subject: [PATCH 02/17] Timeout --- proxy/socks/protocol.go | 8 ++--- proxy/socks/server.go | 7 +++-- proxy/socks/temp_udp_listen.go | 56 ++++++++++++++++++++++++++++------ 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index a52335084795..58a6681a756f 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -137,7 +137,7 @@ func (s *ServerSession) auth5(nMethod byte, reader io.Reader, writer io.Writer) return "", nil } -func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Writer) (*protocol.RequestHeader, *TempUDPConn, error) { +func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Conn) (*protocol.RequestHeader, *TempUDPConn, error) { var ( username string err error @@ -205,9 +205,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri return nil, nil, errors.New("failed to create UDP listener").Base(err) } responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) - tempUDPConn = &TempUDPConn{ - UDPConn: udpHub, - } + tempUDPConn = NewTempUDPConn(udpHub, writer) } if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { common.CloseIfExists(tempUDPConn) @@ -218,7 +216,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer io.Wri } // Handshake performs a Socks4/4a/5 handshake. -func (s *ServerSession) Handshake(reader io.Reader, writer io.Writer) (*protocol.RequestHeader, *TempUDPConn, error) { +func (s *ServerSession) Handshake(reader io.Reader, writer net.Conn) (*protocol.RequestHeader, *TempUDPConn, error) { buffer := buf.StackNew() if _, err := buffer.ReadFullFrom(reader, 2); err != nil { buffer.Release() diff --git a/proxy/socks/server.go b/proxy/socks/server.go index 8c4dc6949991..fea8151512b3 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -166,13 +166,16 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche if tempUDPConn == nil { return errors.New("UDP associate with listen port failed") } - ctx, cancel := context.WithCancel(ctx) + tempUDPConn.SetTimeout(plcy.Timeouts.ConnectionIdle) errCh := make(chan error, 1) go func() { errCh <- s.handleUDPPayload(ctx, tempUDPConn, dispatcher) }() + // Asociate TCP keeps the UDP alive + // Close UDP if TCP connection is closed + // Or Close TCP if UDP is idle timeout io.Copy(buf.DiscardBytes, conn) - cancel() + tempUDPConn.Close() return <-errCh } return nil diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 5fec57f39b12..70150ef706d6 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -1,30 +1,54 @@ package socks import ( + "context" "net" - sync "sync" + "sync/atomic" + "time" "github.com/xtls/xray-core/common/errors" + "github.com/xtls/xray-core/common/signal" ) +func NewTempUDPConn(udpConn *net.UDPConn, tcpConn net.Conn) *TempUDPConn { + return &TempUDPConn{ + UDPConn: udpConn, + AssociateTCPConn: tcpConn, + } +} + +// TempUDPConn wait for the first packet to determine the remote address +// SetTimeout MUST be called before any read/write operation type TempUDPConn struct { *net.UDPConn - once sync.Once - remote net.Addr + AssociateTCPConn net.Conn + + timer *signal.ActivityTimer + firstPacketDone atomic.Bool + remote *net.UDPAddr } func (c *TempUDPConn) Read(b []byte) (n int, err error) { - n, addr, err := c.ReadFrom(b) - if err != nil { - return 0, err + c.timer.Update() + if c.firstPacketDone.CompareAndSwap(false, true) { + n, remote, err := c.UDPConn.ReadFromUDP(b) + c.remote = remote + return n, err + } + for { + n, remote, err := c.UDPConn.ReadFromUDP(b) + if err != nil { + return n, err + } + if remote.AddrPort() != c.remote.AddrPort() { + continue + } + return n, err } - c.once.Do(func() { - c.remote = addr - }) - return n, nil } func (c *TempUDPConn) Write(b []byte) (n int, err error) { + c.timer.Update() if c.remote == nil { return 0, errors.New("remote address not determined yet") } @@ -34,3 +58,15 @@ func (c *TempUDPConn) Write(b []byte) (n int, err error) { func (c *TempUDPConn) RemoteAddr() net.Addr { return c.remote } + +func (c *TempUDPConn) SetTimeout(d time.Duration) { + c.timer = signal.CancelAfterInactivity(context.Background(), func() { + c.Close() + }, d) +} + +func (c *TempUDPConn) Close() error { + c.timer.SetTimeout(0) + c.AssociateTCPConn.Close() + return c.UDPConn.Close() +} From 564c7ab67427294cd902bb5e1d5a8c43e3d50abe Mon Sep 17 00:00:00 2001 From: Fangliding Date: Sun, 17 May 2026 20:21:24 +0800 Subject: [PATCH 03/17] packet --- proxy/socks/protocol.go | 4 +++- proxy/socks/temp_udp_listen.go | 18 +++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 58a6681a756f..7fedb521d521 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -1,6 +1,7 @@ package socks import ( + "context" "encoding/binary" "io" @@ -9,6 +10,7 @@ import ( "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/protocol" + "github.com/xtls/xray-core/transport/internet" ) const ( @@ -200,7 +202,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co // Use conn.LocalAddr() IP as remote address in the response by default responseAddress = s.localAddress } - udpHub, err := net.ListenUDP("udp", &net.UDPAddr{IP: responseAddress.IP()}) + udpHub, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: responseAddress.IP(), Port: 0}, nil) if err != nil { return nil, nil, errors.New("failed to create UDP listener").Base(err) } diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 70150ef706d6..02de5fe93586 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -10,9 +10,9 @@ import ( "github.com/xtls/xray-core/common/signal" ) -func NewTempUDPConn(udpConn *net.UDPConn, tcpConn net.Conn) *TempUDPConn { +func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn) *TempUDPConn { return &TempUDPConn{ - UDPConn: udpConn, + PacketConn: udpConn, AssociateTCPConn: tcpConn, } } @@ -20,27 +20,27 @@ func NewTempUDPConn(udpConn *net.UDPConn, tcpConn net.Conn) *TempUDPConn { // TempUDPConn wait for the first packet to determine the remote address // SetTimeout MUST be called before any read/write operation type TempUDPConn struct { - *net.UDPConn + net.PacketConn AssociateTCPConn net.Conn timer *signal.ActivityTimer firstPacketDone atomic.Bool - remote *net.UDPAddr + remote net.Addr } func (c *TempUDPConn) Read(b []byte) (n int, err error) { c.timer.Update() if c.firstPacketDone.CompareAndSwap(false, true) { - n, remote, err := c.UDPConn.ReadFromUDP(b) + n, remote, err := c.PacketConn.ReadFrom(b) c.remote = remote return n, err } for { - n, remote, err := c.UDPConn.ReadFromUDP(b) + n, remote, err := c.PacketConn.ReadFrom(b) if err != nil { return n, err } - if remote.AddrPort() != c.remote.AddrPort() { + if remote.String() != c.remote.String() { continue } return n, err @@ -52,7 +52,7 @@ func (c *TempUDPConn) Write(b []byte) (n int, err error) { if c.remote == nil { return 0, errors.New("remote address not determined yet") } - return c.UDPConn.WriteTo(b, c.remote) + return c.PacketConn.WriteTo(b, c.remote) } func (c *TempUDPConn) RemoteAddr() net.Addr { @@ -68,5 +68,5 @@ func (c *TempUDPConn) SetTimeout(d time.Duration) { func (c *TempUDPConn) Close() error { c.timer.SetTimeout(0) c.AssociateTCPConn.Close() - return c.UDPConn.Close() + return c.PacketConn.Close() } From b0df5722adc4799e14037e14006df45da30cda95 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Mon, 18 May 2026 13:57:48 +0800 Subject: [PATCH 04/17] Typo --- proxy/socks/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/socks/server.go b/proxy/socks/server.go index fea8151512b3..fab66476617a 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -171,7 +171,7 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche go func() { errCh <- s.handleUDPPayload(ctx, tempUDPConn, dispatcher) }() - // Asociate TCP keeps the UDP alive + // Associated TCP keeps the UDP alive // Close UDP if TCP connection is closed // Or Close TCP if UDP is idle timeout io.Copy(buf.DiscardBytes, conn) From dd3b719722b44a68e9b6561f785ec5e66fec630a Mon Sep 17 00:00:00 2001 From: Fangliding Date: Sat, 23 May 2026 21:59:53 +0800 Subject: [PATCH 05/17] Limit remote --- proxy/socks/temp_udp_listen.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 02de5fe93586..261f36e15554 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -23,24 +23,27 @@ type TempUDPConn struct { net.PacketConn AssociateTCPConn net.Conn - timer *signal.ActivityTimer - firstPacketDone atomic.Bool - remote net.Addr + timer *signal.ActivityTimer + remote atomic.Pointer[net.Addr] } func (c *TempUDPConn) Read(b []byte) (n int, err error) { c.timer.Update() - if c.firstPacketDone.CompareAndSwap(false, true) { - n, remote, err := c.PacketConn.ReadFrom(b) - c.remote = remote - return n, err - } for { n, remote, err := c.PacketConn.ReadFrom(b) if err != nil { return n, err } - if remote.String() != c.remote.String() { + if c.remote.Load() == nil { + tcpRemote, _, _ := net.SplitHostPort(c.AssociateTCPConn.RemoteAddr().String()) + udpRemote, _, _ := net.SplitHostPort(remote.String()) + if tcpRemote != udpRemote { + continue + } else { + c.remote.CompareAndSwap(nil, &remote) + } + } + if remote.String() != (*c.remote.Load()).String() { continue } return n, err @@ -49,14 +52,17 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { func (c *TempUDPConn) Write(b []byte) (n int, err error) { c.timer.Update() - if c.remote == nil { + if c.remote.Load() == nil { return 0, errors.New("remote address not determined yet") } - return c.PacketConn.WriteTo(b, c.remote) + return c.PacketConn.WriteTo(b, *c.remote.Load()) } func (c *TempUDPConn) RemoteAddr() net.Addr { - return c.remote + if c.remote.Load() == nil { + return nil + } + return *c.remote.Load() } func (c *TempUDPConn) SetTimeout(d time.Duration) { From df06d8f3df2046bc8c175f559d96d200eef8f262 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Sun, 24 May 2026 23:15:47 +0800 Subject: [PATCH 06/17] dst addr --- proxy/socks/protocol.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 7fedb521d521..22c741791775 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -4,6 +4,7 @@ import ( "context" "encoding/binary" "io" + gonet "net" "github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common/buf" @@ -208,6 +209,13 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co } responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) tempUDPConn = NewTempUDPConn(udpHub, writer) + if !(request.Address.IP().IsUnspecified() && request.Port == 0) { + var udpRemote gonet.Addr = &gonet.UDPAddr{ + IP: request.Address.IP(), + Port: int(request.Port), + } + tempUDPConn.remote.Store(&udpRemote) + } } if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { common.CloseIfExists(tempUDPConn) From 17dfdb34f11944ee2a5f0d4ca910f9766ce657fe Mon Sep 17 00:00:00 2001 From: Fangliding Date: Tue, 26 May 2026 15:52:55 +0800 Subject: [PATCH 07/17] Handle only IP --- proxy/socks/protocol.go | 13 +++++++++---- proxy/socks/temp_udp_listen.go | 11 +++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 22c741791775..9c388499d893 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -210,11 +210,16 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) tempUDPConn = NewTempUDPConn(udpHub, writer) if !(request.Address.IP().IsUnspecified() && request.Port == 0) { - var udpRemote gonet.Addr = &gonet.UDPAddr{ - IP: request.Address.IP(), - Port: int(request.Port), + // only specified an IP without port + if request.Port == 0 { + tempUDPConn.predefinedRemoteIP = request.Address.String() + } else { // specified both IP and port + var udpRemote gonet.Addr = &gonet.UDPAddr{ + IP: request.Address.IP(), + Port: int(request.Port), + } + tempUDPConn.remote.Store(&udpRemote) } - tempUDPConn.remote.Store(&udpRemote) } } if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 261f36e15554..ef62a76e9109 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -23,6 +23,8 @@ type TempUDPConn struct { net.PacketConn AssociateTCPConn net.Conn + predefinedRemoteIP string + timer *signal.ActivityTimer remote atomic.Pointer[net.Addr] } @@ -35,9 +37,14 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { return n, err } if c.remote.Load() == nil { - tcpRemote, _, _ := net.SplitHostPort(c.AssociateTCPConn.RemoteAddr().String()) + var expectedRemote string + if c.predefinedRemoteIP != "" { + expectedRemote = c.predefinedRemoteIP + } else { + expectedRemote, _, _ = net.SplitHostPort(c.AssociateTCPConn.RemoteAddr().String()) + } udpRemote, _, _ := net.SplitHostPort(remote.String()) - if tcpRemote != udpRemote { + if expectedRemote != udpRemote { continue } else { c.remote.CompareAndSwap(nil, &remote) From bbd6e41228981c4be37cb808d026e140683d62cd Mon Sep 17 00:00:00 2001 From: Fangliding Date: Tue, 26 May 2026 16:03:17 +0800 Subject: [PATCH 08/17] predefine --- proxy/socks/protocol.go | 5 +++-- proxy/socks/temp_udp_listen.go | 14 ++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 9c388499d893..2a0f1d18e284 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -208,11 +208,12 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co return nil, nil, errors.New("failed to create UDP listener").Base(err) } responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) - tempUDPConn = NewTempUDPConn(udpHub, writer) + expectedRemoteIP, _, _ := net.SplitHostPort(writer.RemoteAddr().String()) + tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemoteIP) if !(request.Address.IP().IsUnspecified() && request.Port == 0) { // only specified an IP without port if request.Port == 0 { - tempUDPConn.predefinedRemoteIP = request.Address.String() + tempUDPConn.ExpectedRemoteIP = request.Address.String() } else { // specified both IP and port var udpRemote gonet.Addr = &gonet.UDPAddr{ IP: request.Address.IP(), diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index ef62a76e9109..cc5a753286aa 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -10,10 +10,11 @@ import ( "github.com/xtls/xray-core/common/signal" ) -func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn) *TempUDPConn { +func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, remoteIP string) *TempUDPConn { return &TempUDPConn{ PacketConn: udpConn, AssociateTCPConn: tcpConn, + ExpectedRemoteIP: remoteIP, } } @@ -22,8 +23,7 @@ func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn) *TempUDPConn { type TempUDPConn struct { net.PacketConn AssociateTCPConn net.Conn - - predefinedRemoteIP string + ExpectedRemoteIP string timer *signal.ActivityTimer remote atomic.Pointer[net.Addr] @@ -37,14 +37,8 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { return n, err } if c.remote.Load() == nil { - var expectedRemote string - if c.predefinedRemoteIP != "" { - expectedRemote = c.predefinedRemoteIP - } else { - expectedRemote, _, _ = net.SplitHostPort(c.AssociateTCPConn.RemoteAddr().String()) - } udpRemote, _, _ := net.SplitHostPort(remote.String()) - if expectedRemote != udpRemote { + if c.ExpectedRemoteIP != udpRemote { continue } else { c.remote.CompareAndSwap(nil, &remote) From de17ce549af0ccd2be22225c3e597c9aa85f5d50 Mon Sep 17 00:00:00 2001 From: Fangliding Date: Tue, 26 May 2026 16:11:20 +0800 Subject: [PATCH 09/17] review --- proxy/socks/protocol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 2a0f1d18e284..723311a7c173 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -210,7 +210,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) expectedRemoteIP, _, _ := net.SplitHostPort(writer.RemoteAddr().String()) tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemoteIP) - if !(request.Address.IP().IsUnspecified() && request.Port == 0) { + if request.Address.IP().IsUnspecified() { // only specified an IP without port if request.Port == 0 { tempUDPConn.ExpectedRemoteIP = request.Address.String() From ceaf9ca99ecc00d70c5aab20bbdfcd1d035698cc Mon Sep 17 00:00:00 2001 From: Fangliding Date: Tue, 26 May 2026 16:21:23 +0800 Subject: [PATCH 10/17] reverse --- proxy/socks/protocol.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 723311a7c173..19efa444f116 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -210,7 +210,7 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) expectedRemoteIP, _, _ := net.SplitHostPort(writer.RemoteAddr().String()) tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemoteIP) - if request.Address.IP().IsUnspecified() { + if !request.Address.IP().IsUnspecified() { // only specified an IP without port if request.Port == 0 { tempUDPConn.ExpectedRemoteIP = request.Address.String() From 56c04402be72f3974970b03c783e47e8860d2c87 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 26 May 2026 08:37:24 +0000 Subject: [PATCH 11/17] Update temp_udp_listen.go --- proxy/socks/temp_udp_listen.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index cc5a753286aa..3c3769d5ebeb 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -31,10 +31,11 @@ type TempUDPConn struct { func (c *TempUDPConn) Read(b []byte) (n int, err error) { c.timer.Update() + var remote net.Addr for { - n, remote, err := c.PacketConn.ReadFrom(b) + n, remote, err = c.PacketConn.ReadFrom(b) if err != nil { - return n, err + break } if c.remote.Load() == nil { udpRemote, _, _ := net.SplitHostPort(remote.String()) @@ -44,10 +45,9 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { c.remote.CompareAndSwap(nil, &remote) } } - if remote.String() != (*c.remote.Load()).String() { - continue + if remote.String() == c.remote.Load().String() { + break } - return n, err } } From cc878be23c96de1c4cb80b53019f6d97a073dd33 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 26 May 2026 08:44:21 +0000 Subject: [PATCH 12/17] Update temp_udp_listen.go --- proxy/socks/temp_udp_listen.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 3c3769d5ebeb..d7aa26967fda 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -38,14 +38,11 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { break } if c.remote.Load() == nil { - udpRemote, _, _ := net.SplitHostPort(remote.String()) - if c.ExpectedRemoteIP != udpRemote { - continue - } else { + if remoteIP, _, _ := net.SplitHostPort(remote.String()); remoteIP == c.ExpectedRemoteIP { c.remote.CompareAndSwap(nil, &remote) + break } - } - if remote.String() == c.remote.Load().String() { + } else if remote.String() == (*c.remote.Load()).String() { break } } From 4ed1dafdaa8a5a1f02cf73ae67cd21a053a9d8cb Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 26 May 2026 08:59:07 +0000 Subject: [PATCH 13/17] Update temp_udp_listen.go --- proxy/socks/temp_udp_listen.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index d7aa26967fda..c3f7c4e3f8be 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -31,19 +31,20 @@ type TempUDPConn struct { func (c *TempUDPConn) Read(b []byte) (n int, err error) { c.timer.Update() - var remote net.Addr - for { + for var remote net.Addr; ; { n, remote, err = c.PacketConn.ReadFrom(b) if err != nil { - break + return } if c.remote.Load() == nil { if remoteIP, _, _ := net.SplitHostPort(remote.String()); remoteIP == c.ExpectedRemoteIP { c.remote.CompareAndSwap(nil, &remote) - break + } else { + continue } - } else if remote.String() == (*c.remote.Load()).String() { - break + } + if remote.String() == (*c.remote.Load()).String() { + return } } } From 5cb3e678cecce6eb03bab731cf0eda152aab29cb Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 26 May 2026 09:06:46 +0000 Subject: [PATCH 14/17] Update temp_udp_listen.go --- proxy/socks/temp_udp_listen.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index c3f7c4e3f8be..cdcbf993b1fd 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -31,7 +31,8 @@ type TempUDPConn struct { func (c *TempUDPConn) Read(b []byte) (n int, err error) { c.timer.Update() - for var remote net.Addr; ; { + var remote net.Addr + for { n, remote, err = c.PacketConn.ReadFrom(b) if err != nil { return From dfad7c05bd1e6bb657b826d87be69d6ed18a84dc Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Tue, 26 May 2026 10:30:24 +0000 Subject: [PATCH 15/17] Update temp_udp_listen.go --- proxy/socks/temp_udp_listen.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index cdcbf993b1fd..84d62733434a 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -37,14 +37,12 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { if err != nil { return } - if c.remote.Load() == nil { + if load := c.remote.Load(); load == nil { if remoteIP, _, _ := net.SplitHostPort(remote.String()); remoteIP == c.ExpectedRemoteIP { - c.remote.CompareAndSwap(nil, &remote) - } else { - continue + c.remote.Store(&remote) + return } - } - if remote.String() == (*c.remote.Load()).String() { + } else if remote.String() == (*load).String() { return } } From bdc39bc155e0fbca282d1d09790daca271ed405e Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 28 May 2026 13:12:16 +0000 Subject: [PATCH 16/17] little refactor --- proxy/socks/protocol.go | 20 +++++-------- proxy/socks/temp_udp_listen.go | 53 +++++++++++++++------------------- 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index 19efa444f116..bf4f61a5b762 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -208,20 +208,14 @@ func (s *ServerSession) handshake5(nMethod byte, reader io.Reader, writer net.Co return nil, nil, errors.New("failed to create UDP listener").Base(err) } responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port) - expectedRemoteIP, _, _ := net.SplitHostPort(writer.RemoteAddr().String()) - tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemoteIP) - if !request.Address.IP().IsUnspecified() { - // only specified an IP without port - if request.Port == 0 { - tempUDPConn.ExpectedRemoteIP = request.Address.String() - } else { // specified both IP and port - var udpRemote gonet.Addr = &gonet.UDPAddr{ - IP: request.Address.IP(), - Port: int(request.Port), - } - tempUDPConn.remote.Store(&udpRemote) - } + expectedRemote := &gonet.UDPAddr{} + if request.Address.IP().IsUnspecified() { + expectedRemote.IP = writer.RemoteAddr().(*net.TCPAddr).IP // unix? + } else { + expectedRemote.IP = request.Address.IP() // panic? + expectedRemote.Port = int(request.Port) // 0 is allowed } + tempUDPConn = NewTempUDPConn(udpHub, writer, expectedRemote) } if err := writeSocks5Response(writer, statusSuccess, responseAddress, responsePort); err != nil { common.CloseIfExists(tempUDPConn) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 84d62733434a..06474d9561c8 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -6,71 +6,66 @@ import ( "sync/atomic" "time" - "github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/signal" ) -func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, remoteIP string) *TempUDPConn { - return &TempUDPConn{ - PacketConn: udpConn, - AssociateTCPConn: tcpConn, - ExpectedRemoteIP: remoteIP, +func NewTempUDPConn(udpConn net.PacketConn, tcpConn net.Conn, expectedRemote *net.UDPAddr) *TempUDPConn { + t := &TempUDPConn{ + PacketConn: udpConn, + AssociatedTCPConn: tcpConn, } + t.ExpectedRemote.Store(expectedRemote) + return t } // TempUDPConn wait for the first packet to determine the remote address // SetTimeout MUST be called before any read/write operation type TempUDPConn struct { net.PacketConn - AssociateTCPConn net.Conn - ExpectedRemoteIP string - - timer *signal.ActivityTimer - remote atomic.Pointer[net.Addr] + AssociatedTCPConn net.Conn + ExpectedRemote atomic.Pointer[net.UDPAddr] + Timer *signal.ActivityTimer } func (c *TempUDPConn) Read(b []byte) (n int, err error) { - c.timer.Update() + c.Timer.Update() var remote net.Addr for { n, remote, err = c.PacketConn.ReadFrom(b) if err != nil { return } - if load := c.remote.Load(); load == nil { - if remoteIP, _, _ := net.SplitHostPort(remote.String()); remoteIP == c.ExpectedRemoteIP { - c.remote.Store(&remote) + remote := remote.(*net.UDPAddr) + expected := c.ExpectedRemote.Load() + if remote.IP.Equal(expected.IP) { + if remote.Port == expected.Port { + return + } + if expected.Port == 0 { + c.ExpectedRemote.Store(remote) return } - } else if remote.String() == (*load).String() { - return } } } func (c *TempUDPConn) Write(b []byte) (n int, err error) { - c.timer.Update() - if c.remote.Load() == nil { - return 0, errors.New("remote address not determined yet") - } - return c.PacketConn.WriteTo(b, *c.remote.Load()) + c.Timer.Update() + return c.PacketConn.WriteTo(b, c.ExpectedRemote.Load()) } func (c *TempUDPConn) RemoteAddr() net.Addr { - if c.remote.Load() == nil { - return nil - } - return *c.remote.Load() + return c.ExpectedRemote.Load() } func (c *TempUDPConn) SetTimeout(d time.Duration) { - c.timer = signal.CancelAfterInactivity(context.Background(), func() { + c.Timer = signal.CancelAfterInactivity(context.Background(), func() { c.Close() }, d) } func (c *TempUDPConn) Close() error { - c.timer.SetTimeout(0) - c.AssociateTCPConn.Close() + c.Timer.SetTimeout(0) + c.AssociatedTCPConn.Close() return c.PacketConn.Close() } From 3ad58b8750377fb68e7cf0f1c43887f97f54aa00 Mon Sep 17 00:00:00 2001 From: RPRX <63339210+RPRX@users.noreply.github.com> Date: Thu, 28 May 2026 14:03:37 +0000 Subject: [PATCH 17/17] timer --- proxy/socks/temp_udp_listen.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 06474d9561c8..5e0dab03d977 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -28,7 +28,6 @@ type TempUDPConn struct { } func (c *TempUDPConn) Read(b []byte) (n int, err error) { - c.Timer.Update() var remote net.Addr for { n, remote, err = c.PacketConn.ReadFrom(b) @@ -39,10 +38,12 @@ func (c *TempUDPConn) Read(b []byte) (n int, err error) { expected := c.ExpectedRemote.Load() if remote.IP.Equal(expected.IP) { if remote.Port == expected.Port { + c.Timer.Update() return } if expected.Port == 0 { c.ExpectedRemote.Store(remote) + c.Timer.Update() return } }