项目托管地址:https://github.com/hooow-does-it-work/socks5
Socks5是比较常用的一种代理协议,各浏览器都支持的很好。
这里简单实现下Socks5的协议,实现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等于方法数量字段) |
0x5 | 1-255 | 0x00…0xff |
服务端响应认证请求
版本 | 认证方法 |
---|---|
1字节 | 1字节 |
0x5 | 0x00(不需要认证) |
/// <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字节 |
0x5 | 0x01-0x03 | 0x00 | 0x01,0x03,0x04 | 0x00…0xff |
代理数据根据代理地址类型不同而有差异 |
代理地址类型:0x03,此时远程地址为主机名称(域名)
主机名长度 | 主机名 | 端口 |
---|---|---|
1字节 | N字节(N等于主机名长度) | 2字节 |
0x00-0xff | 0x00…0xff | 0-65535 |
代理地址类型:0x01,0x04,此时远程地址为IPv4地址或IPv6地址
IP地址 | 端口 |
---|---|
4字节(IPv4)或16字节(IPv6) | 2字节 |
0x00-0xff | 0-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。
新建Socket,连接远程主机,连接成功后,服务器需要将连接成功的信息反馈给客户端。
发送给客户端的响应包结构如下:
版本 | 是否成功 | 保留字节 | 代理地址类型 (IPv4或IPv6地址) | Ip地址 | 端口 |
---|---|---|---|---|---|
1字节 | 1字节 | 1字节 | 1字节 | 4字节(IPv4)或16字节(IPv6) | 2字节 |
0x5 | 0x00 | 0x00 | 0x01,0x04 | 0x00…0xff | 0-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、服务器开始客户端和远程服务器的数据交换。