本文作为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 函数的主要作用是:
- 解析客户端发送的协议版本和认证方式信息。
- 确保协议版本是 SOCKS5 (即
0x05)。 - 向客户端返回确认消息,表明认证方式支持。
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()
- 解析连接请求: 按照 SOCKS5 协议规范,解析客户端发来的目标地址和端口。
- 连接目标地址: 使用
net.Dial与目标地址建立 TCP 连接。 - 返回响应: 告诉客户端代理服务器已准备好进行数据转发。
- 启动数据转发: 实现客户端与目标服务器之间的双向数据传输。
1. 解析请求头
前面解析头部的部分和原始代码一样,步骤如下:
- 确认版本号
ver是否为0x05。 - 确认命令
cmd是否为 CONNECT 请求(0x01)。 - 根据地址类型
atyp解析目标地址:- IPv4 地址: 读取 4 字节并格式化为
x.x.x.x。 - 域名: 先读取域名长度,再读取域名字符串。
- IPv6 地址: 返回不支持的错误。
- IPv4 地址: 读取 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(): 阻塞直到上下文被取消,表示数据传输完成。- 作用: 确保程序在数据传输完成后正常返回。
数据流分析
- 客户端发送请求:
- 客户端通过代理服务器发送数据。
- 代理服务器将数据转发到目标服务器。
- 目标服务器响应:
- 目标服务器处理请求并返回响应。
- 代理服务器将响应转发回客户端。
双向传输完成后,连接关闭。
以上就是Socks5代理的全部讲解了!

Comments NOTHING