Go语言高阶实战:通过Socks5代理实现的抓包功能|青训营

高能预警:本实战案例难度较高,涉及计算机网络相关知识,建议学有余力者实践。

前言

Socks5协议的工作原理

Socks5(Socket Secure 5)是一种网络协议,用于在客户端和服务器之间进行代理传输。它是Socks协议的第五个版本,相比于早期版本,Socks5提供了更多的功能和安全性。

Socks5协议的原理如下:

建立连接

客户端与Socks5代理服务器之间建立TCP连接。

认证阶段

客户端发送认证方法的请求给代理服务器,包括支持的认证方法列表。常见的认证方法有无认证(No Authentication),用户名密码认证(Username/Password Authentication)等。

选择认证方法

代理服务器从客户端发送的认证方法列表中选择一种支持的方法,并发送给客户端确认。

认证过程

如果选择了用户名密码认证方法,客户端会发送用户名和密码给代理服务器进行认证。认证成功后,进入下一阶段。

请求阶段

客户端发送代理请求给Socks5代理服务器,包括目标服务器的地址和端口号。

响应阶段

代理服务器接收到客户端的请求后,会解析目标服务器的地址和端口号,并尝试与目标服务器建立连接。

建立连接

代理服务器与目标服务器之间建立TCP连接。

响应客户端

代理服务器将与目标服务器建立的连接的结果发给客户端,包括成功或失败的信息。

数据传输

如果连接建立成功,客户端可以通过代理服务器与目标服务器进行数据传输。客户端发送的数据会经过代理服务器转发给目标服务器,并将目标服务器的响应数据返回给客户端。

通过Socks5协议与代理服务器进行认证阶段时传输的数据

在使用 SOCKS5 协议与代理服务器进行认证阶段时,传输的数据如下所示:

  1. 客户端向代理服务器发送认证方法选择请求

    • VER(Version):协议版本号,通常为 0x05。
    • NMETHODS(Number of Methods):客户端支持的认证方法数量。
    • METHODS(Methods):客户端支持的认证方法列表。
  2. 代理服务器回复客户端认证方法选择响应

    • VER(Version):协议版本号,通常为 0x05。
    • METHOD(Method):代理服务器选择的认证方法。
  3. 如果代理服务器选择了"用户名/密码"认证方法,接下来会进行用户名/密码认证:

    • VER(Version):协议版本号,通常为 0x01。
    • ULEN(Username Length):用户名的长度。
    • UNAME(Username):用户名。
    • PLEN(Password Length):密码的长度。
    • PASSWD(Password):密码。
  4. 代理服务器向客户端发送认证结果

    • VER(Version):协议版本号,通常为 0x01。
    • STATUS(Status):认证结果,0 表示成功,其他值表示失败。

在 SOCKS5 协议中,客户端和代理服务器之间的认证阶段是可选的。如果客户端不需要进行认证,可以跳过认证方法选择和用户名/密码认证阶段,直接进入后续的请求和响应阶段。

需要注意的是,以上是SOCKS5 协议中的认证阶段,用于建立与代理服务器的连接。在建立连接后,客户端和代理服务器之间的数据传输将使用 SOCKS5 协议定义的请求和响应格式进行。

TCP连接

TCP(Transmission Control Protocol)是一种可靠的、面向连接的网络传输协议。它提供了一种可靠的、有序的、基于字节流的传输机制,用于在计算机网络中传输数据。

TCP连接的详细过程

建立连接(Three-Way Handshake)

  • 客户端向服务器发送一个特殊的TCP报文段,称为SYN(同步)包,其中包含一个初始序列号(ISN)。
  • 服务器收到SYN包后,会回复一个SYN+ACK(同步确认)包。该包中包含确认号(ACK)和服务器的初始序列号。
  • 客户端收到服务器的SYN+ACK包后,会发送一个ACK包作为响应。这个ACK包确认了服务器的初始序列号,并且建立了双向的连接。至此,TCP连接建立完成。

数据传输

  • 在TCP连接建立后,双方可以开始传输数据。数据被分割成小的数据块,称为TCP段(Segment),每个TCP段都有一个序列号用于标识。
  • 发送方将TCP段封装在IP数据包中,并通过网络发送给接收方。
  • 接收方收到TCP段后,会发送确认(ACK)给发送方,表示已经成功接收到数据。
  • 如果发送方没有收到接收方的确认,或者接收方收到数据时发现有错误,发送方会重新发送丢失或损坏的数据。

连接终止

  • 当双方中的任一方确定不再需要连接时,可以发起连接的终止。
  • 发起方发送一个特殊的TCP报文段,称为FIN(结束)包,表示不再发送数据。
  • 接收方收到FIN包后,发送一个ACK包进行确认。
  • 接收方也可以发送一个FIN包,表示自己也不再发送数据。
  • 发起方收到接收方的FIN包后,发送一个ACK包进行确认。至此,TCP连接终止完成。

