概述
这篇文档记录了一个使用Go语言编写的简易SOCKS5代理服务器实现的大概的流程。该服务器实现了SOCKS5协议的基本功能,包括与客户端的握手认证、接收客户端的连接请求、解析请求中的目标地址信息,并建立到目标地址的连接。此外,还实现了双向的数据转发,即从客户端到目标服务器的数据传输以及从目标服务器到客户端的数据回传
流程
1. 服务器初始化
- 监听端口:服务器通过
net.Listen函数监听本地的 TCP 端口(例如127.0.0.1:1080),等待客户端的连接请求 - 接受连接:当有新的客户端连接请求时,服务器通过
server.Accept()接受连接,并为每个连接启动一个新的 goroutine(协程) 来处理
2. 认证阶段
- 读取客户端的认证信息:
- 客户端发送一个初始消息,其中包含有协议版本(VER)、支持的方法数量(NMETHODS)和具体方法列表(METHODS)信息
- 服务器会通过
reader.ReadByte()和io.ReadFull()来读取这些数据
- 选择认证方法:
- 服务器检查客户端支持的方法种类,选择其中的一种认证方法(即是不需要认证的方法
0x00) - 服务器向客户端发送一个包含协议版本和选定认证方法的响应消息,格式为
[VER, METHOD]
- 服务器检查客户端支持的方法种类,选择其中的一种认证方法(即是不需要认证的方法
3. 连接请求阶段
- 读取客户端的连接请求:
- 客户端发送一个包含协议版本(VER)、命令码(CMD)、保留字段(RSV)和地址类型(ATYP)的请求头
- 服务器通过
io.ReadFull()读取这些信息
- 解析目标地址和端口:
- 根据地址类型(IPv4、域名或IPv6),服务器解析出目标地址和端口号
- 对于 IPv4 地址,读取 4 个字节并转换为十进制格式
- 对于域名,读取 1 字节表示域名长度,然后读取相应长度的域名字符串
- 对于 IPv6 地址,当前实现中未支持,会返回错误
- 建立与目标地址的连接:
- 服务器使用
net.Dial("tcp", targetAddress)尝试与目标地址建立 TCP 连接 - 如果连接成功,记录连接信息并继续下一步;如果连接失败,返回错误给客户端
- 服务器使用
4. 响应客户端
- 发送连接成功的响应:
- 服务器向客户端发送一个包含协议版本、响应码、保留字段、地址类型、绑定地址和绑定端口的响应消息,格式为
[VER, REP, RSV, ATYP, BND.ADDR, BND.PORT] - 本示例中,绑定地址和端口设置为
0.0.0.0:0,表示由操作系统自动分配
- 服务器向客户端发送一个包含协议版本、响应码、保留字段、地址类型、绑定地址和绑定端口的响应消息,格式为
5. 数据转发
- 启动数据转发协程:
- 服务器启动两个独立的 goroutine 分别处理客户端到目标服务器的数据转发和目标服务器到客户端的数据转发
- 使用
io.Copy(dest, reader)从客户端读取数据并转发到目标服务器 - 使用
io.Copy(conn, dest)从目标服务器读取数据并转发到客户端
- 取消上下文:
- 当任意一个数据转发协程完成后,调用
cancel()取消上下文,结束另一个数据转发协程
- 当任意一个数据转发协程完成后,调用
6. 错误处理
- 日志记录:
- 当然我们会在在每个关键步骤中,服务器都会记录相关的日志信息,用于以后的调试和监控工作
- 错误返回:
- 如果在任何步骤中发生错误,服务器会记录错误信息并返回适当的错误响应给客户端
socks5协议的工作原理
流程图
+-------------------+
| 服务器初始化 |
| 监听端口 |
+-------------------+
|
v
+-------------------+
| 接受客户端连接 |
+-------------------+
|
v
+-------------------+
| 认证阶段 |
| 读取认证信息 |
| 选择认证方法 |
| 发送认证响应 |
+-------------------+
|
v
+-------------------+
| 连接请求阶段 |
| 读取连接请求 |
| 解析目标地址和端口|
| 建立与目标地址的连接|
+-------------------+
|
v
+-------------------+
| 发送连接成功的响应|
+-------------------+
|
v
+-------------------+
| 数据转发 |
| 启动两个协程 |
| 客户端 <-> 目标服务器|
+-------------------+
|
v
+-------------------+
| 错误处理 |
| 记录日志 |
| 返回错误响应 |
+-------------------+
每个函数的具体工作原理
process函数
func process(conn net.Conn) {
defer conn.Close() //关闭
reader := bufio.NewReader(conn) //创建缓冲区用来读入数据
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
}
- 首先在函数返回值后我们要关闭接连
- 先创建一个缓冲区(类似于先排好队,在进场)
- 然后对读到的read进行通信验证(验证函数:auth)
- 验证通过后就对客户端发通过SOCKS5协议发起的请求进行处理
auth函数
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数量
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
// X’00’ NO AUTHENTICATION REQUIRED
// X’02’ USERNAME/PASSWORD
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
- 读取协议版本
- 读取支持的认证方法数量
- 读取具体的认证方法
- 选择并响应认证方法
- 完成认证过程
connect函数
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
// 读取请求的头部信息,定义了版本号、命令、保留字段和地址类型
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf) // 读取版本号、命令、保留字段和地址类型
if err != nil {
return fmt.Errorf("read header failed:%w", err) // 读取失败时返回错误
}
ver, cmd, atyp := buf[0], buf[1], buf[3] // 提取版本号、命令和地址类型
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver) // 验证版本号是否支持
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd) // 验证命令是否为CONNECT请求
}
addr := ""
// 根据地址类型读取相应的地址
switch atyp {
case atypeIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err) // 读取地址类型失败
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3]) // 格式化IPv4地址
case atypeHOST:
hostSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read hostSize failed:%w", err) // 读取主机名长度失败
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read host failed:%w", err) // 读取主机名失败
}
addr = string(host) // 转换主机名为字符串
case atypeIPV6:
return errors.New("IPv6: no supported yet") // 不支持IPv6
default:
return errors.New("invalid atyp") // 无效的地址类型
}
_, err = io.ReadFull(reader, buf[:2]) // 读取端口
if err != nil {
return fmt.Errorf("read port failed:%w", err) // 读取端口失败
}
port := binary.BigEndian.Uint16(buf[:2]) // 解析端口
// 连接到目标地址和端口
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst failed:%w", err) // 连接失败
}
defer dest.Close() // 确保连接关闭
log.Println("dial", addr, port) // 记录连接信息
// 向客户端返回连接成功的响应
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err) // 写入响应失败
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保取消上下文
// 启动两个协程用于数据转发
go func() {
_, _ = io.Copy(dest, reader) // 从客户端读取并转发到目标
cancel() // 取消上下文
}()
go func() {
_, _ = io.Copy(conn, dest) // 从目标读取并转发到客户端
cancel() // 取消上下文
}()
<-ctx.Done() // 等待上下文完成
return nil // 返回成功
}
- 读取请求头
- 验证请求头
- 解析目标地址
- 读取目标端口号
- 建立到目标地址的连接
- 向客户端发送响应
- 启动双向数据传输
注释
buf[:2]是表示前面两个字节的即为[var,cmd]