fix(shadowsocks): treat frame-boundary EOF as clean end; accept empty AEAD chunks like canon
This commit is contained in:
parent
f303cf2892
commit
0a01bbe3f6
2 changed files with 37 additions and 22 deletions
|
|
@ -1,9 +1,9 @@
|
||||||
package chat.vojo.proxy.core.crypto
|
package chat.vojo.proxy.core.crypto
|
||||||
|
|
||||||
import chat.vojo.proxy.core.net.readExact
|
import chat.vojo.proxy.core.net.readExact
|
||||||
|
import chat.vojo.proxy.core.net.readFully
|
||||||
import chat.vojo.proxy.core.net.u16be
|
import chat.vojo.proxy.core.net.u16be
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.EOFException
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
@ -74,27 +74,40 @@ class SsInputStream(
|
||||||
private var plainPos = 0
|
private var plainPos = 0
|
||||||
|
|
||||||
private fun fill(): Boolean {
|
private fun fill(): Boolean {
|
||||||
try {
|
if (subkey == null) {
|
||||||
if (subkey == null) {
|
// Clean EOF before any salt = the peer closed without sending anything.
|
||||||
val salt = inp.readExact(aead.saltSize)
|
val first = inp.read()
|
||||||
subkey = ShadowsocksCrypto.sessionSubkey(masterKey, salt, aead.keySize)
|
if (first < 0) return false
|
||||||
}
|
val salt = ByteArray(aead.saltSize)
|
||||||
val key = subkey!!
|
salt[0] = first.toByte()
|
||||||
val encLen = inp.readExact(2 + aead.tagSize)
|
inp.readFully(salt, 1, salt.size - 1)
|
||||||
val lenPlain = aead.open(key, nonce, encLen)
|
subkey = ShadowsocksCrypto.sessionSubkey(masterKey, salt, aead.keySize)
|
||||||
ShadowsocksCrypto.incrementNonce(nonce)
|
|
||||||
val payloadLen = u16be(lenPlain[0].toInt(), lenPlain[1].toInt())
|
|
||||||
if (payloadLen <= 0 || payloadLen > MAX_PAYLOAD) {
|
|
||||||
throw IOException("invalid shadowsocks chunk length $payloadLen")
|
|
||||||
}
|
|
||||||
val encPayload = inp.readExact(payloadLen + aead.tagSize)
|
|
||||||
plain = aead.open(key, nonce, encPayload)
|
|
||||||
ShadowsocksCrypto.incrementNonce(nonce)
|
|
||||||
plainPos = 0
|
|
||||||
return true
|
|
||||||
} catch (e: EOFException) {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
val key = subkey!!
|
||||||
|
// EOF *at a frame boundary* is a clean end of stream (-1). EOF *inside* a
|
||||||
|
// frame is truncation: once we've consumed any byte of the length header the
|
||||||
|
// payload must follow, so a short read there throws EOFException and propagates
|
||||||
|
// as an error — the relay then full-closes instead of signalling "complete".
|
||||||
|
val firstLen = inp.read()
|
||||||
|
if (firstLen < 0) return false
|
||||||
|
val encLen = ByteArray(2 + aead.tagSize)
|
||||||
|
encLen[0] = firstLen.toByte()
|
||||||
|
inp.readFully(encLen, 1, encLen.size - 1)
|
||||||
|
val lenPlain = aead.open(key, nonce, encLen)
|
||||||
|
ShadowsocksCrypto.incrementNonce(nonce)
|
||||||
|
val payloadLen = u16be(lenPlain[0].toInt(), lenPlain[1].toInt())
|
||||||
|
// Accept payloadLen == 0 (a tag-only keep-alive / empty frame): shadowsocks-rust and
|
||||||
|
// sing-shadowsocks both accept it, so rejecting it would desync against a compliant
|
||||||
|
// peer. An empty record opens to an empty payload and read() simply fills the next
|
||||||
|
// frame. Only an over-MAX length is a real protocol violation.
|
||||||
|
if (payloadLen > MAX_PAYLOAD) {
|
||||||
|
throw IOException("invalid shadowsocks chunk length $payloadLen")
|
||||||
|
}
|
||||||
|
val encPayload = inp.readExact(payloadLen + aead.tagSize)
|
||||||
|
plain = aead.open(key, nonce, encPayload)
|
||||||
|
ShadowsocksCrypto.incrementNonce(nonce)
|
||||||
|
plainPos = 0
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,9 @@ private class ShadowsocksUdpAssociation(
|
||||||
) : UdpAssociation {
|
) : UdpAssociation {
|
||||||
|
|
||||||
private val server = InetSocketAddress(InetAddress.getByName(cfg.host), cfg.port)
|
private val server = InetSocketAddress(InetAddress.getByName(cfg.host), cfg.port)
|
||||||
private val udp = DatagramSocket().also { protector.protect(it) }
|
// Pinned to the SS server so the kernel drops datagrams from any other source.
|
||||||
|
// (AEAD already authenticates each datagram; this just sheds spoofed noise early.)
|
||||||
|
private val udp = DatagramSocket().also { protector.protect(it); runCatching { it.connect(server) } }
|
||||||
|
|
||||||
override fun send(dest: Destination, data: ByteArray, off: Int, len: Int) {
|
override fun send(dest: Destination, data: ByteArray, off: Int, len: Int) {
|
||||||
val plain = ByteArray(dest.raw.size + len)
|
val plain = ByteArray(dest.raw.size + len)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue