这是我参与「第五届青训营」伴学笔记创作活动的第 4 天
本节重点
深刻理解SOCKS5代理服务器的TCP建连和auth阶段,并动手实现着两个阶段。
SOCKS5代理 - TCP echo server
由于整个服务器的整个实现是比较复杂的,我先不尝试直接实现,而是先实现一个简版的TCP echo server。
在main中,我们先用net.Listen去监听一个端口,返回一个server。
server, err := net.Listen("tcp", "127.0.0.1:1080")
接下来在一个死循环中,用server.Accept去接受一个请求,如果成功的话就会返回一个连接。接下来,我们会在一个process函数里面处理这个连接。
注意:这里process前会有一个go关键字,代表启动一个goroutine协程。
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}
下面重点是process函数的实现:
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
}
}
}
用bufio.NewReader去基于这个链接去创建一个只读的带缓冲的流,之后在一个for的死循环里面,使用reader.ReadByte去每次读一个字节,然后用conn.Write把字节写入(正常情况下是直接写入slice,这里使用[]byte包装一下做类型转化)。
注意:此处bufio.NewReader是一个带缓冲的流,那么意味着,reader.ReadByte看起来是一个字节一个字节读的,但是实际上,他在读一个字节时,会提前返回比如下一kB读取完毕,再接下来读剩下的字节时就速度非常快,几乎瞬间返回。
SOCKS5代理 - auth
接下来就要开始实现协议的第一步,认证阶段。 从此处开始代码将变得比较复杂。
首先回忆认证阶段的逻辑,第一步,我向服务器发送一个报文,这个报文有三个字段,第一个字段是version协议版本号,第二个字段,建权方式的数目,后面的话就是每个建权方式的编码,比如,00是不需要建权,02代表用户名密码建权。
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
前面两个报文都是单字节的,可以用ReadByte去读取一个字节,我们先读完版本号,读完版本号后检查是否正确。 接着读取methodsize亦为单个字节。
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
读完methodsize后,我们会用methodsize去创用一个method的一个缓冲区,然后用io.ReadFull去填充满,此时就成功地读到了三个字段,把它打印出来。
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)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
按照协议,我需要返回一个包,告诉浏览器选择那种建权方式。
此处我直接将包构造出来:
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}x
第一个是协议版本号v5,后面选择建权方式00(不需要认证).
阶段小结
本节我动手实现了简单的TCP echo server,并且完成auth阶段的代码实现,加入TCP echo server代码中,虽然现在curl命令肯定是不成功的,我只实现了第一步,离完整实现差得还比较远。但是我看运行情况的话,是能够打印出来version和method两个字段的,这说明我当前的实现是正确的!