这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
SOCKS5
SOCKS5 是一个代理协议,完整协议内容见 RFC1928 文档
简介
SOCKS5 是 SOCKS 协议的第五个版本
SOCKS5 继承了 SOCKS4 的协议模型,支持 UDP 协议,在整体上增强了认证方案,并支持域名和 IPv6 寻址
通信过程: 基于 TCP 的通信
客户端与 SOCKS 服务器连接
客户端与 SOCKS 服务器连接的过程同 TCP 三次握手,在此不做描述
客户端与 SOCKS 服务器协商
在客户端与 SOCKS 服务器建立连接后,两台计算机会协商请求方法
客户端会向代理服务发送一个选择数据,消息格式如下
- VER 表示当前 SOCKS 协议版本
- NMETHODS 表示 METHODS 字段中支持的方法个数
- X'00': 无需验证的方法
- X'01': GSSAPI
- X'02': USERNAME/PASSWORD 验证方法
- X'03' ~ X'7F' IANA 签发
- X'80' ~ X'FE' 私人保留方法
- X'FF': 没有可接受的方法
- METHODS 表示客户端支持请求方法
上述三个字段均使用十六进制表示
SOCKS 客户端在接收到请求后,会从客户端支持的方法中(即 METHODS 中选出一种),并向客户端发送响应消息,消息格式如下
- METHOD: 选定的方法
如果 SOCKS 服务器选择 X'00' 则双方会跳过子协商阶段
客户端发送代理请求
一旦客户端与 SOCKS 服务器协商完毕后,客户端就会将请求的具体信息发送给 SOCKS 服务器
客户端发送的代理请求消息格式如下
- VER: SOCKS 协议版本号
- CMD: 执行命令
- RSV: 保留字段
- ATYPE: 寻址类型
- DTS.ADDR: 请求目的主机的地址
- DTS.PORT: 请求目的主机的端口号
SOCKS 服务器在接收到请求后,会根据目的主机地址和客户端地址进行评估,并依次返回一个或多个响应消息,响应消息格式如下
- REP: 回复字段
- BND.ADDR: 目的服务器绑定地址
- BND.PORT: 目的服务器绑定端口
分析案例中的代码
func main() {
// SOCKS 服务器一般默认 1080 端口
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
// Accept() 会一直监听并等待下一个连接
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}
}
SOCKS 代理请求处理
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
// 对应 客户端与 SOCKS 服务器协商后验证的流程
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
}
}
SOCKS 服务器验证流程
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// 只读一个 Byte
// 由协商请求的消息格式可知 VER 字段占一个字节,且 VER 表示 SOCKS 协议版本号
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)
}
// 只读一个 Byte
// 由协商请求的消息格式可知 NMETHODS 字段占一个字节,且 NMETHODS 表示客户端支持的方法数量
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)
}
// 由协商流程可知,需要返回一个响应消息
// 0x00 对应 X'00' 表示使用无需验证的方法,因此会跳过子协商过程
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
SOCKS 代理转发过程
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER 版本号,socks5的值为0x05
// CMD 0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
// 0x01表示IPv4地址,DST.ADDR为4个字节
// 0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节
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", ver)
}
addr := ""
// SOCKS4 协议仅支持 IPv4 寻址, SOCKS5 拓展到域名寻址和 IPv6 寻址
switch atyp {
case atypIPV4:
_, 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])
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")
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])
// 建立 TCP 链接
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)
// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.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()
// io 包中的Copy() 函数实现单向数据转发, 将src(reader) 中的数据流逐步拷贝到 dst(dest)中
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
// 等待 context 执行完成,此时cancel() 函数执行
// defer 中的 cancel 也会执行,
// cancel 可以执行多次,无影响
<-ctx.Done()
return nil
}
总结
摸清了 SOCKS 代理的总体流程,了解 SOCKS4 与 SOCKS5 的区别,在样例代码的帮助下了解如何在 go 中实现 SOCKS 代理