diff --git a/config.php b/config.php index 8f9188b..d4f1de8 100644 --- a/config.php +++ b/config.php @@ -1,5 +1,14 @@ [ + METHOD_NO_AUTH => true, + METHOD_USER_PASS => function ($request) { + return $request['user'] == 'user' && $request['pass'] == 'pass'; + } + ], + "log_level" => LOG_DEBUG, + "tcp_port" => 1080, + "udp_port" => 0, + "wanIP" => '192.168.1.1', +]; diff --git a/start.php b/start.php index 9e53d35..c307d14 100644 --- a/start.php +++ b/start.php @@ -1,4 +1,5 @@ onConnect = function($connection) -{ +require_once __DIR__ . '/config.php'; + +if (count($config['auth']) == 0) { + $config['auth'] = [METHOD_NO_AUTH => true]; +} + +$worker = new Worker('tcp://0.0.0.0:' . $config['tcp_port']); +$worker->onConnect = function ($connection) { $connection->stage = STAGE_INIT; + $connection->auth_type = NULL; }; -$worker->onMessage = function($connection, $buffer) -{ - global $AUTH_ENABLED, $USERNAME, $PASSWORD; - switch($connection->stage) - { +$worker->onMessage = function ($connection, $buffer) { + global $config; + logger(LOG_DEBUG, "recv:" . bin2hex($buffer)); + switch ($connection->stage) { + // 初始化环节 case STAGE_INIT: - if ($AUTH_ENABLED) - { - $methodslen = ord($buffer[1]); - $methods = array(); - for ($i = 0; $i < strlen($buffer)-3; $i++) - { - array_push($methods, ord($buffer[$i+3])); - } - if (in_array(METHOD_USER_PASS, $methods)) - { - $connection->send("\x05\x02"); - $connection->stage = STAGE_AUTH; - return; - } - echo "client does not support user/pass auth\n"; + $request = []; + // 当前偏移量 + $offset = 0; + + // 检测buffer长度 + if (strlen($buffer) < 2) { + logger(LOG_ERR, "init socks5 failed. buffer too short."); $connection->send("\x05\xff"); $connection->stage = STAGE_DESTROYED; $connection->close(); return; } - $connection->send("\x05\x00"); - $connection->stage = STAGE_ADDR; - return; - case STAGE_AUTH: - $userlen = ord($buffer[1]); - $user = substr($buffer, 2, $userlen); - $passlen = ord($buffer[2 + $userlen]); - $pass = substr($buffer, 3 + $userlen, $passlen); - if ($user == $USERNAME && $pass == $PASSWORD) - { - $connection->send("\x05\x00"); - $connection->stage = STAGE_ADDR; + + // Socks5 版本 + $request['ver'] = ord($buffer[$offset]); + $offset += 1; + + // 认证方法数量 + $request['method_count'] = ord($buffer[$offset]); + $offset += 1; + + if (strlen($buffer) < 2 + $request['method_count']) { + logger(LOG_ERR, "init authentic failed. buffer too short."); + $connection->send("\x05\xff"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); return; } - echo "auth failed\n"; - $connection->send("\x05\x01"); - $connection->stage = STAGE_DESTROYED; - $connection->close(); + + // 客户端支持的认证方法 + $request['methods'] = []; + for ($i = 1; $i <= $request['method_count']; $i++) { + $request['methods'][] = ord($buffer[$offset]); + $offset++; + } + + foreach ($config['auth'] as $k => $v) { + if (in_array($k, $request['methods'])) { + + logger(LOG_INFO, "auth client via method $k"); + logger(LOG_DEBUG, "send:" . bin2hex("\x05" . chr($k))); + + $connection->send("\x05" . chr($k)); + if ($k == 0) { + $connection->stage = STAGE_ADDR; + } else { + $connection->stage = STAGE_AUTH; + } + $connection->auth_type = $k; + return; + } + } + if ($connection->stage != STAGE_AUTH) { + logger(LOG_ERR, "client has no matched auth methods"); + logger(LOG_DEBUG, "send:" . bin2hex("\x05\xff")); + $connection->send("\x05\xff"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); + } + return; + // 认证环节 + case STAGE_AUTH: + + $request = []; + // 当前偏移量 + $offset = 0; + + if (strlen($buffer) < 5) { + logger(LOG_ERR, "auth failed. buffer too short."); + $connection->send("\x01\x01"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); + return; + } + + // var_dump($connection->auth_type); + switch ($connection->auth_type) { + case METHOD_USER_PASS: + // 子协议 协商 版本 + $request['sub_ver'] = ord($buffer[$offset]); + $offset += 1; + + // 用户名 + $request['user_len'] = ord($buffer[$offset]); + $offset += 1; + + if (strlen($buffer) < 2 + $request['user_len'] + 2) { + logger(LOG_ERR, "auth username failed. buffer too short."); + $connection->send("\x01\x01"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); + return; + } + + $request['user'] = substr($buffer, $offset, $request['user_len']); + $offset += $request['user_len']; + + // 密码 + $request['pass_len'] = ord($buffer[$offset]); + $offset += 1; + + + if (strlen($buffer) < 2 + $request['user_len'] + 1 + $request['pass_len']) { + logger(LOG_ERR, "auth password failed. buffer too short."); + $connection->send("\x01\x01"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); + return; + } + + $request['pass'] = substr($buffer, $offset, $request['pass_len']); + $offset += $request['pass_len']; + + if ($config["auth"][METHOD_USER_PASS]($request)) { + logger(LOG_INFO, "auth ok"); + $connection->send("\x01\x00"); + $connection->stage = STAGE_ADDR; + } else { + logger(LOG_INFO, "auth failed"); + $connection->send("\x01\x01"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); + } + break; + default: + logger(LOG_ERR, "unsupport auth type"); + $connection->send("\x01\x01"); + $connection->stage = STAGE_DESTROYED; + $connection->close(); + break; + } return; case STAGE_ADDR: - $cmd = ord($buffer[1]); - if($cmd != CMD_CONNECT) - { - echo "bad cmd $cmd\n"; - $connection->close(); + $request = []; + // 当前偏移量 + $offset = 0; + + if (strlen($buffer) < 4) { + logger(LOG_ERR, "connect init failed. buffer too short."); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_GENERAL; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); return; } - $header_data = parse_socket5_header($buffer); - if(!$header_data) - { - $connection->close(); + + // Socks 版本 + $request['ver'] = ord($buffer[$offset]); + $offset += 1; + + // 命令 + $request['command'] = ord($buffer[$offset]); + $offset += 1; + + // RSV + $request['rsv'] = ord($buffer[$offset]); + $offset += 1; + + // AddressType + $request['addr_type'] = ord($buffer[$offset]); + $offset += 1; + + // DestAddr + switch ($request['addr_type']) { + case ADDRTYPE_IPV4: + + if (strlen($buffer) < 4 + 4) { + logger(LOG_ERR, "connect init failed.[ADDRTYPE_IPV4] buffer too short."); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_GENERAL; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); + return; + } + + $tmp = substr($buffer, $offset, 4); + $ip = 0; + for ($i = 0; $i < 4; $i++) { + // var_dump(ord($tmp[$i])); + $ip += ord($tmp[$i]) * pow(256, 3 - $i); + } + $request['dest_addr'] = long2ip($ip); + $offset += 4; + break; + + case ADDRTYPE_HOST: + $request['host_len'] = ord($buffer[$offset]); + $offset += 1; + + if (strlen($buffer) < 4 + 1 + $request['host_len']) { + logger(LOG_ERR, "connect init failed.[ADDRTYPE_HOST] buffer too short."); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_GENERAL; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); + return; + } + + $request['dest_addr'] = substr($buffer, $offset, $request['host_len']); + $offset += $request['host_len']; + break; + + case ADDRTYPE_IPV6: + default: + logger(LOG_ERR, "unsupport ipv6. [ADDRTYPE_IPV6]."); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_UNKNOW_ADDR_TYPE; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); + return; + break; + } + + // DestPort + + if (strlen($buffer) < $offset + 2) { + logger(LOG_ERR, "connect init failed.[port] buffer too short."); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_GENERAL; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); return; } - $connection->stage = STAGE_CONNECTING; - $remote_connection = new AsyncTcpConnection('tcp://'.$header_data[1].':'.$header_data[2]); - $remote_connection->onConnect = function($remote_connection)use($connection) - { - $connection->state = STAGE_STREAM; - $connection->send("\x05\x00\x00\x01\x00\x00\x00\x00\x10\x10"); - $connection->pipe($remote_connection); - $remote_connection->pipe($connection); - }; - $remote_connection->connect(); + $portData = unpack("n", substr($buffer, $offset, 2)); + $request['dest_port'] = $portData[1]; + $offset += 2; + + // var_dump($request); + switch ($request['command']) { + case CMD_CONNECT: + logger(LOG_DEBUG, 'tcp://' . $request['dest_addr'] . ':' . $request['dest_port']); + if ($request['addr_type'] == ADDRTYPE_HOST) { + if (!filter_var($request['dest_addr'], FILTER_VALIDATE_IP)) { + logger(LOG_DEBUG, 'resolve DNS ' . $request['dest_addr']); + $connection->stage = STAGE_DNS; + $addr = dns_get_record($request['dest_addr'], DNS_A); + $addr = $addr ? array_pop($addr) : null; + logger(LOG_DEBUG, 'DNS resolved ' . $request['dest_addr'] . ' => ' . $addr['ip']); + } else { + $addr['ip'] = $request['dest_addr']; + } + } else { + $addr['ip'] = $request['dest_addr']; + } + if ($addr) { + $connection->stage = STAGE_CONNECTING; + $remote_connection = new AsyncTcpConnection('tcp://' . $addr['ip'] . ':' . $request['dest_port']); + $remote_connection->onConnect = function ($remote_connection) use ($connection, $request) { + $connection->state = STAGE_STREAM; + $response = []; + $response['ver'] = 5; + $response['rep'] = 0; + $response['rsv'] = 0; + $response['addr_type'] = $request['addr_type']; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 18512; + + $connection->send(packResponse($response)); + $connection->pipe($remote_connection); + $remote_connection->pipe($connection); + logger(LOG_DEBUG, 'tcp://' . $request['dest_addr'] . ':' . $request['dest_port'] . ' [OK]'); + }; + $remote_connection->connect(); + } else { + logger(LOG_DEBUG, 'DNS resolve failed.'); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_HOST; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); + } + break; + case CMD_UDP_ASSOCIATE: + $connection->stage = STAGE_UDP_ASSOC; + var_dump("CMD_UDP_ASSOCIATE " . $config['udp_port']); + if ($config['udp_port'] == 0) { + $connection->udpWorker = new Worker('udp://0.0.0.0:0'); + $connection->udpWorker->incId = 0; + $connection->udpWorker->onMessage = function ($udp_connection, $data) use ($connection) { + udpWorkerOnMessage($udp_connection, $data, $connection->udpWorker); + }; + $connection->udpWorker->listen(); + $listenInfo = stream_socket_get_name($connection->udpWorker->getMainSocket(), false); + list($bind_addr, $bind_port) = explode(":", $listenInfo); + } else { + $bind_port = $config['udp_port']; + } + $bind_addr = $config['wanIP']; + + $response['ver'] = 5; + $response['rep'] = 0; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = $bind_addr; + $response['bind_port'] = $bind_port; + + logger(LOG_DEBUG, 'send:' . bin2hex(packResponse($response))); + $connection->send(packResponse($response)); + break; + default: + logger(LOG_ERR, "connect init failed. unknow command."); + $connection->stage = STAGE_DESTROYED; + + $response = []; + $response['ver'] = 5; + $response['rep'] = ERR_UNKNOW_COMMAND; + $response['rsv'] = 0; + $response['addr_type'] = ADDRTYPE_IPV4; + $response['bind_addr'] = '0.0.0.0'; + $response['bind_port'] = 0; + + $connection->close(packResponse($response)); + return; + break; + } } }; +$worker->onClose = function ($connection) { + logger(LOG_INFO, "client closed."); +}; - -function parse_socket5_header($buffer) +function udpWorkerOnMessage($udp_connection, $data, &$worker) { - $addr_type = ord($buffer[3]); - switch($addr_type) - { + + logger(LOG_DEBUG, 'send:' . bin2hex($data)); + $request = []; + $offset = 0; + + $request['rsv'] = substr($data, $offset, 2); + $offset += 2; + + $request['frag'] = ord($data[$offset]); + $offset += 1; + + $request['addr_type'] = ord($data[$offset]); + $offset += 1; + + switch ($request['addr_type']) { case ADDRTYPE_IPV4: - if(strlen($buffer) < 10) - { - echo bin2hex($buffer)."\n"; - echo "buffer too short\n"; - return false; + $tmp = substr($data, $offset, 4); + $ip = 0; + for ($i = 0; $i < 4; $i++) { + $ip += ord($tmp[$i]) * pow(256, 3 - $i); } - $dest_addr = ord($buffer[4]).'.'.ord($buffer[5]).'.'.ord($buffer[6]).'.'.ord($buffer[7]); - $port_data = unpack('n', substr($buffer, -2)); - $dest_port = $port_data[1]; - $header_length = 10; + $request['dest_addr'] = long2ip($ip); + $offset += 4; break; + case ADDRTYPE_HOST: - $addrlen = ord($buffer[4]); - if(strlen($buffer) < $addrlen + 5) - { - echo $buffer."\n"; - echo bin2hex($buffer)."\n"; - echo "buffer too short\n"; - return false; - } - $dest_addr = substr($buffer, 5, $addrlen); - $port_data = unpack('n', substr($buffer, -2)); - $dest_port = $port_data[1]; - $header_length = $addrlen + 7; + $request['host_len'] = ord($data[$offset]); + $offset += 1; + + $request['dest_addr'] = substr($data, $offset, $request['host_len']); + $offset += $request['host_len']; break; - case ADDRTYPE_IPV6: - if(strlen($buffer) < 22) - { + + case ADDRTYPE_IPV6: + if (strlen($data) < 22) { echo "buffer too short\n"; - return false; + $error = true; + break; } echo "todo ipv6\n"; - return false; - default: - echo "unsupported addrtype $addr_type\n"; - return false; + $error = true; + default: + echo "unsupported addrtype {$request['addr_type']}\n"; + $error = true; } - return array($addr_type, $dest_addr, $dest_port, $header_length); + + $portData = unpack("n", substr($data, $offset, 2)); + $request['dest_port'] = $portData[1]; + $offset += 2; + // var_dump($request['dest_addr']); + if ($request['addr_type'] == ADDRTYPE_HOST) { + logger(LOG_DEBUG, '解析DNS'); + $addr = dns_get_record($request['dest_addr'], DNS_A); + $addr = $addr ? array_pop($addr) : null; + logger(LOG_DEBUG, 'DNS 解析完成' . $addr['ip']); + } else { + $addr['ip'] = $request['dest_addr']; + } + // var_dump($request); + + // var_dump($udp_connection); + + $remote_connection = new AsyncUdpConnection('udp://' . $addr['ip'] . ':' . $request['dest_port']); + $remote_connection->id = $worker->incId++; + $remote_connection->udp_connection = $udp_connection; + $remote_connection->onConnect = function ($remote_connection) use ($data, $offset) { + $remote_connection->send(substr($data, $offset)); + }; + $remote_connection->onMessage = function ($remote_connection, $recv) use ($data, $offset, $udp_connection, $worker) { + $udp_connection->close(substr($data, 0, $offset) . $recv); + $remote_connection->close(); + unset($worker->udpConnections[$remote_connection->id]); + }; + $remote_connection->deadTime = time() + 3; + $remote_connection->connect(); + $worker->udpConnections[$remote_connection->id] = $remote_connection; } -// 如果不是在根目录启动,则运行runAll方法 -if(!defined('GLOBAL_START')) +$udpWorker = new Worker('udp://0.0.0.0:1080'); +$udpWorker->incId = 0; +$udpWorker->onWorkerStart = function ($worker) { + $worker->udpConnections = []; + Timer::add(1, function () use ($worker) { + foreach ($worker->udpConnections as $id => $remote_connection) { + if ($remote_connection->deadTime < time()) { + $remote_connection->close(); + $remote_connection->udp_connection->close(); + unset($worker->udpConnections[$id]); + } + } + }); +}; +$udpWorker->onMessage = 'udpWorkerOnMessage'; + +function packResponse($response) { + $data = ""; + $data .= chr($response['ver']); + $data .= chr($response['rep']); + $data .= chr($response['rsv']); + $data .= chr($response['addr_type']); + + switch ($response['addr_type']) { + case ADDRTYPE_IPV4: + $tmp = explode('.', $response['bind_addr']); + foreach ($tmp as $block) { + $data .= chr($block); + } + break; + case ADDRTYPE_HOST: + $host_len = strlen($response['bind_addr']); + $data .= chr($host_len); + $data .= $response['bind_addr']; + break; + } + + $data .= pack("n", $response['bind_port']); + return $data; +} + +function logger($level, $str) +{ + global $config; + if ($config['log_level'] >= $level) { + echo $str . "\n"; + } +} +// 如果不是在根目录启动,则运行runAll方法 +if (!defined('GLOBAL_START')) { Worker::runAll(); }