SOCKS5代理服务器的动手实现(上) | 青训营笔记

107 阅读3分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 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两个字段的,这说明我当前的实现是正确的!