一:基础协议流程参数
1、请求 client -> server,客户端socket连接服务器后立即发送此包
VERSION | METHODS_COUNT | METHODS |
---|---|---|
1字节 | 1字节 | 1到255字节,长度由METHODS_COUNT值决定 |
0x05 | 0x03 | 0x00 0x01 0x02 |
各字段含义
- VERSION SOCKS协议版本,目前固定0x05
- METHODS_COUNT 客户端支持的认证方法数量
- METHODS 客户端支持的认证方法,每个方法占用1个字节
METHODS列表
- 0x00 不需要认证(常用)
- 0x01 GSSAPI认证
- 0x02 账号密码认证(常用)
- 0x03 - 0x7F IANA分配
- 0x80 - 0xFE 私有方法保留
- 0xFF 无支持的认证方法
2、认证过程
2.1、server -> client 无需认证,server返回无需认证,则直接进入第3步,命令过程
VERSION | METHOD |
---|---|
1字节 | 1字节 |
0x05 | 0x00 |
2.2、server -> client 密码认证,server返回需要密码认证,则进入密码认证过程,密码认证成功则进入第3步,命令过程。密码认证失败,则直接断开连接
VERSION | METHOD |
---|---|
1字节 | 1字节 |
0x05 | 0x02 |
2.2.1、client -> server 客户端发送账号密码
VERSION | USERNAME_LENGTH | USERNAME | PASSWORD_LENGTH | PASSWORD |
---|---|---|---|---|
1字节 | 1字节 | 1到255字节 | 1字节 | 1到255字节 |
0x01 | 0x01 | 0x0a | 0x01 | 0x0a |
各字段含义
- VERSION 认证子协商版本(与SOCKS协议版本的0x05无关系)
- USERNAME_LENGTH 用户名长度
- USERNAME 用户名字节数组,长度为USERNAME_LENGTH
- PASSWORD_LENGTH 密码长度
- PASSWORD 密码字节数组,长度为PASSWORD_LENGTH
2.2.2、server -> client 返回认证结果
VERSION | STATUS |
---|---|
1字节 | 1字节 |
0x01 | 0x00 |
各字段含义
- VERSION 认证子协商版本
- STATUS 认证结果,0x00认证成功,大于0x00认证失败
3、命令过程
3.1 client -> server 发送连接请求
VERSION | COMMAND | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
各字段含义
- VERSION SOCKS协议版本,固定0x05
- COMMAND 命令
- 0x01 CONNECT 连接上游服务器
- 0x02 BIND 绑定,客户端会接收来自代理服务器的链接,著名的FTP被动模式
- 0x03 UDP ASSOCIATE UDP中继
- RSV 保留字段
- ADDRESS_TYPE 目标服务器地址类型
- 0x01 IP V4地址
- 0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组
- 0x04 IP V6地址
- DST.ADDR 目标服务器地址
- DST.PORT 目标服务器端口
3.2 server -> client 服务端响应连接结果
VERSION | RESPONSE | RSV | ADDRESS_TYPE | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1字节 | 1字节 | 1字节 | 1字节 | 1-255字节 | 2字节 |
各字段含义
- VERSION SOCKS协议版本,固定0x05
- RESPONSE 响应命令,除0x00外,其它响应都应该直接断开连接
- 0x00 代理服务器连接目标服务器成功
- 0x01 代理服务器故障
- 0x02 代理服务器规则集不允许连接
- 0x03 网络无法访问
- 0x04 目标服务器无法访问(主机名无效)
- 0x05 连接目标服务器被拒绝
- 0x06 TTL已过期
- 0x07 不支持的命令
- 0x08 不支持的目标服务器地址类型
- 0x09 - 0xFF 未分配
- RSV 保留字段
- BND.ADDR 代理服务器连接目标服务器成功后的代理服务器IP
- BND.PORT 代理服务器连接目标服务器成功后的代理服务器端口
4、数据转发
第3步成功后,进入数据转发阶段
- CONNECT:则将client过来的数据原样转发到目标,接着再将目标回来的数据原样返回给client
- BIND
- UDP ASSOCIATE:使用UDP转发
udp转发的数据包
- 收到客户端udp数据包后,解析出目标地址,数据,然后把数据发送过去
- 收到服务端回来的udp数据后,根据相同格式,打包,然后发回客户端
RSV | FRAG | ADDRESS_TYPE | DST.ADDR | DST.PORT | DATA |
---|---|---|---|---|---|
2字节 | 1字节 | 1字节 | 可变长 | 2字节 | 可变长 |
各字段含义
- RSV 保留为
- FRAG 分片位
- ATYP 地址类型
- 0x01 IP V4地址
- 0x03 域名地址(没有打错,就是没有0x02),域名地址的第1个字节为域名长度,剩下字节为域名名称字节数组
- 0x04 IP V6地址
- DST.ADDR 目标地址
- DST.PORT 目标端口
- DATA 数据
二:示例代码
1、服务端代码,暂未实现UDP转发和认证
1.1、基础的封装类和枚举
/// <summary>
/// 客户端和目标服务器的连接
/// </summary>
internal class UserAndToken
{
public TcpClient Client { get; set; }
public NetworkStream ClientStream { get; set; }
public TcpClient TargetClient { get; set; }
public NetworkStream TargetStream { get; set; }
}
/// <summary>
/// socks5地址类型
/// </summary>
public enum Socks5AddressType : byte
{
/// <summary>
/// IPV4
/// </summary>
IPV4 = 0x01,
/// <summary>
/// 域名
/// </summary>
Domain = 0x03,
/// <summary>
/// IPV6
/// </summary>
IPV6 = 0x04
}
/// <summary>
/// socks5命令类型
/// </summary>
public enum Socks5CommandType : byte
{
/// <summary>
/// 连接
/// </summary>
Connect = 0x01,
/// <summary>
/// 绑定
/// </summary>
Bind = 0x02,
/// <summary>
/// UDP转发
/// </summary>
Udp = 0x03
}
1.2、服务端完整代码:
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Server
{
public class TcpClientSocks5Server
{
static ushort port = 8300;
static async Task Main(string[] args)
{
try
{
TcpListener listener = new TcpListener(IPAddress.Any, port);
listener.Start();
Console.WriteLine($"Socks5服务已启动,监听端口:{port}");
while (true)
{
//接受连接创建客户端
TcpClient tcpClient = await listener.AcceptTcpClientAsync();
HandleClientAsync(tcpClient);
}
}
catch (Exception e)
{
Console.WriteLine($"Main的异常:{e.Message}");
}
}
/// <summary>
/// 接收处理客户端的数据
/// </summary>
/// <param name="tcpClient"></param>
/// <returns></returns>
private static async Task HandleClientAsync(TcpClient tcpClient)
{
try
{
UserAndToken token = new UserAndToken()
{
Client = tcpClient,
ClientStream = tcpClient.GetStream(),
};
//握手阶段,读取到客户端发送的协议,可以根据协议进行指定的连接
byte[] buffer = new byte[10];
await token.ClientStream.ReadAsync(buffer, 0, buffer.Length);
//判断协议
if (buffer[0] == 5)
{
int AuthMethod = 0;
//设置不需要认证
await token.ClientStream.WriteAsync(new byte[] { 0x05, 0x00 });
//不需要认证,直接到第三步
if (AuthMethod != 0)
{
//处理认证
}
#region 处理命令阶段
byte[] Commandbuffer = new byte[1024];
await token.ClientStream.ReadAsync(Commandbuffer, 0, Commandbuffer.Length);
Socks5CommandType socks5CommandType = (Socks5CommandType)Commandbuffer[1];
Socks5AddressType addressType = (Socks5AddressType)Commandbuffer[3];
//获取目标服务器IP和端口
string targetHost = null;
ushort TargetPort = 0;
switch (addressType)
{
case Socks5AddressType.IPV4:
{
targetHost = new IPAddress(Commandbuffer.AsSpan(4, 4)).ToString();
TargetPort = BinaryPrimitives.ReadUInt16BigEndian(Commandbuffer.AsSpan(8, 2));
}
break;
case Socks5AddressType.Domain:
{
byte length = Commandbuffer[4];
targetHost = Encoding.UTF8.GetString(Commandbuffer.AsSpan(5, length));
TargetPort = BinaryPrimitives.ReadUInt16BigEndian(Commandbuffer.AsSpan(5 + length, 2));
}
break;
case Socks5AddressType.IPV6:
{
targetHost = new IPAddress(Commandbuffer.AsSpan(4, 16)).ToString();
TargetPort = BinaryPrimitives.ReadUInt16BigEndian(Commandbuffer.AsSpan(20, 2));
}
break;
}
byte[] portArray = BitConverter.GetBytes(port);
// 如果您需要确保使用特定的字节顺序(例如,大端或小端),可以使用以下方式进行转换:
if (BitConverter.IsLittleEndian)
{
// 如果当前系统是小端字节序,反转字节数组以得到大端字节序
Array.Reverse(portArray);
}
if (socks5CommandType == Socks5CommandType.Connect)
{
try
{
token.TargetClient = new TcpClient(targetHost, TargetPort);
token.TargetStream = token.TargetClient.GetStream();
// 返回客户端连接成功
await token.ClientStream.WriteAsync(new byte[] { 0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, portArray[0], portArray[1] }, 0, 10);
}
catch (SocketException ex)
{
//访问目标服务器失败则不断开客户端和代理服务器的连接,仅返回失败原因
if (SocketError.HostNotFound == ex.SocketErrorCode)
{
// 返回客户端连接失败原因:目标服务器无法访问(主机名无效)
await token.ClientStream.WriteAsync(new byte[] { 0x05, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, portArray[0], portArray[1] }, 0, 10);
}
else
{
// 返回客户端连接失败原因:连接目标服务器被拒绝
await token.ClientStream.WriteAsync(new byte[] { 0x05, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, portArray[0], portArray[1] }, 0, 10);
}
}
}
else
{
// 返回客户端连接失败原因:代理服务器规则集不允许连接
await token.ClientStream.WriteAsync(new byte[] { 0x05, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, portArray[0], portArray[1] }, 0, 10);
//关闭和客户端的连接通道
token.Client.Close();
token.Client.Dispose();
}
#endregion
#region 处理转发
if (token.Client.Connected && token.TargetClient.Connected)
{
//两个Task不停地互相转发数据
await Task.WhenAny(
PipeAsync(token.ClientStream, token.TargetStream),
PipeAsync(token.TargetStream, token.ClientStream)
);
}
#endregion
}
else
{
token.Client.Close();
token.Client.Dispose();
}
}
catch (Exception e)
{
Console.WriteLine($"HandleClientAsync的异常:{e.Message}");
}
}
/// <summary>
/// 转发数据
/// </summary>
/// <param name="source">源Stream</param>
/// <param name="target">目标Stream</param>
/// <returns></returns>
static async Task PipeAsync(NetworkStream source, NetworkStream target)
{
byte[] buffer = new byte[1024];
int bytesRead;
try
{
//一直循环,在没有stream传输的时候等待
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
await target.WriteAsync(buffer, 0, bytesRead);
}
}
catch (Exception e)
{
Console.WriteLine($"PipeAsync的异常:{e.Message}");
}
}
}
}
1.3、服务端的个人理解:
SOCKS5代理连接到目标网站的过程通常包括以下四个阶段:
- 建立连接: 在这个阶段,客户端应用程序首先会连接到SOCKS5代理服务器。这是最初的TCP连接,它建立了代理和客户端之间的通信通道,这里只需要客户端和服务端建立一次口可以了,会保持长连接(类似于服务端和客户端之间挖了一条隧道,后续所有的请求都在这个隧道里面进行传输)
- 协商认证: 一旦建立连接,客户端和代理服务器之间会进行协商认证。在SOCKS5中,有两种认证方法可供选择:无认证方法(0x00)和用户名/密码认证方法(0x02)。客户端会发送认证方法的请求,代理服务器会选择一个支持的方法进行认证。如果需要认证,客户端会发送用户名和密码。
- 请求建立连接: 在协商认证成功后,客户端会发送一个连接请求,其中包含目标服务器的IP地址或域名以及端口号。代理服务器会解析该请求并尝试连接到目标服务器。(这里服务器和目标服务器也会搭建一条类似的通道,这里目标服务器和客户端没有直接的关系,代理服务器和两者分别有直接的关系)
- 数据转发: 一旦代理服务器成功连接到目标服务器,它会充当中间人,将来自客户端的数据传输到目标服务器,并将来自目标服务器的响应传输回客户端。数据转发阶段持续到客户端或目标服务器中的一方关闭连接。(短连接的数据转发,可以将目标服务器的连接在使用完后进行断开;长连接请使用异步方法一直监听两端的数据流,一旦有数据传输就开始进行转发,没有则等待,存在循环中一直运行,短连接则不用)