前序
最近各种折腾 Socks5 的代理,有项目使用 socks 代理做爬虫,也有使用代理做流量中转,这反倒是让我对这协议产生了兴趣。
曾有过一个念头出现在脑海里,V2ray 项目既然属于开源作品,那我便可以使用 Golang 对它魔改或者封装,以解决一些特殊场景的需求;不过后来因为一些原因 (懒癌),迟迟未对 Golang 开始学习,也就一直拖着...
最近又想到 Swoole 和 Golang 不也很 “类似” 吗,那我不如干脆先用 Swoole,实现一个简单的 Demo?说干就干,于是乎便有了此篇文章和代码。
过程心得
说实话,敲代码这么久,也是第一次纯手动的使用 TCP,按照规范实现一种标准协议(HTTP 这种就不算哈
),有一些特别的心得。 例如:
- 传输层三元组,客户端、服务端和目标端,IP 和端口需特别留意。
 - 底层数据进制转换,二进制、十进制和十六进制,平常应用层更多注意的是编码。
 - 规范、规范、规范,由为提现了规范的重要性,而不是那么任意所为。
 - 其它的...
 
使用细节
- 利用 Swoole 的 TCP 客户端实现,不支持 UDP。
 - 启动之后的代理支持免验证和账号密码验证。
 - PHP 版本 7.3,Swoole4+,使用的需提前安装。
 - 代码里面写了很详细的注释,欢迎交流学习。
 
实现代码
服务端:
- 
 - 
use Swoole\Coroutine\Client;
 - 
//创建Server对象,监听 127.0.0.1:9501 端口
 - 
$server = new Swoole\Server('0.0.0.0', 6688);
 - 
//监听连接进入事件
 - 
$server->on('Connect', function ($server, $fd) {
 - 
echo "\n\n ----- 连接成功 ----- \n";
 - 
$info = $server->getClientInfo($fd);
 - 
$remote_ip = $info['remote_ip'];
 - 
$remote_port = $info['remote_port'];
 - 
//$server->send($fd, );
 - 
echo "TCP成功连接: $remote_ip:$remote_port\n";
 - 
});
 - 
// TCP 目标服务器连接池
 - 
$pool = [];
 - 
// 已连接客户端列表
 - 
$conn = [];
 - 
// 是否需要账号密码授权
 - 
$isAuth = false;
 - 
// 代理账号
 - 
$user = '123234';
 - 
// 验证密码
 - 
$pass = 'abcd1234';
 - 
//监听数据接收事件
 - 
