Go语言实战2 | 青训营笔记

101 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天

根据课程学习从0构建一个socks5代理服务器。

SOCKS5协议

socks5的功能可以理解为,如果在外网想访问内网资源,但两者之间存在防火墙阻止直接的访问,那通过网络里的socks5代理,代理可以直接访问内网,所以外网用户通知代理去访问内网,此时代理是两者之间的沟通通道。

其主要包含如下过程:

首先,客户端首先向socks5服务器发送协议版本号和认证方式等。验证方式比如X'00'NO AUTHENTICATION REQUIREDX'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

每输入一个字符就会返回相同字符:

image-20230214142947501.png

协议认证

添加协议认证阶段的功能函数auth,其输入参数为只读流以及连接,使用process函数进行鉴权。

报文参数如下:

符号大小含义
VER1字节协议版本号
NMETHODS1字节支持认证的方法数量
METHODvariable鉴权方法
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部分连接发送的报文包括如下信息:

符号大小含义
VER1版本号
CMD10x01是CONNECT请求
RSV1保留字段,值为0x00
ATYP1目标地址类型,DST.ADDR的数据对应这个字段的类型,其中0x02表示IPV4的地址,0x03表示域名。
DST.ADDRvaraible目标地址
DST.PORT2目标端口,固定2个字节

代理服务器返回报文:

符号大小含义
VER1socks版本
REP1响应状态码,X'00'表示成功
RSV1保留字段
ATYPE1地址类型
BND.ADDRvariable服务器绑定的地址
BND.PROT2是服务器绑定的端口
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代理服务器。