前序
最近各种折腾 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;
}
}
});