$server->on('Receive', function ($server, $fd, $reactor_id, $raw) {
 - 
$info = $server->getClientInfo($fd);
 - 
$remote_ip = $info['remote_ip'];
 - 
$remote_port = $info['remote_port'];
 - 
$src = md5($remote_port . $remote_ip);
 - 
echo "[" . date('Y-m-d H:i:s') . "]收到来自客户端:";
 - 
$data = bin2hex($raw);
 - 
$len = mb_strlen($data);
 - 
echo "($len) ";
 - 
echo $data;
 - 
echo "\n";
 - 
global $isAuth;
 - 
global $conn;
 - 
// 已经成功连接
 - 
if ($conn[$src] === true) {
 - 
echo " ----- 传输数据 ----- \n";
 - 
global $pool;
 - 
$client = $pool[$src];
 - 
echo $raw;
 - 
echo "\n";
 - 
$client->send($raw);
 - 
echo " ----- 返回数据 ----- \n";
 - 
while ($recv = $client->recv()) {
 - 
if (!$recv) {
 - 
echo $client->errCode;
 - 
echo "\n";
 - 
// 接收失败,主动断开
 - 
$server->close($fd, true);
 - 
}
 - 
echo $recv;
 - 
$server->send($fd, $recv);
 - 
}
 - 
return;
 - 
}
 - 
// 开始授权验证
 - 
// 需要账号密码
 - 
if ($isAuth) {
 - 
// 选择验证方法
 - 
if ($conn[$src] === null) {
 - 
echo " ----- 首次请求 ----- \n";
 - 
$ver = mb_substr($data, 0, 2);
 - 
$n_mth = mb_substr($data, 2, 2);
 - 
$mths = mb_substr($data, 4);
 - 
echo "版本:$ver\n";
 - 
echo "方法数目:$n_mth\n";
 - 
echo "可选方法:$mths\n";
 - 
//X'00' 无需认证
 - 
//X'01' GSSAPI
 - 
//X'02' 用户名/密码
 - 
//X'03' 一直到 X'7F'分配给IANA
 - 
//X'80' 一直到 X'FE'保留用作私有方法
 - 
//X'FF' 没有方法被接受
 - 
$msg = '0502';
 - 
$conn[$src] = 1;
 - 
$server->send($fd, hex2bin($msg));
 - 
return;
 - 
}
 - 
// 开始验证授权
 - 
if ($conn[$src] === 1) {
 - 
echo " ----- 开始验证授权 ----- \n";
 - 
$ver = mb_substr($data, 0, 2);
 - 
$user_len = hexdec(mb_substr($data, 2, 2)) * 2;
 - 
$username = hex2bin(mb_substr($data, 4, $user_len));
 - 
$pass_len = hexdec(mb_substr($data, 4 + $user_len, 2)) * 2;
 - 
$password = hex2bin(mb_substr($data, 4 + $user_len + 2, $pass_len));
 - 
echo "协议版本:$ver \n";
 - 
echo "账号长度:$user_len\n";
 - 
echo "验证账号:$username \n";
 - 
echo "密码长度:$pass_len \n";
 - 
echo "验证密码:$password \n";
 - 
global $user, $pass;
 - 
if ($user == $username && $pass == $password) {
 - 
// 验证成功
 - 
echo "验证结果:账号密码正确\n";
 - 
$conn[$src] = 2;
 - 
$reply = ['VER' => $ver, 'STATUS' => '00',];
 - 
} else {
 - 
// 验证失败
 - 
echo "验证结果:账号密码错误\n";
 - 
$conn[$src] = null;
 - 
$reply = ['VER' => $ver, 'STATUS' => '01',];
 - 
}
 - 
$server->send($fd, hex2bin(implode('', $reply)));
 - 
return;
 - 
}
 - 
// 建立目标连接
 - 
if ($conn[$src] === 2) {
 - 
$ver = mb_substr($data, 0, 2);
 - 
$cmd = mb_substr($data, 2, 2);
 - 
$rsv = mb_substr($data, 4, 2);
 - 
$atyp = mb_substr($data, 6, 2);
 - 
$dst_addr = long2ip(hexdec(mb_substr($data, 8, 8)));
 - 
$dst_port = hexdec(mb_substr($data, 16));
 - 
echo " ----- 建立目标连接 ----- \n";
 - 
echo "版本:$ver\n";
 - 
echo "命令:$cmd\n";
 - 
echo "保留:$rsv\n";
 - 
echo "地址类型:$atyp\n";
 - 
echo "目标地址:$dst_addr\n";
 - 
echo "目标端口:$dst_port\n";
 - 
global $pool;
 - 
$s = microtime(true);
 - 
$client = new Client(SWOOLE_SOCK_TCP);
 - 
if (!$client->connect($dst_addr, $dst_port, 60)) {
 - 
echo "connect failed. Error: {$client->errCode}\n";
 - 
}
 - 
$info = $client->getsockname();
 - 
$ip = dechex(ip2long($info['address']));
 - 
$port = dechex($info['port']);
 - 
if (mb_strlen($ip) % 2 != 0) {
 - 
$ip = '0' . $ip;
 - 
}
 - 
if (mb_strlen($port) % 2 != 0) {
 - 
$port = '0' . $port;
 - 
}
 - 
echo "连接耗时:" . round(microtime(true) - $s, 2);
 - 
echo "\n";
 - 
$pool[$src] = $client;
 - 
$reply = [
 - 
'VER' => '05', // 协议版本
 - 
'REP' => '00', // 回复字段 00 成功
 - 
'RSV' => '00', // 保留字段
 - 
'ATYP' => '01', // 地址类型 IPV4
 - 
'BND.ADDR' => $ip, // 服务端绑定IP
 - 
'BND.PORT' => $port, // 服务端绑定端口
 - 
];
 - 
$conn[$src] = true;
 - 
$server->send($fd, hex2bin(implode('', $reply)));
 - 
return;
 - 
}
 - 
return;
 - 
}
 - 
// 无需账号密码
 - 
// 首次请求
 - 
if ($conn[$src] === null) {
 - 
echo " ----- 首次请求 ----- \n";
 - 
$msg = '0500'; // 版本5,不用验证
 - 
$server->send($fd, hex2bin($msg));
 - 
$conn[$src] = 1;
 - 
return;
 - 
}
 - 
// 建立目标连接
 - 
if ($conn[$src] === 1) {
 - 
$ver = mb_substr($data, 0, 2);
 - 
$cmd = mb_substr($data, 2, 2);
 - 
$rsv = mb_substr($data, 4, 2);
 - 
$atyp = mb_substr($data, 6, 2);
 - 
$dst_addr = long2ip(hexdec(mb_substr($data, 8, 8)));
 - 
$dst_port = hexdec(mb_substr($data, 16));
 - 
echo " ----- 建立目标连接 ----- \n";
 - 
echo "版本:$ver\n";
 - 
echo "命令:$cmd\n";
 - 
echo "保留:$rsv\n";
 - 
echo "地址类型:$atyp\n";
 - 
echo "目标地址:$dst_addr\n";
 - 
echo "目标端口:$dst_port\n";
 - 
global $pool;
 - 
$s = microtime(true);
 - 
$client = new Client(SWOOLE_SOCK_TCP);
 - 
if (!$client->connect($dst_addr, $dst_port, 60)) {
 - 
echo "connect failed. Error: {$client->errCode}\n";
 - 
}
 - 
$info = $client->getsockname();
 - 
$ip = dechex(ip2long($info['address']));
 - 
$port = dechex($info['port']);
 - 
if (mb_strlen($ip) % 2 != 0) {
 - 
$ip = '0' . $ip;
 - 
}
 - 
if (mb_strlen($port) % 2 != 0) {
 - 
$port = '0' . $port;
 - 
}
 - 
echo "连接耗时:" . round(microtime(true) - $s, 2);
 - 
echo "\n";
 - 
$pool[$src] = $client;
 - 
$reply = [
 - 
'VER' => '05', // 协议版本
 - 
'REP' => '00', // 回复字段 00 成功
 - 
'RSV' => '00', // 保留字段
 - 
'ATYP' => '01', // 地址类型 IPV4
 - 
'BND.ADDR' => $ip, // 服务端绑定IP
 - 
'BND.PORT' => $port, // 服务端绑定端口
 - 
];
 - 
$conn[$src] = true;
 - 
$server->send($fd, hex2bin(implode('', $reply)));
 - 
return;
 - 
}
 - 
});
 - 
