├── .travis.yml ├── CHANGES ├── CONTRIBUTING.md ├── Cakefile ├── LICENSE ├── README.md ├── bin ├── sslocal └── ssserver ├── config.json ├── lib └── shadowsocks │ ├── encrypt.js │ ├── inet.js │ ├── local.js │ ├── merge_sort.js │ ├── server.js │ ├── test.js │ ├── udprelay.js │ └── utils.js ├── package.json ├── src ├── encrypt.coffee ├── local.coffee ├── merge_sort.coffee ├── server.coffee ├── test.coffee ├── udprelay.coffee └── utils.coffee └── test ├── benchmark.sh ├── config-client-multi-port.json ├── config-client-multi-server-port.json ├── config-client-multi-server.json ├── config-server-multi-passwd.json ├── config-server-multi-port.json └── config.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" 5 | before_install: 6 | - npm install coffee-script 7 | script: 8 | - cake build 9 | - cake test 10 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 1.5.3 2014-11-01 2 | - Fix multiple servers on local side 3 | 4 | 1.5.2 2014-09-09 5 | - Fix IPv6 address on client side 6 | 7 | 1.5.1 2014-09-09 8 | - Fix wrong key cached when changing method on the fly 9 | 10 | 1.5.0 2014-09-09 11 | - Add RC4-MD5 encryption 12 | 13 | 1.4.14 2014-07-04 14 | - Fix UDP header length detection 15 | 16 | 1.4.13 2014-06-12 17 | - Better logging 18 | - Extract the 2 function inetNtoa and inetAton to utils 19 | 20 | 1.4.12 2014-04-23 21 | - Support -t timeout 22 | - Fix a potential memory leak during server connect() 23 | - Fix remote value not checked before setNoDelay() 24 | 25 | 1.4.11 2014-04-18 26 | - Lower local latency 27 | - Fix a typo in help 28 | 29 | 1.4.8 2014-04-12 30 | - Fix can not bind two or more IPs 31 | 32 | 1.4.6 2014-04-10 33 | - Fix a typo in help 34 | 35 | 1.4.5 2014-04-07 36 | - Add help 37 | 38 | 1.4.4 2014-03-25 39 | - Support local address binding with -b argument 40 | 41 | 1.4.3 2014-02-17 42 | - Fix bug with ipv6 proxy in utorrent 43 | - Better error message with bad config.json format and wrong password 44 | 45 | 1.4.2 2013-10-29 46 | - Client now support multiple server ports and multiple server/port pairs. See test/config-client-*.json for example 47 | 48 | 1.4.1 2013-07-30 49 | - Change default cipher to AES-256-CFB in config.json 50 | - Fix a typo in local 51 | 52 | 1.4.0 2013-07-21 53 | - Support Socks5 UDP relay 54 | 55 | 1.3.8 2013-07-18 56 | - Fix "Cannot call method 'resume' of null" 57 | - 58 | 1.3.7 2013-07-09 59 | - Fix default key length of rc2 60 | - Call gc() every 30s if --expose-gc is supplied to node args 61 | 62 | 1.3.6 2013-07-04 63 | - Check config and show some warning messages 64 | 65 | 1.3.5 2013-06-29 66 | - Make local.js and server.js runnable 67 | 68 | 1.3.4 2013-06-29 69 | - Flush logs when exit with error 70 | 71 | 1.3.3 2013-06-27 72 | - Fix util not found 73 | 74 | 1.3.2 2013-06-27 75 | - Fix -c arg 76 | 77 | 1.3.1 2013-06-24 78 | - Fix unsupported addrtype: undefined 79 | 80 | 1.3.0 2013-06-22 81 | - Move to npm 82 | 83 | 1.2.4 2013-06-18 84 | - Fix crash on timeout 85 | 86 | 1.2.3 2013-06-18 87 | - Handle close event instead of error event 88 | - Do byte_to_key only once 89 | - Add an arg: -v 90 | 91 | 1.2.2 2013-05-28 92 | - More encryption methods 93 | - Convert local into a runnable module 94 | - Close #41, crash on invalid data 95 | 96 | 1.2.1 2013-05-22 97 | - Fix node 0.8 98 | - Add local.bat for Windows 99 | 100 | 1.2 2013-05-22 101 | - Use random iv, we finally have strong encryption 102 | 103 | 1.1.1 2013-05-21 104 | - Add encryption, AES, blowfish, etc 105 | 106 | 1.1 2013-05-16 107 | - Support IPv6 address 108 | 109 | 1.0 2013-04-03 110 | - Update readme 111 | 112 | 0.9.6 2013-01-15 113 | - Server listens to IP specified in config.json 114 | 115 | 0.9.5 2013-01-15 116 | - Listen on both IPv4 and IPv6 117 | 118 | 0.9.3 2013-01-07 119 | - Change default timeout 120 | 121 | 0.9.1 2013-01-06 122 | - Fix issue #20, error when using multiple ports 123 | 124 | 0.9 2012-12-30 125 | - RC4 encryption 126 | - Use util.log instead of console.log 127 | 128 | 0.4 2012-12-14 129 | - Support multiple ports 130 | 131 | 0.3 2012-06-26 132 | - Convert to coffee 133 | - Move config into a separate file 134 | - Change default timeout 135 | 136 | 0.2 2012-06-06 137 | - Move socks5 negotiation to local 138 | - Bug fixes 139 | 140 | 0.1 2012-05-20 141 | - Initial version 142 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to contribute 2 | ================= 3 | 4 | Before you submit issues, please take a few minutes to read this guide. 5 | 6 | 在你提交问题前,请花两分钟阅读一下这份指南。 7 | 8 | Issues 9 | ------ 10 | 11 | Please include the following information in your submission: 12 | 13 | 1. How did you set up your environment? (OS, version of Shadowsocks) 14 | 2. Where did you see this error, was it on local or on server? 15 | 3. What happened in your browser? Just no response, or any error message? 16 | 4. 10 lines of log on the local side of shadowsocks when the error happened. 17 | 5. 10 lines of log on the server side of shadowsocks when the error happened. 18 | 6. Any other useful information. 19 | 20 | Skip any of them if you don't know its meaning. 21 | 22 | 问题反馈 23 | ------- 24 | 25 | 请提交下面的信息: 26 | 27 | 1. 你是如何搭建环境的?(操作系统,Shadowsocks 版本) 28 | 2. 错误是发生在哪里,本地还是服务器? 29 | 3. 浏览器里的现象是什么?一直转菊花,还是有提示错误? 30 | 4. 发生错误时,本地端的十行完整的日志。 31 | 5. 发生错误时,服务器端的十行完整的日志。 32 | 6. 其它你认为可能和问题有关的信息。 33 | 34 | 如果你不清楚其中某条的含义, 可以直接跳过那一条。 35 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | {print} = require 'util' 2 | {spawn} = require 'child_process' 3 | 4 | build = () -> 5 | os = require 'os' 6 | if os.platform() == 'win32' 7 | coffeeCmd = 'coffee.cmd' 8 | else 9 | coffeeCmd = 'coffee' 10 | coffee = spawn coffeeCmd, ['-c', '-o', 'lib/shadowsocks', 'src'] 11 | coffee.stderr.on 'data', (data) -> 12 | process.stderr.write data.toString() 13 | coffee.stdout.on 'data', (data) -> 14 | print data.toString() 15 | coffee.on 'exit', (code) -> 16 | if code != 0 17 | process.exit code 18 | 19 | test = () -> 20 | os = require 'os' 21 | coffee = spawn 'node', ['lib/shadowsocks/test.js'] 22 | coffee.stderr.on 'data', (data) -> 23 | process.stderr.write data.toString() 24 | coffee.stdout.on 'data', (data) -> 25 | print data.toString() 26 | coffee.on 'exit', (code) -> 27 | if code != 0 28 | process.exit code 29 | 30 | task 'build', 'Build ./ from src/', -> 31 | build() 32 | 33 | task 'test', 'Run unit test', -> 34 | test() 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | shadowsocks 2 | 3 | Copyright (c) 2014 clowwindy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shadowsocks-nodejs 2 | ================== 3 | 4 | [![NPM version]][NPM] [![Build Status]][Travis CI] 5 | 6 | shadowsocks-nodejs is a node.js port of [shadowsocks]. 7 | 8 | **Deprecated; please use [Other versions].** 9 | 10 | Many people are asking why. Here's why. 11 | 12 | - https://github.com/clowwindy/shadowsocks-nodejs/issues/35 13 | - https://github.com/joyent/node/issues/5949 14 | 15 | The GC of node.js sucks. 16 | 17 | Python version [handles 5000 connections with 50MB RAM](https://github.com/clowwindy/shadowsocks/wiki/Optimizing-Shadowsocks) while node.js version 18 | handles 100 connections with 300MB RAM. Why should we continue to support 19 | node.js? 20 | 21 | 22 | [Build Status]: https://img.shields.io/travis/clowwindy/shadowsocks-nodejs/master.svg?style=flat 23 | [NPM]: https://www.npmjs.org/package/shadowsocks 24 | [NPM version]: https://img.shields.io/npm/v/shadowsocks.svg?style=flatp 25 | [Travis CI]: https://travis-ci.org/clowwindy/shadowsocks-nodejs 26 | [shadowsocks]: https://github.com/clowwindy/shadowsocks 27 | [Other versions]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients 28 | -------------------------------------------------------------------------------- /bin/sslocal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); 6 | 7 | require(lib + '/shadowsocks/local').main() 8 | -------------------------------------------------------------------------------- /bin/ssserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); 6 | 7 | require(lib + '/shadowsocks/server').main() 8 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_address":"127.0.0.1", 5 | "local_port":1080, 6 | "password":"barfoo!", 7 | "timeout":600, 8 | "method":"aes-256-cfb" 9 | } 10 | -------------------------------------------------------------------------------- /lib/shadowsocks/encrypt.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | 3 | /* 4 | Copyright (c) 2014 clowwindy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | (function() { 26 | var EVP_BytesToKey, Encryptor, bytes_to_key_results, cachedTables, create_rc4_md5_cipher, crypto, encryptAll, getTable, int32Max, merge_sort, method_supported, substitute, util; 27 | 28 | crypto = require("crypto"); 29 | 30 | util = require("util"); 31 | 32 | merge_sort = require("./merge_sort").merge_sort; 33 | 34 | int32Max = Math.pow(2, 32); 35 | 36 | cachedTables = {}; 37 | 38 | getTable = function(key) { 39 | var ah, al, decrypt_table, hash, i, md5sum, result, table; 40 | if (cachedTables[key]) { 41 | return cachedTables[key]; 42 | } 43 | util.log("calculating ciphers"); 44 | table = new Array(256); 45 | decrypt_table = new Array(256); 46 | md5sum = crypto.createHash("md5"); 47 | md5sum.update(key); 48 | hash = new Buffer(md5sum.digest(), "binary"); 49 | al = hash.readUInt32LE(0); 50 | ah = hash.readUInt32LE(4); 51 | i = 0; 52 | while (i < 256) { 53 | table[i] = i; 54 | i++; 55 | } 56 | i = 1; 57 | while (i < 1024) { 58 | table = merge_sort(table, function(x, y) { 59 | return ((ah % (x + i)) * int32Max + al) % (x + i) - ((ah % (y + i)) * int32Max + al) % (y + i); 60 | }); 61 | i++; 62 | } 63 | i = 0; 64 | while (i < 256) { 65 | decrypt_table[table[i]] = i; 66 | ++i; 67 | } 68 | result = [table, decrypt_table]; 69 | cachedTables[key] = result; 70 | return result; 71 | }; 72 | 73 | substitute = function(table, buf) { 74 | var i; 75 | i = 0; 76 | while (i < buf.length) { 77 | buf[i] = table[buf[i]]; 78 | i++; 79 | } 80 | return buf; 81 | }; 82 | 83 | bytes_to_key_results = {}; 84 | 85 | EVP_BytesToKey = function(password, key_len, iv_len) { 86 | var count, d, data, i, iv, key, m, md5, ms; 87 | if (bytes_to_key_results["" + password + ":" + key_len + ":" + iv_len]) { 88 | return bytes_to_key_results["" + password + ":" + key_len + ":" + iv_len]; 89 | } 90 | m = []; 91 | i = 0; 92 | count = 0; 93 | while (count < key_len + iv_len) { 94 | md5 = crypto.createHash('md5'); 95 | data = password; 96 | if (i > 0) { 97 | data = Buffer.concat([m[i - 1], password]); 98 | } 99 | md5.update(data); 100 | d = md5.digest(); 101 | m.push(d); 102 | count += d.length; 103 | i += 1; 104 | } 105 | ms = Buffer.concat(m); 106 | key = ms.slice(0, key_len); 107 | iv = ms.slice(key_len, key_len + iv_len); 108 | bytes_to_key_results[password] = [key, iv]; 109 | return [key, iv]; 110 | }; 111 | 112 | method_supported = { 113 | 'aes-128-cfb': [16, 16], 114 | 'aes-192-cfb': [24, 16], 115 | 'aes-256-cfb': [32, 16], 116 | 'bf-cfb': [16, 8], 117 | 'camellia-128-cfb': [16, 16], 118 | 'camellia-192-cfb': [24, 16], 119 | 'camellia-256-cfb': [32, 16], 120 | 'cast5-cfb': [16, 8], 121 | 'des-cfb': [8, 8], 122 | 'idea-cfb': [16, 8], 123 | 'rc2-cfb': [16, 8], 124 | 'rc4': [16, 0], 125 | 'rc4-md5': [16, 16], 126 | 'seed-cfb': [16, 16] 127 | }; 128 | 129 | create_rc4_md5_cipher = function(key, iv, op) { 130 | var md5, rc4_key; 131 | md5 = crypto.createHash('md5'); 132 | md5.update(key); 133 | md5.update(iv); 134 | rc4_key = md5.digest(); 135 | if (op === 1) { 136 | return crypto.createCipheriv('rc4', rc4_key, ''); 137 | } else { 138 | return crypto.createDecipheriv('rc4', rc4_key, ''); 139 | } 140 | }; 141 | 142 | Encryptor = (function() { 143 | function Encryptor(key, method) { 144 | var _ref; 145 | this.key = key; 146 | this.method = method; 147 | this.iv_sent = false; 148 | if (this.method === 'table') { 149 | this.method = null; 150 | } 151 | if (this.method != null) { 152 | this.cipher = this.get_cipher(this.key, this.method, 1, crypto.randomBytes(32)); 153 | } else { 154 | _ref = getTable(this.key), this.encryptTable = _ref[0], this.decryptTable = _ref[1]; 155 | } 156 | } 157 | 158 | Encryptor.prototype.get_cipher_len = function(method) { 159 | var m; 160 | method = method.toLowerCase(); 161 | m = method_supported[method]; 162 | return m; 163 | }; 164 | 165 | Encryptor.prototype.get_cipher = function(password, method, op, iv) { 166 | var iv_, key, m, _ref; 167 | method = method.toLowerCase(); 168 | password = new Buffer(password, 'binary'); 169 | m = this.get_cipher_len(method); 170 | if (m != null) { 171 | _ref = EVP_BytesToKey(password, m[0], m[1]), key = _ref[0], iv_ = _ref[1]; 172 | if (iv == null) { 173 | iv = iv_; 174 | } 175 | if (op === 1) { 176 | this.cipher_iv = iv.slice(0, m[1]); 177 | } 178 | iv = iv.slice(0, m[1]); 179 | if (method === 'rc4-md5') { 180 | return create_rc4_md5_cipher(key, iv, op); 181 | } else { 182 | if (op === 1) { 183 | return crypto.createCipheriv(method, key, iv); 184 | } else { 185 | return crypto.createDecipheriv(method, key, iv); 186 | } 187 | } 188 | } 189 | }; 190 | 191 | Encryptor.prototype.encrypt = function(buf) { 192 | var result; 193 | if (this.method != null) { 194 | result = this.cipher.update(buf); 195 | if (this.iv_sent) { 196 | return result; 197 | } else { 198 | this.iv_sent = true; 199 | return Buffer.concat([this.cipher_iv, result]); 200 | } 201 | } else { 202 | return substitute(this.encryptTable, buf); 203 | } 204 | }; 205 | 206 | Encryptor.prototype.decrypt = function(buf) { 207 | var decipher_iv, decipher_iv_len, result; 208 | if (this.method != null) { 209 | if (this.decipher == null) { 210 | decipher_iv_len = this.get_cipher_len(this.method)[1]; 211 | decipher_iv = buf.slice(0, decipher_iv_len); 212 | this.decipher = this.get_cipher(this.key, this.method, 0, decipher_iv); 213 | result = this.decipher.update(buf.slice(decipher_iv_len)); 214 | return result; 215 | } else { 216 | result = this.decipher.update(buf); 217 | return result; 218 | } 219 | } else { 220 | return substitute(this.decryptTable, buf); 221 | } 222 | }; 223 | 224 | return Encryptor; 225 | 226 | })(); 227 | 228 | encryptAll = function(password, method, op, data) { 229 | var cipher, decryptTable, encryptTable, iv, ivLen, iv_, key, keyLen, result, _ref, _ref1, _ref2; 230 | if (method === 'table') { 231 | method = null; 232 | } 233 | if (method == null) { 234 | _ref = getTable(password), encryptTable = _ref[0], decryptTable = _ref[1]; 235 | if (op === 0) { 236 | return substitute(decryptTable, data); 237 | } else { 238 | return substitute(encryptTable, data); 239 | } 240 | } else { 241 | result = []; 242 | method = method.toLowerCase(); 243 | _ref1 = method_supported[method], keyLen = _ref1[0], ivLen = _ref1[1]; 244 | password = Buffer(password, 'binary'); 245 | _ref2 = EVP_BytesToKey(password, keyLen, ivLen), key = _ref2[0], iv_ = _ref2[1]; 246 | if (op === 1) { 247 | iv = crypto.randomBytes(ivLen); 248 | result.push(iv); 249 | } else { 250 | iv = data.slice(0, ivLen); 251 | data = data.slice(ivLen); 252 | } 253 | if (method === 'rc4-md5') { 254 | cipher = create_rc4_md5_cipher(key, iv, op); 255 | } else { 256 | if (op === 1) { 257 | cipher = crypto.createCipheriv(method, key, iv); 258 | } else { 259 | cipher = crypto.createDecipheriv(method, key, iv); 260 | } 261 | } 262 | result.push(cipher.update(data)); 263 | result.push(cipher.final()); 264 | return Buffer.concat(result); 265 | } 266 | }; 267 | 268 | exports.Encryptor = Encryptor; 269 | 270 | exports.getTable = getTable; 271 | 272 | exports.encryptAll = encryptAll; 273 | 274 | }).call(this); 275 | -------------------------------------------------------------------------------- /lib/shadowsocks/inet.js: -------------------------------------------------------------------------------- 1 | // The functions in source file is from phpjs 2 | // https://github.com/kvz/phpjs 3 | // See license below 4 | // 5 | // Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) 6 | // and Contributors (http://phpjs.org/authors) 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | // this software and associated documentation files (the "Software"), to deal in 10 | // the Software without restriction, including without limitation the rights to 11 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 | // of the Software, and to permit persons to whom the Software is furnished to do 13 | // so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | 26 | 27 | function inet_pton (a) { 28 | // http://kevin.vanzonneveld.net 29 | // + original by: Theriault 30 | // * example 1: inet_pton('::'); 31 | // * returns 1: '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0' (binary) 32 | // * example 2: inet_pton('127.0.0.1'); 33 | // * returns 2: '\x7F\x00\x00\x01' (binary) 34 | var r, m, x, i, j, f = String.fromCharCode; 35 | m = a.match(/^(?:\d{1,3}(?:\.|$)){4}/); // IPv4 36 | if (m) { 37 | m = m[0].split('.'); 38 | m = f(m[0]) + f(m[1]) + f(m[2]) + f(m[3]); 39 | // Return if 4 bytes, otherwise false. 40 | return m.length === 4 ? m : false; 41 | } 42 | r = /^((?:[\da-f]{1,4}(?::|)){0,8})(::)?((?:[\da-f]{1,4}(?::|)){0,8})$/; 43 | m = a.match(r); // IPv6 44 | if (m) { 45 | // Translate each hexadecimal value. 46 | for (j = 1; j < 4; j++) { 47 | // Indice 2 is :: and if no length, continue. 48 | if (j === 2 || m[j].length === 0) { 49 | continue; 50 | } 51 | m[j] = m[j].split(':'); 52 | for (i = 0; i < m[j].length; i++) { 53 | m[j][i] = parseInt(m[j][i], 16); 54 | // Would be NaN if it was blank, return false. 55 | if (isNaN(m[j][i])) { 56 | return false; // Invalid IP. 57 | } 58 | m[j][i] = f(m[j][i] >> 8) + f(m[j][i] & 0xFF); 59 | } 60 | m[j] = m[j].join(''); 61 | } 62 | x = m[1].length + m[3].length; 63 | if (x === 16) { 64 | return m[1] + m[3]; 65 | } else if (x < 16 && m[2].length > 0) { 66 | return m[1] + (new Array(16 - x + 1)).join('\x00') + m[3]; 67 | } 68 | } 69 | return false; // Invalid IP. 70 | } 71 | 72 | function inet_ntop (a) { 73 | // http://kevin.vanzonneveld.net 74 | // + original by: Theriault 75 | // * example 1: inet_ntop('\x7F\x00\x00\x01'); 76 | // * returns 1: '127.0.0.1' 77 | // * example 2: inet_ntop('\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1'); 78 | // * returns 2: '::1' 79 | var i = 0, 80 | m = '', 81 | c = []; 82 | if (a.length === 4) { // IPv4 83 | a += ''; 84 | return [ 85 | a.charCodeAt(0), a.charCodeAt(1), a.charCodeAt(2), a.charCodeAt(3)].join('.'); 86 | } else if (a.length === 16) { // IPv6 87 | for (i = 0; i < 16; i += 2) { 88 | var group = (a.slice(i, i + 2)).toString("hex"); 89 | //replace 00b1 => b1 0000=>0 90 | while(group.length > 1 && group.slice(0,1) == '0') 91 | group = group.slice(1); 92 | c.push(group); 93 | } 94 | return c.join(':').replace(/((^|:)0(?=:|$))+:?/g, function (t) { 95 | m = (t.length > m.length) ? t : m; 96 | return t; 97 | }).replace(m || ' ', '::'); 98 | } else { // Invalid length 99 | return false; 100 | } 101 | } 102 | exports.inet_pton = inet_pton; 103 | exports.inet_ntop = inet_ntop; 104 | -------------------------------------------------------------------------------- /lib/shadowsocks/local.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | 3 | /* 4 | Copyright (c) 2014 clowwindy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | (function() { 26 | var Encryptor, connections, createServer, fs, inet, net, path, udpRelay, utils; 27 | 28 | net = require("net"); 29 | 30 | fs = require("fs"); 31 | 32 | path = require("path"); 33 | 34 | udpRelay = require("./udprelay"); 35 | 36 | utils = require('./utils'); 37 | 38 | inet = require('./inet'); 39 | 40 | Encryptor = require("./encrypt").Encryptor; 41 | 42 | connections = 0; 43 | 44 | createServer = function(serverAddr, serverPort, port, key, method, timeout, local_address) { 45 | var getServer, server, udpServer; 46 | if (local_address == null) { 47 | local_address = '127.0.0.1'; 48 | } 49 | udpServer = udpRelay.createServer(local_address, port, serverAddr, serverPort, key, method, timeout, true); 50 | getServer = function() { 51 | var aPort, aServer, r; 52 | aPort = serverPort; 53 | aServer = serverAddr; 54 | if (serverPort instanceof Array) { 55 | aPort = serverPort[Math.floor(Math.random() * serverPort.length)]; 56 | } 57 | if (serverAddr instanceof Array) { 58 | aServer = serverAddr[Math.floor(Math.random() * serverAddr.length)]; 59 | } 60 | r = /^([^:]*)\:(\d+)$/.exec(aServer); 61 | if (r != null) { 62 | aServer = r[1]; 63 | aPort = +r[2]; 64 | } 65 | return [aServer, aPort]; 66 | }; 67 | server = net.createServer(function(connection) { 68 | var addrLen, addrToSend, clean, connected, encryptor, headerLength, remote, remoteAddr, remotePort, stage; 69 | connections += 1; 70 | connected = true; 71 | encryptor = new Encryptor(key, method); 72 | stage = 0; 73 | headerLength = 0; 74 | remote = null; 75 | addrLen = 0; 76 | remoteAddr = null; 77 | remotePort = null; 78 | addrToSend = ""; 79 | utils.debug("connections: " + connections); 80 | clean = function() { 81 | utils.debug("clean"); 82 | connections -= 1; 83 | remote = null; 84 | connection = null; 85 | encryptor = null; 86 | return utils.debug("connections: " + connections); 87 | }; 88 | connection.on("data", function(data) { 89 | var aPort, aServer, addrToSendBuf, addrtype, buf, cmd, e, piece, reply, tempBuf, _ref; 90 | utils.log(utils.EVERYTHING, "connection on data"); 91 | if (stage === 5) { 92 | data = encryptor.encrypt(data); 93 | if (!remote.write(data)) { 94 | connection.pause(); 95 | } 96 | return; 97 | } 98 | if (stage === 0) { 99 | tempBuf = new Buffer(2); 100 | tempBuf.write("\u0005\u0000", 0); 101 | connection.write(tempBuf); 102 | stage = 1; 103 | utils.debug("stage = 1"); 104 | return; 105 | } 106 | if (stage === 1) { 107 | try { 108 | cmd = data[1]; 109 | addrtype = data[3]; 110 | if (cmd === 1) { 111 | 112 | } else if (cmd === 3) { 113 | utils.info("UDP assc request from " + connection.localAddress + ":" + connection.localPort); 114 | reply = new Buffer(10); 115 | reply.write("\u0005\u0000\u0000\u0001", 0, 4, "binary"); 116 | utils.debug(connection.localAddress); 117 | utils.inetAton(connection.localAddress).copy(reply, 4); 118 | reply.writeUInt16BE(connection.localPort, 8); 119 | connection.write(reply); 120 | stage = 10; 121 | } else { 122 | utils.error("unsupported cmd: " + cmd); 123 | reply = new Buffer("\u0005\u0007\u0000\u0001", "binary"); 124 | connection.end(reply); 125 | return; 126 | } 127 | if (addrtype === 3) { 128 | addrLen = data[4]; 129 | } else if (addrtype !== 1 && addrtype !== 4) { 130 | utils.error("unsupported addrtype: " + addrtype); 131 | connection.destroy(); 132 | return; 133 | } 134 | addrToSend = data.slice(3, 4).toString("binary"); 135 | if (addrtype === 1) { 136 | remoteAddr = utils.inetNtoa(data.slice(4, 8)); 137 | addrToSend += data.slice(4, 10).toString("binary"); 138 | remotePort = data.readUInt16BE(8); 139 | headerLength = 10; 140 | } else if (addrtype === 4) { 141 | remoteAddr = inet.inet_ntop(data.slice(4, 20)); 142 | addrToSend += data.slice(4, 22).toString("binary"); 143 | remotePort = data.readUInt16BE(20); 144 | headerLength = 22; 145 | } else { 146 | remoteAddr = data.slice(5, 5 + addrLen).toString("binary"); 147 | addrToSend += data.slice(4, 5 + addrLen + 2).toString("binary"); 148 | remotePort = data.readUInt16BE(5 + addrLen); 149 | headerLength = 5 + addrLen + 2; 150 | } 151 | if (cmd === 3) { 152 | utils.info("UDP assc: " + remoteAddr + ":" + remotePort); 153 | return; 154 | } 155 | buf = new Buffer(10); 156 | buf.write("\u0005\u0000\u0000\u0001", 0, 4, "binary"); 157 | buf.write("\u0000\u0000\u0000\u0000", 4, 4, "binary"); 158 | buf.writeInt16BE(2222, 8); 159 | connection.write(buf); 160 | _ref = getServer(), aServer = _ref[0], aPort = _ref[1]; 161 | utils.info("connecting " + aServer + ":" + aPort); 162 | remote = net.connect(aPort, aServer, function() { 163 | if (remote) { 164 | remote.setNoDelay(true); 165 | } 166 | stage = 5; 167 | return utils.debug("stage = 5"); 168 | }); 169 | remote.on("data", function(data) { 170 | var e; 171 | if (!connected) { 172 | return; 173 | } 174 | utils.log(utils.EVERYTHING, "remote on data"); 175 | try { 176 | if (encryptor) { 177 | data = encryptor.decrypt(data); 178 | if (!connection.write(data)) { 179 | return remote.pause(); 180 | } 181 | } else { 182 | return remote.destroy(); 183 | } 184 | } catch (_error) { 185 | e = _error; 186 | utils.error(e); 187 | if (remote) { 188 | remote.destroy(); 189 | } 190 | if (connection) { 191 | return connection.destroy(); 192 | } 193 | } 194 | }); 195 | remote.on("end", function() { 196 | utils.debug("remote on end"); 197 | if (connection) { 198 | return connection.end(); 199 | } 200 | }); 201 | remote.on("error", function(e) { 202 | utils.debug("remote on error"); 203 | return utils.error("remote " + remoteAddr + ":" + remotePort + " error: " + e); 204 | }); 205 | remote.on("close", function(had_error) { 206 | utils.debug("remote on close:" + had_error); 207 | if (had_error) { 208 | if (connection) { 209 | return connection.destroy(); 210 | } 211 | } else { 212 | if (connection) { 213 | return connection.end(); 214 | } 215 | } 216 | }); 217 | remote.on("drain", function() { 218 | utils.debug("remote on drain"); 219 | if (connection) { 220 | return connection.resume(); 221 | } 222 | }); 223 | remote.setTimeout(timeout, function() { 224 | utils.debug("remote on timeout"); 225 | if (remote) { 226 | remote.destroy(); 227 | } 228 | if (connection) { 229 | return connection.destroy(); 230 | } 231 | }); 232 | addrToSendBuf = new Buffer(addrToSend, "binary"); 233 | addrToSendBuf = encryptor.encrypt(addrToSendBuf); 234 | remote.setNoDelay(false); 235 | remote.write(addrToSendBuf); 236 | if (data.length > headerLength) { 237 | buf = new Buffer(data.length - headerLength); 238 | data.copy(buf, 0, headerLength); 239 | piece = encryptor.encrypt(buf); 240 | remote.write(piece); 241 | } 242 | stage = 4; 243 | return utils.debug("stage = 4"); 244 | } catch (_error) { 245 | e = _error; 246 | utils.error(e); 247 | if (connection) { 248 | connection.destroy(); 249 | } 250 | if (remote) { 251 | remote.destroy(); 252 | } 253 | return clean(); 254 | } 255 | } else if (stage === 4) { 256 | if (remote == null) { 257 | if (connection) { 258 | connection.destroy(); 259 | } 260 | return; 261 | } 262 | data = encryptor.encrypt(data); 263 | remote.setNoDelay(true); 264 | if (!remote.write(data)) { 265 | return connection.pause(); 266 | } 267 | } 268 | }); 269 | connection.on("end", function() { 270 | connected = false; 271 | utils.debug("connection on end"); 272 | if (remote) { 273 | return remote.end(); 274 | } 275 | }); 276 | connection.on("error", function(e) { 277 | utils.debug("connection on error"); 278 | return utils.error("local error: " + e); 279 | }); 280 | connection.on("close", function(had_error) { 281 | connected = false; 282 | utils.debug("connection on close:" + had_error); 283 | if (had_error) { 284 | if (remote) { 285 | remote.destroy(); 286 | } 287 | } else { 288 | if (remote) { 289 | remote.end(); 290 | } 291 | } 292 | return clean(); 293 | }); 294 | connection.on("drain", function() { 295 | utils.debug("connection on drain"); 296 | if (remote && stage === 5) { 297 | return remote.resume(); 298 | } 299 | }); 300 | return connection.setTimeout(timeout, function() { 301 | utils.debug("connection on timeout"); 302 | if (remote) { 303 | remote.destroy(); 304 | } 305 | if (connection) { 306 | return connection.destroy(); 307 | } 308 | }); 309 | }); 310 | if (local_address != null) { 311 | server.listen(port, local_address, function() { 312 | return utils.info("local listening at " + (server.address().address) + ":" + port); 313 | }); 314 | } else { 315 | server.listen(port, function() { 316 | return utils.info("local listening at 0.0.0.0:" + port); 317 | }); 318 | } 319 | server.on("error", function(e) { 320 | if (e.code === "EADDRINUSE") { 321 | return utils.error("Address in use, aborting"); 322 | } else { 323 | return utils.error(e); 324 | } 325 | }); 326 | server.on("close", function() { 327 | return udpServer.close(); 328 | }); 329 | return server; 330 | }; 331 | 332 | exports.createServer = createServer; 333 | 334 | exports.main = function() { 335 | var KEY, METHOD, PORT, REMOTE_PORT, SERVER, config, configContent, configFromArgs, configPath, e, k, local_address, s, timeout, v; 336 | console.log(utils.version); 337 | configFromArgs = utils.parseArgs(); 338 | configPath = 'config.json'; 339 | if (configFromArgs.config_file) { 340 | configPath = configFromArgs.config_file; 341 | } 342 | if (!fs.existsSync(configPath)) { 343 | configPath = path.resolve(__dirname, "config.json"); 344 | if (!fs.existsSync(configPath)) { 345 | configPath = path.resolve(__dirname, "../../config.json"); 346 | if (!fs.existsSync(configPath)) { 347 | configPath = null; 348 | } 349 | } 350 | } 351 | if (configPath) { 352 | utils.info('loading config from ' + configPath); 353 | configContent = fs.readFileSync(configPath); 354 | try { 355 | config = JSON.parse(configContent); 356 | } catch (_error) { 357 | e = _error; 358 | utils.error('found an error in config.json: ' + e.message); 359 | process.exit(1); 360 | } 361 | } else { 362 | config = {}; 363 | } 364 | for (k in configFromArgs) { 365 | v = configFromArgs[k]; 366 | config[k] = v; 367 | } 368 | if (config.verbose) { 369 | utils.config(utils.DEBUG); 370 | } 371 | utils.checkConfig(config); 372 | SERVER = config.server; 373 | REMOTE_PORT = config.server_port; 374 | PORT = config.local_port; 375 | KEY = config.password; 376 | METHOD = config.method; 377 | local_address = config.local_address; 378 | if (!(SERVER && REMOTE_PORT && PORT && KEY)) { 379 | utils.warn('config.json not found, you have to specify all config in commandline'); 380 | process.exit(1); 381 | } 382 | timeout = Math.floor(config.timeout * 1000) || 600000; 383 | s = createServer(SERVER, REMOTE_PORT, PORT, KEY, METHOD, timeout, local_address); 384 | return s.on("error", function(e) { 385 | return process.stdout.on('drain', function() { 386 | return process.exit(1); 387 | }); 388 | }); 389 | }; 390 | 391 | if (require.main === module) { 392 | exports.main(); 393 | } 394 | 395 | }).call(this); 396 | -------------------------------------------------------------------------------- /lib/shadowsocks/merge_sort.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | 3 | /* 4 | Copyright (c) 2014 clowwindy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | (function() { 26 | var merge, merge_sort; 27 | 28 | merge = function(left, right, comparison) { 29 | var result; 30 | result = new Array(); 31 | while ((left.length > 0) && (right.length > 0)) { 32 | if (comparison(left[0], right[0]) <= 0) { 33 | result.push(left.shift()); 34 | } else { 35 | result.push(right.shift()); 36 | } 37 | } 38 | while (left.length > 0) { 39 | result.push(left.shift()); 40 | } 41 | while (right.length > 0) { 42 | result.push(right.shift()); 43 | } 44 | return result; 45 | }; 46 | 47 | merge_sort = function(array, comparison) { 48 | var middle; 49 | if (array.length < 2) { 50 | return array; 51 | } 52 | middle = Math.ceil(array.length / 2); 53 | return merge(merge_sort(array.slice(0, middle), comparison), merge_sort(array.slice(middle), comparison), comparison); 54 | }; 55 | 56 | exports.merge_sort = merge_sort; 57 | 58 | }).call(this); 59 | -------------------------------------------------------------------------------- /lib/shadowsocks/server.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | 3 | /* 4 | Copyright (c) 2014 clowwindy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | (function() { 26 | var Encryptor, fs, inet, net, path, udpRelay, utils; 27 | 28 | net = require("net"); 29 | 30 | fs = require("fs"); 31 | 32 | path = require("path"); 33 | 34 | udpRelay = require("./udprelay"); 35 | 36 | utils = require("./utils"); 37 | 38 | inet = require("./inet"); 39 | 40 | Encryptor = require("./encrypt").Encryptor; 41 | 42 | exports.main = function() { 43 | var METHOD, SERVER, a_server_ip, config, configContent, configFromArgs, configPath, connections, e, k, key, port, portPassword, servers, timeout, v, _results; 44 | console.log(utils.version); 45 | configFromArgs = utils.parseArgs(true); 46 | configPath = 'config.json'; 47 | if (configFromArgs.config_file) { 48 | configPath = configFromArgs.config_file; 49 | } 50 | if (!fs.existsSync(configPath)) { 51 | configPath = path.resolve(__dirname, "config.json"); 52 | if (!fs.existsSync(configPath)) { 53 | configPath = path.resolve(__dirname, "../../config.json"); 54 | if (!fs.existsSync(configPath)) { 55 | configPath = null; 56 | } 57 | } 58 | } 59 | if (configPath) { 60 | utils.info('loading config from ' + configPath); 61 | configContent = fs.readFileSync(configPath); 62 | try { 63 | config = JSON.parse(configContent); 64 | } catch (_error) { 65 | e = _error; 66 | utils.error('found an error in config.json: ' + e.message); 67 | process.exit(1); 68 | } 69 | } else { 70 | config = {}; 71 | } 72 | for (k in configFromArgs) { 73 | v = configFromArgs[k]; 74 | config[k] = v; 75 | } 76 | if (config.verbose) { 77 | utils.config(utils.DEBUG); 78 | } 79 | utils.checkConfig(config); 80 | timeout = Math.floor(config.timeout * 1000) || 300000; 81 | portPassword = config.port_password; 82 | port = config.server_port; 83 | key = config.password; 84 | METHOD = config.method; 85 | SERVER = config.server; 86 | if (!(SERVER && (port || portPassword) && key)) { 87 | utils.warn('config.json not found, you have to specify all config in commandline'); 88 | process.exit(1); 89 | } 90 | connections = 0; 91 | if (portPassword) { 92 | if (port || key) { 93 | utils.warn('warning: port_password should not be used with server_port and password. server_port and password will be ignored'); 94 | } 95 | } else { 96 | portPassword = {}; 97 | portPassword[port.toString()] = key; 98 | } 99 | _results = []; 100 | for (port in portPassword) { 101 | key = portPassword[port]; 102 | servers = SERVER; 103 | if (!(servers instanceof Array)) { 104 | servers = [servers]; 105 | } 106 | _results.push((function() { 107 | var _i, _len, _results1; 108 | _results1 = []; 109 | for (_i = 0, _len = servers.length; _i < _len; _i++) { 110 | a_server_ip = servers[_i]; 111 | _results1.push((function() { 112 | var KEY, PORT, server, server_ip; 113 | PORT = port; 114 | KEY = key; 115 | server_ip = a_server_ip; 116 | utils.info("calculating ciphers for port " + PORT); 117 | server = net.createServer(function(connection) { 118 | var addrLen, cachedPieces, clean, encryptor, headerLength, remote, remoteAddr, remotePort, stage; 119 | connections += 1; 120 | encryptor = new Encryptor(KEY, METHOD); 121 | stage = 0; 122 | headerLength = 0; 123 | remote = null; 124 | cachedPieces = []; 125 | addrLen = 0; 126 | remoteAddr = null; 127 | remotePort = null; 128 | utils.debug("connections: " + connections); 129 | clean = function() { 130 | utils.debug("clean"); 131 | connections -= 1; 132 | remote = null; 133 | connection = null; 134 | encryptor = null; 135 | return utils.debug("connections: " + connections); 136 | }; 137 | connection.on("data", function(data) { 138 | var addrtype, buf; 139 | utils.log(utils.EVERYTHING, "connection on data"); 140 | try { 141 | data = encryptor.decrypt(data); 142 | } catch (_error) { 143 | e = _error; 144 | utils.error(e); 145 | if (remote) { 146 | remote.destroy(); 147 | } 148 | if (connection) { 149 | connection.destroy(); 150 | } 151 | return; 152 | } 153 | if (stage === 5) { 154 | if (!remote.write(data)) { 155 | connection.pause(); 156 | } 157 | return; 158 | } 159 | if (stage === 0) { 160 | try { 161 | addrtype = data[0]; 162 | if (addrtype === void 0) { 163 | return; 164 | } 165 | if (addrtype === 3) { 166 | addrLen = data[1]; 167 | } else if (addrtype !== 1 && addrtype !== 4) { 168 | utils.error("unsupported addrtype: " + addrtype + " maybe wrong password"); 169 | connection.destroy(); 170 | return; 171 | } 172 | if (addrtype === 1) { 173 | remoteAddr = utils.inetNtoa(data.slice(1, 5)); 174 | remotePort = data.readUInt16BE(5); 175 | headerLength = 7; 176 | } else if (addrtype === 4) { 177 | remoteAddr = inet.inet_ntop(data.slice(1, 17)); 178 | remotePort = data.readUInt16BE(17); 179 | headerLength = 19; 180 | } else { 181 | remoteAddr = data.slice(2, 2 + addrLen).toString("binary"); 182 | remotePort = data.readUInt16BE(2 + addrLen); 183 | headerLength = 2 + addrLen + 2; 184 | } 185 | connection.pause(); 186 | remote = net.connect(remotePort, remoteAddr, function() { 187 | var i, piece; 188 | utils.info("connecting " + remoteAddr + ":" + remotePort); 189 | if (!encryptor || !remote || !connection) { 190 | if (remote) { 191 | remote.destroy(); 192 | } 193 | return; 194 | } 195 | i = 0; 196 | connection.resume(); 197 | while (i < cachedPieces.length) { 198 | piece = cachedPieces[i]; 199 | remote.write(piece); 200 | i++; 201 | } 202 | cachedPieces = null; 203 | remote.setTimeout(timeout, function() { 204 | utils.debug("remote on timeout during connect()"); 205 | if (remote) { 206 | remote.destroy(); 207 | } 208 | if (connection) { 209 | return connection.destroy(); 210 | } 211 | }); 212 | stage = 5; 213 | return utils.debug("stage = 5"); 214 | }); 215 | remote.on("data", function(data) { 216 | utils.log(utils.EVERYTHING, "remote on data"); 217 | if (!encryptor) { 218 | if (remote) { 219 | remote.destroy(); 220 | } 221 | return; 222 | } 223 | data = encryptor.encrypt(data); 224 | if (!connection.write(data)) { 225 | return remote.pause(); 226 | } 227 | }); 228 | remote.on("end", function() { 229 | utils.debug("remote on end"); 230 | if (connection) { 231 | return connection.end(); 232 | } 233 | }); 234 | remote.on("error", function(e) { 235 | utils.debug("remote on error"); 236 | return utils.error("remote " + remoteAddr + ":" + remotePort + " error: " + e); 237 | }); 238 | remote.on("close", function(had_error) { 239 | utils.debug("remote on close:" + had_error); 240 | if (had_error) { 241 | if (connection) { 242 | return connection.destroy(); 243 | } 244 | } else { 245 | if (connection) { 246 | return connection.end(); 247 | } 248 | } 249 | }); 250 | remote.on("drain", function() { 251 | utils.debug("remote on drain"); 252 | if (connection) { 253 | return connection.resume(); 254 | } 255 | }); 256 | remote.setTimeout(15 * 1000, function() { 257 | utils.debug("remote on timeout during connect()"); 258 | if (remote) { 259 | remote.destroy(); 260 | } 261 | if (connection) { 262 | return connection.destroy(); 263 | } 264 | }); 265 | if (data.length > headerLength) { 266 | buf = new Buffer(data.length - headerLength); 267 | data.copy(buf, 0, headerLength); 268 | cachedPieces.push(buf); 269 | buf = null; 270 | } 271 | stage = 4; 272 | return utils.debug("stage = 4"); 273 | } catch (_error) { 274 | e = _error; 275 | utils.error(e); 276 | connection.destroy(); 277 | if (remote) { 278 | return remote.destroy(); 279 | } 280 | } 281 | } else { 282 | if (stage === 4) { 283 | return cachedPieces.push(data); 284 | } 285 | } 286 | }); 287 | connection.on("end", function() { 288 | utils.debug("connection on end"); 289 | if (remote) { 290 | return remote.end(); 291 | } 292 | }); 293 | connection.on("error", function(e) { 294 | utils.debug("connection on error"); 295 | return utils.error("local error: " + e); 296 | }); 297 | connection.on("close", function(had_error) { 298 | utils.debug("connection on close:" + had_error); 299 | if (had_error) { 300 | if (remote) { 301 | remote.destroy(); 302 | } 303 | } else { 304 | if (remote) { 305 | remote.end(); 306 | } 307 | } 308 | return clean(); 309 | }); 310 | connection.on("drain", function() { 311 | utils.debug("connection on drain"); 312 | if (remote) { 313 | return remote.resume(); 314 | } 315 | }); 316 | return connection.setTimeout(timeout, function() { 317 | utils.debug("connection on timeout"); 318 | if (remote) { 319 | remote.destroy(); 320 | } 321 | if (connection) { 322 | return connection.destroy(); 323 | } 324 | }); 325 | }); 326 | server.listen(PORT, server_ip, function() { 327 | return utils.info("server listening at " + server_ip + ":" + PORT + " "); 328 | }); 329 | udpRelay.createServer(server_ip, PORT, null, null, key, METHOD, timeout, false); 330 | return server.on("error", function(e) { 331 | if (e.code === "EADDRINUSE") { 332 | utils.error("Address in use, aborting"); 333 | } else { 334 | utils.error(e); 335 | } 336 | return process.stdout.on('drain', function() { 337 | return process.exit(1); 338 | }); 339 | }); 340 | })()); 341 | } 342 | return _results1; 343 | })()); 344 | } 345 | return _results; 346 | }; 347 | 348 | if (require.main === module) { 349 | exports.main(); 350 | } 351 | 352 | }).call(this); 353 | -------------------------------------------------------------------------------- /lib/shadowsocks/test.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | (function() { 3 | var child_process, curlRunning, encrypt, i, local, localReady, runCurl, server, serverReady, tables, target; 4 | 5 | encrypt = require("./encrypt"); 6 | 7 | target = [[60, 53, 84, 138, 217, 94, 88, 23, 39, 242, 219, 35, 12, 157, 165, 181, 255, 143, 83, 247, 162, 16, 31, 209, 190, 171, 115, 65, 38, 41, 21, 245, 236, 46, 121, 62, 166, 233, 44, 154, 153, 145, 230, 49, 128, 216, 173, 29, 241, 119, 64, 229, 194, 103, 131, 110, 26, 197, 218, 59, 204, 56, 27, 34, 141, 221, 149, 239, 192, 195, 24, 155, 170, 183, 11, 254, 213, 37, 137, 226, 75, 203, 55, 19, 72, 248, 22, 129, 33, 175, 178, 10, 198, 71, 77, 36, 113, 167, 48, 2, 117, 140, 142, 66, 199, 232, 243, 32, 123, 54, 51, 82, 57, 177, 87, 251, 150, 196, 133, 5, 253, 130, 8, 184, 14, 152, 231, 3, 186, 159, 76, 89, 228, 205, 156, 96, 163, 146, 18, 91, 132, 85, 80, 109, 172, 176, 105, 13, 50, 235, 127, 0, 189, 95, 98, 136, 250, 200, 108, 179, 211, 214, 106, 168, 78, 79, 74, 210, 30, 73, 201, 151, 208, 114, 101, 174, 92, 52, 120, 240, 15, 169, 220, 182, 81, 224, 43, 185, 40, 99, 180, 17, 212, 158, 42, 90, 9, 191, 45, 6, 25, 4, 222, 67, 126, 1, 116, 124, 206, 69, 61, 7, 68, 97, 202, 63, 244, 20, 28, 58, 93, 134, 104, 144, 227, 147, 102, 118, 135, 148, 47, 238, 86, 112, 122, 70, 107, 215, 100, 139, 223, 225, 164, 237, 111, 125, 207, 160, 187, 246, 234, 161, 188, 193, 249, 252], [151, 205, 99, 127, 201, 119, 199, 211, 122, 196, 91, 74, 12, 147, 124, 180, 21, 191, 138, 83, 217, 30, 86, 7, 70, 200, 56, 62, 218, 47, 168, 22, 107, 88, 63, 11, 95, 77, 28, 8, 188, 29, 194, 186, 38, 198, 33, 230, 98, 43, 148, 110, 177, 1, 109, 82, 61, 112, 219, 59, 0, 210, 35, 215, 50, 27, 103, 203, 212, 209, 235, 93, 84, 169, 166, 80, 130, 94, 164, 165, 142, 184, 111, 18, 2, 141, 232, 114, 6, 131, 195, 139, 176, 220, 5, 153, 135, 213, 154, 189, 238, 174, 226, 53, 222, 146, 162, 236, 158, 143, 55, 244, 233, 96, 173, 26, 206, 100, 227, 49, 178, 34, 234, 108, 207, 245, 204, 150, 44, 87, 121, 54, 140, 118, 221, 228, 155, 78, 3, 239, 101, 64, 102, 17, 223, 41, 137, 225, 229, 66, 116, 171, 125, 40, 39, 71, 134, 13, 193, 129, 247, 251, 20, 136, 242, 14, 36, 97, 163, 181, 72, 25, 144, 46, 175, 89, 145, 113, 90, 159, 190, 15, 183, 73, 123, 187, 128, 248, 252, 152, 24, 197, 68, 253, 52, 69, 117, 57, 92, 104, 157, 170, 214, 81, 60, 133, 208, 246, 172, 23, 167, 160, 192, 76, 161, 237, 45, 4, 58, 10, 182, 65, 202, 240, 185, 241, 79, 224, 132, 51, 42, 126, 105, 37, 250, 149, 32, 243, 231, 67, 179, 48, 9, 106, 216, 31, 249, 19, 85, 254, 156, 115, 255, 120, 75, 16]]; 8 | 9 | tables = encrypt.getTable("foobar!"); 10 | 11 | console.log(JSON.stringify(tables)); 12 | 13 | i = 0; 14 | 15 | while (i < 256) { 16 | console.assert(tables[0][i] === target[0][i]); 17 | console.assert(tables[1][i] === target[1][i]); 18 | i++; 19 | } 20 | 21 | child_process = require('child_process'); 22 | 23 | local = child_process.spawn('bin/sslocal', []); 24 | 25 | server = child_process.spawn('bin/ssserver', []); 26 | 27 | curlRunning = false; 28 | 29 | local.on('exit', function(code) { 30 | server.kill(); 31 | if (!curlRunning) { 32 | return process.exit(code); 33 | } 34 | }); 35 | 36 | server.on('exit', function(code) { 37 | local.kill(); 38 | if (!curlRunning) { 39 | return process.exit(code); 40 | } 41 | }); 42 | 43 | localReady = false; 44 | 45 | serverReady = false; 46 | 47 | curlRunning = false; 48 | 49 | runCurl = function() { 50 | var curl; 51 | curlRunning = true; 52 | curl = child_process.spawn('curl', ['-v', 'http://www.example.com/', '-L', '--socks5-hostname', '127.0.0.1:1080']); 53 | curl.on('exit', function(code) { 54 | local.kill(); 55 | server.kill(); 56 | if (code === 0) { 57 | console.log('Test passed'); 58 | return process.exit(0); 59 | } else { 60 | console.error('Test failed'); 61 | return process.exit(code); 62 | } 63 | }); 64 | curl.stdout.on('data', function(data) { 65 | return process.stdout.write(data); 66 | }); 67 | return curl.stderr.on('data', function(data) { 68 | return process.stderr.write(data); 69 | }); 70 | }; 71 | 72 | local.stderr.on('data', function(data) { 73 | return process.stderr.write(data); 74 | }); 75 | 76 | server.stderr.on('data', function(data) { 77 | return process.stderr.write(data); 78 | }); 79 | 80 | local.stdout.on('data', function(data) { 81 | process.stdout.write(data); 82 | if (data.toString().indexOf('listening at') >= 0) { 83 | localReady = true; 84 | if (localReady && serverReady && !curlRunning) { 85 | return runCurl(); 86 | } 87 | } 88 | }); 89 | 90 | server.stdout.on('data', function(data) { 91 | process.stdout.write(data); 92 | if (data.toString().indexOf('listening at') >= 0) { 93 | serverReady = true; 94 | if (localReady && serverReady && !curlRunning) { 95 | return runCurl(); 96 | } 97 | } 98 | }); 99 | 100 | }).call(this); 101 | -------------------------------------------------------------------------------- /lib/shadowsocks/udprelay.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | 3 | /* 4 | Copyright (c) 2014 clowwindy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | (function() { 26 | var LRUCache, decrypt, dgram, encrypt, encryptor, inet, net, parseHeader, utils; 27 | 28 | utils = require('./utils'); 29 | 30 | inet = require('./inet'); 31 | 32 | encryptor = require('./encrypt'); 33 | 34 | dgram = require('dgram'); 35 | 36 | net = require('net'); 37 | 38 | LRUCache = (function() { 39 | function LRUCache(timeout, sweepInterval) { 40 | var sweepFun, that; 41 | this.timeout = timeout; 42 | that = this; 43 | sweepFun = function() { 44 | return that.sweep(); 45 | }; 46 | this.interval = setInterval(sweepFun, sweepInterval); 47 | this.dict = {}; 48 | } 49 | 50 | LRUCache.prototype.setItem = function(key, value) { 51 | var cur; 52 | cur = process.hrtime(); 53 | return this.dict[key] = [value, cur]; 54 | }; 55 | 56 | LRUCache.prototype.getItem = function(key) { 57 | var v; 58 | v = this.dict[key]; 59 | if (v) { 60 | v[1] = process.hrtime(); 61 | return v[0]; 62 | } 63 | return null; 64 | }; 65 | 66 | LRUCache.prototype.delItem = function(key) { 67 | return delete this.dict[key]; 68 | }; 69 | 70 | LRUCache.prototype.destroy = function() { 71 | return clearInterval(this.interval); 72 | }; 73 | 74 | LRUCache.prototype.sweep = function() { 75 | var dict, diff, k, keys, swept, v, v0, _i, _len; 76 | utils.debug("sweeping"); 77 | dict = this.dict; 78 | keys = Object.keys(dict); 79 | swept = 0; 80 | for (_i = 0, _len = keys.length; _i < _len; _i++) { 81 | k = keys[_i]; 82 | v = dict[k]; 83 | diff = process.hrtime(v[1]); 84 | if (diff[0] > this.timeout * 0.001) { 85 | swept += 1; 86 | v0 = v[0]; 87 | v0.close(); 88 | delete dict[k]; 89 | } 90 | } 91 | return utils.debug("" + swept + " keys swept"); 92 | }; 93 | 94 | return LRUCache; 95 | 96 | })(); 97 | 98 | encrypt = function(password, method, data) { 99 | var e; 100 | try { 101 | return encryptor.encryptAll(password, method, 1, data); 102 | } catch (_error) { 103 | e = _error; 104 | utils.error(e); 105 | return null; 106 | } 107 | }; 108 | 109 | decrypt = function(password, method, data) { 110 | var e; 111 | try { 112 | return encryptor.encryptAll(password, method, 0, data); 113 | } catch (_error) { 114 | e = _error; 115 | utils.error(e); 116 | return null; 117 | } 118 | }; 119 | 120 | parseHeader = function(data, requestHeaderOffset) { 121 | var addrLen, addrtype, destAddr, destPort, e, headerLength; 122 | try { 123 | addrtype = data[requestHeaderOffset]; 124 | if (addrtype === 3) { 125 | addrLen = data[requestHeaderOffset + 1]; 126 | } else if (addrtype !== 1 && addrtype !== 4) { 127 | utils.warn("unsupported addrtype: " + addrtype); 128 | return null; 129 | } 130 | if (addrtype === 1) { 131 | destAddr = utils.inetNtoa(data.slice(requestHeaderOffset + 1, requestHeaderOffset + 5)); 132 | destPort = data.readUInt16BE(requestHeaderOffset + 5); 133 | headerLength = requestHeaderOffset + 7; 134 | } else if (addrtype === 4) { 135 | destAddr = inet.inet_ntop(data.slice(requestHeaderOffset + 1, requestHeaderOffset + 17)); 136 | destPort = data.readUInt16BE(requestHeaderOffset + 17); 137 | headerLength = requestHeaderOffset + 19; 138 | } else { 139 | destAddr = data.slice(requestHeaderOffset + 2, requestHeaderOffset + 2 + addrLen).toString("binary"); 140 | destPort = data.readUInt16BE(requestHeaderOffset + 2 + addrLen); 141 | headerLength = requestHeaderOffset + 2 + addrLen + 2; 142 | } 143 | return [addrtype, destAddr, destPort, headerLength]; 144 | } catch (_error) { 145 | e = _error; 146 | utils.error(e); 147 | return null; 148 | } 149 | }; 150 | 151 | exports.createServer = function(listenAddr, listenPort, remoteAddr, remotePort, password, method, timeout, isLocal) { 152 | var clientKey, clients, listenIPType, server, udpTypeToListen, udpTypesToListen, _i, _len; 153 | udpTypesToListen = []; 154 | if (listenAddr == null) { 155 | udpTypesToListen = ['udp4', 'udp6']; 156 | } else { 157 | listenIPType = net.isIP(listenAddr); 158 | if (listenIPType === 6) { 159 | udpTypesToListen.push('udp6'); 160 | } else { 161 | udpTypesToListen.push('udp4'); 162 | } 163 | } 164 | for (_i = 0, _len = udpTypesToListen.length; _i < _len; _i++) { 165 | udpTypeToListen = udpTypesToListen[_i]; 166 | server = dgram.createSocket(udpTypeToListen); 167 | clients = new LRUCache(timeout, 10 * 1000); 168 | clientKey = function(localAddr, localPort, destAddr, destPort) { 169 | return "" + localAddr + ":" + localPort + ":" + destAddr + ":" + destPort; 170 | }; 171 | server.on("message", function(data, rinfo) { 172 | var addrtype, client, clientUdpType, dataToSend, destAddr, destPort, frag, headerLength, headerResult, key, requestHeaderOffset, sendDataOffset, serverAddr, serverPort, _ref, _ref1; 173 | requestHeaderOffset = 0; 174 | if (isLocal) { 175 | requestHeaderOffset = 3; 176 | frag = data[2]; 177 | if (frag !== 0) { 178 | utils.debug("frag:" + frag); 179 | utils.warn("drop a message since frag is not 0"); 180 | return; 181 | } 182 | } else { 183 | data = decrypt(password, method, data); 184 | if (data == null) { 185 | return; 186 | } 187 | } 188 | headerResult = parseHeader(data, requestHeaderOffset); 189 | if (headerResult === null) { 190 | return; 191 | } 192 | addrtype = headerResult[0], destAddr = headerResult[1], destPort = headerResult[2], headerLength = headerResult[3]; 193 | if (isLocal) { 194 | sendDataOffset = requestHeaderOffset; 195 | _ref = [remoteAddr, remotePort], serverAddr = _ref[0], serverPort = _ref[1]; 196 | } else { 197 | sendDataOffset = headerLength; 198 | _ref1 = [destAddr, destPort], serverAddr = _ref1[0], serverPort = _ref1[1]; 199 | } 200 | key = clientKey(rinfo.address, rinfo.port, destAddr, destPort); 201 | client = clients.getItem(key); 202 | if (client == null) { 203 | clientUdpType = net.isIP(serverAddr); 204 | if (clientUdpType === 6) { 205 | client = dgram.createSocket("udp6"); 206 | } else { 207 | client = dgram.createSocket("udp4"); 208 | } 209 | clients.setItem(key, client); 210 | client.on("message", function(data1, rinfo1) { 211 | var data2, responseHeader, serverIPBuf; 212 | if (!isLocal) { 213 | utils.debug("UDP recv from " + rinfo1.address + ":" + rinfo1.port); 214 | serverIPBuf = utils.inetAton(rinfo1.address); 215 | responseHeader = new Buffer(7); 216 | responseHeader.write('\x01', 0); 217 | serverIPBuf.copy(responseHeader, 1, 0, 4); 218 | responseHeader.writeUInt16BE(rinfo1.port, 5); 219 | data2 = Buffer.concat([responseHeader, data1]); 220 | data2 = encrypt(password, method, data2); 221 | if (data2 == null) { 222 | return; 223 | } 224 | } else { 225 | responseHeader = new Buffer("\x00\x00\x00"); 226 | data1 = decrypt(password, method, data1); 227 | if (data1 == null) { 228 | return; 229 | } 230 | headerResult = parseHeader(data1, 0); 231 | if (headerResult === null) { 232 | return; 233 | } 234 | addrtype = headerResult[0], destAddr = headerResult[1], destPort = headerResult[2], headerLength = headerResult[3]; 235 | utils.debug("UDP recv from " + destAddr + ":" + destPort); 236 | data2 = Buffer.concat([responseHeader, data1]); 237 | } 238 | return server.send(data2, 0, data2.length, rinfo.port, rinfo.address, function(err, bytes) { 239 | return utils.debug("remote to local sent"); 240 | }); 241 | }); 242 | client.on("error", function(err) { 243 | return utils.error("UDP client error: " + err); 244 | }); 245 | client.on("close", function() { 246 | utils.debug("UDP client close"); 247 | return clients.delItem(key); 248 | }); 249 | } 250 | utils.debug("pairs: " + (Object.keys(clients.dict).length)); 251 | dataToSend = data.slice(sendDataOffset, data.length); 252 | if (isLocal) { 253 | dataToSend = encrypt(password, method, dataToSend); 254 | if (dataToSend == null) { 255 | return; 256 | } 257 | } 258 | utils.debug("UDP send to " + destAddr + ":" + destPort); 259 | return client.send(dataToSend, 0, dataToSend.length, serverPort, serverAddr, function(err, bytes) { 260 | return utils.debug("local to remote sent"); 261 | }); 262 | }); 263 | server.on("listening", function() { 264 | var address; 265 | address = server.address(); 266 | return utils.info("UDP server listening " + address.address + ":" + address.port); 267 | }); 268 | server.on("close", function() { 269 | utils.info("UDP server closing"); 270 | return clients.destroy(); 271 | }); 272 | if (listenAddr != null) { 273 | server.bind(listenPort, listenAddr); 274 | } else { 275 | server.bind(listenPort); 276 | } 277 | return server; 278 | } 279 | }; 280 | 281 | }).call(this); 282 | -------------------------------------------------------------------------------- /lib/shadowsocks/utils.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.8.0 2 | 3 | /* 4 | Copyright (c) 2014 clowwindy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | (function() { 26 | var pack, printLocalHelp, printServerHelp, util, _logging_level; 27 | 28 | util = require('util'); 29 | 30 | pack = require('../../package.json'); 31 | 32 | printLocalHelp = function() { 33 | return console.log("usage: sslocal [-h] -s SERVER_ADDR -p SERVER_PORT [-b LOCAL_ADDR] -l LOCAL_PORT -k PASSWORD -m METHOD [-t TIMEOUT] [-c config]\n\noptional arguments:\n -h, --help show this help message and exit\n -s SERVER_ADDR server address\n -p SERVER_PORT server port\n -b LOCAL_ADDR local binding address, default is 127.0.0.1\n -l LOCAL_PORT local port\n -k PASSWORD password\n -m METHOD encryption method, for example, aes-256-cfb\n -t TIMEOUT timeout in seconds\n -c CONFIG path to config file"); 34 | }; 35 | 36 | printServerHelp = function() { 37 | return console.log("usage: ssserver [-h] -s SERVER_ADDR -p SERVER_PORT -k PASSWORD -m METHOD [-t TIMEOUT] [-c config]\n\noptional arguments:\n -h, --help show this help message and exit\n -s SERVER_ADDR server address\n -p SERVER_PORT server port\n -k PASSWORD password\n -m METHOD encryption method, for example, aes-256-cfb\n -t TIMEOUT timeout in seconds\n -c CONFIG path to config file"); 38 | }; 39 | 40 | exports.parseArgs = function(isServer) { 41 | var defination, lastKey, nextIsValue, oneArg, result, _, _ref; 42 | if (isServer == null) { 43 | isServer = false; 44 | } 45 | defination = { 46 | '-l': 'local_port', 47 | '-p': 'server_port', 48 | '-s': 'server', 49 | '-k': 'password', 50 | '-c': 'config_file', 51 | '-m': 'method', 52 | '-b': 'local_address', 53 | '-t': 'timeout' 54 | }; 55 | result = {}; 56 | nextIsValue = false; 57 | lastKey = null; 58 | _ref = process.argv; 59 | for (_ in _ref) { 60 | oneArg = _ref[_]; 61 | if (nextIsValue) { 62 | result[lastKey] = oneArg; 63 | nextIsValue = false; 64 | } else if (oneArg in defination) { 65 | lastKey = defination[oneArg]; 66 | nextIsValue = true; 67 | } else if ('-v' === oneArg) { 68 | result['verbose'] = true; 69 | } else if (oneArg.indexOf('-') === 0) { 70 | if (isServer) { 71 | printServerHelp(); 72 | } else { 73 | printLocalHelp(); 74 | } 75 | process.exit(2); 76 | } 77 | } 78 | return result; 79 | }; 80 | 81 | exports.checkConfig = function(config) { 82 | var _ref; 83 | if ((_ref = config.server) === '127.0.0.1' || _ref === 'localhost') { 84 | exports.warn("Server is set to " + config.server + ", maybe it's not correct"); 85 | exports.warn("Notice server will listen at " + config.server + ":" + config.server_port); 86 | } 87 | if ((config.method || '').toLowerCase() === 'rc4') { 88 | return exports.warn('RC4 is not safe; please use a safer cipher, like AES-256-CFB'); 89 | } 90 | }; 91 | 92 | exports.version = "" + pack.name + " v" + pack.version; 93 | 94 | exports.EVERYTHING = 0; 95 | 96 | exports.DEBUG = 1; 97 | 98 | exports.INFO = 2; 99 | 100 | exports.WARN = 3; 101 | 102 | exports.ERROR = 4; 103 | 104 | _logging_level = exports.INFO; 105 | 106 | exports.config = function(level) { 107 | return _logging_level = level; 108 | }; 109 | 110 | exports.log = function(level, msg) { 111 | if (level >= _logging_level) { 112 | if (level >= exports.DEBUG) { 113 | return util.log(new Date().getMilliseconds() + 'ms ' + msg); 114 | } else { 115 | return util.log(msg); 116 | } 117 | } 118 | }; 119 | 120 | exports.debug = function(msg) { 121 | return exports.log(exports.DEBUG, msg); 122 | }; 123 | 124 | exports.info = function(msg) { 125 | return exports.log(exports.INFO, msg); 126 | }; 127 | 128 | exports.warn = function(msg) { 129 | return exports.log(exports.WARN, msg); 130 | }; 131 | 132 | exports.error = function(msg) { 133 | return exports.log(exports.ERROR, (msg != null ? msg.stack : void 0) || msg); 134 | }; 135 | 136 | exports.inetNtoa = function(buf) { 137 | return buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3]; 138 | }; 139 | 140 | exports.inetAton = function(ipStr) { 141 | var buf, i, parts; 142 | parts = ipStr.split("."); 143 | if (parts.length !== 4) { 144 | return null; 145 | } else { 146 | buf = new Buffer(4); 147 | i = 0; 148 | while (i < 4) { 149 | buf[i] = +parts[i]; 150 | i++; 151 | } 152 | return buf; 153 | } 154 | }; 155 | 156 | setInterval(function() { 157 | var cwd, e, heapdump; 158 | if (_logging_level <= exports.DEBUG) { 159 | exports.debug(JSON.stringify(process.memoryUsage(), ' ', 2)); 160 | if (global.gc) { 161 | exports.debug('GC'); 162 | gc(); 163 | exports.debug(JSON.stringify(process.memoryUsage(), ' ', 2)); 164 | cwd = process.cwd(); 165 | if (_logging_level === exports.DEBUG) { 166 | try { 167 | heapdump = require('heapdump'); 168 | process.chdir('/tmp'); 169 | return process.chdir(cwd); 170 | } catch (_error) { 171 | e = _error; 172 | return exports.debug(e); 173 | } 174 | } 175 | } 176 | } 177 | }, 1000); 178 | 179 | }).call(this); 180 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadowsocks", 3 | "description": "a tunnel proxy that help you get through firewalls", 4 | "keywords": ["shadowsocks", "proxy", "socks5"], 5 | "author": "clowwindy", 6 | "version": "1.5.3", 7 | "licenses": [{ 8 | "type": "MIT", 9 | "url": "https://raw.github.com/clowwindy/shadowsocks-nodejs/master/LICENSE" 10 | }], 11 | "engines": { 12 | "node": ">=0.10.0" 13 | }, 14 | "directories": { 15 | "lib": "./lib/shadowsocks" 16 | }, 17 | "main": "./lib/shadowsocks/local", 18 | "bin": { 19 | "sslocal": "./bin/sslocal", 20 | "ssserver": "./bin/ssserver" 21 | }, 22 | "scripts": { 23 | "test": "cake test" 24 | }, 25 | "homepage": "https://github.com/clowwindy/shadowsocks-nodejs", 26 | "bugs": "https://github.com/clowwindy/shadowsocks-nodejs/issues", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/clowwindy/shadowsocks-nodejs.git" 30 | }, 31 | "devDependencies": { 32 | "coffee-script": ">=1.6.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/encrypt.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2014 clowwindy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ### 22 | 23 | crypto = require("crypto") 24 | util = require("util") 25 | merge_sort = require("./merge_sort").merge_sort 26 | int32Max = Math.pow(2, 32) 27 | 28 | cachedTables = {} # password: [encryptTable, decryptTable] 29 | 30 | getTable = (key) -> 31 | if cachedTables[key] 32 | return cachedTables[key] 33 | util.log "calculating ciphers" 34 | table = new Array(256) 35 | decrypt_table = new Array(256) 36 | md5sum = crypto.createHash("md5") 37 | md5sum.update key 38 | hash = new Buffer(md5sum.digest(), "binary") 39 | al = hash.readUInt32LE(0) 40 | ah = hash.readUInt32LE(4) 41 | i = 0 42 | 43 | while i < 256 44 | table[i] = i 45 | i++ 46 | i = 1 47 | 48 | while i < 1024 49 | table = merge_sort(table, (x, y) -> 50 | ((ah % (x + i)) * int32Max + al) % (x + i) - ((ah % (y + i)) * int32Max + al) % (y + i) 51 | ) 52 | i++ 53 | i = 0 54 | while i < 256 55 | decrypt_table[table[i]] = i 56 | ++i 57 | result = [table, decrypt_table] 58 | cachedTables[key] = result 59 | result 60 | 61 | substitute = (table, buf) -> 62 | i = 0 63 | 64 | while i < buf.length 65 | buf[i] = table[buf[i]] 66 | i++ 67 | buf 68 | 69 | bytes_to_key_results = {} 70 | 71 | EVP_BytesToKey = (password, key_len, iv_len) -> 72 | if bytes_to_key_results["#{password}:#{key_len}:#{iv_len}"] 73 | return bytes_to_key_results["#{password}:#{key_len}:#{iv_len}"] 74 | m = [] 75 | i = 0 76 | count = 0 77 | while count < key_len + iv_len 78 | md5 = crypto.createHash('md5') 79 | data = password 80 | if i > 0 81 | data = Buffer.concat([m[i - 1], password]) 82 | md5.update(data) 83 | d = md5.digest() 84 | m.push(d) 85 | count += d.length 86 | i += 1 87 | ms = Buffer.concat(m) 88 | key = ms.slice(0, key_len) 89 | iv = ms.slice(key_len, key_len + iv_len) 90 | bytes_to_key_results[password] = [key, iv] 91 | return [key, iv] 92 | 93 | 94 | method_supported = 95 | 'aes-128-cfb': [16, 16] 96 | 'aes-192-cfb': [24, 16] 97 | 'aes-256-cfb': [32, 16] 98 | 'bf-cfb': [16, 8] 99 | 'camellia-128-cfb': [16, 16] 100 | 'camellia-192-cfb': [24, 16] 101 | 'camellia-256-cfb': [32, 16] 102 | 'cast5-cfb': [16, 8] 103 | 'des-cfb': [8, 8] 104 | 'idea-cfb': [16, 8] 105 | 'rc2-cfb': [16, 8] 106 | 'rc4': [16, 0] 107 | 'rc4-md5': [16, 16] 108 | 'seed-cfb': [16, 16] 109 | 110 | 111 | create_rc4_md5_cipher = (key, iv, op) -> 112 | md5 = crypto.createHash('md5') 113 | md5.update(key) 114 | md5.update(iv) 115 | rc4_key = md5.digest() 116 | if op == 1 117 | return crypto.createCipheriv('rc4', rc4_key, '') 118 | else 119 | return crypto.createDecipheriv('rc4', rc4_key, '') 120 | 121 | 122 | class Encryptor 123 | constructor: (@key, @method) -> 124 | @iv_sent = false 125 | if @method == 'table' 126 | @method = null 127 | if @method? 128 | @cipher = @get_cipher(@key, @method, 1, crypto.randomBytes(32)) 129 | else 130 | [@encryptTable, @decryptTable] = getTable(@key) 131 | 132 | get_cipher_len: (method) -> 133 | method = method.toLowerCase() 134 | m = method_supported[method] 135 | return m 136 | 137 | get_cipher: (password, method, op, iv) -> 138 | method = method.toLowerCase() 139 | password = new Buffer(password, 'binary') 140 | m = @get_cipher_len(method) 141 | if m? 142 | [key, iv_] = EVP_BytesToKey(password, m[0], m[1]) 143 | if not iv? 144 | iv = iv_ 145 | if op == 1 146 | @cipher_iv = iv.slice(0, m[1]) 147 | iv = iv.slice(0, m[1]) 148 | if method == 'rc4-md5' 149 | return create_rc4_md5_cipher(key, iv, op) 150 | else 151 | if op == 1 152 | return crypto.createCipheriv(method, key, iv) 153 | else 154 | return crypto.createDecipheriv(method, key, iv) 155 | 156 | encrypt: (buf) -> 157 | if @method? 158 | result = @cipher.update buf 159 | if @iv_sent 160 | return result 161 | else 162 | @iv_sent = true 163 | return Buffer.concat([@cipher_iv, result]) 164 | else 165 | substitute @encryptTable, buf 166 | 167 | decrypt: (buf) -> 168 | if @method? 169 | if not @decipher? 170 | decipher_iv_len = @get_cipher_len(@method)[1] 171 | decipher_iv = buf.slice(0, decipher_iv_len) 172 | @decipher = @get_cipher(@key, @method, 0, decipher_iv) 173 | result = @decipher.update(buf.slice(decipher_iv_len)) 174 | return result 175 | else 176 | result = @decipher.update(buf) 177 | return result 178 | else 179 | substitute @decryptTable, buf 180 | 181 | encryptAll = (password, method, op, data) -> 182 | if method == 'table' 183 | method = null 184 | if not method? 185 | [encryptTable, decryptTable] = getTable(password) 186 | if op is 0 187 | return substitute(decryptTable, data) 188 | else 189 | return substitute(encryptTable, data) 190 | else 191 | result = [] 192 | method = method.toLowerCase() 193 | [keyLen, ivLen] = method_supported[method] 194 | password = Buffer(password, 'binary') 195 | [key, iv_] = EVP_BytesToKey(password, keyLen, ivLen) 196 | if op == 1 197 | iv = crypto.randomBytes ivLen 198 | result.push iv 199 | else 200 | iv = data.slice 0, ivLen 201 | data = data.slice ivLen 202 | if method == 'rc4-md5' 203 | cipher = create_rc4_md5_cipher(key, iv, op) 204 | else 205 | if op == 1 206 | cipher = crypto.createCipheriv(method, key, iv) 207 | else 208 | cipher = crypto.createDecipheriv(method, key, iv) 209 | result.push cipher.update(data) 210 | result.push cipher.final() 211 | return Buffer.concat result 212 | 213 | 214 | exports.Encryptor = Encryptor 215 | exports.getTable = getTable 216 | exports.encryptAll = encryptAll 217 | 218 | -------------------------------------------------------------------------------- /src/local.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2014 clowwindy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ### 22 | 23 | 24 | net = require("net") 25 | fs = require("fs") 26 | path = require("path") 27 | udpRelay = require("./udprelay") 28 | utils = require('./utils') 29 | inet = require('./inet') 30 | Encryptor = require("./encrypt").Encryptor 31 | connections = 0 32 | 33 | createServer = (serverAddr, serverPort, port, key, method, timeout, local_address='127.0.0.1') -> 34 | 35 | udpServer = udpRelay.createServer(local_address, port, serverAddr, serverPort, key, method, timeout, true) 36 | 37 | getServer = -> 38 | aPort = serverPort 39 | aServer = serverAddr 40 | if serverPort instanceof Array 41 | # support config like "server_port": [8081, 8082] 42 | aPort = serverPort[Math.floor(Math.random() * serverPort .length)] 43 | if serverAddr instanceof Array 44 | # support config like "server": ["123.123.123.1", "123.123.123.2"] 45 | aServer = serverAddr[Math.floor(Math.random() * serverAddr .length)] 46 | r = /^([^:]*)\:(\d+)$/.exec(aServer) 47 | # support config like "server": "123.123.123.1:8381" 48 | # or "server": ["123.123.123.1:8381", "123.123.123.2:8381", "123.123.123.2:8382"] 49 | if r? 50 | aServer = r[1] 51 | aPort = +r[2] 52 | return [aServer, aPort] 53 | 54 | server = net.createServer((connection) -> 55 | connections += 1 56 | connected = true 57 | encryptor = new Encryptor(key, method) 58 | stage = 0 59 | headerLength = 0 60 | remote = null 61 | addrLen = 0 62 | remoteAddr = null 63 | remotePort = null 64 | addrToSend = "" 65 | utils.debug "connections: #{connections}" 66 | clean = -> 67 | utils.debug "clean" 68 | connections -= 1 69 | remote = null 70 | connection = null 71 | encryptor = null 72 | utils.debug "connections: #{connections}" 73 | 74 | connection.on "data", (data) -> 75 | utils.log utils.EVERYTHING, "connection on data" 76 | if stage is 5 77 | # pipe sockets 78 | data = encryptor.encrypt data 79 | connection.pause() unless remote.write(data) 80 | return 81 | if stage is 0 82 | tempBuf = new Buffer(2) 83 | tempBuf.write "\u0005\u0000", 0 84 | connection.write tempBuf 85 | stage = 1 86 | utils.debug "stage = 1" 87 | return 88 | if stage is 1 89 | try 90 | # +----+-----+-------+------+----------+----------+ 91 | # |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 92 | # +----+-----+-------+------+----------+----------+ 93 | # | 1 | 1 | X'00' | 1 | Variable | 2 | 94 | # +----+-----+-------+------+----------+----------+ 95 | 96 | #cmd and addrtype 97 | cmd = data[1] 98 | addrtype = data[3] 99 | if cmd is 1 100 | # TCP 101 | else if cmd is 3 102 | # UDP 103 | utils.info "UDP assc request from #{connection.localAddress}:#{connection.localPort}" 104 | reply = new Buffer(10) 105 | reply.write "\u0005\u0000\u0000\u0001", 0, 4, "binary" 106 | utils.debug connection.localAddress 107 | utils.inetAton(connection.localAddress).copy reply, 4 108 | reply.writeUInt16BE connection.localPort, 8 109 | connection.write reply 110 | stage = 10 111 | else 112 | utils.error "unsupported cmd: " + cmd 113 | reply = new Buffer("\u0005\u0007\u0000\u0001", "binary") 114 | connection.end reply 115 | return 116 | if addrtype is 3 117 | addrLen = data[4] 118 | else unless addrtype in [1, 4] 119 | utils.error "unsupported addrtype: " + addrtype 120 | connection.destroy() 121 | return 122 | addrToSend = data.slice(3, 4).toString("binary") 123 | # read address and port 124 | if addrtype is 1 125 | remoteAddr = utils.inetNtoa(data.slice(4, 8)) 126 | addrToSend += data.slice(4, 10).toString("binary") 127 | remotePort = data.readUInt16BE(8) 128 | headerLength = 10 129 | else if addrtype is 4 130 | remoteAddr = inet.inet_ntop(data.slice(4, 20)) 131 | addrToSend += data.slice(4, 22).toString("binary") 132 | remotePort = data.readUInt16BE(20) 133 | headerLength = 22 134 | else 135 | remoteAddr = data.slice(5, 5 + addrLen).toString("binary") 136 | addrToSend += data.slice(4, 5 + addrLen + 2).toString("binary") 137 | remotePort = data.readUInt16BE(5 + addrLen) 138 | headerLength = 5 + addrLen + 2 139 | if cmd is 3 140 | utils.info "UDP assc: #{remoteAddr}:#{remotePort}" 141 | return 142 | buf = new Buffer(10) 143 | buf.write "\u0005\u0000\u0000\u0001", 0, 4, "binary" 144 | buf.write "\u0000\u0000\u0000\u0000", 4, 4, "binary" 145 | # 2222 can be any number between 1 and 65535 146 | buf.writeInt16BE 2222, 8 147 | connection.write buf 148 | # connect remote server 149 | [aServer, aPort] = getServer() 150 | utils.info "connecting #{aServer}:#{aPort}" 151 | remote = net.connect(aPort, aServer, -> 152 | remote.setNoDelay true if remote 153 | stage = 5 154 | utils.debug "stage = 5" 155 | ) 156 | remote.on "data", (data) -> 157 | return if !connected # returns when connection disconnected 158 | utils.log utils.EVERYTHING, "remote on data" 159 | try 160 | if encryptor 161 | data = encryptor.decrypt data 162 | remote.pause() unless connection.write(data) 163 | else 164 | remote.destroy() 165 | catch e 166 | utils.error e 167 | remote.destroy() if remote 168 | connection.destroy() if connection 169 | 170 | remote.on "end", -> 171 | utils.debug "remote on end" 172 | connection.end() if connection 173 | 174 | remote.on "error", (e)-> 175 | utils.debug "remote on error" 176 | utils.error "remote #{remoteAddr}:#{remotePort} error: #{e}" 177 | 178 | remote.on "close", (had_error)-> 179 | utils.debug "remote on close:#{had_error}" 180 | if had_error 181 | connection.destroy() if connection 182 | else 183 | connection.end() if connection 184 | 185 | remote.on "drain", -> 186 | utils.debug "remote on drain" 187 | connection.resume() if connection 188 | 189 | remote.setTimeout timeout, -> 190 | utils.debug "remote on timeout" 191 | remote.destroy() if remote 192 | connection.destroy() if connection 193 | 194 | addrToSendBuf = new Buffer(addrToSend, "binary") 195 | addrToSendBuf = encryptor.encrypt addrToSendBuf 196 | remote.setNoDelay false 197 | remote.write addrToSendBuf 198 | 199 | if data.length > headerLength 200 | buf = new Buffer(data.length - headerLength) 201 | data.copy buf, 0, headerLength 202 | piece = encryptor.encrypt buf 203 | remote.write piece 204 | stage = 4 205 | utils.debug "stage = 4" 206 | catch e 207 | # may encounter index out of range 208 | utils.error e 209 | connection.destroy() if connection 210 | remote.destroy() if remote 211 | clean() 212 | else if stage is 4 213 | if not remote? 214 | connection.destroy() if connection 215 | return 216 | data = encryptor.encrypt data 217 | remote.setNoDelay true 218 | connection.pause() unless remote.write(data) 219 | 220 | connection.on "end", -> 221 | connected = false 222 | utils.debug "connection on end" 223 | remote.end() if remote 224 | 225 | connection.on "error", (e)-> 226 | utils.debug "connection on error" 227 | utils.error "local error: #{e}" 228 | 229 | connection.on "close", (had_error)-> 230 | connected = false 231 | utils.debug "connection on close:#{had_error}" 232 | if had_error 233 | remote.destroy() if remote 234 | else 235 | remote.end() if remote 236 | clean() 237 | 238 | connection.on "drain", -> 239 | # calling resume() when remote not is connected will crash node.js 240 | utils.debug "connection on drain" 241 | remote.resume() if remote and stage is 5 242 | 243 | connection.setTimeout timeout, -> 244 | utils.debug "connection on timeout" 245 | remote.destroy() if remote 246 | connection.destroy() if connection 247 | ) 248 | if local_address? 249 | server.listen port, local_address, -> 250 | utils.info "local listening at #{server.address().address}:#{port}" 251 | else 252 | server.listen port, -> 253 | utils.info "local listening at 0.0.0.0:" + port 254 | 255 | server.on "error", (e) -> 256 | if e.code is "EADDRINUSE" 257 | utils.error "Address in use, aborting" 258 | else 259 | utils.error e 260 | 261 | server.on "close", -> 262 | udpServer.close() 263 | 264 | return server 265 | 266 | exports.createServer = createServer 267 | exports.main = -> 268 | console.log(utils.version) 269 | configFromArgs = utils.parseArgs() 270 | configPath = 'config.json' 271 | if configFromArgs.config_file 272 | configPath = configFromArgs.config_file 273 | if not fs.existsSync(configPath) 274 | configPath = path.resolve(__dirname, "config.json") 275 | if not fs.existsSync(configPath) 276 | configPath = path.resolve(__dirname, "../../config.json") 277 | if not fs.existsSync(configPath) 278 | configPath = null 279 | if configPath 280 | utils.info 'loading config from ' + configPath 281 | configContent = fs.readFileSync(configPath) 282 | try 283 | config = JSON.parse(configContent) 284 | catch e 285 | utils.error('found an error in config.json: ' + e.message) 286 | process.exit 1 287 | else 288 | config = {} 289 | for k, v of configFromArgs 290 | config[k] = v 291 | if config.verbose 292 | utils.config(utils.DEBUG) 293 | 294 | utils.checkConfig config 295 | 296 | SERVER = config.server 297 | REMOTE_PORT = config.server_port 298 | PORT = config.local_port 299 | KEY = config.password 300 | METHOD = config.method 301 | local_address = config.local_address 302 | if not (SERVER and REMOTE_PORT and PORT and KEY) 303 | utils.warn 'config.json not found, you have to specify all config in commandline' 304 | process.exit 1 305 | timeout = Math.floor(config.timeout * 1000) or 600000 306 | s = createServer SERVER, REMOTE_PORT, PORT, KEY, METHOD, timeout, local_address 307 | s.on "error", (e) -> 308 | process.stdout.on 'drain', -> 309 | process.exit 1 310 | if require.main is module 311 | exports.main() 312 | -------------------------------------------------------------------------------- /src/merge_sort.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2014 clowwindy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ### 22 | 23 | merge = (left, right, comparison) -> 24 | result = new Array() 25 | while (left.length > 0) and (right.length > 0) 26 | if comparison(left[0], right[0]) <= 0 27 | result.push left.shift() 28 | else 29 | result.push right.shift() 30 | result.push left.shift() while left.length > 0 31 | result.push right.shift() while right.length > 0 32 | result 33 | merge_sort = (array, comparison) -> 34 | return array if array.length < 2 35 | middle = Math.ceil(array.length / 2) 36 | merge merge_sort(array.slice(0, middle), comparison), merge_sort(array.slice(middle), comparison), comparison 37 | exports.merge_sort = merge_sort 38 | -------------------------------------------------------------------------------- /src/server.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2014 clowwindy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ### 22 | 23 | 24 | net = require("net") 25 | fs = require("fs") 26 | path = require("path") 27 | udpRelay = require("./udprelay") 28 | utils = require("./utils") 29 | inet = require("./inet") 30 | Encryptor = require("./encrypt").Encryptor 31 | 32 | exports.main = -> 33 | 34 | console.log(utils.version) 35 | configFromArgs = utils.parseArgs(true) 36 | configPath = 'config.json' 37 | if configFromArgs.config_file 38 | configPath = configFromArgs.config_file 39 | if not fs.existsSync(configPath) 40 | configPath = path.resolve(__dirname, "config.json") 41 | if not fs.existsSync(configPath) 42 | configPath = path.resolve(__dirname, "../../config.json") 43 | if not fs.existsSync(configPath) 44 | configPath = null 45 | if configPath 46 | utils.info 'loading config from ' + configPath 47 | configContent = fs.readFileSync(configPath) 48 | try 49 | config = JSON.parse(configContent) 50 | catch e 51 | utils.error('found an error in config.json: ' + e.message) 52 | process.exit 1 53 | else 54 | config = {} 55 | for k, v of configFromArgs 56 | config[k] = v 57 | if config.verbose 58 | utils.config(utils.DEBUG) 59 | 60 | utils.checkConfig config 61 | 62 | timeout = Math.floor(config.timeout * 1000) or 300000 63 | portPassword = config.port_password 64 | port = config.server_port 65 | key = config.password 66 | METHOD = config.method 67 | SERVER = config.server 68 | 69 | if not (SERVER and (port or portPassword) and key) 70 | utils.warn 'config.json not found, you have to specify all config in commandline' 71 | process.exit 1 72 | 73 | connections = 0 74 | 75 | if portPassword 76 | if port or key 77 | utils.warn 'warning: port_password should not be used with server_port and password. server_port and password will be ignored' 78 | else 79 | portPassword = {} 80 | portPassword[port.toString()] = key 81 | 82 | 83 | for port, key of portPassword 84 | servers = SERVER 85 | unless servers instanceof Array 86 | servers = [servers] 87 | for a_server_ip in servers 88 | (-> 89 | # let's use enclosures to seperate scopes of different servers 90 | PORT = port 91 | KEY = key 92 | server_ip = a_server_ip 93 | utils.info "calculating ciphers for port #{PORT}" 94 | server = net.createServer((connection) -> 95 | connections += 1 96 | encryptor = new Encryptor(KEY, METHOD) 97 | stage = 0 98 | headerLength = 0 99 | remote = null 100 | cachedPieces = [] 101 | addrLen = 0 102 | remoteAddr = null 103 | remotePort = null 104 | utils.debug "connections: #{connections}" 105 | 106 | clean = -> 107 | utils.debug "clean" 108 | connections -= 1 109 | remote = null 110 | connection = null 111 | encryptor = null 112 | utils.debug "connections: #{connections}" 113 | 114 | connection.on "data", (data) -> 115 | utils.log utils.EVERYTHING, "connection on data" 116 | try 117 | data = encryptor.decrypt data 118 | catch e 119 | utils.error e 120 | remote.destroy() if remote 121 | connection.destroy() if connection 122 | return 123 | if stage is 5 124 | connection.pause() unless remote.write(data) 125 | return 126 | if stage is 0 127 | try 128 | addrtype = data[0] 129 | if addrtype is undefined 130 | return 131 | if addrtype is 3 132 | addrLen = data[1] 133 | else unless addrtype in [1, 4] 134 | utils.error "unsupported addrtype: " + addrtype + " maybe wrong password" 135 | connection.destroy() 136 | return 137 | # read address and port 138 | if addrtype is 1 139 | remoteAddr = utils.inetNtoa(data.slice(1, 5)) 140 | remotePort = data.readUInt16BE(5) 141 | headerLength = 7 142 | else if addrtype is 4 143 | remoteAddr = inet.inet_ntop(data.slice(1, 17)) 144 | remotePort = data.readUInt16BE(17) 145 | headerLength = 19 146 | else 147 | remoteAddr = data.slice(2, 2 + addrLen).toString("binary") 148 | remotePort = data.readUInt16BE(2 + addrLen) 149 | headerLength = 2 + addrLen + 2 150 | 151 | # avoid reading from cache before getting connected 152 | connection.pause() 153 | 154 | # connect remote server 155 | remote = net.connect(remotePort, remoteAddr, -> 156 | utils.info "connecting #{remoteAddr}:#{remotePort}" 157 | if not encryptor or not remote or not connection 158 | remote.destroy() if remote 159 | return 160 | i = 0 161 | 162 | # now we get connected, resume data flow 163 | connection.resume() 164 | 165 | while i < cachedPieces.length 166 | piece = cachedPieces[i] 167 | remote.write piece 168 | i++ 169 | cachedPieces = null # save memory 170 | 171 | # use a small timeout for connect() 172 | remote.setTimeout timeout, -> 173 | utils.debug "remote on timeout during connect()" 174 | remote.destroy() if remote 175 | connection.destroy() if connection 176 | 177 | stage = 5 178 | utils.debug "stage = 5" 179 | ) 180 | remote.on "data", (data) -> 181 | utils.log utils.EVERYTHING, "remote on data" 182 | if not encryptor 183 | remote.destroy() if remote 184 | return 185 | data = encryptor.encrypt data 186 | remote.pause() unless connection.write(data) 187 | 188 | remote.on "end", -> 189 | utils.debug "remote on end" 190 | connection.end() if connection 191 | 192 | remote.on "error", (e)-> 193 | utils.debug "remote on error" 194 | utils.error "remote #{remoteAddr}:#{remotePort} error: #{e}" 195 | 196 | remote.on "close", (had_error)-> 197 | utils.debug "remote on close:#{had_error}" 198 | if had_error 199 | connection.destroy() if connection 200 | else 201 | connection.end() if connection 202 | 203 | remote.on "drain", -> 204 | utils.debug "remote on drain" 205 | connection.resume() if connection 206 | 207 | # use a small timeout for connect() 208 | remote.setTimeout 15 * 1000, -> 209 | utils.debug "remote on timeout during connect()" 210 | remote.destroy() if remote 211 | connection.destroy() if connection 212 | 213 | if data.length > headerLength 214 | # make sure no data is lost 215 | buf = new Buffer(data.length - headerLength) 216 | data.copy buf, 0, headerLength 217 | cachedPieces.push buf 218 | buf = null 219 | stage = 4 220 | utils.debug "stage = 4" 221 | catch e 222 | # may encouter index out of range 223 | utils.error e 224 | connection.destroy() 225 | remote.destroy() if remote 226 | else cachedPieces.push data if stage is 4 227 | # remote server not connected 228 | # cache received buffers 229 | # make sure no data is lost 230 | 231 | connection.on "end", -> 232 | utils.debug "connection on end" 233 | remote.end() if remote 234 | 235 | connection.on "error", (e)-> 236 | utils.debug "connection on error" 237 | utils.error "local error: #{e}" 238 | 239 | connection.on "close", (had_error)-> 240 | utils.debug "connection on close:#{had_error}" 241 | if had_error 242 | remote.destroy() if remote 243 | else 244 | remote.end() if remote 245 | clean() 246 | 247 | connection.on "drain", -> 248 | utils.debug "connection on drain" 249 | remote.resume() if remote 250 | 251 | connection.setTimeout timeout, -> 252 | utils.debug "connection on timeout" 253 | remote.destroy() if remote 254 | connection.destroy() if connection 255 | ) 256 | 257 | server.listen PORT, server_ip, -> 258 | utils.info "server listening at #{server_ip}:#{PORT} " 259 | udpRelay.createServer(server_ip, PORT, null, null, key, METHOD, timeout, false) 260 | 261 | server.on "error", (e) -> 262 | if e.code is "EADDRINUSE" 263 | utils.error "Address in use, aborting" 264 | else 265 | utils.error e 266 | process.stdout.on 'drain', -> 267 | process.exit 1 268 | )() 269 | 270 | if require.main is module 271 | exports.main() 272 | -------------------------------------------------------------------------------- /src/test.coffee: -------------------------------------------------------------------------------- 1 | # test encryption 2 | encrypt = require("./encrypt") 3 | target = [ 4 | [ 60, 53, 84, 138, 217, 94, 88, 23, 39, 242, 219, 35, 12, 157, 165, 181, 255, 143, 83, 247, 162, 16, 31, 209, 190, 171, 115, 65, 38, 41, 21, 245, 236, 46, 121, 62, 166, 233, 44, 154, 153, 145, 230, 49, 128, 216, 173, 29, 241, 119, 64, 229, 194, 103, 131, 110, 26, 197, 218, 59, 204, 56, 27, 34, 141, 221, 149, 239, 192, 195, 24, 155, 170, 183, 11, 254, 213, 37, 137, 226, 75, 203, 55, 19, 72, 248, 22, 129, 33, 175, 178, 10, 198, 71, 77, 36, 113, 167, 48, 2, 117, 140, 142, 66, 199, 232, 243, 32, 123, 54, 51, 82, 57, 177, 87, 251, 150, 196, 133, 5, 253, 130, 8, 184, 14, 152, 231, 3, 186, 159, 76, 89, 228, 205, 156, 96, 163, 146, 18, 91, 132, 85, 80, 109, 172, 176, 105, 13, 50, 235, 127, 0, 189, 95, 98, 136, 250, 200, 108, 179, 211, 214, 106, 168, 78, 79, 74, 210, 30, 73, 201, 151, 208, 114, 101, 174, 92, 52, 120, 240, 15, 169, 220, 182, 81, 224, 43, 185, 40, 99, 180, 17, 212, 158, 42, 90, 9, 191, 45, 6, 25, 4, 222, 67, 126, 1, 116, 124, 206, 69, 61, 7, 68, 97, 202, 63, 244, 20, 28, 58, 93, 134, 104, 144, 227, 147, 102, 118, 135, 148, 47, 238, 86, 112, 122, 70, 107, 215, 100, 139, 223, 225, 164, 237, 111, 125, 207, 160, 187, 246, 234, 161, 188, 193, 249, 252 ], 5 | [ 151, 205, 99, 127, 201, 119, 199, 211, 122, 196, 91, 74, 12, 147, 124, 180, 21, 191, 138, 83, 217, 30, 86, 7, 70, 200, 56, 62, 218, 47, 168, 22, 107, 88, 63, 11, 95, 77, 28, 8, 188, 29, 194, 186, 38, 198, 33, 230, 98, 43, 148, 110, 177, 1, 109, 82, 61, 112, 219, 59, 0, 210, 35, 215, 50, 27, 103, 203, 212, 209, 235, 93, 84, 169, 166, 80, 130, 94, 164, 165, 142, 184, 111, 18, 2, 141, 232, 114, 6, 131, 195, 139, 176, 220, 5, 153, 135, 213, 154, 189, 238, 174, 226, 53, 222, 146, 162, 236, 158, 143, 55, 244, 233, 96, 173, 26, 206, 100, 227, 49, 178, 34, 234, 108, 207, 245, 204, 150, 44, 87, 121, 54, 140, 118, 221, 228, 155, 78, 3, 239, 101, 64, 102, 17, 223, 41, 137, 225, 229, 66, 116, 171, 125, 40, 39, 71, 134, 13, 193, 129, 247, 251, 20, 136, 242, 14, 36, 97, 163, 181, 72, 25, 144, 46, 175, 89, 145, 113, 90, 159, 190, 15, 183, 73, 123, 187, 128, 248, 252, 152, 24, 197, 68, 253, 52, 69, 117, 57, 92, 104, 157, 170, 214, 81, 60, 133, 208, 246, 172, 23, 167, 160, 192, 76, 161, 237, 45, 4, 58, 10, 182, 65, 202, 240, 185, 241, 79, 224, 132, 51, 42, 126, 105, 37, 250, 149, 32, 243, 231, 67, 179, 48, 9, 106, 216, 31, 249, 19, 85, 254, 156, 115, 255, 120, 75, 16 ] 6 | ] 7 | tables = encrypt.getTable("foobar!") 8 | console.log JSON.stringify(tables) 9 | i = 0 10 | 11 | while i < 256 12 | console.assert tables[0][i] is target[0][i] 13 | console.assert tables[1][i] is target[1][i] 14 | i++ 15 | 16 | # test proxy 17 | 18 | child_process = require('child_process') 19 | local = child_process.spawn('bin/sslocal', []) 20 | server = child_process.spawn('bin/ssserver', []) 21 | 22 | curlRunning = false 23 | 24 | local.on 'exit', (code)-> 25 | server.kill() 26 | if !curlRunning 27 | process.exit code 28 | 29 | server.on 'exit', (code)-> 30 | local.kill() 31 | if !curlRunning 32 | process.exit code 33 | 34 | localReady = false 35 | serverReady = false 36 | curlRunning = false 37 | 38 | runCurl = -> 39 | curlRunning = true 40 | curl = child_process.spawn 'curl', ['-v', 'http://www.example.com/', '-L', '--socks5-hostname', '127.0.0.1:1080'] 41 | curl.on 'exit', (code)-> 42 | local.kill() 43 | server.kill() 44 | if code is 0 45 | console.log 'Test passed' 46 | process.exit 0 47 | else 48 | console.error 'Test failed' 49 | process.exit code 50 | 51 | curl.stdout.on 'data', (data) -> 52 | process.stdout.write(data) 53 | 54 | curl.stderr.on 'data', (data) -> 55 | process.stderr.write(data) 56 | 57 | local.stderr.on 'data', (data) -> 58 | process.stderr.write(data) 59 | 60 | server.stderr.on 'data', (data) -> 61 | process.stderr.write(data) 62 | 63 | local.stdout.on 'data', (data) -> 64 | process.stdout.write(data) 65 | if data.toString().indexOf('listening at') >= 0 66 | localReady = true 67 | if localReady and serverReady and not curlRunning 68 | runCurl() 69 | 70 | server.stdout.on 'data', (data) -> 71 | process.stdout.write(data) 72 | if data.toString().indexOf('listening at') >= 0 73 | serverReady = true 74 | if localReady and serverReady and not curlRunning 75 | runCurl() 76 | 77 | -------------------------------------------------------------------------------- /src/udprelay.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2014 clowwindy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ### 22 | 23 | 24 | utils = require('./utils') 25 | inet = require('./inet') 26 | encryptor = require('./encrypt') 27 | 28 | dgram = require 'dgram' 29 | net = require 'net' 30 | 31 | class LRUCache 32 | constructor: (@timeout, sweepInterval) -> 33 | that = this 34 | sweepFun = -> 35 | that.sweep() 36 | 37 | @interval = setInterval(sweepFun, sweepInterval) 38 | @dict = {} 39 | 40 | setItem: (key, value) -> 41 | cur = process.hrtime() 42 | @dict[key] = [value, cur] 43 | 44 | getItem: (key) -> 45 | v = @dict[key] 46 | if v 47 | v[1] = process.hrtime() 48 | return v[0] 49 | return null 50 | 51 | delItem: (key) -> 52 | delete @dict[key] 53 | 54 | destroy: -> 55 | clearInterval @interval 56 | 57 | sweep: -> 58 | utils.debug "sweeping" 59 | dict = @dict 60 | keys = Object.keys(dict) 61 | swept = 0 62 | for k in keys 63 | v = dict[k] 64 | diff = process.hrtime(v[1]) 65 | if diff[0] > @timeout * 0.001 66 | swept += 1 67 | v0 = v[0] 68 | v0.close() 69 | delete dict[k] 70 | utils.debug "#{swept} keys swept" 71 | 72 | # SOCKS5 UDP Request 73 | # +----+------+------+----------+----------+----------+ 74 | # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 75 | # +----+------+------+----------+----------+----------+ 76 | # | 2 | 1 | 1 | Variable | 2 | Variable | 77 | # +----+------+------+----------+----------+----------+ 78 | 79 | # SOCKS5 UDP Response 80 | # +----+------+------+----------+----------+----------+ 81 | # |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | 82 | # +----+------+------+----------+----------+----------+ 83 | # | 2 | 1 | 1 | Variable | 2 | Variable | 84 | # +----+------+------+----------+----------+----------+ 85 | 86 | # shadowsocks UDP Request (before encrypted) 87 | # +------+----------+----------+----------+ 88 | # | ATYP | DST.ADDR | DST.PORT | DATA | 89 | # +------+----------+----------+----------+ 90 | # | 1 | Variable | 2 | Variable | 91 | # +------+----------+----------+----------+ 92 | 93 | # shadowsocks UDP Response (before encrypted) 94 | # +------+----------+----------+----------+ 95 | # | ATYP | DST.ADDR | DST.PORT | DATA | 96 | # +------+----------+----------+----------+ 97 | # | 1 | Variable | 2 | Variable | 98 | # +------+----------+----------+----------+ 99 | 100 | # shadowsocks UDP Request and Response (after encrypted) 101 | # +-------+--------------+ 102 | # | IV | PAYLOAD | 103 | # +-------+--------------+ 104 | # | Fixed | Variable | 105 | # +-------+--------------+ 106 | 107 | # HOW TO NAME THINGS 108 | # ------------------ 109 | # `dest` means destination server, which is from DST fields in the SOCKS5 request 110 | # `local` means local server of shadowsocks 111 | # `remote` means remote server of shadowsocks 112 | # `client` means UDP client, which is used for connecting, or the client that connects our server 113 | # `server` means UDP server, which is used for listening, or the server for our client to connect 114 | 115 | encrypt = (password, method, data) -> 116 | try 117 | return encryptor.encryptAll(password, method, 1, data) 118 | catch e 119 | utils.error e 120 | return null 121 | 122 | decrypt = (password, method, data) -> 123 | try 124 | return encryptor.encryptAll(password, method, 0, data) 125 | catch e 126 | utils.error e 127 | return null 128 | 129 | parseHeader = (data, requestHeaderOffset) -> 130 | try 131 | addrtype = data[requestHeaderOffset] 132 | if addrtype is 3 133 | addrLen = data[requestHeaderOffset + 1] 134 | else unless addrtype in [1, 4] 135 | utils.warn "unsupported addrtype: " + addrtype 136 | return null 137 | if addrtype is 1 138 | destAddr = utils.inetNtoa(data.slice(requestHeaderOffset + 1, requestHeaderOffset + 5)) 139 | destPort = data.readUInt16BE(requestHeaderOffset + 5) 140 | headerLength = requestHeaderOffset + 7 141 | else if addrtype is 4 142 | destAddr = inet.inet_ntop(data.slice(requestHeaderOffset + 1, requestHeaderOffset + 17)) 143 | destPort = data.readUInt16BE(requestHeaderOffset + 17) 144 | headerLength = requestHeaderOffset + 19 145 | else 146 | destAddr = data.slice(requestHeaderOffset + 2, requestHeaderOffset + 2 + addrLen).toString("binary") 147 | destPort = data.readUInt16BE(requestHeaderOffset + 2 + addrLen) 148 | headerLength = requestHeaderOffset + 2 + addrLen + 2 149 | return [addrtype, destAddr, destPort, headerLength] 150 | catch e 151 | utils.error e 152 | return null 153 | 154 | 155 | exports.createServer = (listenAddr, listenPort, remoteAddr, remotePort, 156 | password, method, timeout, isLocal) -> 157 | # if listen to ANY, listen to both IPv4 and IPv6 158 | # or listen to IP family of IP address 159 | udpTypesToListen = [] 160 | if not listenAddr? 161 | udpTypesToListen = ['udp4', 'udp6'] 162 | else 163 | listenIPType = net.isIP(listenAddr) 164 | if listenIPType == 6 165 | udpTypesToListen.push 'udp6' 166 | else 167 | udpTypesToListen.push 'udp4' 168 | for udpTypeToListen in udpTypesToListen 169 | server = dgram.createSocket(udpTypeToListen) 170 | clients = new LRUCache(timeout, 10 * 1000) 171 | 172 | clientKey = (localAddr, localPort, destAddr, destPort) -> 173 | return "#{localAddr}:#{localPort}:#{destAddr}:#{destPort}" 174 | 175 | server.on("message", (data, rinfo) -> 176 | # Parse request 177 | requestHeaderOffset = 0 178 | if isLocal 179 | requestHeaderOffset = 3 180 | frag = data[2] 181 | if frag != 0 182 | utils.debug "frag:#{frag}" 183 | utils.warn "drop a message since frag is not 0" 184 | return 185 | else 186 | # on remote, client to server 187 | data = decrypt(password, method, data) 188 | if not data? 189 | # drop 190 | return 191 | headerResult = parseHeader(data, requestHeaderOffset) 192 | if headerResult == null 193 | # drop 194 | return 195 | [addrtype, destAddr, destPort, headerLength] = headerResult 196 | 197 | if isLocal 198 | sendDataOffset = requestHeaderOffset 199 | [serverAddr, serverPort] = [remoteAddr, remotePort] 200 | else 201 | sendDataOffset = headerLength 202 | [serverAddr, serverPort] = [destAddr, destPort] 203 | 204 | key = clientKey(rinfo.address, rinfo.port, destAddr, destPort) 205 | client = clients.getItem(key) 206 | if not client? 207 | # Create IPv6 UDP socket if serverAddr is an IPv6 address 208 | clientUdpType = net.isIP(serverAddr) 209 | if clientUdpType == 6 210 | client = dgram.createSocket("udp6") 211 | else 212 | client = dgram.createSocket("udp4") 213 | clients.setItem(key, client) 214 | 215 | client.on "message", (data1, rinfo1) -> 216 | # utils.debug "client got #{data1} from #{rinfo1.address}:#{rinfo1.port}" 217 | if not isLocal 218 | # on remote, server to client 219 | # append shadowsocks response header 220 | # TODO: support receive from IPv6 addr 221 | utils.debug "UDP recv from #{rinfo1.address}:#{rinfo1.port}" 222 | serverIPBuf = utils.inetAton(rinfo1.address) 223 | responseHeader = new Buffer(7) 224 | responseHeader.write('\x01', 0) 225 | serverIPBuf.copy(responseHeader, 1, 0, 4) 226 | responseHeader.writeUInt16BE(rinfo1.port, 5) 227 | data2 = Buffer.concat([responseHeader, data1]) 228 | data2 = encrypt(password, method, data2) 229 | if not data2? 230 | # drop 231 | return 232 | else 233 | # on local, server to client 234 | # append socks5 response header 235 | responseHeader = new Buffer("\x00\x00\x00") 236 | data1 = decrypt(password, method, data1) 237 | if not data1? 238 | # drop 239 | return 240 | headerResult = parseHeader(data1, 0) 241 | if headerResult == null 242 | # drop 243 | return 244 | [addrtype, destAddr, destPort, headerLength] = headerResult 245 | utils.debug "UDP recv from #{destAddr}:#{destPort}" 246 | data2 = Buffer.concat([responseHeader, data1]) 247 | server.send data2, 0, data2.length, rinfo.port, rinfo.address, (err, bytes) -> 248 | utils.debug "remote to local sent" 249 | 250 | client.on "error", (err) -> 251 | utils.error "UDP client error: #{err}" 252 | 253 | client.on "close", -> 254 | utils.debug "UDP client close" 255 | clients.delItem(key) 256 | 257 | utils.debug "pairs: #{Object.keys(clients.dict).length}" 258 | 259 | dataToSend = data.slice(sendDataOffset, data.length) 260 | if isLocal 261 | # on local, client to server 262 | dataToSend = encrypt password, method, dataToSend 263 | if not dataToSend? 264 | # drop 265 | return 266 | 267 | utils.debug "UDP send to #{destAddr}:#{destPort}" 268 | client.send dataToSend, 0, dataToSend.length, serverPort, serverAddr, (err, bytes) -> 269 | utils.debug "local to remote sent" 270 | 271 | ) 272 | 273 | server.on "listening", -> 274 | address = server.address() 275 | utils.info("UDP server listening " + address.address + ":" + address.port) 276 | 277 | server.on "close", -> 278 | utils.info "UDP server closing" 279 | clients.destroy() 280 | 281 | if listenAddr? 282 | server.bind(listenPort, listenAddr) 283 | else 284 | server.bind(listenPort) 285 | 286 | return server 287 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) 2014 clowwindy 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | ### 22 | 23 | 24 | util = require 'util' 25 | pack = require '../../package.json' 26 | 27 | printLocalHelp = -> 28 | console.log """ 29 | usage: sslocal [-h] -s SERVER_ADDR -p SERVER_PORT [-b LOCAL_ADDR] -l LOCAL_PORT -k PASSWORD -m METHOD [-t TIMEOUT] [-c config] 30 | 31 | optional arguments: 32 | -h, --help show this help message and exit 33 | -s SERVER_ADDR server address 34 | -p SERVER_PORT server port 35 | -b LOCAL_ADDR local binding address, default is 127.0.0.1 36 | -l LOCAL_PORT local port 37 | -k PASSWORD password 38 | -m METHOD encryption method, for example, aes-256-cfb 39 | -t TIMEOUT timeout in seconds 40 | -c CONFIG path to config file 41 | """ 42 | 43 | printServerHelp = -> 44 | console.log """ 45 | usage: ssserver [-h] -s SERVER_ADDR -p SERVER_PORT -k PASSWORD -m METHOD [-t TIMEOUT] [-c config] 46 | 47 | optional arguments: 48 | -h, --help show this help message and exit 49 | -s SERVER_ADDR server address 50 | -p SERVER_PORT server port 51 | -k PASSWORD password 52 | -m METHOD encryption method, for example, aes-256-cfb 53 | -t TIMEOUT timeout in seconds 54 | -c CONFIG path to config file 55 | """ 56 | 57 | exports.parseArgs = (isServer=false)-> 58 | defination = 59 | '-l': 'local_port' 60 | '-p': 'server_port' 61 | '-s': 'server' 62 | '-k': 'password', 63 | '-c': 'config_file', 64 | '-m': 'method', 65 | '-b': 'local_address', 66 | '-t': 'timeout' 67 | 68 | result = {} 69 | nextIsValue = false 70 | lastKey = null 71 | for _, oneArg of process.argv 72 | if nextIsValue 73 | result[lastKey] = oneArg 74 | nextIsValue = false 75 | else if oneArg of defination 76 | lastKey = defination[oneArg] 77 | nextIsValue = true 78 | else if '-v' == oneArg 79 | result['verbose'] = true 80 | else if oneArg.indexOf('-') == 0 81 | if isServer 82 | printServerHelp() 83 | else 84 | printLocalHelp() 85 | process.exit 2 86 | result 87 | 88 | exports.checkConfig = (config) -> 89 | if config.server in ['127.0.0.1', 'localhost'] 90 | exports.warn "Server is set to #{config.server}, maybe it's not correct" 91 | exports.warn "Notice server will listen at #{config.server}:#{config.server_port}" 92 | if (config.method or '').toLowerCase() == 'rc4' 93 | exports.warn 'RC4 is not safe; please use a safer cipher, like AES-256-CFB' 94 | 95 | exports.version = "#{pack.name} v#{pack.version}" 96 | 97 | exports.EVERYTHING = 0 98 | exports.DEBUG = 1 99 | exports.INFO = 2 100 | exports.WARN = 3 101 | exports.ERROR = 4 102 | 103 | _logging_level = exports.INFO 104 | 105 | exports.config = (level) -> 106 | _logging_level = level 107 | 108 | exports.log = (level, msg)-> 109 | if level >= _logging_level 110 | if level >= exports.DEBUG 111 | util.log(new Date().getMilliseconds() + 'ms ' + msg) 112 | else 113 | util.log msg 114 | 115 | exports.debug = (msg)-> 116 | exports.log exports.DEBUG, msg 117 | 118 | exports.info = (msg)-> 119 | exports.log exports.INFO, msg 120 | 121 | exports.warn = (msg)-> 122 | exports.log exports.WARN, msg 123 | 124 | exports.error = (msg)-> 125 | exports.log exports.ERROR, msg?.stack or msg 126 | 127 | exports.inetNtoa = (buf) -> 128 | buf[0] + "." + buf[1] + "." + buf[2] + "." + buf[3] 129 | 130 | exports.inetAton = (ipStr) -> 131 | parts = ipStr.split(".") 132 | unless parts.length is 4 133 | null 134 | else 135 | buf = new Buffer(4) 136 | i = 0 137 | while i < 4 138 | buf[i] = +parts[i] 139 | i++ 140 | buf 141 | 142 | setInterval(-> 143 | if _logging_level <= exports.DEBUG 144 | exports.debug(JSON.stringify(process.memoryUsage(), ' ', 2)) 145 | if global.gc 146 | exports.debug 'GC' 147 | gc() 148 | exports.debug(JSON.stringify(process.memoryUsage(), ' ', 2)) 149 | cwd = process.cwd() 150 | if _logging_level == exports.DEBUG 151 | try 152 | heapdump = require 'heapdump' 153 | process.chdir '/tmp' 154 | # heapdump.writeSnapshot() 155 | process.chdir cwd 156 | catch e 157 | exports.debug e 158 | , 1000) 159 | -------------------------------------------------------------------------------- /test/benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in {1..10} ; do 4 | echo $i 5 | curl --socks5-hostname 127.0.0.1:$1 http://www.google.com/ >/dev/null 6 | done 7 | -------------------------------------------------------------------------------- /test/config-client-multi-port.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "123.123.123.1", 3 | "server_port": [8380, 8381, 8382, 8383], 4 | "local_port": 1081, 5 | "password": "barfoo!", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /test/config-client-multi-server-port.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": ["123.123.123.1:8381", "123.123.123.2:8381", "123.123.123.2:8382"], 3 | "server_port":8380, 4 | "local_port":1081, 5 | "password":"barfoo!", 6 | "timeout":60, 7 | "method":"aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /test/config-client-multi-server.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": ["123.123.123.1", "123.123.123.2", "123.123.123.3"], 3 | "server_port": 8380, 4 | "local_port": 1081, 5 | "password": "barfoo!", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /test/config-server-multi-passwd.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": 8380, 4 | "local_port": 1081, 5 | "password": "barfoo!", 6 | "port_password": { 7 | "8381": "foobar1", 8 | "8382": "foobar2", 9 | "8383": "foobar3", 10 | "8384": "foobar4", 11 | "8385": "foobar5", 12 | "8386": "foobar6", 13 | "8387": "foobar7", 14 | "8388": "foobar8", 15 | "8389": "foobar9", 16 | "8390": "foobar10", 17 | "8391": "foobar11", 18 | "8392": "foobar12" 19 | }, 20 | "timeout": 60, 21 | "method": "aes-256-cfb" 22 | } 23 | -------------------------------------------------------------------------------- /test/config-server-multi-port.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": 8380, 4 | "local_port": 1081, 5 | "password": "barfoo!", 6 | "port_password": { 7 | "8380": "barfoo!", 8 | "8381": "barfoo!", 9 | "8382": "barfoo!", 10 | "8383": "barfoo!", 11 | "8384": "barfoo!", 12 | "8385": "barfoo!", 13 | "8386": "barfoo!", 14 | "8387": "barfoo!" 15 | }, 16 | "timeout": 60, 17 | "method": "aes-256-cfb" 18 | } 19 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": 8388, 4 | "local_port": 1080, 5 | "password": "barfoo!", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } --------------------------------------------------------------------------------