写在前面
本文是自己进入青训营以来的第七篇笔记,这次实操的项目是我自己之前鸽了很久的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)
}
其中
net
是Go中的一个包;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 |
- VER socks版本,这里为0x05
- REP Relay field,内容取值如下 X'00' succeeded
- RSV 保留字段
- ATYPE 地址类型
- BND.ADDR 服务绑定的地址
- 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
总结
这个项目总体来说还是非常有趣的,跟着老师的步骤一步步来,遇到不懂的地方进行查阅相关资料,极大地提升了自己对于网络连接的认知,非常感谢老师!