//监听连接关闭事件
 - 
$server->on('Close', function ($server, $fd) {
 - 
$info = $server->getClientInfo($fd);
 - 
$remote_ip = $info['remote_ip'];
 - 
$remote_port = $info['remote_port'];
 - 
$src = md5($remote_port . $remote_ip);
 - 
global $pool;
 - 
if ($client = $pool[$src]) {
 - 
$client->close();
 - 
unset($pool[$src]);
 - 
}
 - 
echo "TCP成功断开: $remote_ip:$remote_port\n";
 - 
echo " ----- 连接断开 ----- \n\n";
 - 
});
 - 
//启动服务器
 - 
$server->start();
 
客户端:
此部分是次日追加的,仅实现了非授权的客户端请求实例,演示的是:使用 Socks5 代理 请求 myip.ipip.net 的过程。
- 
 - 
use Swoole\Coroutine\Client;
 - 
use function Swoole\Coroutine\run;
 - 
run(function () {
 - 
$client = new Client(SWOOLE_SOCK_TCP);
 - 
$socks_ip = '127.0.0.1';
 - 
$socks_port = 1080;
 - 
if (!$client->connect($socks_ip, $socks_port, 60)) {
 - 
echo "Socks5连接失败:{$client->errCode}\n";
 - 
return;
 - 
}
 - 
echo "\n ----- 首次请求 ----- \n";
 - 
$msg = [
 - 
'VER' => '05',
 - 
'NMETHODS' => '01',
 - 
'METHODS' => '00',
 - 
];
 - 
$client->send(hex2bin(implode('', $msg)));
 - 
$res = bin2hex($client->recv());
 - 
echo "收到响应:$res \n";
 - 
echo "\n ----- 建立连接 ----- \n";
 - 
$dst_port = dechex(80);
 - 
$msg = [
 - 
'ver' => '05',
 - 
'cmd' => '01',
 - 
'rsv' => '00', // 保留字段
 - 
'type' => '01', // IPV4
 - 
'dst_ip' => dechex(ip2long(gethostbyname('myip.ipip.net'))), // 四字节
 - 
'dst_port' => str_pad($dst_port, 4, '0', STR_PAD_LEFT) // 两字节
 - 
];
 - 
$client->send(hex2bin(implode('', $msg)));
 - 
$res = bin2hex($client->recv());
 - 
echo "收到响应:$res \n";
 - 
$ver = mb_substr($res, 0, 2);
 - 
$cmd = mb_substr($res, 2, 2);
 - 
$type = mb_substr($res, 6, 2);
 - 
$ip = long2ip(hexdec(mb_substr($res, 8, -4)));
 - 
$port = hexdec(mb_substr($res, -4));
 - 
echo "协议版本:$ver\n";
 - 
echo "响应命令:$cmd\n";
 - 
echo "地址类型:$type\n";
 - 
echo "IP 地址:$ip\n";
 - 
echo "IP 端口:$port\n";
 - 
if ($cmd !== '00') {
 - 
echo "代理服务器连接目标服务器失败\n";
 - 
return;
 - 
}
 - 
echo "代理服务器连接目标服务器成功\n";
 - 
echo "\n ----- 发送数据 ----- \n";
 - 
$msg = "GET / HTTP/1.1\r\n";
 - 
$msg .= "Host: myip.ipip.net\r\n";
 - 
$msg .= "Accept: */*\r\n";
 - 
$msg .= "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36\r\n";
 - 
$msg .= "Accept-Encoding: gzip, deflate\r\n";
 - 
$msg .= "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n\r\n";
 - 
echo $msg;
 - 
$client->send($msg);
 - 
$isRecvHead = false;
 - 
echo "收到响应:\n";
 - 
while (true) {
 - 
$res = $client->recv();
 - 
if (mb_strlen($res) > 0) {
 - 
if ($isRecvHead === false && mb_strpos($res, "\r\n\r\n") !== false) {
 - 
[$head, $content] = explode("\r\n\r\n", $res);
 - 
$isRecvHead = true;
 - 
echo $head;
 - 
echo "\r\n\r\n";
 - 
echo $content;
 - 
preg_match("/Content-Length: (\d+?)\r\n/", $head, $match);
 - 
if ($match) {
 - 
$len = $match[1];
 - 
$hex = bin2hex($content);
 - 
if (mb_strlen($hex) == 2 * $len) { // 传输结束
 - 
$client->close();
 - 
break;
 - 
} else { // 继续传
 - 
continue;
 - 
}
 - 
}
 - 
}
 - 
echo $res;
 - 
} else { // 未知错误
 - 
$client->close();
 - 
var_dump($res);
 - 
var_dump($client->errCode);
 - 
break;
 - 
}
 - 
}
 - 
});
 
					                
小林博客![[PHP] 利用 Swoole 实现一个简易 Socks5 代理 [PHP] 利用 Swoole 实现一个简易 Socks5 代理](https://www.xlin.cn/wp-content/uploads/2021/11/7698896f24aaa9d-1.png)
![[PHP] 利用 Swoole 实现一个简易 Socks5 代理 [PHP] 利用 Swoole 实现一个简易 Socks5 代理](https://www.xlin.cn/wp-content/uploads/2021/11/6f259dd342dec0f-1.png)
![[PHP] 利用 Swoole 实现一个简易 Socks5 代理 [PHP] 利用 Swoole 实现一个简易 Socks5 代理](https://www.xlin.cn/wp-content/uploads/2021/11/1638c89b34e2494-1.png)






