fix(socks5): rewrite unroutable UDP relay to proxy host, connect() socket anti-spoof, validate reply versions
This commit is contained in:
parent
0a01bbe3f6
commit
6b75c65503
1 changed files with 39 additions and 2 deletions
|
|
@ -21,6 +21,9 @@ class Socks5Upstream(
|
|||
private val protector: SocketProtector,
|
||||
) : Upstream {
|
||||
|
||||
// RFC 1929 username/password fields are each 1–255 bytes, so user/pass auth
|
||||
// needs a username — a password-only config can't form a valid request (it would
|
||||
// send ULEN=0). Gate auth on the username; a strict server would reject otherwise.
|
||||
private val auth: Boolean get() = cfg.username.isNotEmpty()
|
||||
|
||||
override fun connectTcp(dest: Destination): UpstreamConn {
|
||||
|
|
@ -81,11 +84,16 @@ class Socks5Upstream(
|
|||
out.write(msg)
|
||||
out.flush()
|
||||
val res = inp.readExact(2)
|
||||
// RFC 1929 reply is [VER=1][STATUS]. Validate the version too: a misframed
|
||||
// reply whose 2nd byte happens to be 0x00 must not be read as auth success
|
||||
// and then corrupt the following CONNECT exchange.
|
||||
if (res[0].toInt() != 0x01) throw IOException("bad socks5 auth version ${res[0].toInt() and 0xff}")
|
||||
if (res[1].toInt() != 0x00) throw IOException("socks5 auth failed")
|
||||
}
|
||||
|
||||
private fun readReply(inp: InputStream) {
|
||||
val head = inp.readExact(4) // VER, REP, RSV, ATYP
|
||||
if (head[0].toInt() != 0x05) throw IOException("bad socks5 reply version ${head[0].toInt() and 0xff}")
|
||||
if (head[1].toInt() != 0x00) throw IOException("socks5 connect failed, rep=${head[1].toInt() and 0xff}")
|
||||
// consume BND.ADDR + BND.PORT
|
||||
when (head[3].toInt() and 0xff) {
|
||||
|
|
@ -131,6 +139,7 @@ private class Socks5UdpAssociation(
|
|||
out.write(byteArrayOf(0x05, 0x03, 0x00, ATYP_IPV4.toByte(), 0, 0, 0, 0, 0, 0))
|
||||
out.flush()
|
||||
val head = inp.readExact(4)
|
||||
if (head[0].toInt() != 0x05) throw IOException("bad socks5 reply version ${head[0].toInt() and 0xff}")
|
||||
if (head[1].toInt() != 0x00) throw IOException("udp associate failed rep=${head[1].toInt() and 0xff}")
|
||||
val addr: InetAddress
|
||||
when (head[3].toInt() and 0xff) {
|
||||
|
|
@ -145,9 +154,21 @@ private class Socks5UdpAssociation(
|
|||
}
|
||||
val portBytes = inp.readExact(2)
|
||||
val port = ((portBytes[0].toInt() and 0xff) shl 8) or (portBytes[1].toInt() and 0xff)
|
||||
// Some proxies return 0.0.0.0 — fall back to the proxy host.
|
||||
val target = if (addr.isAnyLocalAddress) InetAddress.getByName(cfg.host) else addr
|
||||
// The relay BND.ADDR is frequently unusable: 0.0.0.0 ("send to the host
|
||||
// you connected to") or an internal/private address only routable inside
|
||||
// the proxy's own network (this matters in practice — e.g. a proxy that
|
||||
// returns 10.x). In every such case the relay actually lives at the proxy
|
||||
// host we already reached over the control channel, so use that and keep
|
||||
// only the advertised port. Without this, UDP/DNS silently black-holes.
|
||||
val target = if (addr.isUnroutableRelay()) InetAddress.getByName(cfg.host) else addr
|
||||
relay = InetSocketAddress(target, port)
|
||||
// Pin the UDP socket to the relay endpoint. RFC 1928 §7 requires dropping any
|
||||
// datagram whose source isn't the association's relay; a kernel connect()
|
||||
// enforces that on the (IP, port) 2-tuple for free, and the ephemeral local-port
|
||||
// binding is the off-path DNS-spoofing defence. This is what mainstream clients
|
||||
// (sing-box, v2ray) do — a compliant relay always answers from BND.PORT (here the
|
||||
// post-rewrite endpoint), so connecting never black-holes a legitimate reply.
|
||||
runCatching { udp.connect(relay) }
|
||||
control.soTimeout = 0
|
||||
watchControl()
|
||||
} catch (e: Throwable) {
|
||||
|
|
@ -204,3 +225,19 @@ private class Socks5UdpAssociation(
|
|||
runCatching { control.close() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a SOCKS5 UDP-associate BND.ADDR can't be reached by this client and the
|
||||
* relay should instead be addressed at the proxy host: the wildcard (0.0.0.0/::),
|
||||
* loopback, link-local, or a private/internal address the proxy advertised from inside
|
||||
* its own network — RFC1918 + fec0::/10 (isSiteLocalAddress), CGNAT 100.64/10, ULA fc00::/7.
|
||||
*/
|
||||
private fun InetAddress.isUnroutableRelay(): Boolean {
|
||||
if (isAnyLocalAddress || isLoopbackAddress || isLinkLocalAddress || isSiteLocalAddress) return true
|
||||
val a = address
|
||||
return when (a.size) {
|
||||
4 -> (a[0].toInt() and 0xff) == 100 && (a[1].toInt() and 0xff) in 64..127 // CGNAT 100.64/10
|
||||
16 -> (a[0].toInt() and 0xfe) == 0xfc // ULA fc00::/7
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue