Socks5代理服务器的原理以及具体实现 | 青训营

写在前面

本文是自己进入青训营以来的第七篇笔记,这次实操的项目是我自己之前鸽了很久的Socks5代理服务器项目,之前因为种种原因不是很想实操,而今痛下决心,一定要啃下这个项目的代码,本次实践参考的是字节青训营王克纯老师的ppt(‍⁢⁢⁣Go 语言上手 - 基础语法 .pptx - 飞书云文档 (feishu.cn)),那么让我们开始。

代理服务器简介

是网络信息的中转站,这是一种特殊的网络服务,简单来说使用IP代理可以更改用户的IP地址。代理IP是介于浏览器和Web服务器之间的一台服务器,如果使用代理IP,Request信号就会先送到代理服务器,并由代理服务器得到浏览器所需要的信息并传送到你的浏览器。

代理服务器的主要作用就是负责转发,转发合法的网络信息,对转发进行控制和登记;它可以组织内部针对特定网站进行访问控制;还能利用缓存技术减少网络带宽的流量。

Socks5代理简介

socks5协议是一款广泛使用的代理协议,它在使用TCP/IP通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。SOCKS5 服务器通过将前端发来的请求转发给真正的目标服务器,前端和SOCKS5之间也是通过TCP/IP协议进行通讯,前端将原本要发送给真正服务器的请求发送给SOCKS5服务器,然后SOCKS5服务器将请求转发给真正的服务器。

工作的简单模型如下

rust 复制代码
client---->proxy(socks5)---->server

Socks5代理服务器的实现

接下来开始按照步骤,一步步地实现Socks5代理服务器

简易代理服务器

首先我们要在main函数里面监听一个端口,把自己电脑看成一个服务器,有下列代码

go 复制代码
server, err := net.Listen("tcp", "127.0.0.1:8080")
	if err != nil {
		panic(err)
	}

其中

  1. net是Go中的一个包;
  2. net.Listen("tcp","具体")监听的具体地址。

在此基础上,我们添加下列循环:

go 复制代码
for {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept failed %v", err)
			continue
		}
		go process(client)
	}

其中server.Accept()来接受建立连接的请求,连接成功后,用client变量来表示建立的连接,连接成功后,执行process这一进程,接下来开始写process函数。

go 复制代码
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
		}
	}
}

其中defer conn.Close()作用为在函数退出时把连接关掉,以防资源泄露。

bufio.NewReader()用于创建一个缓冲的只读流。

再通过循环读取字节,再把每个字节写进去连接。

最终运行效果如图。输入hello,就会输出hello,即输入什么就会输出什么。

这样,一个简易的代理服务器就做好了。

服务器plus

第一形态 协商阶段

要先实现协议的第一步,我们可以对代码进行优化,把process函数的过程抽象成一个函数,然后在process中调用,协商阶段的逻辑是:浏览器给代理服务器一个包,包含三个字段:版本号、methods、每个methods的编号,先把版本号读出来,不符合就退出,然后开始读methods。

代理服务器返回一个回复,包括两个字段,版本号和method。 发送和返回的报文如下。

version nmethods methods
1 1 1 to 255
version method
1 1

先将process函数调用的方法抽象成auth()

go 复制代码
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
	}
	log.Println("auth success")
}

auth函数中,先读版本号,不符直接退出。

go 复制代码
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)
	}

下一步,读取第二个字节,读取成功再继续。

go 复制代码
methodSize, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read methodSize failed:%w", err)
	}

读取剩余字节。

go 复制代码
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)

到这里,协商阶段结束,我们可以来验证一下。 将curl --socks5 127.0.0.1:8080 -v http://www.qq.com输入命令行,可以发现无法正常连接。

vbnet 复制代码
*   Trying 127.0.0.1:8080...
* connect to 127.0.0.1 port 8080 failed: Connection refused
* Failed to connect to 127.0.0.1 port 8080 after 2051 ms: Couldn't connect to server
* Closing connection 0
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 2051 ms: Couldn't connect to server

C:\Users\87488>curl --socks5 127.0.0.1:8080 -v http://www.qq.com
*   Trying 127.0.0.1:8080...
* connect to 127.0.0.1 port 8080 failed: Connection refused
* Failed to connect to 127.0.0.1 port 8080 after 2046 ms: Couldn't connect to server
* Closing connection 0
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 2046 ms: Couldn't connect to server

但服务器端有输出。

css 复制代码
ver 5 method [0 1]
auth success

侧面说明第一阶段成功。

第二形态 请求阶段

VER REP RSV ATYP BND.ADDR BND.PORT
1 1 X'00' 1 Variable 2
  1. VER socks版本,这里为0x05
  2. REP Relay field,内容取值如下 X'00' succeeded
  3. RSV 保留字段
  4. ATYPE 地址类型
  5. BND.ADDR 服务绑定的地址
  6. BND.PORT 服务绑定的端口DST.PORT

这里也是依次读取这些字节,理论同第一步,直接上代码。

go 复制代码
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)
	}
	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])

	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
}

第三形态 Relay阶段

建立TCP连接。

go 复制代码
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)

net包的Dial函数用于建立连接,第一个参数是连接类型,第二个参数是IP地址和端口。连接成功建立后,用dest变量表示,并打印日志。

实现数据的双向传输。确保双向数据传输完成后再进行下一步。

scss 复制代码
ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func() {
		_, _ = io.Copy(dest, reader)
		cancel()
	}()
	go func() {
		_, _ = io.Copy(conn, dest)
		cancel()
	}()

	<-ctx.Done()

最终验证:curl --socks5 127.0.0.1:8080 -v http://www.qq.com

成功连接后命令行呈现出网页的html代码,服务器输出:

yaml 复制代码
2023/08/14 11:37:09 dial 112.53.42.114 80

总结

这个项目总体来说还是非常有趣的,跟着老师的步骤一步步来,遇到不懂的地方进行查阅相关资料,极大地提升了自己对于网络连接的认知,非常感谢老师!

相关推荐
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刷题
青训营笔记