Go 语言上手-Socks5服务器的搭建| 青训营笔记

405 阅读7分钟

0x0 Pre

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记.

SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是"SOCKet Secure"的缩写。 当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到SOCKS4。最新协议是SOCKS5,与前一版本相比,增加支持UDP、验证,以及IPv6。 根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。SOCKS协定不提供加密(来自维基百科[2])。

在本章中会掌握以下几点:

  1. golang的基础使用
  2. 字节流读取
  3. context 包的使用
  4. socks5服务器的搭建

0x1 流的读取

golang中基础的读取流大致有三种:os,bufio,ioutil,以下分别举例读取文件的方式:

  1. OS 读取方式:
    file, err := os.Open("F:/a.txt")
    if err != nil {
        fmt.Println("打开文件失败")
        return
    }
    var data [1024]byte
    n, err := file.Read(data[:])
    if err != nil {
        fmt.Println("Read Error")
        return
    }
    if err == io.EOF{	// 读取完毕
        fmt.Println("Read Over")
        return
    }
    fmt.Println(n, string(data[:n]))
    file.Close()	// 重要
    
  2. bufio 读取
    func bufioRead(){
            file, err := os.Open("F:/win10应用.txt")
            if err != nil {
                    fmt.Println("文件打开失败", err)
                    return
            }
            defer file.Close()
            // 创建读取内容的对象
            reader := bufio.NewReader(file)
            line, err := reader.ReadString('\n')
            if err != nil{
                    fmt.Printf("err:%v\n", err)
                    return
            }
            if err == io.EOF{
                    fmt.Printf("%v\n", "Read Over")
                    return
            }
            fmt.Println(line)
    }
    
    通常情况下,bufio用于一行一行的读取流,使用reader.ReadString('\n')方法,以\n为分隔符进行读取到变量中。对于字节流数据想要读取一个 byte 可以使用方法 reader.ReadByte()方法。
  3. ioutil 读取
    func ioutilRead() {
            content, err := ioutil.ReadFile("F:/win10应用.txt")
            if err != nil {
                    fmt.Printf("\"File Read Err, err: %v\n", err)
                    return
            }
            fmt.Println(string(content))
    }
    
    ioutil 会自动帮忙进行打开和关闭文件,适用于读取整个文件。

0x2 context包

上下文 context.Context 在 Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。

  1. 使用 context 实现倒计时通信事件
    func main() {
       log.SetFlags(log.LstdFlags | log.Lmicroseconds)
       log.Printf("Start")
       ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
       defer cancel()
    
       go handle(ctx, 3*time.Second)
       select {
       case <-ctx.Done():
          log.Println("main", ctx.Err())
       }
       time.Sleep(1 * time.Second)
    }
    
    func handle(ctx context.Context, duration time.Duration) {
       select {
       case <-ctx.Done():
          log.Println("handle", ctx.Err())
       case <-time.After(duration):
          log.Println("process request with", duration)
       }
    }
    
    输出:
    2022/05/07 21:01:41.536615 Start
    2022/05/07 21:01:44.567277 process request with 3s
    2022/05/07 21:01:46.574656 main context deadline exceeded
    
    可见五秒后输出定时结果。
  2. 使用 context 实现父子协程之间的同步中断
    func main() {
       fatherCtx, cancel := context.WithCancel(context.Background())
       // 在父context下创建子context
       sonCtx, _ := context.WithCancel(fatherCtx)
    
       go fatherWatch(fatherCtx)
       go sonWatch(sonCtx)
    
       time.Sleep(3 * time.Second)
       log.Printf("Cancel All.")
       cancel()
       time.Sleep(time.Second)
    }
    
    func fatherWatch(ctx context.Context) {
       for true {
          select {
          case <-ctx.Done():
             log.Printf("Father exit")
             return
          default:
             log.Printf("Father is working")
             time.Sleep(time.Second)
          }
       }
    }
    
    func sonWatch(ctx context.Context) {
       for true {
          select {
          case <-ctx.Done():
             log.Printf("Son exit")
             return
          default:
             log.Printf("Son is working")
             time.Sleep(time.Second)
          }
       }
    }
    
    输出:
    2022/05/07 21:17:05 Father is working
    2022/05/07 21:17:05 Son is working
    2022/05/07 21:17:06 Father is working
    2022/05/07 21:17:06 Son is working
    2022/05/07 21:17:07 Son is working
    2022/05/07 21:17:07 Father is working
    2022/05/07 21:17:08 Cancel All.
    2022/05/07 21:17:08 Father exit
    2022/05/07 21:17:08 Son exit
    

0x3 搭建 socks5 服务器

一个sock5服务器需要进行两个阶段,验证(auth)和连接(connect):

Socks5服务器的验证

建立与SOCKS5服务器的TCP连接后客户端需要先发送请求来确认协议版本及认证方式,格式为(以字节为单位):

VERNMETHODSMETHODS
111-255
  • VER是SOCKS版本,这里应该是0x05;

  • NMETHODS是METHODS部分的长度;

  • METHODS是客户端支持的认证方式列表,每个方法占1字节。(我们选择不需要验证)

    • 0x00 不需要认证

    • 0x02 用户名、密码认证

之后服务器需要返回客户端选择的认定的认证方式:

