Go语言学习笔记:简单的 SOCKS5 代理服务器实现

275 阅读4分钟

SOCKS5 协议简介

SOCKS5 是一种网络代理协议,广泛用于穿透防火墙、隐藏客户端 IP 等场景。它工作在会话层,支持多种认证方式和命令类型。

SOCKS5 协议的基本流程:

  1. 握手阶段:客户端向服务器发送支持的认证方法列表,服务器选择一种认证方式并回应。
  2. 认证阶段(可选):如果需要认证,进行相应的认证操作。
  3. 请求阶段:客户端发送连接请求,包含目标地址和端口。
  4. 应答阶段:服务器回应连接结果,并开始数据转发。

实现步骤

1. 创建 TCP 监听器

首先,在指定的端口上监听来自客户端的连接请求。

listener, err := net.Listen("tcp", ":1080")
if err != nil {
    log.Fatalf("监听失败: %v", err)
}
defer listener.Close()
log.Println("SOCKS5 代理服务器正在运行,监听端口 1080")

2. 接受客户端连接

使用 listener.Accept() 方法接受新的客户端连接,并为每个连接启动一个新的协程进行处理。

for {
    conn, err := listener.Accept()
    if err != nil {
        log.Printf("接受连接失败: %v", err)
        continue
    }
    go handleConn(conn)
}

3. 处理 SOCKS5 握手

客户端会先发送握手请求,包含 SOCKS 协议版本和支持的认证方法。服务器需要读取该信息并回应。

func handleConn(conn net.Conn) {
    defer conn.Close()

    // 读取握手请求
    buf := make([]byte, 262)
    _, err := conn.Read(buf)
    if err != nil {
        log.Printf("读取握手数据失败: %v", err)
        return
    }
    if buf[0] != 0x05 {
        log.Printf("不支持的协议版本: %v", buf[0])
        return
    }
    // 回复不需要认证
    _, err = conn.Write([]byte{0x05, 0x00})
    if err != nil {
        log.Printf("发送握手回复失败: %v", err)
        return
    }
    // 继续处理...
}

4. 读取客户端请求

客户端会发送请求细节,包含请求的命令(例如 CONNECT)、目标地址类型、目标地址和端口。

_, err = conn.Read(buf)
if err != nil {
    log.Printf("读取请求失败: %v", err)
    return
}
if buf[0] != 0x05 {
    return
}
// 只处理 CONNECT 命令
if buf[1] != 0x01 {
    log.Printf("不支持的命令: %v", buf[1])
    return
}

5. 解析目标地址

根据地址类型(IPV4、域名、IPV6),解析目标地址和端口。

var addr string
switch buf[3] {
case 0x01:
    // IPv4
    ip := net.IP(buf[4:8]).String()
    port := int(buf[8])<<8 | int(buf[9])
    addr = net.JoinHostPort(ip, fmt.Sprintf("%d", port))
case 0x03:
    // 域名
    domainLen := int(buf[4])
    domain := string(buf[5 : 5+domainLen])
    port := int(buf[5+domainLen])<<8 | int(buf[6+domainLen])
    addr = net.JoinHostPort(domain, fmt.Sprintf("%d", port))
case 0x04:
    // IPv6
    ip := net.IP(buf[4:20]).String()
    port := int(buf[20])<<8 | int(buf[21])
    addr = net.JoinHostPort(ip, fmt.Sprintf("%d", port))
default:
    log.Printf("未知的地址类型: %v", buf[3])
    return
}

6. 与目标服务器建立连接

使用解析出的地址,与目标服务器建立 TCP 连接。

destConn, err := net.Dial("tcp", addr)
if err != nil {
    log.Printf("连接目标服务器失败: %v", err)
    // 响应失败
    conn.Write([]byte{0x05, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    return
}
defer destConn.Close()
// 响应成功
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
    log.Printf("发送连接成功回复失败: %v", err)
    return
}

7. 转发数据

成功建立连接后,开始在客户端和目标服务器之间转发数据。

go io.Copy(destConn, conn)
io.Copy(conn, destConn)

测试代理服务器

1. 运行代理服务器

go run main.go

image.png

2. 使用 curl 测试

curl -I -x socks5://127.0.0.1:1080 https://www.douyin.com/

image.png 注意:使用 socks5h:// 可以确保 DNS 解析通过 SOCKS5 代理完成,避免出现协议版本错误。

常见问题

1. 不支持的协议版本

如果收到类似 不支持的协议版本: 71 的错误,可能是因为客户端没有正确使用 SOCKS5 协议。确保使用了正确的代理协议前缀,如 socks5h://

2. 未使用的变量错误

在 Go 中,声明的变量必须被使用。如果不需要某个返回值,可以使用 _ 忽略。

_, err := conn.Read(buf)

总结

通过本次学习,我们了解了 SOCKS5 代理服务器的基本原理,并使用 Go 语言实现了一个简单的代理服务器。这个过程涉及到 TCP 网络编程、并发处理以及协议的解析,实现过程中需要注意数据的读取和解析顺序,以及错误的处理。