diff --git a/proxy/socks/protocol.go b/proxy/socks/protocol.go index d71c0fb1e..ab87495ed 100644 --- a/proxy/socks/protocol.go +++ b/proxy/socks/protocol.go @@ -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 diff --git a/proxy/socks/server.go b/proxy/socks/server.go index d68e5c433..06913303a 100644 --- a/proxy/socks/server.go +++ b/proxy/socks/server.go @@ -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 diff --git a/proxy/socks/temp_udp_listen.go b/proxy/socks/temp_udp_listen.go index 5e0dab03d..0be413963 100644 --- a/proxy/socks/temp_udp_listen.go +++ b/proxy/socks/temp_udp_listen.go @@ -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 } diff --git a/proxy/socks/udp_associate_test.go b/proxy/socks/udp_associate_test.go new file mode 100644 index 000000000..50950c408 --- /dev/null +++ b/proxy/socks/udp_associate_test.go @@ -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) + } +}