Socks5协议简介

socks5协议是一种代理协议,用于在客户端与服务器直接转发数据。

简单来说,一般适用于如下场景,若Client无法访问目标服务器,但能访问socks5代理服务器,而socks5服务器能够访问目标服务器,那么Client就可以通过socks5代理服务器访问目标服务器。

socks5协议的具体RFC1928文档如下

https://www.ietf.org/rfc/rfc1928.txt

工作过程

无代理情况下

1
2
3
4
5
@startuml
Client->Server++: 1. 建立TCP连接
Client->Server++: 2. 发送HTTP请求
return 3.返回HTTP响应
@enduml

有代理情况下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@startuml
participant Client as c
participant Proxy as p
participant Server as s
c->p++: 建立TCP连接
return TCP连接建立成功

c->p++: 协商阶段
return 通过协商

c->p++: 发送目标服务器请求
p->s++: 建立TCP连接
return TCP连接建立成功
return 返回状态

c->p++: 发送数据
p->s++: 转发数据
return 响应结果
return 响应结果
@enduml

代理服务器实现

接受客户端的TCP连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
server, err := net.Listen(
"tcp",
"0.0.0.0:1080")
if err != nil {
panic(err)
}
for {
conn, err := server.Accept()
if err != nil {
log.Panicf("Accept failed %v", err)
}
go process(conn)
}
}

当TCP连接建立完成后,对于每个Client的处理流程在每个goroutine中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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("Client auth success")
err = connect(reader, conn)
if err != nil {
log.Printf("Client %v auth failed: %v", conn.RemoteAddr(), err)
return
}
}

协商阶段

首先客户端向socks5代理服务器发送请求,其中包含了可用的鉴权方式,内容如下,

Field VER NMETHODS METHODS
Length 1 1 1 to 255

VER: 协议版本socks5为0x05
NMETHODS: 支持的认证方法数
METHODS: 有NMETHODS个字节,其中包含了各种认证方式

  • 0x00: NO AUTHENTICATION REQUIRED

    无需验证

  • 0x01: GSSAPI

    通用安全服务应用程序接口

  • 0x02: USERNAME/PASSWORD

    用户名密码

  • 0x03 to 0x7F: IANA ASSIGNED

    一般不用。INNA保留

  • 0x80 to 0xFE: RESERVED FOR PRIVATE METHODS

    保留作私有用处

  • 0xFF: NO ACCEPTABLE METHODS

    不接受任何方法/没有合适的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const Socks5Version = 0x05

func readMethods(reader *bufio.Reader) (ver byte, methods []byte, err error) {
// 读取版本号
ver, err = reader.ReadByte()
if err != nil {
err = fmt.Errorf("read ver failed:%w", err)
return
}
if ver != Socks5Version {
err = fmt.Errorf("not supported ver:%v", ver)
return
}
// 读取方法数
methodSize, err := reader.ReadByte()
if err != nil {
err = fmt.Errorf("read methodSize failed:%w", err)
return
}
// 读取所有的鉴权方法
methods = make([]byte, methodSize)
_, err = io.ReadFull(reader, methods)
if err != nil {
err = fmt.Errorf("read method failed:%w", err)
return
}
log.Println("version", ver, "methods", methods)
return
}

然后服务器选中一个支持的METHOD返回给客户端,格式如下

Field VER METHOD
Length 1 1

当客户端收到的METHOD字段值为0x00时,直接跳过认证阶段进入请求阶段,当收到0xFF时,直接断开连接,其他的值则进入相应的认证阶段。

1
2
3
4
5
6
7
func sendSupportedMethod(method byte, conn net.Conn) (err error) {
_, err = conn.Write([]byte{socks5Version, method})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return
}

最后,协商阶段的函数为

1
2
3
4
5
6
7
8
9
10
11
12
func auth(reader *bufio.Reader, conn net.Conn) error {
_, err := readMethods(reader)
if err != nil {
return err
}
// TODO: 暂时只支持不需要验证手段
err = sendSupportedMethod(0x00, conn)
if err != nil {
return err
}
return nil
}

请求阶段

协商完毕后,客户端向socks5服务器发起请求,格式如下,

Field VER CMD RSV ATYP DST.ADDR DST.PORT
Length 1 1 0x00 1 Variable 2
  • VER: 协议版本,0x05

  • CMD: 控制字,表示要干啥

    • 0x01: CONNECT连接

    • 0x02: 端口监听

    • 0x03: 使用UDP

  • RSV: 保留位0x00

  • ATYP: address type 目标地址类型

    • 0x01: IPv4

    • 0x03: 域名

    • 0x04: IPv6

  • DST.ADDR: 目标地址值

    • 若IPv4则有四字节

    • 若IPv6则有16字节

    • 若为域名,则第一个字节为字符串长度,接下来有第一个字节这么大的目标地址

  • DST.PORT: 目标端口号

之后socks5向客户端返回如下信息

Field VER REP RSV ATYP BND.ADDR BND.PORT
Length 1 1 0x00 1 Variable 2
      o  VER    protocol version: X'05'
      o  REP    Reply field:
         o  X'00' succeeded
         o  X'01' general SOCKS server failure
         o  X'02' connection not allowed by ruleset
         o  X'03' Network unreachable
         o  X'04' Host unreachable
         o  X'05' Connection refused
         o  X'06' TTL expired
         o  X'07' Command not supported
         o  X'08' Address type not supported
         o  X'09' to X'FF' unassigned
      o  RSV    RESERVED
      o  ATYP   address type of following address
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package main

import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)

func readHeader(reader *bufio.Reader) (ver, cmd, rsv, atyp byte, err error) {
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
return
}
ver, cmd, rsv, atyp = buf[0], buf[1], buf[2], buf[3]
return
}

func readDstAddr(atyp byte, reader *bufio.Reader) (addr string, err error) {
switch atyp {
case atypIPV4:
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
err = fmt.Errorf("read atyp failed: %w", err)
return
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypHOST:
hostSize, err := reader.ReadByte()
if err != nil {
err = fmt.Errorf("read hostSize failed:%w", err)
return "", err
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
err = fmt.Errorf("read host failed:%w", err)
return "", err
}
addr = string(host)
case atypIPV6:
// TODO: IPv6暂不支持
err = errors.New("IPv6: no supported yet")
default:
// 未知的atyp
err = errors.New("invalid atyp")
}
return
}
func readDstPort(reader *bufio.Reader) (port uint16, err error) {
buf := make([]byte, 2)
_, err = io.ReadFull(reader, buf)
if err != nil {
err = fmt.Errorf("read port failed:%w", err)
return
}
port = binary.BigEndian.Uint16(buf)
return
}
func connect(reader *bufio.Reader, conn net.Conn) error {
ver, cmd, _, atyp, err := readHeader(reader)
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
if ver != socks5Version {
return fmt.Errorf("not supported ver:%v", ver)
}
// TODO: 目前仅支持CONNECT连接
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd)
}

addr, err := readDstAddr(atyp, reader)
if err != nil {
return err
}
port, err := readDstPort(reader)
if err != nil {
return err
}

// 与目标服务器建立连接
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)

// TODO: 这里暂时直接简单地响应
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}

// 建立双向转发
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
}

所有常量定义

1
2
3
4
5
6
7
package main

const socks5Version = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypHOST = 0x03
const atypIPV6 = 0x04

完整代码如下

zhangzqs/socks5-server (github.com)