Skip to content

[inbound][socks] UDP timeout option not applied for socks inbound #3754

Description

@StoneMoe

Operating system

Others

System version

FreeBSD

Installation type

Original sing-box Command Line

If you are using a graphical client, please provide the version of the client.

No response

Version

sing-box version 1.12.17

Environment: go1.25.6 freebsd/amd64
Tags: with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
Revision: b27d70766856f84c545d5c737ed43232aaa68d93
CGO: disabled

Description

When using socks inbound to handle UDP packets,
UDP connections will hang forever
due to UDPTimeout option never applied to them.

Reproduction

singbox config:

{
    "route": {
        "rules": [
            {
                "action": "route",
                "outbound": "direct"
            }
        ]
    },
    "inbounds": [
        {
            "tag": "socks-in",
            "type": "socks",
            "listen": "127.0.0.1",
            "listen_port": 12000,
            "udp_timeout": "3s"
        }
    ],
    "outbounds": [
    ],
    "experimental": {
        "clash_api": {
            "external_controller": "0.0.0.0:9090",
            "external_ui": "ui",
            "external_ui_download_url": "https://github.com/Zephyruso/zashboard/releases/latest/download/dist-no-fonts.zip",
            "secret": "aaa"
        }
    }
}

test script:

//go:build ignore

package main

import (
	"fmt"
	"net"
	"time"

	M "github.com/sagernet/sing/common/metadata"
	"github.com/sagernet/sing/protocol/socks"
	"github.com/sagernet/sing/protocol/socks/socks5"
)

func main() {
	proxyAddr := "127.0.0.1:12000"
	// Use Cloudflare DNS as UDP target
	targetAddr := M.ParseSocksaddr("1.1.1.1:9999")

	fmt.Printf("Testing UDP SOCKS5 proxy at %s\n", proxyAddr)
	fmt.Printf("Target: %s\n", targetAddr)

	// Connect to SOCKS5 proxy
	tcpConn, err := net.Dial("tcp", proxyAddr)
	if err != nil {
		fmt.Printf("❌ Failed to connect to proxy: %v\n", err)
		return
	}
	defer tcpConn.Close()

	// SOCKS5 UDP Associate handshake
	response, err := socks.ClientHandshake5(tcpConn, socks5.CommandUDPAssociate, M.Socksaddr{}, "", "")
	if err != nil {
		fmt.Printf("❌ Handshake failed: %v\n", err)
		return
	}
	fmt.Printf("✓ UDP relay address: %s\n", response.Bind)

	// Resolve UDP relay address
	if response.Bind.Port == 0 {
		fmt.Printf("❌ Invalid relay port\n")
		return
	}

	// Create UDP connection to the relay
	udpConn, err := net.Dial("udp", response.Bind.String())
	if err != nil {
		fmt.Printf("❌ Failed to create UDP connection: %v\n", err)
		return
	}
	defer udpConn.Close()

	// Build DNS query for google.com (type A)
	dnsQuery := buildDNSQuery("google.com")

	// Build SOCKS5 UDP packet header + DNS query
	// Format: RSV(2) + FRAG(1) + ATYP(1) + DST.ADDR(variable) + DST.PORT(2) + DATA
	packet := make([]byte, 0, 512)
	packet = append(packet, 0, 0) // RSV
	packet = append(packet, 0)    // FRAG
	packet = append(packet, 1)    // ATYP = IPv4
	ip := targetAddr.Addr.As4()
	packet = append(packet, ip[:]...)
	packet = append(packet, byte(targetAddr.Port>>8), byte(targetAddr.Port)) // Port
	packet = append(packet, dnsQuery...)

	fmt.Printf("Sending DNS query (%d bytes)...\n", len(packet))

	// Send the packet
	udpConn.SetWriteDeadline(time.Now().Add(5 * time.Second))
	_, err = udpConn.Write(packet)
	if err != nil {
		fmt.Printf("❌ Failed to send packet: %v\n", err)
		return
	}

	// Wait for response
	udpConn.SetReadDeadline(time.Now().Add(5 * time.Second))
	buf := make([]byte, 4096)
	n, err := udpConn.Read(buf)
	if err != nil {
		fmt.Printf("❌ Failed to receive response: %v\n", err)
		return
	}

	fmt.Printf("\n✅ SUCCESS! Received %d bytes\n", n)

	// Parse SOCKS5 UDP header (skip 10 bytes for IPv4)
	if n > 10 {
		payload := buf[10:n]
		fmt.Printf("DNS response bytes: %x\n", payload[:min(32, len(payload))])

		// Parse DNS response to show answer count
		if len(payload) > 12 {
			answerCount := int(payload[6])<<8 | int(payload[7])
			fmt.Printf("DNS answer count: %d\n", answerCount)
		}
	}
}

