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

写在前面

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

代理服务器简介

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

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

Socks5代理简介

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

工作的简单模型如下

client---->proxy(socks5)---->server

Socks5代理服务器的实现

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

简易代理服务器

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

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

其中

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

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

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

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

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,即输入什么就会输出什么。

image.png

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

服务器plus

第一形态 协商阶段

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

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

versionnmethodsmethods
111 to 255
versionmethod
11

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

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函数中,先读版本号,不符直接退出。

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)

到这里,协商阶段结束,我们可以来验证一下。 将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 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

但服务器端有输出。

ver 5 method [0 1]
auth success

侧面说明第一阶段成功。

第二形态 请求阶段

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

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

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连接。

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变量表示,并打印日志。

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

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代码,服务器输出:

2023/08/14 11:37:09 dial 112.53.42.114 80

总结

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

全部评论

相关推荐

03-29 12:10
门头沟学院 C++
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务