Go语言原理与实践-实战小案例3-Socks5代理

犀利的毛毛虫 发布于 2025-01-30 232 次阅读


本文作为Go语言的个人学习笔记,多使用个人笔记习惯,同时使用AI帮忙注解部分代码,更加易懂。

SOCKS5代理

先来一个简单的例程练练手:我们用tcp发送啥,对方就给我们返回啥

TCP echo server

package main  
  
import (  
    "bufio"  
    "log"    "net")  
  
func main() {  
    server, err := net.Listen("tcp", "127.0.0.1:8080")  
    if err != nil {  
       panic(err)  
    }  
    for {  
       client, err := server.Accept()  
       if err != nil {  
          log.Printf("Accept failed %v", err)  
          continue  
       }  
       go process(client)  
    }  
}  
  
func process(conn net.Conn) {  
    defer conn.Close()  
    reader := bufio.NewReader(conn)  
    for {  
       b, err := reader.ReadByte()  
       if err != nil {  
          break  
       }  
       _, err = conn.Write([]byte{b})  
       if err != nil {  
          break  
       }  
    }  
}
  • 监听连接: 使用 net.Listen 创建一个 TCP 监听器,绑定到 127.0.0.1:1080,表示服务器监听本地端口 1080
  • 接受连接: server.Accept 会阻塞,直到有客户端连接,返回与客户端通信的 net.Conn
  • 处理连接: 使用 go process(client) 启动一个 goroutine 并发处理每个客户端连接。

运行这个例程,在cmd中输入nc 127.0.0.1 8080 hello去向8080端口发送消息,随后程序会返回你一个一模一样的消息。 需要注意的是,netcat软件的安装:https://eternallybored.org/misc/netcat/

auth

package main  
  
import (  
    "bufio"  
    "fmt"    "io"    "log"    "net")  
  
const socks5Ver = 0x05  
const cmdBind = 0x01  
const atypeIPV4 = 0x01  
const atypeHOST = 0x03  
const atypeIPV6 = 0x04  
  
func process(conn net.Conn) {  
    defer conn.Close()  
    // 读取客户端请求  
    reader := bufio.NewReader(conn)  
    err := auth(reader, conn)  
    if err != nil {  
       log.Printf("auth failed: %v", err)  
       return  
    }  
    log.Println("auth success")  
}  
  
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:%d", ver)  
    }  
    methodSize, err := reader.ReadByte()  
    if err != nil {  
       return fmt.Errorf("read methodSize failed:%w", err)  
    }  
    method := make([]byte, methodSize) //创建一个长度为 methodSize 的字节切片,用于存储客户端支持的认证方法。  
    _, err = io.ReadFull(reader, method)  
    if err != nil {  
       return fmt.Errorf("read method failed:%w", err)  
    }  
    log.Println("ver:", ver, "method:", method)  
    // +----+--------+  
    // |VER | METHOD |    
    // +----+--------+    
    // | 1  |   1    |    
    // +----+--------+    
    _, err = conn.Write([]byte{socks5Ver, 0x00})  
    if err != nil {  
       return fmt.Errorf("write failed:%w", err)  
    }  
    return nil  
}  
  
func main() {  
    server, err := net.Listen("tcp", "127.0.0.1:1080")  
    if err != nil {  
       log.Fatalf("listen failed:%v", err)  
    }  
    for {  
       client, err := server.Accept()  
       if err != nil {  
          log.Printf("accept failed:%v", err)  
          continue  
       }  
       go process(client)  
    }  
}

浏览器给服务器发送一个报文:ver版本号,nmethods鉴权方式,methods每个鉴权方式的编码 auth函数是 SOCKS5 代理服务器实现的一部分,用于处理客户端发来的认证请求和协议版本协商。以下是代码逐步解析:

func auth(reader *bufio.Reader, conn net.Conn) (err error)
  • 参数:
    • reader: 用于从客户端读取数据的 bufio.Reader
    • conn: 与客户端的网络连接,用于通信。
    • err: 如果认证过程中出现错误,会返回一个 error 类型的描述信息。

auth 函数的主要作用是:

  1. 解析客户端发送的协议版本和认证方式信息。
  2. 确保协议版本是 SOCKS5 (即 0x05)。
  3. 向客户端返回确认消息,表明认证方式支持。

1. 读取协议版本

