这是我参与「第五届青训营」伴学笔记创作活动的第 17 天
SOCKS5代理
实现一个 SOCKS5 代理服务器的简单版本。
前言
通过之前对于Go语言的学习,我上手学习和完善实战的例子,文本主要内容:SOCKS5代理
本文作为笔记,记录重要的内容和个人理解,具体仍需要看ppt和实操。
原理
1 SOCKS5代理——TCP echo server
-
创建一个
TCP
的服务器并一直监听ip和对应端口号:server, err := net.Listen("tcp", "127.0.0.1:1080")
-
for死循环接收客户端请求:
client, err := server.Accept()
-
goroutine将客户端发送来的请求进行循环读取并且将读取数据原样返回给客户端:
go process(client)
- process函数:
- 由于Goroutine 是 Go 语言中的轻量级线程,它可以在单个进程中并发执行多个任务。通过 goroutine 处理这个连接,可以无需等待当前的客户端处理完毕断开后再处理下一个
- 优点:能处理多个客户端,也能提高服务器的吞吐量
-
利用
nc ip地址 对应端口号
进行tcp连接 -
效果如图
-
连接后,客户端输入什么,就echo输入的内容回来
2 SOCKS5代理——auth
-
认证功能:
auth
函数。 -
在
process(conn net.Conn)
函数中使用 -
认证阶段的报文(client->server)
- 三个字段分别是:
- VER: 协议版本(我们需要的代理类型:socks5 为 0x05)、
- NMETHODS: 支持认证的方法数量
- METHODS: 对应的支持认证的方法
- NMETHODS 决定了他的长度
- X’00’ 表示不需要身份验证
- X’02’ 表示用户名/密码认证
- NMETHODS 决定了他的长度
- 三个字段分别是:
-
由于是认证功能,我们需要保证只要有一个地方出错,那它就是错误的,需要有错误信息的处理,并且断开连接
-
前三个字段通过
reader.ReadByte()
进行提取,提取后进行字段对比,并写好错误处理 -
_, err = conn.Write([]byte{socks5Ver, 0x00})
,回复给连接的客户端
运行了v2版本的程序并在终端执行 curl --socks5 127.0.0.1:1080 -v http://www.qq.com
,由下图和服务端2023/02/05 02:06:55 ver 5 method [0 1] 2023/02/05 02:06:55 auth success
可知,auth 成功
3 SOCKS5代理——请求
-
connect函数,参数是auth后的客户端,process函数中调用
-
基于auth正常后,客户端会发来下一个报文
- 字段:
- VER 版本号,socks5 的值为 0x05
- CMD 0x01 表示 CONNECT 请求
- RSV 保留字段,值为 0x00
- ATYP 目标地址类型,DST. ADDR 的数据对应这个字段的类型。 0x01 表示 IPv4 地址,DST. ADDR 为 4 个字节 0x03 表示域名,DST. ADDR 是一个可变长度的域名
- DST. ADDR 一个可变长度的值,对应的是地址或者域名
- DST. PORT 目标端口,固定 2 个字节
- 字段:
-
由于前四个字段VER、CMD、RSV、ATYP 长度固定,可以直接创建一个长度为 4 的 byte 缓冲区,一次将这四个字段读取进来,并验证他们的合法性。
- 关键代码:
- 容器:
buf := make([]byte, 4)
- 读取:
_, err = io.ReadFull(reader, buf)
- 字段提取:
ver, cmd, atyp := buf[0], buf[1], buf[3]
-
使用 switch 语句进入 ATYP 对应的流程来处理目标地址,存在多个类型,代码如下
addr := "" //读第五个字段 DST.ADDR
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) //slice
_, 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")
}
-
用上述类似的方法,获取到端口号
-
tcp连接(dial拨号连接到指定网络上的地址):
-
-
返回包(server->client)
- 包含以下字段:
- VER:SOCKS5版本,这里为 0x05
- REP:Relay field, 内容取值如下 0x00 succeeded
- RSV:保留字段
- ATYPE:地址类型
- BND. ADDR:服务绑定的地址
- BND. PORT:服务绑定的端口 DST. PORT
- 包含以下字段:
-
前面连接请求做完,需要有个返回包(自己构造写入一个,并做错误处理):
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) //server->client返回包
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
4 SOCKS5代理——relay 阶段
-
加在connect函数尾部
-
完善
connect
函数,进行双向数据转发- 建立与目标网站的连接
dest
, - 使用两个 协程 调用 io 包中的
Copy
函数,即可实现数据的双向转发。 - 为了避免主进程退出,我们要用到 WithCancel函数创造的
context
。- 原因:在 Goroutine 执行出错时即会调用
cancel()
,完成错误处理
- 原因:在 Goroutine 执行出错时即会调用
- <-ctx.Done():协程的异步,需要利用它来保证两个协程都能完成
- 建立与目标网站的连接
-
代码:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //报错使用cancel,没有多大用
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
5 实现与效果
- 启动最终版本的程序,然后在浏览器里面配置使用这个代理,此时我们打开网页。
- 代理服务器的日志,会打印出你访问的网站的域名或者
IP
,这说明我们的网络流量是通过这个代理服务器的。 - 同时,在命令行去测试我们的代理服务器,可以用 curl --socks5 + 代理服务器地址:端口号 -v 可用网址(即后面加一个可访问的 URL)
- 如果代理服务器工作正常的话,那么 curl 命令就会正常返回。
- 完整代码在github的proxy的v4,想学习可以看看
总结
本文主要剖析了SOCKS5代理这个实战项目。通过对这个项目的探究,我不仅温习了 Go 语言基础知识,还加深了对于tcp连接,认证,relay,context的认识,我认识到自己还有很多不足之处,还有许多要学习的地方,比如<-ctx.Done()
,用于控制goroutine的两个过程都走完,明天继续加油学习。
引用
PPT:Go 语言上手 - 基础语法 .pptx - 飞书云文档 (feishu.cn)