Go语言入门:实现Socks5代理服务器|青训营笔记

420 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记

青训营作业-3 代理服务器

1. SOCKS5代理原理

第一阶段,首先是clientSocks5 Server进行协商,通过协商后进入第二阶段

第二阶段,client发送请求到Socks5 Server,Server与Host三次握手后建立TCP连接,建立完成后返回响应给server,Server再返回状态给client

第三阶段,client发送数据给Socke5 Server,接着relay数据到Host,接着一层层回传数据到client

2. 代码实现-TCP echo server

  • 实现要点
    • 建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
    • 去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
    • 将接收到的client信息传入process函数中进行处理
    • 使用go协程处理每一个请求
package main

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

func process(conn net.Conn) { //处理传回来的信息的逻辑
	//完成该函数处理自动关闭
        //do something
}

func main() {
	//1.建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
	server, err := net.Listen("tcp", "127.0.0.1:1080")
	//2.处理异常
	if err != nil {
		panic(err)
	}
	//3.死循环,相当于一个压力测试(操作系统),接收服务端给来的信息
	for {
		client, err := server.Accept() //去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
		if err != nil {
			log.Printf("Accepted failed!", err)
			continue
		}
		//4.将接收到的client信息传入process函数中进行处理
		go process(client) //使用go 协程处理
	}
}

如何测试该函数? 首先启动go run该程序 如果是在Windows下的话,要事先安装NetCat并且配置环境变量后执行nc 127.0.0.1 1080,即可监听 如果是在Linux下NetCat是内置的,因此直接输入指令即可

3.代码实现-auth

解析协议: // +----+----------+----------+ // |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 首先需要弄明白的是协议下方的数字约定着我们写代码需要用几个字节来接收此数据,这是很关键的 同时其预定义的方法数量与方法规约也决定着我们的返回结果,我们以代码形式展示逻辑


const (
	socks5Ver = 0x05
	cmdBind   = 0x01
	atypIPV4  = 0x01
	atypeHOST = 0x03
	atypeIPV6 = 0x04
)

/**

 */
func auth(reader *bufio.Reader, conn net.Conn) (err error) { //实现一个认证方法
	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 method size failed!%v", err)
	}
	/*创建合适的缓冲区大小,并用reader剩余的内容填满method*/
	method := make([]byte, methodSize)
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("full method failed!%v", err)
	}
	/*获取得到了三个报文,打印得到*/
	log.Println("ver:", ver, "method", method)

	//解析该包后,将该包返回给浏览器,告诉它我们用了什么版本协议和什么方法
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("Write connection failed!%v", err)
	}
	return nil
}

运行代码,首先启动main函数,启动监听,然后启动终端,输入curl --socks5 127.0.0.1:1080 -v http://www.qq.com 代理进程输出:ver: 5 method [0 1] 请求代理进程输出:

  • Trying 127.0.0.1:1080...
  • SOCKS5 connect to IPv4 121.14.77.221:80 (locally resolved)
  • Failed to receive SOCKS5 connect request ack.
  • Closing connection 0 curl: (97) Failed to receive SOCKS5 connect request ack 这是正常的,因为我们还没有编写请求函数,因此没办法正常完成代理

4. 代码实现-connection

解析协议: // +----+-----+-------+------+----------+----------+ // |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个字节 理解协议的方法和上面的大同小异 // +----+-----+-------+------+----------+----------+ // |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 //将解析得到的报文写入连接

  • 实现要点
    • 接收报文,以限定好字节数的数据变量来接收各个字段
    • 验证字段和预定义字段之间的差异,以验证合法性
    • 将解析完毕且无误的报文写回传回

connection函数的逻辑是接收来自浏览器发送过来的一个报文。解析请求后返回

func connection(reader *bufio.Reader, conn net.Conn) (err error) { //请求函数

	/**
	请求函数的逻辑,我们的第一步是解析来自浏览器的报文
	*/
	//对于定长的字节,我们是很好处理的,这里采取的方法是创建一个4字节切片来接收前4个字节
	data := make([]byte, 4)
	_, err = io.ReadFull(reader, data)
	if err != nil {
		return fmt.Errorf("read header failed:%w", err)
	}
	ver, cmd, aytp := data[0], data[1], data[3]
	//验证前三个字段的合法性
	if ver != socks5Ver {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	if cmd != cmdBind {
		return fmt.Errorf("not supported cmd:%v", cmd)
	}
	//aytp的合法性验证比较特殊,要与后面的地址结合起来看
	addr := ""
	switch aytp {
	case atypIPV4:
		//如果是IPV4的类型的话,就是要读取后面4个字节的数据,我们直接利用上面的data切片
		_, err = io.ReadFull(reader, data)
		if err != nil {
			return fmt.Errorf("read atyp failed:%w", err)
		}
		addr = fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3])
	case atypeHOST:
		//如果是HOST域名类型,由于域名是不定长的,因此要读取不定长的变量
		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, data[:2]) //用前两个位置来接受端口,切片语法
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(data[:2]) //按照大端字节序解析byte数组
	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)
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err != nil {
		return fmt.Errorf("write failed: %w", err)
	}

	return nil
}

func main() {
	//1.建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
	server, err := net.Listen("tcp", "127.0.0.1:1080")
	//2.处理异常
	if err != nil {
		panic(err)
	}
	//3.死循环,相当于一个压力测试(操作系统),接收服务端给来的信息
	for {
		client, err := server.Accept() //去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
		if err != nil {
			log.Printf("Accepted failed!", err)
			continue
		}
		//4.将接收到的client信息传入process函数中进行处理
		go process(client) //使用go 协程处理
	}
}

5. 代码实现-relay阶段

双向拷贝

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		_, err = io.Copy(dest, reader)
		cancel()
	}()
	go func() {
		_, err = io.Copy(conn, dest)
		cancel()
	}()
	<-ctx.Done()
	return nil
}