func buildDNSQuery(domain string) []byte {
	// Simple DNS query for type A record
	query := make([]byte, 0, 512)

	// Transaction ID
	query = append(query, 0x00, 0x01)
	// Flags: standard query
	query = append(query, 0x01, 0x00)
	// Questions: 1
	query = append(query, 0x00, 0x01)
	// Answer RRs: 0
	query = append(query, 0x00, 0x00)
	// Authority RRs: 0
	query = append(query, 0x00, 0x00)
	// Additional RRs: 0
	query = append(query, 0x00, 0x00)

	// Query name
	for _, part := range splitDomain(domain) {
		query = append(query, byte(len(part)))
		query = append(query, []byte(part)...)
	}
	query = append(query, 0x00) // End of name

	// Type A
	query = append(query, 0x00, 0x01)
	// Class IN
	query = append(query, 0x00, 0x01)

	return query
}

func splitDomain(domain string) []string {
	var parts []string
	start := 0
	for i, c := range domain {
		if c == '.' {
			parts = append(parts, domain[start:i])
			start = i + 1
		}
	}
	parts = append(parts, domain[start:])
	return parts
}

UDP connection hangs forever if we are not hitting any default value at:

sing-box/route/conn.go

Lines 203 to 218 in 4efe70b

var udpTimeout time.Duration
if metadata.UDPTimeout > 0 {
udpTimeout = metadata.UDPTimeout
} else {
protocol := metadata.Protocol
if protocol == "" {
protocol = C.PortProtocols[metadata.Destination.Port]
}
if protocol != "" {
udpTimeout = C.ProtocolTimeouts[protocol]
}
}
if udpTimeout > 0 {
ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout)
}
destination := bufio.NewPacketConn(remotePacketConn)

but if we connect to standard port like 53, it will set a default 20 seconds timeout, which is wierd since I don't see any 20 seconds default in code.

Logs

# when sending to port 53, got a 20s timeout
INFO[0329] [3784212324 0ms] inbound/socks[socks-in]: inbound connection from 127.0.0.1:53622
INFO[0329] [3784212324 0ms] inbound/socks[socks-in]: inbound packet connection to 1.1.1.1:53
DEBUG[0329] [3784212324 0ms] router: match[0] => route(direct)
INFO[0329] [3784212324 0ms] outbound/direct: outbound packet connection
INFO[0330] [1146788257 0ms] inbound/socks[socks-in]: inbound connection from 127.0.0.1:51306
INFO[0330] [1146788257 0ms] inbound/socks[socks-in]: inbound packet connection to 1.1.1.1:53
DEBUG[0330] [1146788257 0ms] router: match[0] => route(direct)
INFO[0330] [1146788257 0ms] outbound/direct: outbound packet connection
INFO[0331] [51098268 0ms] inbound/socks[socks-in]: inbound connection from 127.0.0.1:51307
INFO[0331] [51098268 0ms] inbound/socks[socks-in]: inbound packet connection to 1.1.1.1:53
DEBUG[0331] [51098268 0ms] router: match[0] => route(direct)
INFO[0331] [51098268 0ms] outbound/direct: outbound packet connection
TRACE[0349] [3784212324 20.0s] connection: packet upload closed
TRACE[0349] [3784212324 20.0s] connection: packet download closed
TRACE[0350] [1146788257 20.0s] connection: packet upload closed
TRACE[0350] [1146788257 20.0s] connection: packet download closed
TRACE[0351] [51098268 20.0s] connection: packet upload closed
TRACE[0351] [51098268 20.0s] connection: packet download closed
# when sending to port 9999, hangs forever
INFO[0468] [2881373146 0ms] inbound/socks[socks-in]: inbound connection from 127.0.0.1:62669
INFO[0468] [2881373146 0ms] inbound/socks[socks-in]: inbound packet connection to 1.1.1.1:9999
DEBUG[0468] [2881373146 0ms] router: match[0] => route(direct)
INFO[0468] [2881373146 0ms] outbound/direct: outbound packet connection

Supporter

Integrity requirements

  • I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
  • I confirm that I have provided the server and client configuration files and process that can be reproduced locally, instead of a complicated client configuration file that has been stripped of sensitive data.
  • I confirm that I have provided the simplest configuration that can be used to reproduce the error I reported, instead of depending on remote servers, TUN, graphical interface clients, or other closed-source software.
  • I confirm that I have provided the complete configuration files and logs, rather than just providing parts I think are useful out of confidence in my own intelligence.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions