前序
最近各种折腾 Socks5
的代理,有项目使用 socks 代理做爬虫,也有使用代理做流量中转,这反倒是让我对这协议产生了兴趣。
曾有过一个念头出现在脑海里,V2ray
项目既然属于开源作品,那我便可以使用 Golang
对它魔改或者封装,以解决一些特殊场景的需求;不过后来因为一些原因 (懒癌),迟迟未对 Golang
开始学习,也就一直拖着...
最近又想到 Swoole
和 Golang
不也很 “类似” 吗,那我不如干脆先用 Swoole,实现一个简单的 Demo?说干就干,于是乎便有了此篇文章和代码。
过程心得
说实话,敲代码这么久,也是第一次纯手动的使用 TCP,按照规范实现一种标准协议(HTTP 这种就不算哈),有一些特别的心得。 例如:
- 传输层三元组,客户端、服务端和目标端,IP 和端口需特别留意。
- 底层数据进制转换,二进制、十进制和十六进制,平常应用层更多注意的是编码。
- 规范、规范、规范,由为提现了规范的重要性,而不是那么任意所为。
- 其它的...
使用细节
- 利用 Swoole 的 TCP 客户端实现,不支持 UDP。
- 启动之后的代理支持免验证和账号密码验证。
- PHP 版本 7.3,Swoole4+,使用的需提前安装。
- 代码里面写了很详细的注释,欢迎交流学习。
服务端效果:
客户端效果:
实现代码
Ps:这份代码完全是为了写 Demo 而写的 Demo,里面很多代码规范不符合
Swoole
规范,大家不要学我。
服务端:
- <?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;
- }
- }
- });