Go语言的实战案例三 | 青训营笔记

102 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

demo3. Proxy

0. Socks5

Pasted image 20230122192229.png Socks5: SOCKS - Wikipedia

1. 客户端发送认证信息

SOCKS5 比 SOCKS4a 多了IPv6、UDP等支持。创建与 SOCKS5 服务器的 TCP 连接后客户端需要先发送请求来确认协议版本及认证方式,格式为(以字节为单位):

VERNMETHODSMETHODS
111 to 255
  • VER 是 SOCKS 版本,这里应该是 0x05
  • NMETHODS 是 METHODS 部分的长度;
  • METHODS 是客户端支持的认证方式列表,每个方法占 1 字节。例如:
    • 0x00 不需要认证
    • 0x02 用户名、密码认证

2. 服务端返回认证信息

服务器从客户端提供的方法中选择一个并通过以下消息通知客户端(以字节为单位):

VERMETHOD
11
  • VER 是 SOCKS 版本,这里应该是 0x05
  • METHOD 是服务端选中的方法。如果返回 0xFF 表示没有一个认证方法被选中,客户端需要关闭连接
The values currently defined for METHOD are:
    -  X'00' NO AUTHENTICATION REQUIRED
    -  X'01' GSSAPI
    -  X'02' USERNAME/PASSWORD
    -  X'03' to X'7F' IANA ASSIGNED
    -  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
    -  X'FF' NO ACCEPTABLE METHODS

3. 客户端发送请求信息

认证结束后客户端就可以发送请求信息。如果认证方法有特殊封装要求,请求必须按照方法所定义的方式进行封装。

SOCKS5 请求格式(以字节为单位):

VERCMDRSVATYPDST.ADDRDST.PORT
11X'00'1Variable2
  • 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 剩余的内容为域名
    • 0x04 IPv6 地址,16 个字节长度
  • DST.ADDR 目的地址
  • DST.PORT 网络字节序表示的目的端口

4. 服务端回应请求信息

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

VERREPRSVATYPBND.ADDRBND.PORT
11X'00'1Variable2
  • VER 是 SOCKS 版本,这里应该是 0x05
  • REP 应答字段,例如:
    • 0x00 表示成功
    • 0x01 SOCKS 服务器连接失败
  • RSV 0x00,保留
  • ATYP 指明 BND.ADDR 类型,内容同上
  • BND.ADDR 服务器绑定的地址
  • BND.PORT 服务器绑定的端口

1. Socks5 服务端

  1. 启动服务

    // 启动一个监听 localhost:1080 的 net 服务
    server, err := net.Listen("tcp", "127.0.0.1:1080") 
    
  2. 监听服务

    for {  
       client, err := server.Accept()  // 接收到请求信息
       if err != nil {  
          log.Printf("Accept failed %v", err)  
          continue  
    	}  
    	   go process(client)  // 启动协程处理请求
    }
    
  3. 处理请求

    func process(conn net.Conn) {  
       defer conn.Close()  // 在函数结束时关闭连接
       reader := bufio.NewReader(conn)  // 新建一个 bufio.Reader 流
       err := auth(reader, conn)  // 认证 socks5 连接 
       err = connect(reader, conn)  // 连接 socks5 客户端
    }
    
  4. 认证 socks5 连接

    func auth(reader *bufio.Reader, conn net.Conn) (err error) {  
    	ver, err := reader.ReadByte()  // 从流中读取 1 字节
    	if ver != 0x05 {  // 验证该字节是否等于 0x05
    		return fmt.Errorf("not supported ver:%v", ver)  
    	}  
    	methodSize, err := reader.ReadByte()  // 再读取 1 字节, 该字节表示 method 的大小
    	method := make([]byte, methodSize)  // 建立一个切片,大小为 methodSize
    	_, err = io.ReadFull(reader, method)  // 从流中读取数据,直到填满 method 切片
    	_, err = conn.Write([]byte{0x05, 0x00}) // 向连接中写入两字节数据,第一个字节表示 socks 版本,第二个字节表示认证方式
    	return nil  
    }
    
  5. 连接 socks5 客户端

    func connect(reader *bufio.Reader, conn net.Conn) (err error) {  
    	buf := make([]byte, 4)  // 建立 4 字节的切片作为缓冲
    	_, err = io.ReadFull(reader, buf)  // 从流中读取 4 字节
    	 
    	ver, cmd, atyp := buf[0], buf[1], buf[3]  // 读取该缓冲中的有效信息
    	if ver != 0x05 {  // 验证是否为 socks5
    		return fmt.Errorf("not supported ver:%v", ver)  
    	}  
    	
    	if cmd != 0x01 {  // 是否为 connect 请求
    		return fmt.Errorf("not supported cmd:%v", ver)  
    	}  
    	
    	addr := ""  
    	switch atyp { }  // 根据 atyp 处理地址信息
    	
    	_, err = io.ReadFull(reader, buf[:2])  // 从流中读入 2 字节
    	port := binary.BigEndian.Uint16(buf[:2])  // 将这 2 字节转为 uint16 表示端口信息
    	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))  // 与主机建立连接
    	defer dest.Close()  在函数结束时关闭 net 连接
    
    	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})  // 将 socks5 服务端的应答信息返回给客户端
    
    	ctx, cancel := context.WithCancel(context.Background())  // 建立上下文信息,相当于 wait := sync.WaitGroup{}
    	defer cancel()  
    	go func() {  
    		_, _ = io.Copy(dest, reader)  // 从主机拷贝数据到服务端
    		cancel()  // 通知主进程该协程的结束
    	}()  
    	go func() {  
    		_, _ = io.Copy(conn, dest)  // 从服务端拷贝数据给客户端
    		cancel()  
    	}()  
    	<-ctx.Done()  // 用于阻塞主进程的关闭,等待协程执行的完成
    	return nil