这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天
根据课程学习从0构建一个socks5代理服务器。
SOCKS5协议
socks5的功能可以理解为,如果在外网想访问内网资源,但两者之间存在防火墙阻止直接的访问,那通过网络里的socks5代理,代理可以直接访问内网,所以外网用户通知代理去访问内网,此时代理是两者之间的沟通通道。
其主要包含如下过程:
首先,客户端首先向socks5服务器发送协议版本号和认证方式等。验证方式比如X'00'NO AUTHENTICATION REQUIRED,X'02 USERNAME/PASSEWORD等方式。这里以不需要验证方式为例,服务端收到客户端请求后,就会向客户端返回协议版本号以及选择的认证方式。协商完成后, 客户端会发起连接,发送对远程服务器的请求信息,包括服务器地址端口信息以及请求的类型等。而socks5服务器会与远程服务器发起连接,并给客户端响应,relay阶段建立客户端与服务器的双向数据转发。
代码编写
echo服务器
编写echo服务器,用来作为SOCKS5的代理服务器基础并测试。
package main
import(
"bufio"
"log"
"net"
)
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 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
}
}
}
测试该服务端是通过windows系统上采用telnet工具,发起与echo服务器的连接,显示如下:
telnet 127.0.0.1 1080
每输入一个字符就会返回相同字符:
协议认证
添加协议认证阶段的功能函数auth,其输入参数为只读流以及连接,使用process函数进行鉴权。
报文参数如下:
| 符号 | 大小 | 含义 |
|---|---|---|
| VER | 1字节 | 协议版本号 |
| NMETHODS | 1字节 | 支持认证的方法数量 |
| METHOD | variable | 鉴权方法 |
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const 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: %w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported versioon: %w", err)
}
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("failed to read method: %w", err)
}
log.Println("ver", ver, "method", method)
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
process函数如下:
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.Printf("auth success")
}
运行代理服务器,并启动客户端,使用curl输入:
curl --socks5 127.0.0.1:1080 -v http://www.qq.com
客户端显示:
* Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv4 101.91.22.57:80 (locally resolved)
* Failed to receive SOCKS5 connect request ack.
* Closing connection 0
curl: (97) Failed to receive SOCKS5 connect request ack.
服务端打印日志:
2023/02/14 14:58:18 ver 5 method [0 1]
2023/02/14 14:58:18 auth success
证明能够成功通过鉴权。
连接阶段
连接部分connect,首先函数签名是Reader, 连接是Conn,而connect部分连接发送的报文包括如下信息:
| 符号 | 大小 | 含义 |
|---|---|---|
| VER | 1 | 版本号 |
| CMD | 1 | 0x01是CONNECT请求 |
| RSV | 1 | 保留字段,值为0x00 |
| ATYP | 1 | 目标地址类型,DST.ADDR的数据对应这个字段的类型,其中0x02表示IPV4的地址,0x03表示域名。 |
| DST.ADDR | varaible | 目标地址 |
| DST.PORT | 2 | 目标端口,固定2个字节 |
代理服务器返回报文:
| 符号 | 大小 | 含义 |
|---|---|---|
| VER | 1 | socks版本 |
| REP | 1 | 响应状态码,X'00'表示成功 |
| RSV | 1 | 保留字段 |
| ATYPE | 1 | 地址类型 |
| BND.ADDR | variable | 服务器绑定的地址 |
| BND.PROT | 2 | 是服务器绑定的端口 |
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
buf := make([]byte, 4) //读入包括ver, cmd,rsv以及atyp字段
_, 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 support version:%w", err)
}
if cmd != cmdBind {
return fmt.Errorf("not support cmd: %w", err)
}
addr := ""
switch atyp {
case atypIPV4:
_, 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()
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
}
客户端发送信息打印内容如下:
* Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv4 101.91.22.57:80 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080 (#0)
> GET / HTTP/1.1
> Host: www.qq.com
> User-Agent: curl/7.83.1
> Accept: */*
>
* Received HTTP/0.9 when not allowed
* Closing connection 0
curl: (1) Received HTTP/0.9 when not allowed
服务端打印日志如下:
2023/02/14 16:00:34 ver 5 method [0 1]
2023/02/14 16:00:34 dial 101.91.22.57 80
2023/02/14 16:00:34 auth and connect success
说明connect函数成功通过。
relay阶段
relay阶段,与目标域名建立TCP连接,并建立客户端和下游服务器的双向数据转发copy(dst Writer, src Reader)
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)
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
为防止connect 函数在启动了两个协程后,直接返回关闭连接,使用了context机制。此时connect函数会等待ctx.done,而ctx.done执行的时机也就是cancel函数执行的时机,所以说只有当双向数据转发中,有一个出现error才会启动cancel函数,从而导致服务终止并关闭。
最后运行curl,客户端打印:
$curl --socks5 127.0.0.1:1080 -v http://www.qq.com
* Trying 127.0.0.1:1080...
* SOCKS5 connect to IPv4 101.91.42.232:80 (locally resolved)
* SOCKS5 request granted.
* Connected to 127.0.0.1 (127.0.0.1) port 1080 (#0)
> GET / HTTP/1.1
> Host: www.qq.com
> User-Agent: curl/7.83.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Moved Temporarily
< Server: stgw
< Date: Tue, 14 Feb 2023 08:59:55 GMT
< Content-Type: text/html
< Content-Length: 137
< Connection: keep-alive
< Location: https://www.qq.com/
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>stgw</center>
</body>
</html>
* Connection #0 to host 127.0.0.1 left intact
服务端打印
2023/02/14 16:59:05 dial 101.91.42.232 80
2023/02/14 16:59:13 dial 101.91.42.232 80
2023/02/14 16:59:55 dial 101.91.42.232 80
基于上述步骤完成一个socks5代理服务器。