[PHP] 利用 Swoole 实现一个简易 Socks5 代理

前序

最近各种折腾 Socks5 的代理,有项目使用 socks 代理做爬虫,也有使用代理做流量中转,这反倒是让我对这协议产生了兴趣。

曾有过一个念头出现在脑海里,V2ray 项目既然属于开源作品,那我便可以使用 Golang 对它魔改或者封装,以解决一些特殊场景的需求;不过后来因为一些原因 (懒癌),迟迟未对 Golang 开始学习,也就一直拖着…

最近又想到 Swoole 和 Golang 不也很 “类似” 吗,那我不如干脆先用 Swoole,实现一个简单的 Demo?说干就干,于是乎便有了此篇文章和代码。

过程心得

说实话,敲代码这么久,也是第一次纯手动的使用 TCP,按照规范实现一种标准协议(HTTP 这种就不算哈[PHP] 利用 Swoole 实现一个简易 Socks5 代理),有一些特别的心得。 例如:

  1. 传输层三元组,客户端、服务端和目标端,IP 和端口需特别留意。
  2. 底层数据进制转换,二进制、十进制和十六进制,平常应用层更多注意的是编码。
  3. 规范、规范、规范,由为提现了规范的重要性,而不是那么任意所为。
  4. 其它的…

使用细节

  1. 利用 Swoole 的 TCP 客户端实现,不支持 UDP。
  2. 启动之后的代理支持免验证和账号密码验证。
  3. PHP 版本 7.3,Swoole4+,使用的需提前安装。
  4. 代码里面写了很详细的注释,欢迎交流学习。

服务端效果:
[PHP] 利用 Swoole 实现一个简易 Socks5 代理

客户端效果:
[PHP] 利用 Swoole 实现一个简易 Socks5 代理

实现代码

Ps:这份代码完全是为了写 Demo 而写的 Demo,里面很多代码规范不符合 Swoole 规范,大家不要学我。[PHP] 利用 Swoole 实现一个简易 Socks5 代理

服务端:

  • <?php
  • 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 的过程。

  • <?php
  • 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;
  • }
  • }
  • });
 收藏 (0) 打赏

您可以选择一种方式赞助本站

支付宝扫一扫赞助

微信钱包扫描赞助

除特别注明外,本站所有文章均基于CC-BY-NC-SA 4.0原创,转载请注明出处。
文章名称:《[PHP] 利用 Swoole 实现一个简易 Socks5 代理》
文章链接:https://www.vvso.cn/xlbk/16072.html
分享到: 更多 (0)

热门文章

评论 抢沙发

切换注册

登录

忘记密码 ?

切换登录

注册