这是我参与「第五届青训营 」笔记创作活动的第 2 天 。
一、本堂课重点内容:
这篇笔记主要梳理socks5代理服务器的搭建流程。
二、详细知识点介绍
1.客户端与代理服务器之间的通信图解
客户端与服务端可以简单分为三个阶段:协商阶段,请求连接阶段,relay阶段。所以代码也将从这三个阶段一一实现。
2.基本服务端搭建
server, err := net.Listen("tcp", "127.0.0.1:1080")
服务端开启监听,第一个参数为传输协议,第二个参数为监听的地址+端口号
client, err := server.Accept()
开始接收客户端的连接,连接成功后可以开启一个子协程进行该客户的处理,下面定义为process()函数,传入一个net.Conn对象,即为上文中的client变量。
3.客户Conn对象消息的处理
①读入消息
reader := bufio.NewReader(conn)//conn类型为net.Conn
创建一个读入流,用于读入客户端发来的数据
Reader对象用于读入的方法:
- ReadByte() (c byte,err error) - 读取并返回一个字节
- ReadBytes(delim byte) (line []byte,err error) - 读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的切片。
- ReadString(delim byte) (line string, err error) - 读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的字符串。
io包下的读入函数
- io.ReadFull(r Reader, buf []byte) (n int, err error) - ReadFull从r精确地读取len(buf)字节数据填充进buf。函数返回写入的字节数和错误(如果没有读取足够的字节)。只有没有读取到字节时才可能返回EOF;如果读取了有但不够的字节时遇到了EOF,函数会返回ErrUnexpectedEOF。 只有返回值err为nil时,返回值n才会等于len(buf)。
②写出消息
writer := bufio.NewWriter(conn)
创建一个写出流
Writer对象下的方法:
- Write(p []byte) (nn int, err error) - 将p的内容写入缓冲。返回写入的字节数。如果返回值nn < len(p),还会返回一个错误说明原因。
- WriteString(s string) (nn int, error) - 写入一个字符串。返回写入的字节数。如果返回值nn < len(s),还会返回一个错误说明原因。
- WriteByte(c byte) error - 写入单个字节。
2.协商阶段
协商阶段主要判断客户请求的是否是socks5协议,如果不是则拒绝请求。
①协商阶段用户发送来的参数:
+----+----------+----------+
|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
判断协议是否为socks5,只需要验证读入的第一个字节是否为0x05即可;若是,则对下面的两个参数进行读取即可。
②通过协商
_, err = conn.Write([]byte{socks5Ver, 0x00}) //socks5=0x05
将两个参数传回客户端表示协商成功。
3.请求连接阶段
①读入参数
+----+-----+-------+------+----------+----------+
|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个字节
前面五个参数依然是正常读入即可,最后一个端口参数读入后要进行二进制转换:
port := binary.BigEndian.Uint16(buf[:2])
②请求连接
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", 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})
4.relay阶段
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()