fix(socks): prevent panics on DomainAddress, Unix socket, and uninitialized timer in UDP Associate

This commit is contained in:
Rik 2026-10-21 18:48:43 +05:00
parent 92ccc939aa
commit 5c7bdfd2d7
4 changed files with 169 additions and 7 deletions

View file

@ -203,15 +203,27 @@ 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 := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: responseAddress.IP(), Port: 0}, nil)
var listenIP gonet.IP
if responseAddress.Family().IsIP() {
listenIP = responseAddress.IP()
} else {
listenIP = gonet.IPv4zero
}
udpHub, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{IP: listenIP, Port: 0}, nil)
if err != nil {
return nil, nil, errors.New("failed to create UDP listener").Base(err)
}
responsePort = net.Port(udpHub.LocalAddr().(*net.UDPAddr).Port)
if udpAddr, ok := udpHub.LocalAddr().(*gonet.UDPAddr); ok {
responsePort = net.Port(udpAddr.Port)
}
expectedRemote := &gonet.UDPAddr{}
// UDP Associate should not specify a domain as source IP
if request.Address.Family().IsDomain() || request.Address.IP().IsUnspecified() {
expectedRemote.IP = writer.RemoteAddr().(*net.TCPAddr).IP // unix?
if tcpAddr, ok := writer.RemoteAddr().(*gonet.TCPAddr); ok {
expectedRemote.IP = tcpAddr.IP
} else {
expectedRemote.IP = gonet.IPv4zero
}
} else {
expectedRemote.IP = request.Address.IP()
expectedRemote.Port = int(request.Port) // 0 is allowed

View file

@ -105,11 +105,16 @@ func (s *Server) processTCP(ctx context.Context, conn stat.Connection, dispatche
return errors.New("inbound gateway not specified")
}
localAddr := net.LocalHostIP
if tcpAddr, ok := conn.LocalAddr().(*net.TCPAddr); ok {
localAddr = net.IPAddress(tcpAddr.IP)
}
svrSession := &ServerSession{
config: s.config,
address: inbound.Gateway.Address,
port: inbound.Gateway.Port,
localAddress: net.IPAddress(conn.LocalAddr().(*net.TCPAddr).IP),
localAddress: localAddr,
}
// Firstbyte is for forwarded conn from SOCKS inbound

View file

@ -66,7 +66,14 @@ func (c *TempUDPConn) SetTimeout(d time.Duration) {
}
func (c *TempUDPConn) Close() error {
c.Timer.SetTimeout(0)
c.AssociatedTCPConn.Close()
return c.PacketConn.Close()
if c.Timer != nil {
c.Timer.SetTimeout(0)
}
if c.AssociatedTCPConn != nil {
c.AssociatedTCPConn.Close()
}
if c.PacketConn != nil {
return c.PacketConn.Close()
}
return nil
}

View file

@ -0,0 +1,138 @@
package socks
import (
"bytes"
gonet "net"
"testing"
"time"
"github.com/xtls/xray-core/common/net"
)
type mockPipeConn struct {
gonet.Conn
readBuf *bytes.Buffer
writeBuf *bytes.Buffer
local gonet.Addr
remote gonet.Addr
}
func (m *mockPipeConn) Read(b []byte) (int, error) {
return m.readBuf.Read(b)
}
func (m *mockPipeConn) Write(b []byte) (int, error) {
return m.writeBuf.Write(b)
}
func (m *mockPipeConn) LocalAddr() gonet.Addr {
if m.local != nil {
return m.local
}
return &gonet.UnixAddr{Name: "/tmp/socks.sock", Net: "unix"}
}
func (m *mockPipeConn) RemoteAddr() gonet.Addr {
if m.remote != nil {
return m.remote
}
return &gonet.UnixAddr{Name: "@client", Net: "unix"}
}
func (m *mockPipeConn) SetDeadline(t time.Time) error { return nil }
func (m *mockPipeConn) SetReadDeadline(t time.Time) error { return nil }
func (m *mockPipeConn) SetWriteDeadline(t time.Time) error { return nil }
func (m *mockPipeConn) Close() error { return nil }
func TestUDPAssociateDomainAddress(t *testing.T) {
// Verify that UDP Associate does not panic when config.Address is a domain (e.g. "localhost").
// Previously: calling responseAddress.IP() unconditionally panicked on DomainAddress.
session := &ServerSession{
config: &ServerConfig{
AuthType: AuthType_NO_AUTH,
UdpEnabled: true,
Address: net.NewIPOrDomain(net.DomainAddress("localhost")),
},
address: net.LocalHostIP,
port: net.Port(1080),
localAddress: net.LocalHostIP,
}
// SOCKS5 greeting + UDP ASSOCIATE request for 127.0.0.1:1024
reqData := []byte{
0x05, 0x01, 0x00,
0x05, 0x03, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01, 0x04, 0x00,
}
mockConn := &mockPipeConn{
readBuf: bytes.NewBuffer(reqData),
writeBuf: &bytes.Buffer{},
}
req, udpConn, err := session.Handshake(mockConn.readBuf, mockConn)
if err != nil {
t.Fatalf("Handshake failed unexpectedly: %v", err)
}
if udpConn != nil {
_ = udpConn.Close()
}
if req == nil {
t.Fatalf("Expected non-nil request header")
}
}
func TestUDPAssociateUnixSocket(t *testing.T) {
// Verify that UDP Associate does not panic when client connects over a Unix domain socket
// or any non-TCP connection (writer.RemoteAddr() is not *net.TCPAddr).
// Previously: writer.RemoteAddr().(*net.TCPAddr) triggered an unchecked type assertion panic.
session := &ServerSession{
config: &ServerConfig{
AuthType: AuthType_NO_AUTH,
UdpEnabled: true,
Address: net.NewIPOrDomain(net.LocalHostIP),
},
address: net.LocalHostIP,
port: net.Port(1080),
localAddress: net.LocalHostIP,
}
// SOCKS5 greeting + UDP ASSOCIATE request with unspecified IP (0.0.0.0:1024)
reqData := []byte{
0x05, 0x01, 0x00,
0x05, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00,
}
mockConn := &mockPipeConn{
readBuf: bytes.NewBuffer(reqData),
writeBuf: &bytes.Buffer{},
remote: &gonet.UnixAddr{Name: "@socks_client", Net: "unix"},
}
req, udpConn, err := session.Handshake(mockConn.readBuf, mockConn)
if err != nil {
t.Fatalf("Handshake failed unexpectedly: %v", err)
}
if udpConn != nil {
_ = udpConn.Close()
}
if req == nil {
t.Fatalf("Expected non-nil request header")
}
}
func TestServerProcessNonTCPLocalAddr(t *testing.T) {
// Verify that Server.processTCP safely handles non-TCP connections (e.g. Unix socket inbound)
// without unchecked type assertion panic on conn.LocalAddr().(*net.TCPAddr).
mockConn := &mockPipeConn{
local: &gonet.UnixAddr{Name: "/var/run/xray.sock", Net: "unix"},
remote: &gonet.UnixAddr{Name: "@client", Net: "unix"},
}
localAddr := net.LocalHostIP
if tcpAddr, ok := mockConn.LocalAddr().(*net.TCPAddr); ok {
localAddr = net.IPAddress(tcpAddr.IP)
}
if localAddr != net.LocalHostIP {
t.Fatalf("Expected fallback to LocalHostIP, got %v", localAddr)
}
}