├── .github └── workflows │ └── stale.yml ├── .gitignore ├── README.md ├── README.v1.md ├── README.v2.md └── src └── decrypt.cc /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v4 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.' 13 | days-before-stale: 30 14 | days-before-close: 7 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.[oa] 3 | 4 | build/ 5 | 6 | *.sw[po] 7 | 8 | .DS_Store 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snell Server 2 | 3 | 对 snell 协议版本 v3.0 RC 的逆向,旧版本详见 [v2](README.v2.md), [v1](README.v1.md) 4 | 5 | 开源实现可参考 [open-snell](https://github.com/icpz/open-snell) 和 [clash](https://github.com/Dreamacro/clash) 6 | 7 | # Overview 8 | 9 | ## Encryption Schema 10 | 11 | Schema 同 shadowsocks aead 模式,分组密码选用了 chacha20-poly1305-ietf,详见 [Aead Schema](http://shadowsocks.org/en/spec/AEAD-Ciphers.html) 12 | 13 | 会话密钥生成方式为 14 | 15 | ``` 16 | crypto_pwhash(__out key, 32, psk, psk_len, salt, 3ULL, 0x2000ULL, crypto_pwhash_ALG_ARGON2ID13); 17 | ``` 18 | 参数意义详见 [libsodium documentation](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function#key-derivation) 19 | 20 | ## The Snell Protocol for UDP Session 21 | 22 | ** Snell v3.0 TCP 连接协议与 v1 相同,详见 [v1](README.v1.md) ** 23 | 24 | 后文主要表述 v3.0 新增的 UDP 转发模式 25 | 26 | ### Definitions 27 | 28 | ``` 29 | [udp-req-hdr] := [0x01][0x06][1-byte client_id length][variable-length client_id] 30 | 31 | [udp-pkt] := [remote-addr][2-byte port][application data...] 32 | 33 | [udp-resp] := [ip-addr][2-byte port][application data...] 34 | 35 | [remote-addr] := [domain-addr] | [0x00][ip-addr] 36 | 37 | [domain-addr] := [1-byte length][variable-length domain] 38 | 39 | [ip-addr] := [ip4-addr] | [ip6-addr] 40 | 41 | [ip4-addr] := [0x04][4-byte ipv4] 42 | 43 | [ip6-addr] := [0x06][16-byte ipv6] 44 | ``` 45 | 46 | 其中 47 | 48 | ``` 49 | client_id length: length of client_id 50 | client_id: arbitrary string including empty string 51 | 52 | remote-addr: new target address format, either [domain-addr] or [ip*-addr] 53 | 54 | domain-addr: first byte is the length of domain (must NOT be zero) 55 | 56 | ip4-addr: ipv4 is in network order binary data 57 | 58 | ip6-addr: ipv6 is in network order binary data 59 | 60 | port: network order port 61 | ``` 62 | 63 | ### C to S 64 | 65 | ``` 66 | [udp-req-hdr][0x01][udp-pkt][0x01][udp-pkt][0x01][udp-pkt]... 67 | ``` 68 | 69 | ### S to C 70 | 71 | ``` 72 | [1-byte command][content 0][content 1].... 73 | ``` 74 | 75 | 其中 76 | 77 | ``` 78 | command: 79 | 0x00: READY 80 | 0x02: ERROR 81 | ``` 82 | 83 | 当 command=0x00 时,表明服务端可以进行 UDP 转发,此时 content 具有如下模式 84 | 85 | ``` 86 | [udp-resp][udp-resp].... 87 | ``` 88 | 89 | 当 command=0x02 时,content 具有如下模式 90 | 91 | ``` 92 | [1-byte error code][1-byte error msg length][variable-length error message] 93 | ``` 94 | 95 | ### Example Stream 96 | 97 | ``` 98 | 99 | C->S : [udp-req-hdr] [0x01][udp-pkt] [0x01][udp-pkt] [0x01][udp-pkt] [0x01][udp-pkt] ... 100 | S->C : [0x00] [udp-resp] [udp-resp] [udp-resp] ... 101 | 102 | ``` 103 | 104 | 注意,表示 `S->C` 方向中 `[udp-resp]` 中的目标地址仅支持 `[ip4-addr][port]` 或 `[ip6-addr][port]` 格式,内容为原始 target 的地址。 105 | 每个 udp packet 长度可由一次 chunk decrypt 的长度计算 106 | 107 | ## Obfuscating Algorithm 108 | 109 | 同 [v1](README.v1.md) 110 | 111 | # Contact 112 | 113 | 欢迎大家交流讨论,如果有问题欢迎在 issue 区提出,或 Email [y@icpz.dev](mailto:y@icpz.dev)。 114 | 115 | -------------------------------------------------------------------------------- /README.v1.md: -------------------------------------------------------------------------------- 1 | # Snell Server 2 | 3 | 对snell协议版本1.1的初步逆向 4 | 5 | # Overview 6 | 7 | ## Encryption Schema 8 | 9 | Schema 同 shadowsocks aead 模式,分组密码选用了 chacha20-poly1305-ietf,详见[Aead Schema](http://shadowsocks.org/en/spec/AEAD-Ciphers.html) 10 | 11 | 会话密钥生成方式为 12 | 13 | ``` 14 | crypto_pwhash(__out key, 32, psk, psk_len, salt, 3ULL, 0x2000ULL, crypto_pwhash_ALG_ARGON2ID13); 15 | ``` 16 | 参数意义详见[libsodium documentation](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function#key-derivation) 17 | 18 | ## The Snell Protocol 19 | 20 | ### C to S 21 | 22 | ``` 23 | [1-byte version][1-byte command][1-byte client_id length][variable-length client_id][1-byte host length][variable-length host][2-byte port][application data...] 24 | ``` 25 | 26 | 其中 27 | 28 | ``` 29 | 30 | version: 0x01 31 | 32 | command: 33 | 0x00: PING 34 | 0x01: CONNECT 35 | 36 | client_id length: zero currently (maybe reserved for multi-user) 37 | ``` 38 | 39 | * 本repo给出的demo没有实现obfs功能 40 | 41 | * host总是字符串格式的,即使是ip地址 42 | 43 | * port为网络字节序 44 | 45 | ### S to C 46 | 47 | ``` 48 | [1-byte command][content...] 49 | ``` 50 | 51 | 其中 52 | 53 | ``` 54 | command: 55 | 0x00: TUNNEL 56 | 0x02: ERROR 57 | ``` 58 | 59 | 当command=0x00时,content即是应用层数据,正常建立隧道将远端数据原封不动传递 60 | 61 | 当command=0x02时,content具有如下模式 62 | 63 | ``` 64 | [1-byte error code][1-byte error msg length][variable-length error message] 65 | ``` 66 | 67 | ## Obfuscating Algorithm 68 | 69 | ### HTTP 70 | 71 | 目前的http就是[simple-obfs](https://github.com/shadowsocks/simple-obfs)的http mode,不想实现demo了,但可作如下验证 72 | 73 | ``` 74 | ./snell-server -c ./snell-server.conf & 75 | 76 | obfs-server -s 0.0.0.0 -p 8787 -r 127.0.0.1:9898 --obfs=http 77 | ``` 78 | 79 | 假定snell-server.conf内容如下: 80 | 81 | ``` 82 | $ cat ./snell-server.conf 83 | [snell-server] 84 | listen = 0.0.0.0:9898 85 | psk = zzz 86 | ``` 87 | 88 | 现在surge中添加代理```test_snell = snell, [SERVER ADDRESS], 8787, psk=zzz, obfs=http```可成功访问网络 89 | 90 | ### TLS 91 | 92 | TLS也就是[simple-obfs](https://github.com/shadowsocks/simple-obfs)的tls mode,验证方式同上 93 | 94 | -------------------------------------------------------------------------------- /README.v2.md: -------------------------------------------------------------------------------- 1 | # Snell Server 2 | 3 | 对 snell 协议版本 v2.0b 的初步逆向,1.1 版本详见 [v1](README.v1.md) 4 | 5 | # Overview 6 | 7 | ## Encryption Schema 8 | 9 | Schema 同 shadowsocks aead 模式,分组密码选用了 chacha20-poly1305-ietf,详见 [Aead Schema](http://shadowsocks.org/en/spec/AEAD-Ciphers.html) 10 | 11 | **注意**,shadowsocks aead 模式隐含了 chunk size 大于 0 的条件,因为解密一个 chunk nounce 需要递增两次。而 snell v2 则利用了这个 chunk size 等于 0 的情况,将其作为子序列连接的分割。 12 | 13 | 会话密钥生成方式为 14 | 15 | ``` 16 | crypto_pwhash(__out key, 32, psk, psk_len, salt, 3ULL, 0x2000ULL, crypto_pwhash_ALG_ARGON2ID13); 17 | ``` 18 | 参数意义详见 [libsodium documentation](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function#key-derivation) 19 | 20 | ## The Snell Protocol 21 | 22 | ### Definitions 23 | 24 | ``` 25 | 26 | [] := , this will result in a single [[zero length][length tag]] block after encryption 27 | 28 | [request-header] := [1-byte version][1-byte command][1-byte client_id length][variable-length client_id][1-byte host length][variable-length host][2-byte port] 29 | 30 | [sub-conn-request] := [request-header][application data...][] 31 | 32 | [sub-conn-reply] := [application data...][] 33 | ``` 34 | 35 | 其中 36 | 37 | ``` 38 | version: 0x01 /* why not 0x02 ? */ 39 | 40 | command: 41 | 0x00: PING 42 | 0x01: CONNECT /* for snell v1 */ 43 | 0x05: CONNECTv2 44 | 45 | client_id length: length of client_id 46 | client_id: arbitrary string including empty string 47 | ``` 48 | 49 | * CONNECT 指令则表示这是一个 snell v1 的连接,详见 [v1](README.v1.md) 50 | 51 | * ~~本 repo 尚未给出 v2.0b 版本的 demo~~ [open-snell](https://github.com/icpz/open-snell) 52 | 53 | * host 总是字符串格式的,即使是 ip 地址 54 | 55 | * port 为网络字节序 56 | 57 | ### C to S 58 | 59 | ``` 60 | [sub-connection-request 0][sub-connection-request 1].... 61 | ``` 62 | 63 | ### S to C 64 | 65 | ``` 66 | [1-byte command][content 0][1-byte command][content 1].... 67 | ``` 68 | 69 | 其中 70 | 71 | ``` 72 | command: 73 | 0x00: TUNNEL 74 | 0x02: ERROR 75 | ``` 76 | 77 | 当 command=0x00 时,表明 snell 配置正常,服务端可以进行转发,content 具有如下模式 78 | 79 | ``` 80 | [sub-connection-reply] 81 | ``` 82 | 83 | 84 | 当 command=0x02 时,content 具有如下模式 85 | 86 | ``` 87 | [1-byte error code][1-byte error msg length][variable-length error message] 88 | ``` 89 | 90 | ### Example Stream 91 | 92 | ``` 93 | 94 | C->S : [request-header 0][app data] [app data][] [request-header 1][app data] [app data][] ... 95 | S->C : [0x00] [app data] [app data...][] [0x00][app data][] ... 96 | 97 | ``` 98 | 99 | 注意,表示 `TUNNEL` 成功的 0x00 每个子链接都需要发送。子链接以 `[]` 作为分割,可将其视为子链接的半关闭,即不再写入。当双方都半关闭时子链接彻底关闭,当超时无新连接请求到来则主连接关闭。 100 | 101 | ## Obfuscating Algorithm 102 | 103 | 同 [v1](README.v1.md) 104 | 105 | # Contact 106 | 107 | 欢迎大家交流讨论,如果有问题欢迎在 issue 区提出,或 Email [y@icpz.dev](mailto:y@icpz.dev)。 108 | 109 | -------------------------------------------------------------------------------- /src/decrypt.cc: -------------------------------------------------------------------------------- 1 | 2 | // g++ -o decrypt -std=c++14 decrypt.cc -I/opt/homebrew/include /opt/homebrew/lib/libsodium.a /opt/homebrew/lib/libmbedcrypto.a 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | template 16 | inline void assert_eq(const T &a, const T &b) { 17 | if (a != b) { 18 | std::cerr << a << " not equal to " << b << std::endl; 19 | abort(); 20 | } 21 | } 22 | 23 | char psk[] = "kkk"; 24 | 25 | static int aes_decrypt_chunk( 26 | uint8_t *m, uint64_t *mlen, uint8_t *nsec, 27 | const uint8_t *c, uint64_t clen, 28 | const uint8_t *ad, uint64_t adlen, 29 | const uint8_t *n, const uint8_t *k) { 30 | 31 | mbedtls_cipher_context_t ctx{}; 32 | mbedtls_cipher_init(&ctx); 33 | assert_eq(mbedtls_cipher_setup(&ctx, mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_GCM)), 0); 34 | assert_eq(mbedtls_cipher_setkey(&ctx, k, 16 * 8, MBEDTLS_DECRYPT), 0); 35 | size_t olen = 0; 36 | assert_eq(mbedtls_cipher_auth_decrypt_ext(&ctx, n, 12, ad, adlen, c, clen, m, clen - 16, &olen, 16), 0); 37 | *mlen = olen; 38 | mbedtls_cipher_free(&ctx); 39 | 40 | return 0; 41 | } 42 | 43 | struct crypto_context { 44 | uint8_t nonce[12]; 45 | uint8_t key[32]; 46 | int initialized; 47 | 48 | crypto_context() { 49 | std::fill(std::begin(nonce), std::end(nonce), 0); 50 | std::fill(std::begin(key), std::end(key), 0); 51 | initialized = 0; 52 | } 53 | 54 | void genkey(uint8_t *salt) { 55 | assert_eq(crypto_pwhash(key, 32, psk, strlen(psk), salt, 3ULL, 0x2000ULL, crypto_pwhash_ALG_ARGON2ID13), 0); 56 | initialized = 1; 57 | } 58 | 59 | int decrypt_chunk(const uint8_t *ctext, size_t ccapacity, uint8_t *mtext, size_t *mlen) { 60 | if (!initialized) { return -1; } 61 | 62 | uint16_t len; 63 | unsigned long long llen; 64 | int ret; 65 | ret = aes_decrypt_chunk((uint8_t *)&len, &llen, nullptr, ctext, 2 + 16, nullptr, 0, nonce, key); 66 | if (ret < 0) { 67 | return ret; 68 | } 69 | assert_eq(ret, 0); 70 | assert_eq(llen, 2ULL); 71 | ctext += 2 + 16; 72 | sodium_increment(nonce, sizeof nonce); 73 | len = ntohs(len); 74 | fprintf(stderr, "chunk size %d\n", len); 75 | if (len == 0) { 76 | *mlen = 0; 77 | return ret; 78 | } 79 | ret = aes_decrypt_chunk(mtext, &llen, nullptr, ctext, len + 16, nullptr, 0, nonce, key); 80 | if (ret < 0) { 81 | return ret; 82 | } 83 | assert_eq(ret, 0); 84 | *mlen = llen; 85 | sodium_increment(nonce, sizeof nonce); 86 | return ret; 87 | } 88 | }; 89 | 90 | int main(int argc, char *argv[]) { 91 | assert_eq(sodium_init(), 0); 92 | std::ifstream ifs{argv[1], std::ios::binary}; 93 | assert(ifs.is_open()); 94 | std::vector ctext; 95 | { 96 | std::string content{ 97 | std::istreambuf_iterator{ifs}, 98 | std::istreambuf_iterator{} 99 | }; 100 | std::copy(content.begin(), content.end(), std::back_inserter(ctext)); 101 | } 102 | fprintf(stderr, "%lu bytes read\n", ctext.size()); 103 | 104 | auto *chead = ctext.data(); 105 | auto *ctail = chead + ctext.size(); 106 | 107 | std::vector ptext; 108 | 109 | crypto_context ctx; 110 | ctx.genkey(chead); 111 | chead += 16; 112 | while (chead != ctail) { 113 | uint8_t buf[65536]; 114 | size_t mlen; 115 | int ret = ctx.decrypt_chunk(chead, ctail - chead, buf, &mlen); 116 | if (ret < 0) { 117 | fprintf(stderr, "decrypt chunk error: %d\n", ret); 118 | break; 119 | } 120 | assert_eq(ret, 0); 121 | std::copy_n(buf, mlen, std::back_inserter(ptext)); 122 | chead += 2 + 16 + (mlen ? mlen + 16 : 0); 123 | } 124 | 125 | fprintf(stderr, "decrypt done, total ptext %lu bytes\n", ptext.size()); 126 | for (auto c : ptext) { 127 | printf("%c", c); 128 | } 129 | puts(""); 130 | 131 | return 0; 132 | } 133 | 134 | --------------------------------------------------------------------------------