(续接上文)
当客户端与Socks5代理服务器TCP握手完成后,会进行一次初始化报文发送,来确认版本协议以及认证方式,也就是auth函数。函数输入只读流reader和Conn链接(TCP链接)
协商阶段:
浏览器给代理服务器发送一个报文,报文中包含三个字段:第一个字段协议版本号VER。socks5的固定版本号为0x05;第二个字段为鉴权方式的数目NMETHODS,也就是支持认证的方法数量;第三个字段是METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。
METHODS 客户端支持的认证方式列表,每个方法占1字节。0x00表示不需要认证,0x01表示GSSAPI,0x02表示用户名、密码认证;0x80-0xFE是为私人方法保留,0xFF表示无可接受的方法。
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
const socks5Ver = 0x05
// +----+----------+----------+
// |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
在auth函数中,我们首先要读取报文信息,通过ReadByte()将reader中的信息先读取一个字节。如果读取信息失败,则返回读取失败信息。如果版本号VER不是socks5Ver:0x05,则返回版本信息错误,并关闭链接。如果成功,则用ReadByte()继续读取一个字节methodSize,失败则返回方法大小错误,若成功,则用methodSize去创建一个method的一个缓冲区,并通过使用io.ReadFull()将缓冲区填满。到目前为止,我们已经读到报文的全部三个字段,我们可以将上述得到的信息打印出来。
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
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)
}
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)
}
return nil
}