TCP连接建立时的三次握手的数据包内容·详解

下面是每次握手中数据包内包含的字段及其含义:

第一次握手(SYN)

  • 源端口(Source Port):发送方的端口号。
  • 目标端口(Destination Port):接收方的端口号。
  • 序列号(Sequence Number):发送方的初始序列号。
  • SYN标志位(SYN Flag):用于建立连接的标志位,设置为1表示请求建立连接。
  • 窗口大小(Window Size):接收方的接收窗口大小,用于流量控制。

第二次握手(SYN+ACK)

  • 源端口(Source Port):发送方的端口号。
  • 目标端口(Destination Port):接收方的端口号。
  • 序列号(Sequence Number):发送方的初始序列号。
  • 确认号(Acknowledgment Number):接收方期望收到的下一个序列号。
  • SYN标志位(SYN Flag):用于建立连接的标志位,设置为1表示请求建立连接。
  • ACK标志位(ACK Flag):用于确认连接的标志位,设置为1表示确认收到握手请求。
  • 窗口大小(Window Size):接收方的接收窗口大小,用于流量控制。

第三次握手(ACK)

  • 源端口(Source Port):发送方的端口号。
  • 目标端口(Destination Port):接收方的端口号。
  • 序列号(Sequence Number):发送方的初始序列号。
  • 确认号(Acknowledgment Number):接收方期望收到的下一个序列号。
  • ACK标志位(ACK Flag):用于确认连接的标志位,设置为1表示确认收到握手请求。
  • 窗口大小(Window Size):接收方的接收窗口大小,用于流量控制。

用Go语言实现一个自制的Socks5代理服务器

Socks5代理服务器可以用来做什么?

  1. 匿名浏览:通过将您的网络流量路由到代理服务器,您可以隐藏自己的真实IP地址和位置,从而实现匿名浏览。这对于维护个人隐私、避免被追踪或访问受限制的内容很有用。

  2. 绕过网络限制:如果您所在的网络有特定的限制,例如某些网站或服务被屏蔽,您可以使用Socks5代理服务器来绕过这些限制。通过连接到代理服务器,您可以访问被封锁的内容。

  3. 加密通信:Socks5代理服务器可以提供加密通信的功能,保护您的数据免受窃听或篡改。当您连接到代理服务器时,您的通信将被加密,增加了安全性。

  4. 访问地理限制的内容:某些网站或服务可能根据您的地理位置限制访问。通过连接到位于允许访问的地理位置的Socks5代理服务器,您可以绕过这些地理限制,访问受限制的内容。

  5. 提高网络性能:在某些情况下,使用Socks5代理服务器可以提高网络性能。代理服务器可以缓存网页内容,减少网络传输的数据量,从而加快页面加载速度。

Socks5协议各个阶段的具体实现

实现一个简单的TCP echo server

什么是TCP echo server

TCP echo server实现的功能是:将客户端请求的数据原封不动的返回给用户,该组件一般用于前期开发时测试是否能与服务器正常连接并传输数据。

TCP echo server的具体实现

注意,此处我们需要使用net包来处理网络连接和通信 。以下是一段实现TCP echo server功能的代码。

go 复制代码
package main

import (
	"fmt"
	"net"
)

func main() {
	// 监听地址和端口
	address := "localhost"
	port := 8080

	// 启动服务器并监听指定地址和端口
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", address, port))
	if err != nil {
		fmt.Printf("无法启动服务器:%s\n", err.Error())
		return
	}
	defer listener.Close()

	fmt.Printf("服务器正在监听 %s:%d\n", address, port)

	for {
		// 等待客户端连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("无法接受客户端连接:%s\n", err.Error())
			continue
		}

		fmt.Printf("客户端 %s 已连接\n", conn.RemoteAddr().String())

		// 启动一个新的 goroutine 处理客户端连接
		go handleClient(conn)
	}
}

func handleClient(conn net.Conn) {
	defer conn.Close()

	// 创建一个缓冲区来存储客户端发送的数据
	buffer := make([]byte, 1024)

	for {
		// 从客户端读取数据
		n, err := conn.Read(buffer)
		if err != nil {
			fmt.Printf("从客户端读取数据时出错:%s\n", err.Error())
			break
		}

		// 将接收到的数据发送回客户端
		_, err = conn.Write(buffer[:n])
		if err != nil {
			fmt.Printf("向客户端发送数据时出错:%s\n", err.Error())
			break
		}
	}

	fmt.Printf("与客户端 %s 断开连接\n", conn.RemoteAddr().String())
}

