MENU

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

June 9, 2021 • Read: 1687 • 技术杂谈

前序

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

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

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

过程心得

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

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

使用细节

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

服务端效果:
服务端效果.png

客户端效果:
客户端效果.png

实现代码

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;
        }
    }
});

参考文章