GO语言工程实践:socks5代理服务器 | 青训营

75 阅读7分钟

总览和socks5代理服务器实现的基本逻辑梳理

SOCKS5 代理服务器通过在客户端和服务器之间的代理服务器路由网络数据包来工作,以绕过互联网限制并访问被阻止的网站或服务。从技术上讲,SOCKS5(最新版本)使用代理服务器通过用户数据报协议(UDP)或传输控制协议(TCP)建立连接。

SOCKS5 代理通常用于安装为浏览器扩展程序或配置种子客户端以使用 VPN 提供商的代理服务器。SOCKS 代理通过通过代理服务器传输您的流量来工作,然后将信息传递到预期的目的地。

TCP 协议是 Internet 协议套件的主要协议之一。它起源于最初的网络实现,其中它补充了 Internet 协议(IP)。因此,整个套件通常称为 TCP/IP。TCP 提供可靠、有序且经过错误检查的字节流(八位字节)传输,用于在通过 IP 网络通信的主机上运行的应用程序之间。

TCP 是面向连接的,客户端和服务器之间必须先建立连接才能发送数据。服务器必须监听(被动打开)来自客户端的连接请求才能建立连接。三次握手(主动打开)、重传和错误检测增加了可靠性但延长了延迟。

三次握手是指 TCP 协议中建立连接时客户端和服务器之间交换三个报文段。第一个报文段由客户端发送,表示希望建立连接。第二个报文段由服务器发送,表示同意建立连接。第三个报文段由客户端发送,表示确认连接已经建立。

四次挥手是指 TCP 协议中断开连接时客户端和服务器之间交换四个报文段。第一个报文段由客户端或服务器发送,表示希望断开连接。第二个报文段由另一方发送,表示确认收到了断开连接的请求。第三个报文段由另一方发送,表示同意断开连接。第四个报文段由发起断开连接请求的一方发送,表示确认连接已经断开。

代码功能介绍

这段代码是一个简单的 SOCKS5 代理服务器的实现。它监听本地端口 1080 并接受来自客户端的连接。当客户端连接到此代理服务器时,它会进行身份验证并建立与目标服务器的连接。

在 main 函数中,代码创建了一个 TCP 服务器并监听本地端口 1080。当有新的客户端连接时,它会调用 process 函数来处理该连接。

process 函数首先调用 auth 函数进行身份验证。在这个例子中,身份验证过程非常简单,只是检查客户端发送的 SOCKS5 版本号是否正确,并返回一个响应表示不需要进一步的认证。

接下来,process 函数调用 connect 函数来建立与目标服务器的连接。这个函数首先读取客户端发送的连接请求报文,解析出目标服务器的地址和端口号,然后使用 net.Dial 函数建立到目标服务器的 TCP 连接。最后,它向客户端发送一个响应报文表示连接已经建立。

在 connect 函数中,还有一些辅助函数用于解析客户端发送的报文。例如,readByte 函数用于从读取器中读取一个字节,readFull 函数用于从读取器中读取指定长度的数据。

auth函数

auth 函数用于处理 SOCKS5 协议中的身份验证过程。它接受两个参数:reader 和 connectreader 是一个 bufio.Reader 类型的读取器,用于从客户端连接中读取数据。connect 是一个 net.Conn 类型的连接,表示与客户端的连接。

在身份验证过程中,浏览器会向 SOCKS5 代理服务器发送一段报文,包括版本号、方法数量和方法列表。代理服务器会检查报文中的版本号是否为支持的版本,并从方法列表中选择一种认证方法。

在这个例子中,身份验证过程非常简单。代码首先使用 ReadByte 函数从读取器中读取版本号,并检查是否为支持的版本。然后再次使用 ReadByte 函数读取方法数量,接着使用 ReadFull 函数读取方法列表。

最后,代码向客户端发送一个响应报文,表示选择了不需要进一步认证的方法。这个响应报文包括两个字节:版本号和选择的认证方法。

总之,auth 函数实现了 SOCKS5 协议中的身份验证过程。它从客户端连接中读取报文,并向客户端发送响应报文表示选择了不需要进一步认证的方法。

connect函数

connect 函数用于处理 SOCKS5 协议中的连接建立过程。它接受两个参数:reader 和 connreader 是一个 bufio.Reader 类型的读取器,用于从客户端连接中读取数据。conn 是一个 net.Conn 类型的连接,表示与客户端的连接。

在连接建立过程中,浏览器会向 SOCKS5 代理服务器发送一段报文,包括版本号、命令、保留字节和地址类型。代理服务器会检查报文中的版本号是否为支持的版本,并根据命令和地址类型来建立到目标服务器的连接。

在这个例子中,代码首先使用 ReadFull 函数从读取器中读取前 4 个字节,分别表示版本号、命令、保留字节和地址类型。然后检查版本号是否为支持的版本,并根据命令和地址类型来解析出目标服务器的地址和端口号。

接下来,代码使用 net.Dial 函数建立到目标服务器的 TCP 连接。如果连接成功,它会向客户端发送一个响应报文表示连接已经建立。

在 connect 函数中,还有一些辅助函数用于解析客户端发送的报文。例如,readByte 函数用于从读取器中读取一个字节,readFull 函数用于从读取器中读取指定长度的数据。

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"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("accpet 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 failer %v", conn.RemoteAddr(), err)
		return
	}
	log.Printf("auth success!")
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v auth failer %v", conn.RemoteAddr(), err)
		return
	}
}

func auth(reader *bufio.Reader, connect net.Conn) (err error) {
	//第一步,认证阶段浏览器向socks5协议发送一段报文,包括version, methodSize & methods
	//然后用户反馈给浏览器一部分内容确认自己的协议版本号和建传方式(认证方式)
	//认证方式有两种常见的:不需要认证(0)和用户名密码认证(2)
	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed: %v", 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: %v", err)
	}
	methods := make([]byte, methodSize)
	_, err = io.ReadFull(reader, methods)
	if err != nil {
		return fmt.Errorf("read methods failed: %v", err)
	}
	log.Printf("ver: %v, methodSize: %v, methods: %v", ver, methodSize, methods)
	_, err = connect.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%v", err)
	}
	return nil
}

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!: %v", err)
	}
	ver, cmd, atyp := buf[0], buf[1], buf[3]
	if ver != socks5Ver {
		return fmt.Errorf("not supported version: %v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported version: %v", ver)
	}
	addr := ""
	switch atyp {
	case atypeIPV4:
		_, err = io.ReadFull(reader, buf)
		if err != nil {
			return fmt.Errorf("read atyp failed: %v", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
	case atypeHOST:
		hostSize, err := reader.ReadByte()
		if err != nil {
			return fmt.Errorf("read hostSize failed: %v", err)
		}
		host := make([]byte, hostSize)
		_, err = io.ReadFull(reader, host)
		if err != nil {
			return fmt.Errorf("read host failed: %v", err)
		}
		addr = string(host)
	case atypeIPV6:
		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: %v", 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: %v", 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: %v", 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
}