Socks5协议

socks5 是一个简单的代理协议,这里是 RFC

firefox proxy
settings

整个协议其实就是在建立TCP连接之后,真正的内容传输之前,加一点内容。以下是简述:

首先定义一下名词:

/-> | Firewall(防火墙) | ->

Client -> Server(代理服务器) -> Dst(目标地址)

然后定义一下表示形式:

+—-+———-+———-+

|VER | NMETHODS | METHODS |

+—-+———-+———-+

| 1 | 1 | 1 to 255 |

+—-+———-+———-+

例如上述,1就是指长度是一个byte,因此 1 to 255 也就是
1~255个byte。如果是 X’05’ 那么就是八进制的 05 也就是 0x05 的意思。

第一步,Client建立与Server之间的连接

建立TCP连接之后,Client发送如下数据:

+—-+———-+———-+

|VER | NMETHODS | METHODS |

+—-+———-+———-+

| 1 | 1 | 1 to 255 |

+—-+———-+———-+

  • VER 是指协议版本,因为是 socks5,所以值是 0x05

  • NMETHODS 是指有多少个可以使用的方法,也就是客户端支持的认证方法,有以下值:

  • METHODS 就是方法值,有多少个方法就有多少个byte

第二步,Server返回可以使用的方法

收到Client的请求之后,Server选择一个自己也支持的认证方案,然后返回:

+—-+——–+

|VER | METHOD |

+—-+——–+

| 1 | 1 |

+—-+——–+

VER 和 METHOD 的取值与上一节相同

第三步,客户端告知目标地址

+—-+—–+——-+——+———-+———-+

|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |

+—-+—–+——-+——+———-+———-+

| 1 | 1 | X’00’ | 1 | Variable | 2 |

+—-+—–+——-+——+———-+———-+

  • VER 还是版本,取值是 0x05

  • CMD 是指要做啥,取值如下:

    • CONNECT 0x01 连接

    • BIND 0x02 端口监听(也就是在Server上监听一个端口)

    • UDP ASSOCIATE 0x03 使用UDP

  • RSV 是保留位,值是 0x00

  • ATYP 是目标地址类型,有如下取值:

    • 0x01 IPv4

    • 0x03 域名

    • 0x04 IPv6

  • DST.ADDR 就是目标地址的值了,如果是IPv4,那么就是4
    bytes,如果是IPv6那么就是16 bytes,如果是域名,那么第一个字节代表
    接下来有多少个字节是表示目标地址

  • DST.PORT 两个字节代表端口号

第四步,服务端回复

+—-+—–+——-+——+———-+———-+

|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |

+—-+—–+——-+——+———-+———-+

| 1 | 1 | X’00’ | 1 | Variable | 2 |

+—-+—–+——-+——+———-+———-+

  • VER 还是版本,值是 0x05

  • REP 是状态码,取值如下:

    • 0x00 succeeded

    • 0x01 general SOCKS server failure

    • 0x02 connection not allowed by ruleset

    • 0x03 Network unreachable

    • 0x04 Host unreachable

    • 0x05 Connection refused

    • 0x06 TTL expired

    • 0x07 Command not supported

    • 0x08 Address type not supported

    • 0x09 to 0xff unassigned

  • RSV 保留位,取值为 0x00

  • ATYP 是目标地址类型,有如下取值:

    • 0x01 IPv4

    • 0x03 域名

    • 0x04 IPv6

  • DST.ADDR 就是目标地址的值了,如果是IPv4,那么就是4
    bytes,如果是IPv6那么就是16 bytes,如果是域名,那么第一个字节代表
    接下来有多少个字节是表示目标地址

  • DST.PORT 两个字节代表端口号

socks5 server
traffic

第五步,开始传输流量

到这一步,就成功了,接下来就该咋传输流量咋传输流量了。

总结

socks5是一个非常通用的代理协议,因此,无论我们自己要实现什么加密传输,都需要在client端设置一个socks5服务器,用于将
客户端例如浏览器等的请求理解之后,转换成私有协议。这篇文章中我们初步的看了一下socks5的结构,了解了一下socks5协议的
传输流程。

上一篇文章翻译完了 RFC 1928
,但是感觉仅仅只看协议标准文档的话会有点不够具体,所以抓包分析了一波
SOCKS 5
协议的具体交互流程,这里记录一下分析过程。前期的软件准备相信来看文章的人都知道如何去准备,这里不再赘述,直接步入正题。本文可能要对照标准文档阅读,传送门: RFC
1928 - SOCKS 5
协议中文文档「译」

1、交互第一步操作

根据标准文档,所有的操作第一步显然是建立 TCP
连接,然后进行指令交互。整个流程抓包如下图
(点击想看的图片细节位置即可放大对应位置),最左侧为数据包序号:

