部署SOCKS5代理服务器 | 豆包 Mars Code AI 刷题

62 阅读7分钟

部署SOCKS5代理服务器

尝试一些更复杂的实现

  • 对于不熟悉的函数,善用官方文档,这是你了解函数最正规最详细的地方

SOCKS5 简介

SOCKS5 是一个传输层代理协议,主要用于通过中间代理服务器来转发客户端和目标 服务器之间的网络通信。它并不关心数据的具体内容,只负责将数据从一个网络位置转发到另一个网络位置。与 HTTP 代理相比,SOCKS5 可以处理任何协议的流量,包括 HTTP、FTP、SMTP、POP3 等。原理


从简单的TCP echo server做起

  • 由于整个协议的实现较为复杂,先尝试一个简单版的
  • 这次的测试需要用到nc命令,所以我们先配置好环境
    1. 官方网站下载netcat
    2. 将包含netcat.exe的文件夹添加到系统变量Path
    3. 这里使用GoLand的powershell终端,因此还需要再手动添加路径
       $env:PATH += ";C:\path\to\nc"
    
    用你的路径替换掉path\to\nc
  • 代码如下echo server
    • 行9-22 定义函数process处理客户端conn发来的数据
      • 行10 调用结束后关闭连接防止数据泄露
      • 行12-20 创建死循环持续读取数据
      • 行17 将读取到的数据用Write函数发送回客户端(需要用[]byte{}切片形式)
    • 行25 用net.Listen启动tcp服务器,并用server监听127.0.0.1:1080
    • 行30 将客户端连接放至client
    • 行35 用go关键字创建一个goroutine并行执行函数
  • 运行代码,测试 运行成功

实现认证阶段

  • 协议的第一步:处理客户端与代理服务器的认证

  • 首先声明一些常量,这些常量和我们的协议有关,常量

    • 行11 socks5Ver存储协议版本号0x05
    • 行12 cmdBind存储一个SOCKS5请求命令0x01代表“Bind”
    • 行13-15 表示IP地址类型IPv40x01,主机0x03,IPv60x04
  • 我们不再需要之前的死循环,而是改用一个auth函数处理输入,改写后的process如下,process

    • 行20 调用auth函数处理conn
    • 行21 如果返回错误,打印日志,**conn.RemoteAddr()**返回当前连接的地址
    • 看到26行的:bulb:了吗,像不像现在的你头上冒出来的那个?
  • 然后是重头戏auth,首先我们要明确SOCKS5采用固定顺序协议,服务器会按序发送VERNMETHODSMETHODS三个字段,auth

    • 行39 读取第一个字节即VER

    • 行46 读取第二个字节NMETHODS

    • 行50 根据NMETHODS给出的字节数创造一个对应大小的[]byte切片

    • 行51 读入METHODS直至填满method

  • 按照协议,我们需要返回我们使用的VER版本和认证形式0x00即不需要认证

    • 行61 将VER0x00写回客户端
  • 我们可以试着运行一下程序,注意在powershell中curl要使用curl.exe确保调用系统命令trying

  • 运行毫无疑问会出问题,毕竟我们离完成还有很长的距离, back

  • 不过也应该能看到auth函数的运行是没有问题的,可喜可贺


实现请求阶段

  • 协议的第二步:读取客户端的请求,解析出目标服务器的地址,端口等并尝试建立连接
  • 编写connect函数,先写出解析客户端请求报文的部分处理请求
    • 回复报文的格式已经给出不再赘述。这次换一种读取的方法不再读取per byte而是创建一个四个字节的切片buf
    • 行102 ReadFull读取直至切片被填满
    • 行106 从前四个字节中切出VERCMDATYP用于检验
  • 处理目标地址类型,处理地址
    • 行119-124 IPV4情况,会返回固定四个字节的长度,直接使用buf读取,把IP地址打印到addr里去
    • 行125-135 域名情况,先返回域名长度再给出地址,与先前的METHOD类似
    • 行136-137 偷个懒,IPV6不常见
  • 处理端口数据,处理端口
    • 行141 死马当牛马用,用切片方式再次填充buf的前两个字节
    • 行145 按^大端字节序(SOCKS5要求)解析字节切片为无符号十六位整数作为端口号填入port
  • 按照协议代理服务器需要返回一个响应报文,返回报文
    • 必要的返回并没有多少,基本都写在注释里了:thumbsup:
  • 同样在process中添加对connect的调用,process
  • 这样基本就完成了请求部分,离成为王仍需一步

实现relay

  • 和代理服务器建立TCP连接,双向转换数据
  • 让我们回到connect中去,建立连接
    • 行147 用net包的Dial函数建立连接
    • 行151 函数调用结束后关闭连接
  • 然后实现数据交换,这里使用的是io库的Copy函数,这个函数会从一个只读reader里拷贝数据到可写writer数据交换
    • 行172-179 并行执行两个goroutine,双向转换数据
      • 由于goroutine迅速执行,若不采取措施函数将很快返回nil,而我们需要任何一方不在发送数据后结束,这里的解决方案是context包的WithCancel函数,这个函数创建一个ctx上下文,直到一个cancel被执行时结束所有内部的goroutine
    • 行181 阻塞外层函数的进行,直到ctx结束再返回nil
  • 让我们试着curl一下,try
  • 这样就完成了
  • 想要在Chrome浏览器玩的话需要先安装一个SwitchyOmega插件,插件
    • 选择新建情景模式然后填入服务器属性,在插件里选择应用,这样你访问的新界面就会通过你完成的SOCKS5代理服务器
    • 妈妈再也不用担心我的IP地址暴露啦

由于这次的代码还挺长的,在下方给出完整代码供参考

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 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
	}
	err = connect(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 | NMETHODS | METHODS  |
	// +-----+----------+----------+
	// |  1  |     1    | 1 to 255 |
	// +-----+----------+----------+
	// VER: 协议版本,socks5为0x05
	// NMETHODS: 支持认证的方法数
	//METHODS: 对应NMETHODS,为METHODS的字节数。一些RFC预定义如下
	// X'00' NO AUTHENTICATION REQUIRED
	// X'02' USERNAME/PASSWORD
	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)
	}
	log.Println("ver", ver, "method", method)
	// +-----+--------+
	// | VER | METHOD |
	// +-----+--------+
	// |  1  |   1    |
	// +-----+--------+
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}
	return nil
}

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 connect(reader *bufio.Reader, conn net.Conn) (err 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是一个可变长度的域名
	// DST.ADDR 一个可变长度的值
	// DST.PORT 目标端口,固定2个字节

	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)
	}
	addr := ""
	switch atyp {
	case atypeIPV4:
		_, 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 atypeHOST:
		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 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:%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)

	// +----+-----+-------+------+----------+----------+
	// |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, 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
}

*今日推荐:好累不想推荐