ver, err := reader.ReadByte()
if err != nil {
    return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
    return fmt.Errorf("not supported ver:%d", ver)
}
  • reader.ReadByte():
    • 读取客户端发送的第一个字节,表示协议版本。
    • SOCKS5 的版本号是 0x05,因此需要进行版本校验。
  • 错误处理:
    • 如果读取失败,返回错误信息。
    • 如果版本不是 0x05,返回 "不支持的版本" 错误。

2. 读取认证方法数量

methodSize, err := reader.ReadByte()
if err != nil {
    return fmt.Errorf("read methodSize failed:%w", err)
}
  • methodSize:
    • 读取第二个字节,表示客户端支持的认证方法数量。
    • 此值决定了接下来需要读取的认证方法字节数。

3. 读取具体的认证方法

method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
    return fmt.Errorf("read method failed:%w", err)
}
log.Println("ver:", ver, "method:", method)
  • make([]byte, methodSize):
    • 创建一个长度为 methodSize 的字节切片,用于存储客户端支持的认证方法。
  • io.ReadFull:
    • reader 中读取 methodSize 个字节并填充到 method 中。
    • 如果未能读取到足够的字节,返回错误。
  • log.Println:
    • 打印读取到的版本号和支持的认证方法,方便调试。

4. 发送响应

_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
    return fmt.Errorf("write failed:%w", err)
}
  • conn.Write:
    • 向客户端发送一个响应:
      • 第一个字节是协议版本号 socks5Ver (0x05)。
      • 第二个字节是选中的认证方法 (0x00 表示 "无认证")。
  • 错误处理:
    • 如果写入失败,返回错误信息。

请求阶段

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:%d", ver)  
    }  
    if cmd != cmdBind {  
       return fmt.Errorf("not supported cmd:%d", cmd)  
    }  
    addr := ""  
    switch atyp {  
    case atypeIPV4:  
       _, err = io.ReadFull(reader, buf)  
       if err != nil {  
          return fmt.Errorf("read atype:ipv4 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 atype:host 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 fmt.Errorf("IPv6: %d not supported", atyp)  
    default:  
       return fmt.Errorf("invalid atype:%d", atyp)  
    }  
    _, err = io.ReadFull(reader, buf[:2])  
    if err != nil {  
       return fmt.Errorf("read port failed:%w", err)  
    }  
    port := binary.BigEndian.Uint16(buf[:2])  
    log.Println("addr:", addr, "port:", 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{socks5Ver, 0x00, 0x00, atypeIPV4, 0, 0, 0, 0, 0, 0})  
    if err != nil {  
       return fmt.Errorf("write failed:%w", err)  
    }  
    return nil  
}  

connect 函数是 SOCKS5 协议的关键部分,负责解析客户端请求的目标地址和端口,并返回是否允许连接。以下是详细的逐步解析:

代码逐步解析

1. 读取请求头

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]
  • buf[0] (ver) 版本号,必须是 0x05(SOCKS5 协议)。
  • buf[1] (cmd) 客户端的请求类型:
    • 0x01:CONNECT(连接目标地址)。
    • 0x02:BIND(绑定端口)。
    • 0x03:UDP ASSOCIATE(UDP 转发)。
  • buf[3] (atyp) 地址类型,定义目标地址的格式:
    • 0x01:IPv4 地址。
    • 0x03:域名。
    • 0x04:IPv6 地址。

检查是否支持:

if ver != socks5Ver {
    return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
    return fmt.Errorf("not supported cmd:%v", cmd)
}
  • 如果版本不是 SOCKS5(0x05)或请求类型不是 CONNECT(0x01),则返回错误。

2. 解析目标地址

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 地址 (atypeIPV4):

  • 从客户端读取 4 个字节,表示目标地址。
  • 格式化为字符串形式,例如 192.168.1.1

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)

域名 (atypeHOST):

  • 先读取 1 个字节,表示域名长度。
  • 根据长度读取实际域名内容,例如 "example.com"

case atypeIPV6:
    return errors.New("IPv6: no supported yet")

IPv6 地址 (atypeIPV6):

  • IPv6 地址需要 16 个字节。当前代码不支持 IPv6 地址,直接返回错误。

3. 解析目标端口

_, err = io.ReadFull(reader, buf[:2])
if err != nil {
    return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2])
  • 读取目标端口: 从客户端读取 2 个字节,表示端口号。
  • 字节序转换: 使用 binary.BigEndian.Uint16 将两字节转换为整数值。例如,[0x00, 0x50] 转换为 80(十进制)。

4. 打印目标地址和端口

log.Println("dial", addr, port)
  • 输出日志,记录代理请求的目标地址和端口。例如:dial example.com 80

