Merge pull request #10 from linkec/master

Rebuild and Support UDP ASSOCIATE
This commit is contained in:
walkor 2020-09-13 10:04:16 +08:00 committed by GitHub
commit 663e8e8fcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 505 additions and 101 deletions

View File

@ -1,5 +1,14 @@
<?php
$AUTH_ENABLED = 0;
$USERNAME = 'changeme';
$PASSWORD = '1234';
$config = [
"auth" => [
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',
];

591
start.php
View File

@ -1,4 +1,5 @@
<?php
/**
* This file is part of https://github.com/walkor/php-socks5.
*
@ -11,14 +12,15 @@
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
ini_set("memory_limit", "512M");
use \Workerman\Worker;
use \Workerman\WebServer;
use \Workerman\Connection\TcpConnection;
use \Workerman\Timer;
use \Workerman\Connection\AsyncTcpConnection;
use \Workerman\Connection\AsyncUdpConnection;
// 自动加载类
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/config.php';
define('STAGE_INIT', 0);
define('STAGE_AUTH', 1);
@ -33,6 +35,16 @@ define('CMD_CONNECT', 1);
define('CMD_BIND', 2);
define('CMD_UDP_ASSOCIATE', 3);
define('ERR_GENERAL', 1);
define('ERR_NOT_ALLOW', 2);
define('ERR_NETWORK', 3);
define('ERR_HOST', 4);
define('ERR_REFUSE', 5);
define('ERR_TTL_EXPIRED', 6);
define('ERR_UNKNOW_COMMAND', 7);
define('ERR_UNKNOW_ADDR_TYPE', 8);
define('ERR_UNKNOW', 9);
define('ADDRTYPE_IPV4', 1);
define('ADDRTYPE_IPV6', 4);
define('ADDRTYPE_HOST', 3);
@ -41,132 +53,515 @@ define('METHOD_NO_AUTH', 0);
define('METHOD_GSSAPI', 1);
define('METHOD_USER_PASS', 2);
$worker = new Worker('tcp://0.0.0.0:1080');
$worker->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();
}