├── README.md ├── composer.json ├── config.php └── start.php /README.md: -------------------------------------------------------------------------------- 1 | # socks5-proxy 2 | Socks5 proxy written in PHP based on [workerman](https://github.com/walkor/Workerman). Now with username/password authentication according to RFC 1929. 3 | 4 | ## Install 5 | 1. ```git clone https://github.com/walkor/php-socks5 && cd php-socks5``` 6 | 7 | 2. ```composer install``` 8 | 9 | ## Config 10 | Edit file ```config.php``` 11 | 12 | ## Start 13 | ```php start.php start -d``` 14 | 15 | ## Stop 16 | ```php start.php stop``` 17 | 18 | ## Status 19 | ```php start.php status``` 20 | 21 | ## Other links 22 | https://github.com/walkor/shadowsocks-php 23 | https://github.com/walkor/php-http-proxy 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "workerman/phptty", 3 | "type" : "project", 4 | "keywords": ["web","tty"], 5 | "homepage": "http://www.workerman.net", 6 | "license" : "MIT", 7 | "require": { 8 | "workerman/workerman" : ">=3.3.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | [ 5 | METHOD_NO_AUTH => true, 6 | METHOD_USER_PASS => function ($request) { 7 | return $request['user'] == 'user' && $request['pass'] == 'pass'; 8 | } 9 | ], 10 | "log_level" => LOG_DEBUG, 11 | "tcp_port" => 1080, 12 | "udp_port" => 0, 13 | "wanIP" => '192.168.1.1', 14 | ]; 15 | -------------------------------------------------------------------------------- /start.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 | ini_set("memory_limit", "512M"); 16 | 17 | use \Workerman\Worker; 18 | use \Workerman\Timer; 19 | use \Workerman\Connection\AsyncTcpConnection; 20 | use \Workerman\Connection\AsyncUdpConnection; 21 | 22 | // 自动加载类 23 | require_once __DIR__ . '/vendor/autoload.php'; 24 | 25 | define('STAGE_INIT', 0); 26 | define('STAGE_AUTH', 1); 27 | define('STAGE_ADDR', 2); 28 | define('STAGE_UDP_ASSOC', 3); 29 | define('STAGE_DNS', 4); 30 | define('STAGE_CONNECTING', 5); 31 | define('STAGE_STREAM', 6); 32 | define('STAGE_DESTROYED', -1); 33 | 34 | define('CMD_CONNECT', 1); 35 | define('CMD_BIND', 2); 36 | define('CMD_UDP_ASSOCIATE', 3); 37 | 38 | define('ERR_GENERAL', 1); 39 | define('ERR_NOT_ALLOW', 2); 40 | define('ERR_NETWORK', 3); 41 | define('ERR_HOST', 4); 42 | define('ERR_REFUSE', 5); 43 | define('ERR_TTL_EXPIRED', 6); 44 | define('ERR_UNKNOW_COMMAND', 7); 45 | define('ERR_UNKNOW_ADDR_TYPE', 8); 46 | define('ERR_UNKNOW', 9); 47 | 48 | define('ADDRTYPE_IPV4', 1); 49 | define('ADDRTYPE_IPV6', 4); 50 | define('ADDRTYPE_HOST', 3); 51 | 52 | define('METHOD_NO_AUTH', 0); 53 | define('METHOD_GSSAPI', 1); 54 | define('METHOD_USER_PASS', 2); 55 | 56 | require_once __DIR__ . '/config.php'; 57 | 58 | if (count($config['auth']) == 0) { 59 | $config['auth'] = [METHOD_NO_AUTH => true]; 60 | } 61 | 62 | $worker = new Worker('tcp://0.0.0.0:' . $config['tcp_port']); 63 | $worker->onConnect = function ($connection) { 64 | $connection->stage = STAGE_INIT; 65 | $connection->auth_type = NULL; 66 | }; 67 | $worker->onMessage = function ($connection, $buffer) { 68 | global $config; 69 | logger(LOG_DEBUG, "recv:" . bin2hex($buffer)); 70 | switch ($connection->stage) { 71 | // 初始化环节 72 | case STAGE_INIT: 73 | $request = []; 74 | // 当前偏移量 75 | $offset = 0; 76 | 77 | // 检测buffer长度 78 | if (strlen($buffer) < 2) { 79 | logger(LOG_ERR, "init socks5 failed. buffer too short."); 80 | $connection->send("\x05\xff"); 81 | $connection->stage = STAGE_DESTROYED; 82 | $connection->close(); 83 | return; 84 | } 85 | 86 | // Socks5 版本 87 | $request['ver'] = ord($buffer[$offset]); 88 | $offset += 1; 89 | 90 | // 认证方法数量 91 | $request['method_count'] = ord($buffer[$offset]); 92 | $offset += 1; 93 | 94 | if (strlen($buffer) < 2 + $request['method_count']) { 95 | logger(LOG_ERR, "init authentic failed. buffer too short."); 96 | $connection->send("\x05\xff"); 97 | $connection->stage = STAGE_DESTROYED; 98 | $connection->close(); 99 | return; 100 | } 101 | 102 | // 客户端支持的认证方法 103 | $request['methods'] = []; 104 | for ($i = 1; $i <= $request['method_count']; $i++) { 105 | $request['methods'][] = ord($buffer[$offset]); 106 | $offset++; 107 | } 108 | 109 | foreach ($config['auth'] as $k => $v) { 110 | if (in_array($k, $request['methods'])) { 111 | 112 | logger(LOG_INFO, "auth client via method $k"); 113 | logger(LOG_DEBUG, "send:" . bin2hex("\x05" . chr($k))); 114 | 115 | $connection->send("\x05" . chr($k)); 116 | if ($k == 0) { 117 | $connection->stage = STAGE_ADDR; 118 | } else { 119 | $connection->stage = STAGE_AUTH; 120 | } 121 | $connection->auth_type = $k; 122 | return; 123 | } 124 | } 125 | if ($connection->stage != STAGE_AUTH) { 126 | logger(LOG_ERR, "client has no matched auth methods"); 127 | logger(LOG_DEBUG, "send:" . bin2hex("\x05\xff")); 128 | $connection->send("\x05\xff"); 129 | $connection->stage = STAGE_DESTROYED; 130 | $connection->close(); 131 | } 132 | return; 133 | // 认证环节 134 | case STAGE_AUTH: 135 | 136 | $request = []; 137 | // 当前偏移量 138 | $offset = 0; 139 | 140 | if (strlen($buffer) < 5) { 141 | logger(LOG_ERR, "auth failed. buffer too short."); 142 | $connection->send("\x01\x01"); 143 | $connection->stage = STAGE_DESTROYED; 144 | $connection->close(); 145 | return; 146 | } 147 | 148 | // var_dump($connection->auth_type); 149 | switch ($connection->auth_type) { 150 | case METHOD_USER_PASS: 151 | // 子协议 协商 版本 152 | $request['sub_ver'] = ord($buffer[$offset]); 153 | $offset += 1; 154 | 155 | // 用户名 156 | $request['user_len'] = ord($buffer[$offset]); 157 | $offset += 1; 158 | 159 | if (strlen($buffer) < 2 + $request['user_len'] + 2) { 160 | logger(LOG_ERR, "auth username failed. buffer too short."); 161 | $connection->send("\x01\x01"); 162 | $connection->stage = STAGE_DESTROYED; 163 | $connection->close(); 164 | return; 165 | } 166 | 167 | $request['user'] = substr($buffer, $offset, $request['user_len']); 168 | $offset += $request['user_len']; 169 | 170 | // 密码 171 | $request['pass_len'] = ord($buffer[$offset]); 172 | $offset += 1; 173 | 174 | 175 | if (strlen($buffer) < 2 + $request['user_len'] + 1 + $request['pass_len']) { 176 | logger(LOG_ERR, "auth password failed. buffer too short."); 177 | $connection->send("\x01\x01"); 178 | $connection->stage = STAGE_DESTROYED; 179 | $connection->close(); 180 | return; 181 | } 182 | 183 | $request['pass'] = substr($buffer, $offset, $request['pass_len']); 184 | $offset += $request['pass_len']; 185 | 186 | if ($config["auth"][METHOD_USER_PASS]($request)) { 187 | logger(LOG_INFO, "auth ok"); 188 | $connection->send("\x01\x00"); 189 | $connection->stage = STAGE_ADDR; 190 | } else { 191 | logger(LOG_INFO, "auth failed"); 192 | $connection->send("\x01\x01"); 193 | $connection->stage = STAGE_DESTROYED; 194 | $connection->close(); 195 | } 196 | break; 197 | default: 198 | logger(LOG_ERR, "unsupport auth type"); 199 | $connection->send("\x01\x01"); 200 | $connection->stage = STAGE_DESTROYED; 201 | $connection->close(); 202 | break; 203 | } 204 | return; 205 | case STAGE_ADDR: 206 | $request = []; 207 | // 当前偏移量 208 | $offset = 0; 209 | 210 | if (strlen($buffer) < 4) { 211 | logger(LOG_ERR, "connect init failed. buffer too short."); 212 | $connection->stage = STAGE_DESTROYED; 213 | 214 | $response = []; 215 | $response['ver'] = 5; 216 | $response['rep'] = ERR_GENERAL; 217 | $response['rsv'] = 0; 218 | $response['addr_type'] = ADDRTYPE_IPV4; 219 | $response['bind_addr'] = '0.0.0.0'; 220 | $response['bind_port'] = 0; 221 | 222 | $connection->close(packResponse($response)); 223 | return; 224 | } 225 | 226 | // Socks 版本 227 | $request['ver'] = ord($buffer[$offset]); 228 | $offset += 1; 229 | 230 | // 命令 231 | $request['command'] = ord($buffer[$offset]); 232 | $offset += 1; 233 | 234 | // RSV 235 | $request['rsv'] = ord($buffer[$offset]); 236 | $offset += 1; 237 | 238 | // AddressType 239 | $request['addr_type'] = ord($buffer[$offset]); 240 | $offset += 1; 241 | 242 | // DestAddr 243 | switch ($request['addr_type']) { 244 | case ADDRTYPE_IPV4: 245 | 246 | if (strlen($buffer) < 4 + 4) { 247 | logger(LOG_ERR, "connect init failed.[ADDRTYPE_IPV4] buffer too short."); 248 | $connection->stage = STAGE_DESTROYED; 249 | 250 | $response = []; 251 | $response['ver'] = 5; 252 | $response['rep'] = ERR_GENERAL; 253 | $response['rsv'] = 0; 254 | $response['addr_type'] = ADDRTYPE_IPV4; 255 | $response['bind_addr'] = '0.0.0.0'; 256 | $response['bind_port'] = 0; 257 | 258 | $connection->close(packResponse($response)); 259 | return; 260 | } 261 | 262 | $tmp = substr($buffer, $offset, 4); 263 | $ip = 0; 264 | for ($i = 0; $i < 4; $i++) { 265 | // var_dump(ord($tmp[$i])); 266 | $ip += ord($tmp[$i]) * pow(256, 3 - $i); 267 | } 268 | $request['dest_addr'] = long2ip($ip); 269 | $offset += 4; 270 | break; 271 | 272 | case ADDRTYPE_HOST: 273 | $request['host_len'] = ord($buffer[$offset]); 274 | $offset += 1; 275 | 276 | if (strlen($buffer) < 4 + 1 + $request['host_len']) { 277 | logger(LOG_ERR, "connect init failed.[ADDRTYPE_HOST] buffer too short."); 278 | $connection->stage = STAGE_DESTROYED; 279 | 280 | $response = []; 281 | $response['ver'] = 5; 282 | $response['rep'] = ERR_GENERAL; 283 | $response['rsv'] = 0; 284 | $response['addr_type'] = ADDRTYPE_IPV4; 285 | $response['bind_addr'] = '0.0.0.0'; 286 | $response['bind_port'] = 0; 287 | 288 | $connection->close(packResponse($response)); 289 | return; 290 | } 291 | 292 | $request['dest_addr'] = substr($buffer, $offset, $request['host_len']); 293 | $offset += $request['host_len']; 294 | break; 295 | 296 | case ADDRTYPE_IPV6: 297 | default: 298 | logger(LOG_ERR, "unsupport ipv6. [ADDRTYPE_IPV6]."); 299 | $connection->stage = STAGE_DESTROYED; 300 | 301 | $response = []; 302 | $response['ver'] = 5; 303 | $response['rep'] = ERR_UNKNOW_ADDR_TYPE; 304 | $response['rsv'] = 0; 305 | $response['addr_type'] = ADDRTYPE_IPV4; 306 | $response['bind_addr'] = '0.0.0.0'; 307 | $response['bind_port'] = 0; 308 | 309 | $connection->close(packResponse($response)); 310 | return; 311 | break; 312 | } 313 | 314 | // DestPort 315 | 316 | if (strlen($buffer) < $offset + 2) { 317 | logger(LOG_ERR, "connect init failed.[port] buffer too short."); 318 | $connection->stage = STAGE_DESTROYED; 319 | 320 | $response = []; 321 | $response['ver'] = 5; 322 | $response['rep'] = ERR_GENERAL; 323 | $response['rsv'] = 0; 324 | $response['addr_type'] = ADDRTYPE_IPV4; 325 | $response['bind_addr'] = '0.0.0.0'; 326 | $response['bind_port'] = 0; 327 | 328 | $connection->close(packResponse($response)); 329 | return; 330 | } 331 | $portData = unpack("n", substr($buffer, $offset, 2)); 332 | $request['dest_port'] = $portData[1]; 333 | $offset += 2; 334 | 335 | // var_dump($request); 336 | switch ($request['command']) { 337 | case CMD_CONNECT: 338 | logger(LOG_DEBUG, 'tcp://' . $request['dest_addr'] . ':' . $request['dest_port']); 339 | if ($request['addr_type'] == ADDRTYPE_HOST) { 340 | if (!filter_var($request['dest_addr'], FILTER_VALIDATE_IP)) { 341 | logger(LOG_DEBUG, 'resolve DNS ' . $request['dest_addr']); 342 | $connection->stage = STAGE_DNS; 343 | $addr = dns_get_record($request['dest_addr'], DNS_A); 344 | $addr = $addr ? array_pop($addr) : null; 345 | logger(LOG_DEBUG, 'DNS resolved ' . $request['dest_addr'] . ' => ' . $addr['ip']); 346 | } else { 347 | $addr['ip'] = $request['dest_addr']; 348 | } 349 | } else { 350 | $addr['ip'] = $request['dest_addr']; 351 | } 352 | if ($addr) { 353 | $connection->stage = STAGE_CONNECTING; 354 | $remote_connection = new AsyncTcpConnection('tcp://' . $addr['ip'] . ':' . $request['dest_port']); 355 | $remote_connection->onConnect = function ($remote_connection) use ($connection, $request) { 356 | $connection->state = STAGE_STREAM; 357 | $response = []; 358 | $response['ver'] = 5; 359 | $response['rep'] = 0; 360 | $response['rsv'] = 0; 361 | $response['addr_type'] = $request['addr_type']; 362 | $response['bind_addr'] = '0.0.0.0'; 363 | $response['bind_port'] = 18512; 364 | 365 | $connection->send(packResponse($response)); 366 | $connection->pipe($remote_connection); 367 | $remote_connection->pipe($connection); 368 | logger(LOG_DEBUG, 'tcp://' . $request['dest_addr'] . ':' . $request['dest_port'] . ' [OK]'); 369 | }; 370 | $remote_connection->connect(); 371 | } else { 372 | logger(LOG_DEBUG, 'DNS resolve failed.'); 373 | $connection->stage = STAGE_DESTROYED; 374 | 375 | $response = []; 376 | $response['ver'] = 5; 377 | $response['rep'] = ERR_HOST; 378 | $response['rsv'] = 0; 379 | $response['addr_type'] = ADDRTYPE_IPV4; 380 | $response['bind_addr'] = '0.0.0.0'; 381 | $response['bind_port'] = 0; 382 | 383 | $connection->close(packResponse($response)); 384 | } 385 | break; 386 | case CMD_UDP_ASSOCIATE: 387 | $connection->stage = STAGE_UDP_ASSOC; 388 | var_dump("CMD_UDP_ASSOCIATE " . $config['udp_port']); 389 | if ($config['udp_port'] == 0) { 390 | $connection->udpWorker = new Worker('udp://0.0.0.0:0'); 391 | $connection->udpWorker->incId = 0; 392 | $connection->udpWorker->onMessage = function ($udp_connection, $data) use ($connection) { 393 | udpWorkerOnMessage($udp_connection, $data, $connection->udpWorker); 394 | }; 395 | $connection->udpWorker->listen(); 396 | $listenInfo = stream_socket_get_name($connection->udpWorker->getMainSocket(), false); 397 | list($bind_addr, $bind_port) = explode(":", $listenInfo); 398 | } else { 399 | $bind_port = $config['udp_port']; 400 | } 401 | $bind_addr = $config['wanIP']; 402 | 403 | $response['ver'] = 5; 404 | $response['rep'] = 0; 405 | $response['rsv'] = 0; 406 | $response['addr_type'] = ADDRTYPE_IPV4; 407 | $response['bind_addr'] = $bind_addr; 408 | $response['bind_port'] = $bind_port; 409 | 410 | logger(LOG_DEBUG, 'send:' . bin2hex(packResponse($response))); 411 | $connection->send(packResponse($response)); 412 | break; 413 | default: 414 | logger(LOG_ERR, "connect init failed. unknow command."); 415 | $connection->stage = STAGE_DESTROYED; 416 | 417 | $response = []; 418 | $response['ver'] = 5; 419 | $response['rep'] = ERR_UNKNOW_COMMAND; 420 | $response['rsv'] = 0; 421 | $response['addr_type'] = ADDRTYPE_IPV4; 422 | $response['bind_addr'] = '0.0.0.0'; 423 | $response['bind_port'] = 0; 424 | 425 | $connection->close(packResponse($response)); 426 | return; 427 | break; 428 | } 429 | } 430 | }; 431 | $worker->onClose = function ($connection) { 432 | logger(LOG_INFO, "client closed."); 433 | }; 434 | 435 | function udpWorkerOnMessage($udp_connection, $data, &$worker) 436 | { 437 | 438 | logger(LOG_DEBUG, 'send:' . bin2hex($data)); 439 | $request = []; 440 | $offset = 0; 441 | 442 | $request['rsv'] = substr($data, $offset, 2); 443 | $offset += 2; 444 | 445 | $request['frag'] = ord($data[$offset]); 446 | $offset += 1; 447 | 448 | $request['addr_type'] = ord($data[$offset]); 449 | $offset += 1; 450 | 451 | switch ($request['addr_type']) { 452 | case ADDRTYPE_IPV4: 453 | $tmp = substr($data, $offset, 4); 454 | $ip = 0; 455 | for ($i = 0; $i < 4; $i++) { 456 | $ip += ord($tmp[$i]) * pow(256, 3 - $i); 457 | } 458 | $request['dest_addr'] = long2ip($ip); 459 | $offset += 4; 460 | break; 461 | 462 | case ADDRTYPE_HOST: 463 | $request['host_len'] = ord($data[$offset]); 464 | $offset += 1; 465 | 466 | $request['dest_addr'] = substr($data, $offset, $request['host_len']); 467 | $offset += $request['host_len']; 468 | break; 469 | 470 | case ADDRTYPE_IPV6: 471 | if (strlen($data) < 22) { 472 | echo "buffer too short\n"; 473 | $error = true; 474 | break; 475 | } 476 | echo "todo ipv6\n"; 477 | $error = true; 478 | default: 479 | echo "unsupported addrtype {$request['addr_type']}\n"; 480 | $error = true; 481 | } 482 | 483 | $portData = unpack("n", substr($data, $offset, 2)); 484 | $request['dest_port'] = $portData[1]; 485 | $offset += 2; 486 | // var_dump($request['dest_addr']); 487 | if ($request['addr_type'] == ADDRTYPE_HOST) { 488 | logger(LOG_DEBUG, '解析DNS'); 489 | $addr = dns_get_record($request['dest_addr'], DNS_A); 490 | $addr = $addr ? array_pop($addr) : null; 491 | logger(LOG_DEBUG, 'DNS 解析完成' . $addr['ip']); 492 | } else { 493 | $addr['ip'] = $request['dest_addr']; 494 | } 495 | // var_dump($request); 496 | 497 | // var_dump($udp_connection); 498 | 499 | $remote_connection = new AsyncUdpConnection('udp://' . $addr['ip'] . ':' . $request['dest_port']); 500 | $remote_connection->id = $worker->incId++; 501 | $remote_connection->udp_connection = $udp_connection; 502 | $remote_connection->onConnect = function ($remote_connection) use ($data, $offset) { 503 | $remote_connection->send(substr($data, $offset)); 504 | }; 505 | $remote_connection->onMessage = function ($remote_connection, $recv) use ($data, $offset, $udp_connection, $worker) { 506 | $udp_connection->close(substr($data, 0, $offset) . $recv); 507 | $remote_connection->close(); 508 | unset($worker->udpConnections[$remote_connection->id]); 509 | }; 510 | $remote_connection->deadTime = time() + 3; 511 | $remote_connection->connect(); 512 | $worker->udpConnections[$remote_connection->id] = $remote_connection; 513 | } 514 | 515 | $udpWorker = new Worker('udp://0.0.0.0:1080'); 516 | $udpWorker->incId = 0; 517 | $udpWorker->onWorkerStart = function ($worker) { 518 | $worker->udpConnections = []; 519 | Timer::add(1, function () use ($worker) { 520 | foreach ($worker->udpConnections as $id => $remote_connection) { 521 | if ($remote_connection->deadTime < time()) { 522 | $remote_connection->close(); 523 | $remote_connection->udp_connection->close(); 524 | unset($worker->udpConnections[$id]); 525 | } 526 | } 527 | }); 528 | }; 529 | $udpWorker->onMessage = 'udpWorkerOnMessage'; 530 | 531 | function packResponse($response) 532 | { 533 | $data = ""; 534 | $data .= chr($response['ver']); 535 | $data .= chr($response['rep']); 536 | $data .= chr($response['rsv']); 537 | $data .= chr($response['addr_type']); 538 | 539 | switch ($response['addr_type']) { 540 | case ADDRTYPE_IPV4: 541 | $tmp = explode('.', $response['bind_addr']); 542 | foreach ($tmp as $block) { 543 | $data .= chr($block); 544 | } 545 | break; 546 | case ADDRTYPE_HOST: 547 | $host_len = strlen($response['bind_addr']); 548 | $data .= chr($host_len); 549 | $data .= $response['bind_addr']; 550 | break; 551 | } 552 | 553 | $data .= pack("n", $response['bind_port']); 554 | return $data; 555 | } 556 | 557 | function logger($level, $str) 558 | { 559 | global $config; 560 | if ($config['log_level'] >= $level) { 561 | echo $str . "\n"; 562 | } 563 | } 564 | // 如果不是在根目录启动,则运行runAll方法 565 | if (!defined('GLOBAL_START')) { 566 | Worker::runAll(); 567 | } 568 | --------------------------------------------------------------------------------