5. 响应客户端

_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
    return fmt.Errorf("write failed: %w", err)
}
  • 响应格式: diff代码解读复制代码+----+-----+-------+------+----------+----------+ |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | +----+-----+-------+------+----------+----------+
    • VER (1字节): 协议版本号,0x05
    • REP (1字节): 回复字段,0x00 表示成功。
    • RSV (1字节): 保留字段,必须为 0x00
    • ATYP (1字节): 地址类型,0x01(IPv4)。
    • BND.ADDR (4字节): 代理服务器绑定的地址(这里为 0.0.0.0)。
    • BND.PORT (2字节): 代理服务器绑定的端口(这里为 0)。

relay阶段

func Copy(dst Writer, src Reader) (written int64, err error)
//从src死循环拷贝到dst
dest, err := net.Dial("tcp", fmt.Sprintf("%s:%d", addr, port))  
if err != nil {  
    return fmt.Errorf("dial dst failed:%w", err)  
}  
defer dest.Close()  
log.Println("addr:", addr, "port:", 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{socks5Ver, 0x00, 0x00, atypeIPV4, 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() {  
    _, err := io.Copy(dest, reader)  
    if err != nil {  
       log.Printf("proxy read failed:%v", err)  
    }  
    cancel()  
}()  
go func() {  
    _, err := io.Copy(conn, dest)  
    if err != nil {  
       log.Printf("proxy write failed:%v", err)  
    }  
    cancel()  
}()  
<-ctx.Done()
  1. 解析连接请求: 按照 SOCKS5 协议规范,解析客户端发来的目标地址和端口。
  2. 连接目标地址: 使用 net.Dial 与目标地址建立 TCP 连接。
  3. 返回响应: 告诉客户端代理服务器已准备好进行数据转发。
  4. 启动数据转发: 实现客户端与目标服务器之间的双向数据传输。

1. 解析请求头

前面解析头部的部分和原始代码一样,步骤如下:

  1. 确认版本号 ver 是否为 0x05
  2. 确认命令 cmd 是否为 CONNECT 请求(0x01)。
  3. 根据地址类型 atyp 解析目标地址:
    • IPv4 地址: 读取 4 字节并格式化为 x.x.x.x
    • 域名: 先读取域名长度,再读取域名字符串。
    • IPv6 地址: 返回不支持的错误。
  4. 读取目标端口号(2 字节,大端序)。

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)
  • net.Dial 创建与目标服务器的 TCP 连接,地址格式为 目标地址:目标端口
  • 错误处理: 如果连接失败,返回错误,代理无法继续。
  • 日志: 打印目标地址和端口以便调试。

3. 返回响应给客户端

_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
  • 成功响应格式:
  • 目的: 通知客户端,代理服务器已准备好转发数据。
+----+-----+-------+------+----------+----------+ 

|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 

+----+-----+-------+------+----------+----------+

4. 实现数据转发

数据转发的核心逻辑是通过两个协程实现双向数据流

  • 一个方向是从客户端到目标服务器。
  • 另一个方向是从目标服务器到客户端。

创建上下文

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
  • context.WithCancel 创建一个可取消的上下文 ctx,用于监听数据传输的结束。
  • cancel() 在任何一方结束时调用取消函数,终止数据转发。

双向转发

go func() {
    _, _ = io.Copy(dest, reader) // 从客户端读取数据并发送到目标服务器
    cancel()                     // 数据传输结束时取消上下文
}()
go func() {
    _, _ = io.Copy(conn, dest)   // 从目标服务器读取数据并发送到客户端
    cancel()                     // 数据传输结束时取消上下文
}()
  • io.Copy 高效的数据拷贝工具,直接在两个流之间传输数据。
    • 第一个协程将客户端发来的数据转发给目标服务器。
    • 第二个协程将目标服务器的响应数据转发给客户端。
  • cancel() 任何一方数据传输结束后,取消上下文以停止其他协程。

等待上下文结束

<-ctx.Done()
  • ctx.Done() 阻塞直到上下文被取消,表示数据传输完成。
  • 作用: 确保程序在数据传输完成后正常返回。

数据流分析
  1. 客户端发送请求:
    • 客户端通过代理服务器发送数据。
    • 代理服务器将数据转发到目标服务器。
  2. 目标服务器响应:
    • 目标服务器处理请求并返回响应。
    • 代理服务器将响应转发回客户端。

双向传输完成后,连接关闭。

以上就是Socks5代理的全部讲解了!