数据包抓包截图 -
整体流程

首先看前三个数据包,很典型的 TCP 握手连接,服务端为本地 1080
端口,源端口为本地 48398 端口,建立连接完毕之后,第 4 个数据包 Wireshark
已经自动识别了其为 SOCKS 数据包,查看其详情:

SOCKS 协商认证方式数据包 -
抓包截图

与文档一致,版本号 X’05’ ,可选方法数目 X’01’
(一种),方法列表此时显然只有一个字节,其值也对应为 X’00’
。根据文档显然其对应为 SOCKS 5 版本,客户端只有一种协商方法 ——
无需认证。其实仔细看上图, Wireshark
这些信息都有对应翻译成英文。紧随其后的第 5 个数据包为 TCP 的 ACK
包,跳过,第 6 个数据包如下:

SOCKS 协商认证方式回复数据包 -
抓包截图

这一步也与文档一致,为协商认证方式的回复数据包,版本号 X’05’
,选择的认证方式为 X’00’
。可见无需认证,后续直接进行指令交互就可以了。同样的 Wireshark
也已经对应翻译了。

2、TCP 请求流程

第 7 个数据包为应用发送到 SOCKS 服务端的 TCP ACK 包,第 8
个数据包正如协议描述,为指令请求,抓包样例为 CONNECT 指令,具体如下图:

SOCKS 5 TCP CONNECT
指令请求报文

只需要注意一下新碰到的字段: CMD 字段为 X’01’ 表示 CONNECT
指令,保留字段确实为 X’00’ ,地址类型为 X’03’ 也就是域名,域名开头为
X’0d’ 也就是十三,数一数 www.googoe.cm 确实长度为 13
,是的,测试的时候把 com 打错成了 cm ,不过该域名也是合法的。所有内容
Wireshark 依旧有翻译。

第 9 个数据包为第八个数据包的 TCP ACK 数据包,同时稍带了数据,也就是
SOCKS CONNECT 指令的应答报文,报文详情如下:

SOCKS 5 TCP CONNECT 指令请求 -
应答报文

报文内容 REP 字段为 X’00’
,表示指令执行成功,其他各个字段前面基本都有遇到了,但是需要注意一下其返回的地址与端口。按照标准文档,其应当为
SOCKS 5 服务连接目标服务采用的地址和端口,其中端口为 4112
倒没什么,为什么 IP
地址为 0.0.0.0 呢?首先需要确认一点,事实上,这个对应的真实地址,应用软件的客户端是用不到的,所以事实上是返回什么都没关系。这个样例里面返回为 0.0.0.0 是因为测试抓包的时候,我所使用的提供
SOCKS 5
服务的软件的自身原因(我测试过多台机器,也尝试更改过该软件的各项配置,发现最终返回的地址和端口都是固定的)。

在应用软件收到第 9
个数据包(且标记为执行成功)后,就可以直接开始数据发送了,所以可以看到第
10 个数据包已经是 TLS 握手包了,这里不再展开描述。

3、UDP 请求流程

与 TCP 类似, UDP 连接也需要上述第一步 ——
“交互第一步操作”。待认证完毕,即可以开始 UDP 中继操作。提到 UDP
最先想到的应用便是 DNS 服务协议。所以博主最开始也是想着强行劫持本地 DNS
流量来进行抓包操作,但是遇到了问题, TCP 测试流量简单配置 proxychains
就可以做到了,但 proxychains 不支持 UDP
流量自动转换走对应的代理协议,没办法只能自己写一个认证流程附带发个 UDP
的包,显然没有时间完整模拟 DNS ,所以下面分析的其实只是简单通过 UDP
协议发送字符串 “mierhuo” 到 8.8.8.8 的 53 端口。

这里从 UDP 中继指令开始,数据包如下:

UDP
中继指令数据包

可以看到其 CMD 字段为 X’03’ , 即 UDP ASSOCIATE
指令,执行UDP关联操作,需要中继 UDP 数据包到 8.8.8.8 地址的 53 端口,
SOCKS 5 服务端收到该信息后,应答报文如下:

UDP 中继指令 -
应答数据包

可见 SOCKS 服务端建立关联完毕,根据标准文档可见,中继服务的地址为
127.0.0.1 ,中继服务端口为 9050 ,那么客户端直接把 UDP
数据包按照文档要求添加相应包头后,即可直接发送到这个目标进行中继,如下:

UDP
中继数据包样例

前面部分 10 个字节为 UDP
包头,包含了中继信息(与前面关联信息一致),后面从 X’6d’
开始为具体数据,转换为 ASCII 码即为 “mierhuo”
。可见前面的部分基本符合实验预期。那么 SOCKS 5
服务端收到该信息后会有什么动作呢?我所使用的服务和标准文档说的一样,是个”多机系统”,显然可以猜测,其会通过”内部数据沟通方式”,会发送到”出口端”机器,如下:

