C#实现Socks5服务器

项目托管地址:https://github.com/hooow-does-it-work/socks5

Socks5是比较常用的一种代理协议,各浏览器都支持的很好。
这里简单实现下Sock5的协议,实现TCP代理。
代码里面注释了详细的协议内容。
代码中所有的Stream均为NetworkStream。

0、监听服务器实现

类继承前面的一篇文章实现的监听框架:C#实现一个简单的IOCP模式的服务端监听框架

public class Socks5Server : TcpIocpServer
{
    /// <summary>
    /// 实现NewClient方法,处理Socks5请求
    /// </summary>
    /// <param name="client"></param>
    protected override void NewClient(Socket client)
    {
        Socks5ProtocolExchanger exchanger = new Socks5ProtocolExchanger();
        try
        {
            //实例化NetWorkStream,让实例化NetWorkStream拥有基础Socket的处理权限
            exchanger.Start(new NetworkStream(client, true));
        }
        catch
        {
            //异常,销毁
            exchanger.Dispose();
        }
    }
}

1、认证

客户端认证请求
版本认证方法数量认证方法列表
1字节1字节N字节(N等于方法数量字段)
0x51-2550x00…0xff
服务端响应认证请求
版本认证方法
1字节1字节
0x50x00(不需要认证)
/// <summary>
/// 开始认证
/// </summary>
/// <param name="stream">客户端数据流</param>
private void StartExchange(Stream stream)
{
    //从客户端读取数据,20字节为保守大小
    byte[] header = new byte[20];

    //首先读取两字节
    ReadPackage(stream, header, 0, 2);

    //第一个字节为版本号,固定为5
    byte version = header[0];

    //第二个字节代表支持的方法数量
    byte nMethods = header[1];

    if (nMethods > 18)
    {
        //缓冲区不够用的话,扩展
        byte[] tempBuffer = new byte[nMethods + 2];
        Array.Copy(header, 0, tempBuffer, 0, 2);
        header = tempBuffer;
    }

    /*
     * 读取所有客户端支持的方法
     * 0x00 不需要认证
     * 0x01 GSSAPI认证
     * 0x02 账号密码认证
     * 0x03-0x7F IANA分配
     * 0x80-0xFE 私有方法保留
     * 0xFF 无支持的认证方法
     */
    ReadPackage(stream, header, 2, nMethods);
    
    //服务器选择不需要认证,发送响应数据到客户端
    byte[] response = new byte[] {
        0x5, /*版本号*/
        0x00 /*00代表无需认证,客户端可以继续发送代理请求*/
    };

    stream.Write(response, 0, 2);

}

2、处理客户端代理请求

代理请求头部
版本命令保留字节代理地址类型代理数据
1字节1字节1字节1字节N字节
0x50x01-0x030x000x01,0x03,0x040x00…0xff

代理数据根据代理地址类型不同而有差异

代理地址类型:0x03,远程地址为主机名称(域名)时的代理数据结构
主机名长度主机名端口
1字节N字节(N等于主机名长度)2字节
0x00-0xff0x00…0xff0-65535
代理地址类型:0x01,0x04,远程地址为IPv4地址或IPv6地址时的代理数据结构
IP地址端口
4字节(IPv4)或16字节(IPv6)2字节
0x00-0xff0-65535
举例1(代理请求www.baidu.com的80端口)

总数据长度:20字节=4字节头部+1字节主机长度+13字节主机名+2字节端口

0x05, 0x01, 0x00, 0x03,  //头
0x0c, //主机长度
0x77, 0x77, 0x77, 0x2e, 0x62, 0x61, 0x69, 0x64, 0x75, 0x2e, 0x63, 0x6f, 0x6d, //主机
0x00,0x50 //端口
举例2(代理请求127.0.0.1的80端口)

总数据长度:10字节=4字节头部+4字节IPv4地址+2字节端口

0x05, 0x01, 0x00, 0x01,  //头
0x7f,0x00,0x00,0x01, //IP地址
0x00,0x50 //端口
/// <summary>
/// 读取代理请求
/// </summary>
/// <param name="stream">客户端数据流</param>
/// <returns></returns>
private EndPoint StartReadRequest(Stream stream)
{
    //创建一个足够大的缓冲区
    byte[] buffer = new byte[512];
    ReadPackage(stream, buffer, 0, 5);

    //第一个字节,版本号
    byte version = buffer[0];

    /*
     * 第二个字节,命令类型,这里我们处理CONNECT方法
     * 0x01 CONNECT 连接上游服务器
     * 0x02 BIND 绑定,客户端会接收来自代理服务器的链接,著名的FTP被动模式
     * 0x03 UDP ASSOCIATE UDP中继
     */
    byte command = buffer[1];

    //保留字节
    byte rsv = buffer[2];

    /*
     * 地址类型
     * 0x01 IP V4地址
     * 0x03 域名
     * 0x04 IP V6地址
     * 
     * 目前为止,从版本号,到地理类型,我们用到了四个字节
     */
    byte addressType = buffer[3];
    int hostLength = 0;
    
    //如果地址类型为主机,第5个字节则为主机长度
    if(addressType == 0x03)
    {
        hostLength = buffer[4];
    }

    /*
     * 确认后续需要读取的数据长度
     * 地址类型为0x03 时,读取长度为hostLength
     * 地址类型为0x01 IPv4地址时,读取长度为3,其中第一个字节我们已经提前读取了,即buffer[4]。
     * 地址类型为0x04 IPv6地址时,读取长度为15,其中第一个字节我们已经提前读取了,即buffer[4]。
     */
    int remainLength = addressType == 0x03 ? hostLength : (addressType == 0x01 ? 3 : 15);

    //同时,端口号为固定两位,可以直接读取
    remainLength += 2;

    /*从偏移为5开始,读取所有需要的数据*/
    ReadPackage(stream, buffer, 5, remainLength);

    /*至此,总读取的数据长度如下*/
    int totalLength = remainLength + 5;

    /*
     * 端口号为最后两个字节,我们先读出来。
     * 传输顺序为网络字节序,高位在前
     */
    int destPort = (buffer[totalLength - 2] << 8) | buffer[totalLength - 1];

    //地址类型为主机,返回DnsHostEndPoint
    if(addressType == 0x03)
    {
        return new DnsEndPoint(Encoding.ASCII.GetString(buffer, 5, hostLength), destPort);
    }

    /*确认IP地址长度,IPv4是4个字节,IPv6是16个字节*/
    byte[] ipBuffer = new byte[addressType == 0x01 ? 4 : 16];

    /*
     * 从缓冲区中把IP数据读出来
     * 注意,读取索引为4
     */
    Array.Copy(buffer, 4, ipBuffer, 0, ipBuffer.Length);

    return new IPEndPoint( new IPAddress(ipBuffer), destPort);
}

