GO语言工程实践课后作业:实现思路、代码以及路径记录 | 豆包MarsCode AI刷题

166 阅读5分钟

GO语言实战案例(三)


Proxy

对 Go 语言实现的 SOCKS5 代理服务器的四个版本进行详细分析,展示如何从一个简单的回显服务器开始,逐步扩展功能,最终实现一个完整的 SOCKS5 代理服务。


第一段代码:基础 TCP 服务器

package main

import (
	"bufio"
	"log"
	"net"
)

func main() {
	server, err := net.Listen("tcp", "127.0.0.1:1080")
	if err != nil {
		panic(err)
	}
	for {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept failed %v", err)
			continue
		}
		go process(client)
	}
}

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
		}
	}
}

1.1. 代码结构分析

这是一个最基本的 TCP 代理服务器实现。它在本地的 127.0.0.1:1080 地址上监听 TCP 请求,并为每一个连接创建一个新的 goroutine 来处理。该服务器只是简单地实现了一个“回显”功能,即接收到的数据会被原封不动地写回到客户端。

主要功能:

  • 使用 net.Listen 在指定端口上监听连接。
  • 对每一个连接,使用 bufio.NewReader 来读取客户端的数据。
  • 数据会被简单地读取(通过 ReadByte)并通过 Write 方法返回给客户端。

问题:

  • 此代码没有对客户端请求做任何协议处理,它仅仅实现了一个 TCP 回显服务器,不具备 SOCKS5 协议的任何功能。

1.2. 代码扩展需求

为了使其成为一个 SOCKS5 代理服务器,我们需要添加 SOCKS5 协议的握手过程、认证方式、命令解析以及目标连接功能。接下来,我们将逐步在此基础上进行迭代。


第二段代码:实现 SOCKS5 协议握手

在第二段代码中,基本结构保持不变,但增加了 SOCKS5 协议的握手和认证部分。

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
	server, err := net.Listen("tcp", "127.0.0.1:1080")
	if err != nil {
		panic(err)
	}
	for {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept failed %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
	}
}

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)
	}

	// Respond with no authentication required
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

2.1. 握手协议解析

在这段代码中,我们开始实现了 SOCKS5 协议的握手过程。

握手过程:

  • 客户端首先向服务器发送一个版本号(VER),后面紧跟着一个表示支持的认证方法数量的字节(NMETHODS),以及这些认证方法的字节(METHODS)。
  • 服务器收到后,判断是否支持这些认证方法,并回复一个版本号(VER)和一个选择的认证方法(METHOD)。

代码解读:

  • 通过 reader.ReadByte() 读取客户端发送的协议版本(VER)。
  • 服务器检查版本是否为 SOCKS5(0x05),并回应客户端支持的认证方法。此代码实现了最简单的“不需要认证”方法,即 0x00
  • 通过 conn.Write([]byte{socks5Ver, 0x00}) 向客户端发送握手完成的确认信息。

2.2. 不足之处

  • 目前代码只实现了握手部分,缺少对客户端后续请求的处理(如 CONNECT 命令解析)。
  • 对于 METHODS 中的其他认证方法(如用户名密码认证)并没有做任何处理。

第三段代码:支持 CONNECT 命令

接下来,我们添加了 SOCKS5 协议中的 CONNECT 命令处理逻辑,使服务器能够根据客户端请求的目标地址和端口建立连接。

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", cmd)
	}

	// Process address
	// Additional logic for IPv4, host, and port parsing...
}

3.1. CONNECT 命令解析

命令字段:

  • VER: 协议版本,固定为 0x05
  • CMD: 客户端请求的操作,0x01 表示 CONNECT 命令,要求建立一个 TCP 连接。
  • ATYP: 地址类型,0x01 表示 IPv4 地址,0x03 表示域名,0x04 表示 IPv6 地址。
  • DST.ADDRDST.PORT: 目标地址和目标端口。

此代码通过 reader.ReadFull() 读取请求头部信息,并判断客户端请求是否为 CONNECT 命令。如果是,就进一步解析目标地址和端口。

3.2. 新增功能

  • 支持根据不同地址类型(IPv4 和域名)解析目标地址。
  • 通过 net.Dial() 建立与目标服务器的连接。

第四段代码:数据转发与完整代理功能

在最后一个版本中,我们加入了数据转发功能,确保客户端与目标服务器之间能够双向传输数据。

// Handle bidirectional data forwarding
go func() {
	_, _ = io.Copy(dest, reader)
	cancel()
}()
go func() {
	_, _ = io.Copy(conn, dest)
	cancel()
}()

4.1. 数据转发

此版本实现了 SOCKS5 代理的核心功能——数据转发。通过 io.Copy,服务器在客户端和目标服务器之间建立了数据流通道。当客户端请求成功时,服务器将客户端和目标服务器之间的通信数据进行转发。

代码实现:

  • 使用 go func() 启动两个并发 goroutine,将客户端请求数据转发到目标服务器,反之亦然。
  • 使用 context.WithCancel 来确保当其中一个数据流关闭时,另一个流也能够及时终止。

4.2. 增强功能

  • 完整支持 SOCKS5 CONNECT 命令。
  • 实现了双向数据转发,确保客户端和目标服务器的通信无缝进行。

完整代码

总结

通过四个版本的迭代,我们从一个简单的回显服务器逐步演进为一个功能完备的 SOCKS5 代理服务器。每次迭代都针对 SOCKS5 协议的不同部分进行扩展,最终实现了协议握手、命令解析、目标连接以及数据转发等核心功能。通过这样的实践,不仅能更好地理解 SOCKS5 协议的工作原理,还能掌握 Go 语言处理网络通信的基本技巧。