让我们逐行解释代码的主要部分:

  1. 导入必要的包:fmt用于打印输出,net用于处理网络连接和通信。

  2. main函数是程序的入口点。我们首先定义要监听的地址和端口。然后使用net.Listen函数创建一个监听器,该函数接受网络类型("tcp")和地址(由地址和端口组成的字符串)作为参数。如果监听器创建失败,我们打印错误消息并退出程序。最后,我们使用defer语句在程序退出时关闭监听器。

  3. 在一个无限循环中,我们使用listener.Accept函数接受客户端的连接请求。如果接受连接时出现错误,我们打印错误消息并继续等待下一个连接。一旦成功接受连接,我们打印客户端的地址,并使用go关键字启动一个新的goroutine来处理客户端连接,这样可以同时处理多个客户端的请求。

  4. handleClient函数用于处理每个客户端连接。我们首先使用defer语句在函数退出时关闭连接。然后,我们创建一个缓冲区来存储客户端发送的数据。在一个无限循环中,我们使用conn.Read函数从客户端读取数据,并将读取到的数据发送回客户端使用conn.Write函数。如果在读取或写入数据时出现错误,我们打印错误消息并退出循环。

  5. 最后,我们在循环外打印与客户端断开连接的消息。

注意:以上所有使用defer语句的位置,如果缺少语句,则有可能存在潜在的内存泄漏风险。

Socks5代理的AUTH阶段的实现

在Auth阶段,客户端会发送一条认证请求,服务器需要对该请求进行验证并返回相应的认证结果。

go 复制代码
func handleClient(conn net.Conn) {
    ...
        
    // 进行Socks5协议的Auth阶段
    err := socks5Auth(conn)
    if err != nil {
            fmt.Printf("Socks5 Auth阶段出错:%s\n", err.Error())
            return
    }
    
    ...
}

func socks5Auth(conn net.Conn) 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  在本示例中,我们暂时不实现该方法
        
	// 读取客户端发送的认证请求
	buffer := make([]byte, 2)
	_, err := conn.Read(buffer)
	if err != nil {
		return fmt.Errorf("无法读取认证请求:%s", err.Error())
	}

	// 检查认证版本
	if buffer[0] != 0x05 {
		return fmt.Errorf("不支持的认证版本")
	}
        
   // +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	// 发送认证响应
	_, err = conn.Write([]byte{0x05, 0x00})
	if err != nil {
		return fmt.Errorf("发送认证响应时出错:%s", err.Error())
	}

	return nil
}

socks5Auth函数用于处理Socks5协议的Auth阶段。我们首先读取客户端发送的认证请求,并检查认证版本和认证方法是否符合要求。如果不符合,我们返回错误。然后,我们发送认证响应给客户端,表示认证成功。

Socks5代理的请求阶段的实现

在请求阶段,客户端会发送一条请求,指示服务器要建立的目标连接。服务器需要解析该请求,并根据请求中的目标地址和端口建立与目标服务器的连接。

go 复制代码
func handleClient(conn net.Conn) {
    ...
    address, port, err := socks5Request(conn) 
    if err != nil { 
        fmt.Printf("Socks5 请求阶段出错:%s\n", err.Error()) 
        return
    }
	
	...
}

func socks5Request(conn net.Conn) (string, int, error) {
	// +----+-----+-------+------+----------+----------+
	// |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是一个可变长度的域名
   //   0x04表示IPv6地址, 本示例不实现它
	// DST.ADDR 一个可变长度的值
	// DST.PORT 目标端口,固定2个字节
	// 读取客户端发送的请求
	buffer := make([]byte, 4)
	_, err := conn.Read(buffer)
	if err != nil {
		return "", 0, fmt.Errorf("无法读取请求:%s", err.Error())
	}

	// 检查请求版本和请求命令
	if buffer[0] != 0x05 || buffer[1] != 0x01 {
		return "", 0, fmt.Errorf("不支持的请求版本或请求命令")
	}

	// 检查请求地址类型
	addressType := buffer[3]
	if addressType != 0x01 && addressType != 0x03 {
		return "", 0, fmt.Errorf("不支持的请求地址类型")
	}

	// 读取请求地址
	address := ""
	if addressType == 0x01 { // IPv4地址
		ip := make([]byte, 4)
		_, err := conn.Read(ip)
		if err != nil {
			return "", 0, fmt.Errorf("无法读取IPv4地址:%s", err.Error())
		}
		address = net.IP(ip).String()
	} else if addressType == 0x03 { // 域名
		// 读取域名长度
		lengthBuffer := make([]byte, 1)
		_, err := conn.Read(lengthBuffer)
		if err != nil {
			return "", 0, fmt.Errorf("无法读取域名长度:%s", err.Error())
		}
		length := int(lengthBuffer[0])

		// 读取域名
		domain := make([]byte, length)
		_, err = conn.Read(domain)
		if err != nil {
			return "", 0, fmt.Errorf("无法读取域名:%s", err.Error())
		}
		address = string(domain)
	}

	// 读取请求端口
	portBuffer := make([]byte, 2)
	_, err = conn.Read(portBuffer)
	if err != nil {
		return "", 0, fmt.Errorf("无法读取请求端口:%s", err.Error())
	}
	port := int(portBuffer[0])*256 + int(portBuffer[1])

	fmt.Printf("请求的目标地址:%s:%d\n", address, port)

    // +----+-----+-------+------+----------+----------+
	// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
	// +----+-----+-------+------+----------+----------+
	// | 1  |  1  | X'00' |  1   | Variable |    2     |
	// +----+-----+-------+------+----------+----------+
	// VER socks版本,这里为0x05
	// REP Relay field,内容取值如下 X'00' succeeded
	// RSV 保留字段
	// ATYPE 地址类型
	// BND.ADDR 服务绑定的地址
	// BND.PORT 服务绑定的端口DST.PORT
	// 发送请求响应
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
	if err != nil {
		return "", 0, fmt.Errorf("发送请求响应时出错:%s", err.Error())
	}

	return address, port, nil
}

