├── .gitignore ├── README.md ├── config.php ├── lib ├── Client.php ├── Encryptor.php ├── ShadowSocks.php ├── Sock5.php ├── Trace.php └── autoload.php └── ss-server.php /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.buildpath 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shadowsocks-swoole-php 2 | 3 | 须要swoole扩展 4 | ------ 5 | https://github.com/swoole/swoole-src 6 | 7 | 启动方式 8 | ------ 9 | ``` 10 | php ss-server.php 11 | ``` 12 | 守护运行 13 | ``` 14 | php ss-server.php -d 15 | ``` 16 | 17 | 配置文件 18 | ------ 19 | config.php 20 | ``` 21 | return [ 22 | 'port' => 8388, 23 | 'passwd' => '123456', 24 | 'method' => 'aes-256-cfb' 25 | ]; 26 | ``` 27 | 28 | 问题 29 | ------ 30 | HTTPS 存在问题 31 | 32 | 其它 33 | ------ 34 | 加密类与sock5头部参考 35 | https://github.com/walkor/shadowsocks-php -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 8388, 4 | 'passwd' => '123456', 5 | 'method' => 'aes-256-cfb' 6 | ]; -------------------------------------------------------------------------------- /lib/Client.php: -------------------------------------------------------------------------------- 1 | '','ip' => '','port' => '']; 10 | 11 | /** 12 | * @var Encryptor 13 | */ 14 | public $cryptor = null; 15 | 16 | /** 17 | * @var swoole_client 18 | */ 19 | protected $cli = null; 20 | 21 | /** 22 | * @var swoole_server 23 | */ 24 | protected $serv = null; 25 | 26 | /** 27 | * @var SplQueue 28 | */ 29 | protected $queue = null; 30 | 31 | /** 32 | * @var boolean 33 | */ 34 | protected $lock = true; 35 | 36 | /** 37 | * @var boolean 38 | */ 39 | protected $status = false; 40 | 41 | /** 42 | * @var boolean 43 | */ 44 | protected $reconn = false; 45 | 46 | /** 47 | * @var int 48 | */ 49 | protected $fd = null; 50 | 51 | private static $instance = array(); 52 | 53 | /** 54 | * 创建对象 55 | * @param int $fd 56 | * @param swoole_server $serv 57 | * @param Encryptor $cryptor 58 | * @return self 59 | */ 60 | public static function getInstance($fd, swoole_server $serv = null, $cryptor = null) 61 | { 62 | if (! isset(self::$instance[$fd])) { 63 | self::$instance[$fd] = new self($fd, $serv, $cryptor); 64 | } 65 | return self::$instance[$fd]; 66 | } 67 | 68 | /** 69 | * 清除对象 70 | * @param int $fd 71 | */ 72 | public static function remove($fd) 73 | { 74 | if (isset(self::$instance[$fd])) { 75 | unset(self::$instance[$fd]); 76 | } 77 | } 78 | 79 | public function __construct($fd, swoole_server $serv, $cryptor) 80 | { 81 | $this->serv = $serv; 82 | $this->fd = $fd; 83 | $this->cryptor = $cryptor; 84 | $this->queue = new SplQueue(); 85 | } 86 | 87 | public function onConnect(swoole_client $cli) 88 | { 89 | Trace::debug("*********cli {$this->fd} connect"); 90 | $this->lock = false; 91 | $this->send(); 92 | } 93 | 94 | public function onReceive(swoole_client $cli, $data) 95 | { 96 | Trace::debug("*********cli {$this->fd} receive lenght:" . strlen($data) . "."); 97 | false !== $this->serv->connection_info($this->fd) && $this->serv->send($this->fd, $this->cryptor->encrypt($data)); 98 | $this->lock = false; 99 | $this->send(); 100 | } 101 | 102 | public function onClose(swoole_client $cli) 103 | { 104 | Trace::debug("*********cli {$this->fd} close"); 105 | $this->reconn = true; 106 | } 107 | 108 | public function onError(swoole_client $cli) 109 | { 110 | Trace::debug("*********cli {$this->fd} error"); 111 | $this->serv->close($this->fd); 112 | $cli->close(); 113 | } 114 | 115 | public function send($data = null) 116 | { 117 | //锁定状态写入队列 118 | if (! empty($data)) { 119 | $this->queue->push($data); 120 | } 121 | if ($this->reconn) { 122 | Trace::debug("*********cli {$this->fd} reconn \n"); 123 | $this->cli->connect($this->conf['ip'], $this->conf['port']); 124 | $this->reconn = false; 125 | $this->lock = true; 126 | } 127 | if ($this->queue->isEmpty()) { 128 | $this->lock = false; 129 | } elseif (! $this->lock) { 130 | $this->lock = true; 131 | $data = $this->queue->shift(); 132 | Trace::debug("*********cli $this->fd send " . strlen($data) . "\n==================\n" . substr($data, 0, 50) . "...\n=============="); 133 | Trace::info(sprintf("Host: %-25s %s", $this->conf['host'], strstr($data, "\n", true))); //;'Host:' . $this->conf['host'] . strstr($data, "\n", true) 134 | $this->cli->send($data); 135 | } 136 | } 137 | 138 | public function init($host) 139 | { 140 | $this->conf['host'] = $host; 141 | $this->status = true; 142 | $this->cli = $cli = new swoole_client(SWOOLE_TCP, SWOOLE_SOCK_ASYNC); 143 | $cli->on('connect', [$this,'onConnect']); 144 | $cli->on('receive', [$this,'onReceive']); 145 | $cli->on('close', [$this,'onClose']); 146 | $cli->on('error', [$this,'onError']); 147 | } 148 | 149 | public function connect($ip, $port) 150 | { 151 | $this->conf['ip'] = is_int($ip) ? long2ip($ip) : $ip; 152 | $this->conf['port'] = $port; 153 | $this->cli->connect($this->conf['ip'], $this->conf['port']); 154 | } 155 | 156 | /** 157 | * 是否被初始化过 158 | * @return boolean 159 | */ 160 | public function hasInit() 161 | { 162 | return $this->status; 163 | } 164 | 165 | public function __destruct() 166 | { 167 | Trace::debug("*********cli $this->fd __destruct"); 168 | if (isset($this->cli)) { 169 | $this->cli->isConnected() && $this->cli->close(); 170 | unset($this->cli); 171 | } 172 | unset($this->queue); 173 | } 174 | } -------------------------------------------------------------------------------- /lib/Encryptor.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright walkor 12 | * @link http://www.workerman.net/ 13 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | 16 | /** 17 | * 加密解密类 18 | * @author walkor 19 | */ 20 | class Encryptor 21 | { 22 | 23 | protected $_key; 24 | 25 | protected $_method; 26 | 27 | protected $_cipher; 28 | 29 | protected $_decipher; 30 | 31 | protected $_bytesToKeyResults = array(); 32 | 33 | protected static $_cachedTables = array(); 34 | 35 | protected static $_encryptTable = array(); 36 | 37 | protected static $_decryptTable = array(); 38 | 39 | protected $_cipherIv; 40 | 41 | protected $_ivSent; 42 | 43 | protected static $_methodSupported = array( 44 | 'aes-128-cfb' => array(16,16), 45 | 'aes-192-cfb' => array(24,16), 46 | 'aes-256-cfb' => array(32,16), 47 | 'bf-cfb' => array(16,8), 48 | 'camellia-128-cfb' => array(16,16), 49 | 'camellia-192-cfb' => array(24,16), 50 | 'camellia-256-cfb' => array(32,16), 51 | 'cast5-cfb' => array(16,8), 52 | 'des-cfb' => array(8,8), 53 | 'idea-cfb' => array(16,8), 54 | 'rc2-cfb' => array(16,8), 55 | 'rc4' => array(16,0), 56 | 'rc4-md5' => array(16,16), 57 | 'seed-cfb' => array(16,16)); 58 | 59 | public static function initTable($key) 60 | { 61 | $_ref = self::getTable($key); 62 | self::$_encryptTable = $_ref[0]; 63 | self::$_decryptTable = $_ref[1]; 64 | } 65 | 66 | public function __construct($key, $method) 67 | { 68 | $this->_key = $key; 69 | $this->_method = $method; 70 | if ($this->_method == 'table') { 71 | $this->_method = null; 72 | } 73 | if ($this->_method) { 74 | $iv_size = openssl_cipher_iv_length($this->_method); 75 | $iv = openssl_random_pseudo_bytes($iv_size); 76 | $this->_cipher = $this->getcipher($this->_key, $this->_method, 1, $iv); 77 | } else { 78 | if (! self::$_encryptTable) { 79 | $_ref = self::getTable($this->_key); 80 | self::$_encryptTable = $_ref[0]; 81 | self::$_decryptTable = $_ref[1]; 82 | } 83 | } 84 | } 85 | 86 | protected static function getTable($key) 87 | { 88 | if (isset(self::$_cachedTables[$key])) { 89 | return self::$_cachedTables[key]; 90 | } 91 | $int32Max = pow(2, 32); 92 | $table = array(); 93 | $decrypt_table = array(); 94 | $hash = md5($key, true); 95 | $tmp = unpack('V2', $hash); 96 | $al = $tmp[1]; 97 | $ah = $tmp[2]; 98 | $i = 0; 99 | while ($i < 256) { 100 | $table[$i] = $i; 101 | $i ++; 102 | } 103 | $i = 1; 104 | while ($i < 1024) { 105 | $table = merge_sort($table, 106 | function ($x, $y) use($ah, $al, $i, $int32Max) 107 | { 108 | return (($ah % ($x + $i)) * $int32Max + $al) % ($x + $i) - (($ah % ($y + $i)) * $int32Max + $al) % ($y + $i); 109 | }); 110 | $i ++; 111 | } 112 | $table = array_values($table); 113 | $i = 0; 114 | while ($i < 256) { 115 | $decrypt_table[$table[$i]] = $i; 116 | ++ $i; 117 | } 118 | ksort($decrypt_table); 119 | $decrypt_table = array_values($decrypt_table); 120 | $result = array($table,$decrypt_table); 121 | self::$_cachedTables[$key] = $result; 122 | return $result; 123 | } 124 | 125 | public static function substitute($table, $buf) 126 | { 127 | $i = 0; 128 | $len = strlen($buf); 129 | while ($i < $len) { 130 | $buf[$i] = chr($table[ord($buf[$i])]); 131 | $i ++; 132 | } 133 | return $buf; 134 | } 135 | 136 | protected function getCipher($password, $method, $op, $iv) 137 | { 138 | $method = strtolower($method); 139 | $m = $this->getCipherLen($method); 140 | if ($m) { 141 | $ref = $this->EVPBytesToKey($password, $m[0], $m[1]); 142 | $key = $ref[0]; 143 | $iv_ = $ref[1]; 144 | if ($iv == null) { 145 | $iv = $iv_; 146 | } 147 | if ($op === 1) { 148 | $this->_cipherIv = substr($iv, 0, $m[1]); 149 | } 150 | $iv = substr($iv, 0, $m[1]); 151 | if ($method === 'rc4-md5') { 152 | return $this->createRc4Md5Cipher($key, $iv, $op); 153 | } else { 154 | if ($op === 1) { 155 | return new Encipher($method, $key, $iv); 156 | } else { 157 | return new Decipher($method, $key, $iv); 158 | } 159 | } 160 | } 161 | } 162 | 163 | public function encrypt($buffer) 164 | { 165 | if ($this->_method) { 166 | $result = $this->_cipher->update($buffer); 167 | if ($this->_ivSent) { 168 | return $result; 169 | } else { 170 | $this->_ivSent = true; 171 | return $this->_cipherIv . $result; 172 | } 173 | } else { 174 | return self::substitute(self::$_encryptTable, $buffer); 175 | } 176 | } 177 | 178 | public function decrypt($buffer) 179 | { 180 | if ($this->_method) { 181 | if (! $this->_decipher) { 182 | $decipher_iv_len = $this->getCipherLen($this->_method); 183 | $decipher_iv_len = $decipher_iv_len[1]; 184 | $decipher_iv = substr($buffer, 0, $decipher_iv_len); 185 | $this->_decipher = $this->getCipher($this->_key, $this->_method, 0, $decipher_iv); 186 | $result = $this->_decipher->update(substr($buffer, $decipher_iv_len)); 187 | return $result; 188 | } else { 189 | $result = $this->_decipher->update($buffer); 190 | return $result; 191 | } 192 | } else { 193 | return self::substitute(self::$_decryptTable, $buffer); 194 | } 195 | } 196 | 197 | protected function createRc4Md5Cipher($key, $iv, $op) 198 | { 199 | $rc4_key = md5($key . $iv); 200 | if ($op === 1) { 201 | return new Encipher('rc4', $rc4_key, ''); 202 | } else { 203 | return new Decipher('rc4', $rc4_key, ''); 204 | } 205 | } 206 | 207 | protected function EVPBytesToKey($password, $key_len, $iv_len) 208 | { 209 | $cache_key = "$password:$key_len:$iv_len"; 210 | if (isset($this->_bytesToKeyResults[$cache_key])) { 211 | return $this->_bytesToKeyResults[$cache_key]; 212 | } 213 | $m = array(); 214 | $i = 0; 215 | $count = 0; 216 | while ($count < $key_len + $iv_len) { 217 | $data = $password; 218 | if ($i > 0) { 219 | $data = $m[$i - 1] . $password; 220 | } 221 | $d = md5($data, true); 222 | $m[] = $d; 223 | $count += strlen($d); 224 | $i += 1; 225 | } 226 | $ms = ''; 227 | foreach ($m as $buf) { 228 | $ms .= $buf; 229 | } 230 | $key = substr($ms, 0, $key_len); 231 | $iv = substr($ms, $key_len, $key_len + $iv_len); 232 | $this->bytesToKeyResults[$password] = array($key,$iv); 233 | return array($key,$iv); 234 | } 235 | 236 | protected function getCipherLen($method) 237 | { 238 | $method = strtolower($method); 239 | return isset(self::$_methodSupported[$method]) ? self::$_methodSupported[$method] : null; 240 | } 241 | } 242 | 243 | class Encipher 244 | { 245 | 246 | protected $_algorithm; 247 | 248 | protected $_key; 249 | 250 | protected $_iv; 251 | 252 | protected $_tail; 253 | 254 | protected $_ivLength; 255 | 256 | public function __construct($algorithm, $key, $iv) 257 | { 258 | $this->_algorithm = $algorithm; 259 | $this->_key = $key; 260 | $this->_iv = $iv; 261 | $this->_ivLength = openssl_cipher_iv_length($algorithm); 262 | } 263 | 264 | public function update($data) 265 | { 266 | if (strlen($data) == 0) 267 | return ''; 268 | $tl = strlen($this->_tail); 269 | if ($tl) 270 | $data = $this->_tail . $data; 271 | $b = openssl_encrypt($data, $this->_algorithm, $this->_key, OPENSSL_RAW_DATA, $this->_iv); 272 | $result = substr($b, $tl); 273 | $dataLength = strlen($data); 274 | $mod = $dataLength % $this->_ivLength; 275 | if ($dataLength >= $this->_ivLength) { 276 | $iPos = - ($mod + $this->_ivLength); 277 | $this->_iv = substr($b, $iPos, $this->_ivLength); 278 | } 279 | $this->_tail = $mod != 0 ? substr($data, - $mod) : ''; 280 | return $result; 281 | } 282 | } 283 | 284 | class Decipher extends Encipher 285 | { 286 | 287 | public function update($data) 288 | { 289 | if (strlen($data) == 0) 290 | return ''; 291 | $tl = strlen($this->_tail); 292 | if ($tl) 293 | $data = $this->_tail . $data; 294 | $b = openssl_decrypt($data, $this->_algorithm, $this->_key, OPENSSL_RAW_DATA, $this->_iv); 295 | $result = substr($b, $tl); 296 | $dataLength = strlen($data); 297 | $mod = $dataLength % $this->_ivLength; 298 | if ($dataLength >= $this->_ivLength) { 299 | $iPos = - ($mod + $this->_ivLength); 300 | $this->_iv = substr($data, $iPos, $this->_ivLength); 301 | } 302 | $this->_tail = $mod != 0 ? substr($data, - $mod) : ''; 303 | return $result; 304 | } 305 | } 306 | 307 | function merge_sort($array, $comparison) 308 | { 309 | if (count($array) < 2) { 310 | return $array; 311 | } 312 | $middle = ceil(count($array) / 2); 313 | return merge(merge_sort(slice($array, 0, $middle), $comparison), merge_sort(slice($array, $middle), $comparison), $comparison); 314 | } 315 | 316 | function slice($table, $start, $end = null) 317 | { 318 | $table = array_values($table); 319 | if ($end) { 320 | return array_slice($table, $start, $end); 321 | } else { 322 | return array_slice($table, $start); 323 | } 324 | } 325 | 326 | function merge($left, $right, $comparison) 327 | { 328 | $result = array(); 329 | while ((count($left) > 0) && (count($right) > 0)) { 330 | if (call_user_func($comparison, $left[0], $right[0]) <= 0) { 331 | $result[] = array_shift($left); 332 | } else { 333 | $result[] = array_shift($right); 334 | } 335 | } 336 | while (count($left) > 0) { 337 | $result[] = array_shift($left); 338 | } 339 | while (count($right) > 0) { 340 | $result[] = array_shift($right); 341 | } 342 | return $result; 343 | } 344 | 345 | -------------------------------------------------------------------------------- /lib/ShadowSocks.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected $clients = array(); 28 | 29 | /** 30 | * @var swoole_process 31 | */ 32 | protected $process = array(); 33 | 34 | /** 35 | * @var Encryptor 36 | */ 37 | protected $cryptor = null; 38 | 39 | /** 40 | * @var Encryptor 41 | */ 42 | protected $querytimes = 0; 43 | 44 | public static function getInstance(array $conf) 45 | { 46 | static $instance = null; 47 | if (! isset($instance)) { 48 | self::$conf = $conf; 49 | $instance = new self(); 50 | } 51 | return $instance; 52 | } 53 | 54 | public function __construct() 55 | { 56 | $ip = self::$conf['ip'] = empty(self::$conf['ip']) ? '0.0.0.0' : self::$conf['ip']; 57 | $this->serv = new swoole_server($ip, self::$conf['port'], SWOOLE_PROCESS, SWOOLE_TCP); 58 | $this->serv->on('connect', [$this,'onConnect']); 59 | $this->serv->on('receive', [$this,'onReceive']); 60 | $this->serv->on('close', [$this,'onClose']); 61 | } 62 | 63 | public function onConnect($serv, $fd) 64 | { 65 | Trace::debug("ShadowSocks :: onConnect $fd."); 66 | Client::getInstance($fd, $this->serv, new Encryptor(self::$conf['passwd'], self::$conf['method'])); 67 | } 68 | 69 | public function onReceive($serv, $fd, $from_id, $rdata) 70 | { 71 | $client = Client::getInstance($fd); 72 | $data = $client->cryptor->decrypt($rdata); 73 | 74 | Trace::debug( 75 | "\n\n\n\n" . str_repeat('#', 20) . "\nquerytimes:" . ++ $this->querytimes . "\n" . str_repeat('#', 20) . 76 | "\n=======================\nonReceive {$from_id} : {$fd} lenght:" . strlen($data) . " content:\n=======================\n" . 77 | substr($data, 0, 50) . "...\n======================="); 78 | if (false === $client->hasInit()) { 79 | $header = Sock5::parseHeader($data); 80 | if (! $header) { 81 | return $serv->close($fd); 82 | } 83 | $client->init($header['addr']); 84 | if (strlen($data) > $header['length']) { 85 | $data = substr($data, $header['length']); 86 | $client->send($data); 87 | } 88 | swoole_async_dns_lookup($header['addr'], 89 | function ($host, $ip) use($header, $client, $fd) 90 | { 91 | Trace::debug("dnslookup >$fd, $host, $ip "); 92 | $client->connect(ip2long($ip), $header['port']); 93 | }); 94 | } else { 95 | $client->send($data); 96 | } 97 | } 98 | 99 | public function onClose($serv, $fd) 100 | { 101 | Client::remove($fd); 102 | Trace::debug("Client: $fd Close."); 103 | } 104 | 105 | public function start($setting = array()) 106 | { 107 | Trace::info(str_repeat('-', 100)); 108 | Trace::info(str_pad('[ ShadowSocks Swoole PHP ]', 100, ' ', STR_PAD_BOTH)); 109 | Trace::info(str_pad("IP:" . self::$conf['ip'] . " PORT:" . self::$conf['port'], 100, ' ', STR_PAD_BOTH)); 110 | Trace::info(str_pad("PassWord : " . self::$conf['passwd'], 100, ' ', STR_PAD_BOTH)); 111 | Trace::info(str_pad("Encode :" . self::$conf['method'], 100, ' ', STR_PAD_BOTH)); 112 | Trace::info(str_repeat('-', 100)); 113 | $default = ['timeout' => 1,'poll_thread_num' => 1,'worker_num' => 4,'backlog' => 128,'dispatch_mode' => 2]; 114 | $setting = array_merge($default, $setting); 115 | $this->serv->set($setting); 116 | $this->serv->start(); 117 | } 118 | } -------------------------------------------------------------------------------- /lib/Sock5.php: -------------------------------------------------------------------------------- 1 | $addr_type,'addr' => $dest_addr,'port' => $dest_port,'length' => $header_length); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/Trace.php: -------------------------------------------------------------------------------- 1 | start(['daemonize' => DAEMON,'worker_num' => 4]); --------------------------------------------------------------------------------