SOCKS 5 服务端中继 UDP
操作

显然在其”内部数据沟通方式”中,其数据经过了加密,这部分不属于 SOCKS 5
标准,具体提供 SOCKS 5 服务的应用可自行根据情况去实现。

至此,标准文档的整个流程基本分析完毕。

十三.用SOCKS5实现proxy功能。

以上关于SOCKS协议的介绍均来自维基百科连接为:https://zh.wikipedia.org/wiki/SOCKS。知道这个协议的工作原理,那么我们看关于用Golang实现的代理就会很简单了,具体代码如下:

复制代码

1 /*

2 #!/usr/bin/env gorun

3 @author :yinzhengjie

4
Blog:http://www.cnblogs.com/yinzhengjie/tag/GO%E8%AF%AD%E8%A8%80%E7%9A%84%E8%BF%9B%E9%98%B6%E4%B9%8B%E8%B7%AF/

5 EMAIL:y1053419035@qq.com

6 */

7

8 package main

9

10 import (

11 “net”

12 “flag”

13 “log”

14 “bufio”

15 “errors”

16 “io”

17 “encoding/binary”

18 “fmt”

19 “sync”

20 )

21

22 /*

23 1.了解什么是socks5协议;

24 2.握手

25 3.获取客户端代理的请求

26 4.开始代理

27 */

28 func Hand_shake(r *bufio.Reader, conn net.Conn) error {

29 versiom,_ := r.ReadByte()
//用”*bufio.Reader”的”ReadByte”方法读取一个字节,即socks的版本号

30 log.Printf(“版本号是:%d”,versiom) //解析版本

31 if versiom != 5 {

32 return errors.New(“该协议不是socks5协议”)

33 }

34 nmethods,_ := r.ReadByte()
//nmethods是记录methods的长度的。nmethods的长度是1个字节。methods表示客户端支持的验证方式,可以有多种,他的尝试是1-255个字节。

35 log.Printf(“METHODS长度是:%d”,nmethods)

36

37 buf := make([]byte,nmethods)

38 io.ReadFull(r,buf)
//这个方法和”io.Copy”效果是看起来很相反,”io.ReadFull”循环读取”r”的数据并依次写入到”buf”中,直到吧”buf”写满为止。

39 log.Printf(“验证方式为:%v”,buf) /*常见的几种方式如下::

40 1>.数字”0”:表示不需要用户名或者密码验证;,

41 2>.数字”1”:GSSAPI是SSH支持的一种验证方式;

42 3>.数字”2”:表示需要用户名和密码进行验证;

43 4>.数字”3”至”7F”:表示用于IANA 分配(IANA ASSIGNED)

44 5>.数字”80”至”FE”表示私人方法保留(RESERVED FOR PRIVATE METHODS)

45 4>.数字”FF”:不支持所有的验证方式,这样的话就无法进行连接啦!

46

47 */

48

49 resp :=[]byte{5,0}
//以上操作实现了接受客户端消息,所以服务器需要回应客户端消息。第一个参数表示版本号为5,即socks5协议,第二个参数表示认证方式为0,即无需密码访问。

50 conn.Write(resp)

51 return nil

52 }

53