socks5Request函数用于处理Socks5协议的请求阶段。我们首先读取客户端发送的请求,并检查请求的版本和命令是否符合要求。然后,我们检查请求的地址类型,并根据不同的类型读取请求的地址。最后,我们读取请求的端口,并打印目标地址和端口的信息。在发送请求响应时,我们使用一个固定的响应,表示请求成功。

Socks5代理的Relay阶段的实现

要实现Socks5协议中的Relay阶段,我们需要对之前的代码进行进一步的修改。在Relay阶段,服务器需要将客户端发送的数据原样转发给目标服务器,并将目标服务器返回的数据原样转发给客户端。以下是修改后的代码,包含了详细的注释来解释每个步骤的作用:

go 复制代码
package main

import (
	"fmt"
	"io"
	"net"
)

func main() {
	// 监听地址和端口
	address := "localhost"
	port := 8080

	// 启动服务器并监听指定地址和端口
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", address, port))
	if err != nil {
		fmt.Printf("无法启动服务器:%s\n", err.Error())
		return
	}
	defer listener.Close()

	fmt.Printf("服务器正在监听 %s:%d\n", address, port)

	for {
		// 等待客户端连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("无法接受客户端连接:%s\n", err.Error())
			continue
		}

		fmt.Printf("客户端 %s 已连接\n", conn.RemoteAddr().String())

		// 启动一个新的 goroutine 处理客户端连接
		go handleClient(conn)
	}
}

func handleClient(conn net.Conn) {
	defer conn.Close()

	// 进行Socks5协议的Auth阶段
	err := socks5Auth(conn)
	if err != nil {
		fmt.Printf("Socks5 Auth阶段出错:%s\n", err.Error())
		return
	}

	// 进行Socks5协议的请求阶段
	err = socks5Request(conn)
	if err != nil {
		fmt.Printf("Socks5 请求阶段出错:%s\n", err.Error())
		return
	}

	// 创建与目标服务器的连接
	targetConn, err := net.Dial("tcp", "example.com:80")
	if err != nil {
		fmt.Printf("无法连接到目标服务器:%s\n", err.Error())
		return
	}
	defer targetConn.Close()

	// 在客户端和目标服务器之间进行数据传输
	go io.Copy(targetConn, conn)
	io.Copy(conn, targetConn)

	fmt.Printf("与客户端 %s 断开连接\n", conn.RemoteAddr().String())
}

func socks5Auth(conn net.Conn) error {
	// TODO: 实现Socks5协议的Auth阶段

	return nil
}

func socks5Request(conn net.Conn) error {
	// TODO: 实现Socks5协议的请求阶段

	return nil
}

func handleClient(conn net.Conn) {
        ...
	// 创建与目标服务器的连接
	targetConn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", address, port))
	if err != nil {
		fmt.Printf("无法连接到目标服务器:%s\n", err.Error())
		return
	}
	defer targetConn.Close()

	// 在客户端和目标服务器之间进行数据传输
   //此处启动一个goroutine,避免线程阻塞
	go io.Copy(targetConn, conn)
	io.Copy(conn, targetConn)

	fmt.Printf("与客户端 %s 断开连接\n", conn.RemoteAddr().String())
}

在上面的代码中,我们使用io包中提供的io.Copy函数在两个连接之间进行数据传输。go io.Copy(targetConn, conn)将客户端发送的数据拷贝到目标服务器的连接,而io.Copy(conn, targetConn)将目标服务器返回的数据拷贝到客户端的连接。这样,服务器就能够实现Socks5协议中的Relay阶段。至此,Socks5代理服务器的单次请求的全部阶段就结束了。

相关推荐
Find3 个月前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子3 个月前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子3 个月前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子3 个月前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵3 个月前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六3 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz3 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5653 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml3 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932423 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记