Day2 socks5代理服务端搭建 | 青训营笔记

305 阅读4分钟

这是我参与「第五届青训营 」笔记创作活动的第 2 天 。

一、本堂课重点内容:

这篇笔记主要梳理socks5代理服务器的搭建流程。

二、详细知识点介绍

1.客户端与代理服务器之间的通信图解

image.png 客户端与服务端可以简单分为三个阶段:协商阶段,请求连接阶段,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()