Shadowsocks for Java

Preface

自從來了大陸之後,VPN 就是日常必需品,經過了多次的血淚測試,證實 PPTP=容易被檔 IP,L2TP=速度太慢,目光轉向了新寵 Shadowsocks(以下簡稱 SS),用起來也都很愉快方便,瀏覽器跟手機都有對應的 Client 端可以使用,但不知為何在我的手機上效率很容易下滑,於是就想 trace 手機版 soure code (shadowsocks-android)來找原因,但發現他需要採用 NDK 的方式來編譯,這跟我想的有些出入,我以為 Java 就可以搞定了,於是就促使了下面研究。

What’s Shadowsocks?

Shadowsocks 是個輕量化的自定義的 VPN 協定,封包格式是採用 Socks5 的 Header + 加密的 payload,其他細節可以參考網路上的其他說明。

Java shadowsocks client

trace shadowsocks server 程式後,覺得寫一個純 Java 的 Client 應該不難,就是搞懂 Socks5 的 Protocol + 加密資料後送出,然後等待回應,解密。於是乎,問題回到了為何 shadowsocks-android 要用 NDK 來編譯,可能是原作者不想重造輪子,所以直接拿現成的 C 實作來兜,我自己有個猜測(後述),但令我好奇的是也沒有人寫出完整的 Java Client,唯一的找到實作也被原作者捨棄了!?

想當然爾的,自己動手豐衣足食,正當覺得世界如此美好之時,Bug 隨之而來OOXX…只有第一次的 Request 會取得正確的 Response,之後的 Response 解密後都會是亂碼。在研究了七七四十九天後得到了以下的推論:

Java 加密跟 Shadowsocks Server 不相容(我使用的是 AES-256-CFB 加密)

Java 資料的加解密是有以下步驟,init、update、doFinal;而 SS 是把資料視為 Stream,除非 socket 中斷,不然就是一直處在 update 的步驟 (SS 用的是 OpenSSL),讓我們來看看 Java 的 Code

updateCipherCore.java
1
2
3
4
5
6
7
8
9
10
11
12
int update(byte[] input, int inputOffset, int inputLen, byte[] output,
int outputOffset) throws ShortBufferException
{

// figure out how much can be sent to crypto function
int len = buffered + inputLen - minBytes;
if (padding != null && decrypting) {
// do not include the padding bytes when decrypting
len -= blockSize;
}
// do not count the trailing bytes which do not make up a unit
len = (len > 0 ? (len - (len%unitBytes)) : 0);
....
}

這樣乍看之下也沒有太大的問題,But,人生就是有這個 But,AES 是 Block cipher,換言之資料的加解密是以 Block (128 bit) 為單位進行,在 Java 的實作中,所以如果資料長度不到 128 bit,呼叫 update,Java 會把長度不夠的資料存在 buffer,不吐資料出來,舉個例子:

SS                 Java Client         Browser         
 |(1)-------------> |                     |
 |                  |-----+               |
 |                  |(2)  |               |
 |                  |<----+               |
 |                  |                     |(3)
  1. SS 送出 5 bytes 給 Java Client
  2. Java Client 進行解密,但因為長度不夠把資料暫存了起來
  3. Browser 傻傻的在等待

兇手在這裡,unitBytes 的 AES 常數定義

len = (len > 0 ? (len - (len%unitBytes)) : 0);

所以就是這樣囉,江湖一點訣,說破不值錢。

最後我用了 Bouncy Castle 來繞過原生的 Java 限制。 可以在 Github 上找到我的 shadowsocks-java

Why NDK?

差點忘了,為何 Android 版的需要用 NDK,撇去 SS client 的部分,我猜是因為沒有 Java 版的 tun2socks…XD

以上,打包收工。