54 func Read_Addr(r *bufio.Reader) (string ,error) {

55 version,_ := r.ReadByte()
//读取一个字节,获取Socks协议的版本,Socks5默认为0x05,其值长度为1个字节。

56 log.Printf(“客户端协议版本:%d”,version)

57 if version != 5 {

58 return “”,errors.New(“该协议不是socks5协议”)

59 }

60 cmd ,_ := r.ReadByte()
/*从上一次读取的位置再往下读取一个字节。cmd代表客户端请求的类型,值长度也是1个字节,

61 有三种类型:

62 1>.数字”1”:表示客户端需要你帮忙代理连接,即CONNECT ;

63 2>.数字”2”:表示让你代理服务器,帮他建立端口,即BIND ;

64
3>.数字”3”:表示UDP连接请求用来建立一个在UDP延迟过程中操作UDP数据报的连接,即UDP
ASSOCIATE;

65 */

66 log.Printf(“客户端请求的类型是:%d”,cmd)

67 if cmd != 1 { //此处表示我们只处理客户端请求类型为”1”的连接。

68 return
“”,errors.New(“客户端请求类型不为”1”,即请求类型必须是代理连接!.”)

69 }

70

71 r.ReadByte() //跳过RSV字段,即RSV保留字端,值长度为1个字节。

72

73 addrtype,_ := r.ReadByte()

74 log.Printf(“客户端请求的远程服务器地址类型是:%d”,addrtype)
/*”addrtype”代表请求的远程服务器地址类型,它是一个可变参数,但是它值的长度1个字节,

75 有三种类型:

76 1>.数字”1”:表示是一个IPV4地址(IP V4 address);

77 2>.数字”3”:表示是一个域名(DOMAINNAME);

78 3>.数字”4”:表示是一个IPV6地址(IP V6 address);

79 */

80 if addrtype != 3 { //表示只处理请求的远程服务器地址类型是域名的。

81 return
“”,errors.New(“请求的远程服务器地址类型部位”3”,即请求的远程服务器必须地址必须是域名!”)

82 }

83

84 addrlen,_ := r.ReadByte()
//读取一个字节以得到域名的长度。因为服务器地址类型的长度就是”1”,所以它是IP还是域名我们都能获取到完整的内容。如果能走到这一行代码说明一定是域名,如果没有上面的一行过滤代码我们就还需要考虑IPV4和IPV6的两种情况啦!

85 addr := make([]byte,addrlen) //定义一个和域名长度一样大小的容器。

86 io.ReadFull(r,addr) //将域名的内容读取出来。

87 log.Printf(“域名为:%s”,addr)

88

89 var port int16
//因为端口是有2个字节来表示的,所以我们用int16来定义它的取值范围就OK。

90 binary.Read(r,binary.BigEndian,&port)
//读取2个字节,并将读取到的内容赋值给port变量。

91

92 return fmt.Sprintf(“%s:%d”,addr,port),nil

93

94 }

95

96

97 func handle_conn(conn net.Conn) {

98 defer conn.Close()

99 r := bufio.NewReader(conn)
//把”conn”进行包装,这样方便我们处理”conn”的数据。

100 Hand_shake(r,conn)
//进行握手,该函数是建立服务端和客户端的连接,但是仅仅建立握手并没有什么卵用,只是服务器收到了客户端的请求,我们还需要继续往下走。

101 addr,err := Read_Addr(r)
//获取客户端代理的请求,即让客户端发起请求,告诉Socks服务端客户端需要访问哪个远程服务器,其中包含,远程服务器的地址和端口,地址可以是IP4,IP6,也可以是域名。

102 if err != nil {

103 log.Print(err)

104 }

105 log.Print(“得到的完整的地址是:”,addr)
//注意:HTTP对应的是80端口,HTTPS对应的是443端口。

106 resp := []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00}
//详情请参考:http://www.cnblogs.com/yinzhengjie/p/7357860.html

107 conn.Write(resp)
//现在客户端把要请求的远程服务器的信息都告诉Socks5代理服务器了,那么Socks5代理服务器就可以和远程服务器建立连接了,不管连接是否成功等,都要给客户端回应。

108

109
//实现代理部分需要字节填充。首先你得会用switchyomega软件来调试上面的代码。

110 var (

111 remote net.Conn //定义远端的服务器连接。

112 )

113

114 remote,err = net.Dial(“tcp”,addr) //建立到目标服务器的连接。

115 if err != nil {

116 log.Print(err)

117 conn.Close()

118 return

119 }

120

121 wg := new(sync.WaitGroup)

122 wg.Add(2)

123

124 go func() {

125 defer wg.Done()

126 io.Copy(remote,r)
//读取原地址请求(conn),然后将读取到的数据发送给目标主机。这里建议用”r”,不建议用conn哟!因为它有重传机制!

127 remote.Close()

128 }()

129

130 go func() {

131 defer conn.Close()

132 io.Copy(conn,remote)
//与上面相反,就是讲目标主机的数据返回给客户端。

133 conn.Close()

134 }()

135 wg.Wait()

136

137 }

138

139 func main() {

140 flag.Parse()

141 listener,err := net.Listen(“tcp”,”:8888”)

142 if err != nil {

143 log.Fatal(err)

144 }

145 for {

146 conn,err := listener.Accept()

147 if err != nil {

148 log.Fatal(err)

149 }

150 go handle_conn(conn)

151 }

152 }

复制代码

   以上代码可以直接放在内网上做proxy使用,但是一些学习网络的小伙伴又会提出这个软件安全吗?存在哪些问题呢?在这里,我不得不说明一下,的确是存在安全隐患的,数据是没有被加密的,很容易出现中间人攻击的情况,以上代码存在3点问题:

  1>.客户端的请求数据未经过加密处理,安全性能很低;

  2>.错误处理没有考虑完整,比如:上面的代码只处理域名的请求;

  3>.这个代码放在服务器端运行时会有大量的输出字符,这样会对服务性能会有一些影响的,应该尽量减少这样的输入,我之所以那样写是为了说明代码的作用,熟练Golang的小伙伴可以自行更改。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!