VERMETHOD
11
  • VER是SOCKS版本,这里应该是0x05;
  • METHOD是服务端选中的方法。如果返回0xFF表示没有一个认证方法被选中,客户端需要关闭连接。

编写代码:

func auth(reader *bufio.Reader, conn net.Conn) error {
   ver, err := reader.ReadByte()
   if err != nil {
      return fmt.Errorf("read version number failed: %v\n", err)
   }
   if ver != socks5Ver {
      return fmt.Errorf("Not supported version: %v, only socks5\n", 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)
   }
   _, err = conn.Write([]byte{socks5Ver, 0x00})
   if err != nil {
      return fmt.Errorf("write failed:%w", err)
   }
   return nil
}

Socks5服务器的连接

当验证通过之后,就可以建立两者之间的TCP连接了。

客户端发送给服务器端的SOCKS5请求格式(以字节为单位):

VERCMDRSVATYPDST.ADDRDST.PORT
110x001动态2
  • VER是SOCKS版本,这里应该是0x05;

  • CMD是SOCK的命令码

    • 0x01表示CONNECT请求
    • 0x02表示BIND请求
    • 0x03表示UDP转发
  • RSV 0x00,保留

  • ATYP DST.ADDR类型

    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03 域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
    • 0x04 IPv6地址,16个字节长度。
  • DST.ADDR 目的地址

  • DST.PORT 网络字节序表示的目的端口

服务器按以下格式回应客户端的请求(以字节为单位):

VERREPRSVATYPBND.ADDRBND.PORT
110x001动态2
  • VER是SOCKS版本,这里应该是0x05;

  • REP应答字段

    • 0x00表示成功
    • 0x01普通SOCKS服务器连接失败
    • 0x02现有规则不允许连接
    • 0x03网络不可达
    • 0x04主机不可达
    • 0x05连接被拒
    • 0x06 TTL超时
    • 0x07不支持的命令
    • 0x08不支持的地址类型
    • 0x09 - 0xFF未定义
  • RSV 0x00,保留

  • ATYP BND.ADDR类型

    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
    • 0x04 IPv6地址,16个字节长度。
  • BND.ADDR 服务器绑定的地址

  • BND.PORT 网络字节序表示的服务器绑定的端口

按照以上编程可得:

const (
   cmdBind   = 0x1
   atypIPV4  = 0x1
   atypHOST  = 0x3
   atypIPV6  = 0x4
   socks5Ver = 0x5
)

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   buf := make([]byte, 4)
   _, err = io.ReadFull(reader, buf)
   if err != nil {
      return fmt.Errorf("read header failed:%w", err)
   }
   ver, cmd, atyp := buf[0], buf[1], buf[3]
   if ver != socks5Ver {
      return fmt.Errorf("not supported ver:%v", ver)
   }
   if cmd != cmdBind {
      return fmt.Errorf("not supported cmd:%v", ver)
   }
   addr := ""
   switch atyp {
   case atypIPV4:
      _, err = io.ReadFull(reader, buf)
      if err != nil {
         return fmt.Errorf("read atyp failed:%w", err)
      }
      addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
   case atypHOST:
      hostSize, err := reader.ReadByte()
      if err != nil {
         return fmt.Errorf("read hostSize failed:%w", err)
      }
      host := make([]byte, hostSize)
      _, err = io.ReadFull(reader, host)
      if err != nil {
         return fmt.Errorf("read host failed:%w", err)
      }
      addr = string(host)
   case atypIPV6:
      return errors.New("IPv6: no supported yet")
   default:
      return errors.New("invalid atyp")
   }
   _, err = io.ReadFull(reader, buf[:2])
   if err != nil {
      return fmt.Errorf("read port failed:%w", err)
   }
   port := binary.BigEndian.Uint16(buf[:2])

   dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
   if err != nil {
      return fmt.Errorf("dial dst failed:%w", err)
   }
   defer dest.Close()
   log.Println("dial", addr, port)
   _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
   if err != nil {
      return fmt.Errorf("write failed: %w", err)
   }
   ctx, cancel := context.WithCancel(context.Background())
   defer cancel()

   go func() {
      _, _ = io.Copy(dest, reader)
      cancel()
   }()
   go func() {
      _, _ = io.Copy(conn, dest)
      cancel()
   }()

   <-ctx.Done()
   return nil
}

Socks5服务器启动

func main() {
   server, err := net.Listen("tcp", "localhost:1080")    // 监听端口
   if err != nil {
      panic(err)
   }
   for {
      client, err := server.Accept() // 阻塞方法,等待客户端连接
      if err != nil {
         log.Printf("Accept client err: %v", err)
         continue
      }
      go process(client) // 交给另一个协程处理连接后的操作
   }
}

func process(conn net.Conn) {
   defer conn.Close()
   reader := bufio.NewReader(conn)
   err := auth(reader, conn)
   if err != nil {
      log.Printf("Client: %v auth failed: %v", conn.RemoteAddr(), err)
      return
   }
   log.Printf("Client(%v) auth pass.", conn.RemoteAddr())
   err = connect(reader, conn)
   if err != nil {
      log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
      return
   }
}

参考:

  1. Go 语言并发编程与 Context
  2. SOCKS - 维基百科,自由的百科全书 (wikipedia.org)