3、连接远程服务器

上一步分析代理请求后,会获取到需要代理的服务器信息:主机或IP,下面方法会解析主机为IP,再进行连接。
连接成功后,服务器需要将连接成功的信息反馈给客户端。
发送给客户端的响应包结构如下:

版本是否成功保留字节代理地址类型 (IPv4或IPv6地址)Ip地址端口
1字节1字节1字节1字节4字节(IPv4)或16字节(IPv6)2字节
0x50x000x000x01,0x040x00…0xff0-65535
连接服务器
/// <summary>
/// 连接远程服务器
/// </summary>
/// <param name="endpoint">待连接的远程结点</param>
/// <param name="connectedEndPoint">连接成功的远程结点</param>
/// <returns></returns>
private Stream ConnectRemote(EndPoint endpoint, out IPEndPoint connectedEndPoint)
{
    if(endpoint is DnsEndPoint dnsEndPoint)
    {
        //解析主机
        endpoint = new IPEndPoint( ResolveDnsHost(dnsEndPoint.Host), dnsEndPoint.Port) ;
    }

    Socket remoteSocket = new Socket(endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
    remoteSocket.NoDelay = true;
    remoteSocket.Connect(endpoint);

    connectedEndPoint = endpoint as IPEndPoint;

    return new NetworkStream(remoteSocket, true);
}
发送连接成功的信息到客户端
/// <summary>
/// 发送连接成功的响应到客户端
/// </summary>
/// <param name="stream">客户端流</param>
/// <param name="endpoint">连接成功的终结点</param>
private void SendConnectResult(Stream stream, IPEndPoint endpoint)
{
    IPAddress ipaddress = endpoint.Address;
    byte[] addressBuffer = ipaddress.GetAddressBytes();
    int responseLength = 4 + 2 + addressBuffer.Length;
    byte[] response = new byte[responseLength];
    //协议版本
    response[0] = 0x05;
    //00代表连接成功
    response[1] = 0x00;
    //保留字段
    response[2] = 0;
    //地址类型,IPv4或IPv6
    response[3] = (byte)(ipaddress.AddressFamily == AddressFamily.InterNetwork ? 0x01 : 0x04);
    //写入IP以及端口,告诉客户端,服务器实际连接的IP和端口
    addressBuffer.CopyTo(response, 4);
    response[responseLength - 1] = (byte)(endpoint.Port & 0xff);
    response[responseLength - 2] = (byte)((endpoint.Port >> 8) & 0xff);

    stream.Write(response, 0, responseLength);
}

4、数据交换。

连接成功后,客户端进入正常收发的流程,任何基于TCP的协议均可,例如http,ssl,远程桌面等。
服务端需要做的就是转发两端的数据,下面汇总上面从认证到连接服务器的方法。
直接使用Stream的CopyToAsync方法转发数据。

 //开始进行协议交换,读取头部信息
 StartExchange(stream);
 //开始读取代理请求,返回一个需要代理的远程终结点
 EndPoint remoteEndPoint = StartReadRequest(stream);

 //连接远程服务器
 Stream remoteStream = ConnectRemote(remoteEndPoint, out IPEndPoint connectedEndPoint);
 //连接成功后,发送响应数据到客户端
 SendConnectResult(stream, connectedEndPoint);

 //协议结束,客户端接收到响应后,开始正常收发数据,服务器唯一需要做的就是转发数据
 //这里用异步方式处理
 _clientStream = stream;
 _remoteStream = remoteStream;
 stream.CopyToAsync(remoteStream).ContinueWith(clientCopyFinished);
 remoteStream.CopyToAsync(stream).ContinueWith(remoteCopyFinished);

5、总结

1、客户端发送认证请求,包含支持的认证方法。
2、服务器解析认证请求,返回选择的认证方法。
3、客户端发送代理请求,包含远程主机的IP(或主机名)以及端口。
4、代理服务器连接远程服务器,连接成功后通知客户端。
5、服务器开始客户端和远程服务器的数据交换。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anlige

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值