Skip to content

fix: SOCKS5 UDP ignores authentication, and IN-USER never matches - #3108

Open
IsoLeyN wants to merge 2 commits into
MetaCubeX:Alphafrom
IsoLeyN:Alpha
Open

fix: SOCKS5 UDP ignores authentication, and IN-USER never matches#3108
IsoLeyN wants to merge 2 commits into
MetaCubeX:Alphafrom
IsoLeyN:Alpha

Conversation

@IsoLeyN

@IsoLeyN IsoLeyN commented Aug 14, 2026

Copy link
Copy Markdown

Two bugs on the UDP path, same root cause:

  1. With authentication set, anyone who can reach the UDP port can still relay through it. lan-allowed-ips / lan-disallowed-ips aren't checked there either.
  2. IN-USER rules never match UDP, because metadata.InUser is always empty.

The second one hurts more. If you route per user over TCP, the same client's UDP quietly goes somewhere else. When your catch-all is DIRECT that's a leak: TCP is tunnelled while QUIC and plain DNS leave on the real IP. Chrome talking to Google over HTTP/3 is enough to hit it.

Why

Auth happens on the TCP control connection. The datagrams then arrive on a separate UDP socket carrying no credentials of their own, so that control connection is the only thing tying them to a user, and per RFC 1928 §4 it's supposed to live as long as the association does.

We authenticate, then throw the result away:

// listener/socks/tcp.go
if command == socks5.CmdUDPAssociate {
    defer conn.Close()
    io.Copy(io.Discard, conn)
    return
}

That leaves listener/socks/udp.go with nothing to work with, and it doesn't try anyway: no authenticator, no auth store, no inbound.WithInUser anywhere in the file. Every datagram hits the rule engine with InUser == "". The TCP path right next to it does the correct thing:

additions = append(additions, inbound.WithInUser(user))

Same asymmetry shows up in inbound.SkipAuthRemoteAddr, called from socks/tcp.go, http/server.go and mixed/mixed.go, all TCP. No UDP listener touches auth at all.

Reproduce

Set authentication, put IN-USER,<user>,DIRECT above MATCH,REJECT, then send a DNS query over SOCKS5 UDP as that user. The rule should match and the query should resolve. It doesn't: the datagram falls through to MATCH and gets rejected.

For the bypass, change the catch-all to MATCH,DIRECT and fire a SOCKS5 UDP datagram straight at the port, with no TCP handshake and no credentials. It gets relayed.

The fix

  • association.go (new): refcounted peer -> user table.
  • tcp.go: register the user on UDP ASSOCIATE, release it when the control connection closes. The connection is still drained exactly as before.
  • udp.go: run the same admission checks the TCP listener runs, then look the peer up. If auth is on and there's no association, drop the datagram and return its buffer; otherwise attach WithInUser.

additions is copied before appending, the way shadowsocks/tcp.go and snell/server.go already do it, so the shared listener slice never gets mutated from a per-packet goroutine.

Heads up

With authentication on, a UDP peer now has to complete UDP ASSOCIATE first. Normal clients already do this. Anything that was firing datagrams at the port without associating will stop working, which is the bypass closing.

Associations are keyed by IP, not IP+port, since a client's datagrams come from a different source port than its control connection. Two users behind one IP can't be told apart and the newest association wins. Fine on a LAN, but worth knowing.

@IsoLeyN
IsoLeyN marked this pull request as ready for review August 14, 2026 09:07
@wwqgtxx
wwqgtxx force-pushed the Alpha branch 2 times, most recently from 3c70e67 to 7259bbb Compare August 15, 2026 02:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants