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
1 | int update(byte[] input, int inputOffset, int inputLen, byte[] output, |
這樣乍看之下也沒有太大的問題,But,人生就是有這個 But,AES 是 Block cipher,換言之資料的加解密是以 Block (128 bit) 為單位進行,在 Java 的實作中,所以如果資料長度不到 128 bit,呼叫 update,Java 會把長度不夠的資料存在 buffer,不吐資料出來,舉個例子:
SS Java Client Browser
|(1)-------------> | |
| |-----+ |
| |(2) | |
| |<----+ |
| | |(3)
- SS 送出 5 bytes 給 Java Client
- Java Client 進行解密,但因為長度不夠把資料暫存了起來
- 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
以上,打包收工。