(续接上文)
按照协议,我们需要给浏览器返回一个数据包,数据包中包含上述过程我们已经读取到的信息。在这里,我们就直接创建一个了。包中第一个字节是版本号,第二个字节是METHOD。这里我们分别选择0x05和0x00.
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
3.请求阶段
在之前的process函数中,当鉴权成功后,我们会调用connect函数,下面我们就来介绍一下connect函数实现了什么功能。
connect函数与上述Auth函数具有相同的签名。
// +----+-----+-------+------+----------+----------+
// |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个字节
请求阶段中浏览器会发送一个报文,报文里面一共包含六个字段,如上图所示。第一个字段VER版本号,socks5的值为0x05,第二个字节CMD值为0x01表示CONNECT请求,第三个字节RSV是保留字段,值为0x00,第四个字节ATYP是目标地址类型,DST.ADDR的数据对应这个字段的类型。第五个字节DST.ADDR是一个可变长度的值。最后,第六个字节DST.PORT是目标端口,固定2个字节。
读取这个报文,我们可以先创建一个4个字节大小的缓冲区,并通过io.ReadFull对其填充,则我们可以一次性读取到4个字节。对于不同的字节,每个字节在一个数组里,可以直接获取。ver, cmd, atyp := buf[0], buf[1], buf[3]。
针对不同类型的ATYP,有不同的处理方式。
- 如果是atypeIPV4类型的话,我们需要读取4个字节,并将这4个字节按照ip地址的方式进行填充,即X.X.X.X(其中每一个X为读取到的一个字节)。
- 如果是
atypeHOST类型,那么我们先读一个字节 host 的长度,然后再 make 一个对应长度的一个字符串,然后再用io.ReadFull(reader, host)把它填充满,填充满之后把它转换成一个字符串即可。