Socks5协议
socks5 是一个简单的代理协议,这里是 RFC。
整个协议其实就是在建立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 是指有多少个可以使用的方法,也就是客户端支持的认证方法,有以下值:
0x00 NO AUTHENTICATION REQUIRED 不需要认证
0x01 GSSAPI
参考:https://en.wikipedia.org/wiki/Generic_Security_Services_Application_Program_Interface0x02 USERNAME/PASSWORD 用户名密码认证
0x03 to 0x7f IANA ASSIGNED 一般不用。INNA保留。
0x80 to 0xfe RESERVED FOR PRIVATE METHODS 保留作私有用处。
0xFF NO ACCEPTABLE METHODS 不接受任何方法/没有合适的方法
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是一个非常通用的代理协议,因此,无论我们自己要实现什么加密传输,都需要在client端设置一个socks5服务器,用于将
客户端例如浏览器等的请求理解之后,转换成私有协议。这篇文章中我们初步的看了一下socks5的结构,了解了一下socks5协议的
传输流程。
上一篇文章翻译完了 RFC 1928
,但是感觉仅仅只看协议标准文档的话会有点不够具体,所以抓包分析了一波
SOCKS 5
协议的具体交互流程,这里记录一下分析过程。前期的软件准备相信来看文章的人都知道如何去准备,这里不再赘述,直接步入正题。本文可能要对照标准文档阅读,传送门: RFC
1928 - SOCKS 5
协议中文文档「译」
1、交互第一步操作
根据标准文档,所有的操作第一步显然是建立 TCP
连接,然后进行指令交互。整个流程抓包如下图
(点击想看的图片细节位置即可放大对应位置),最左侧为数据包序号:
首先看前三个数据包,很典型的 TCP 握手连接,服务端为本地 1080
端口,源端口为本地 48398 端口,建立连接完毕之后,第 4 个数据包 Wireshark
已经自动识别了其为 SOCKS 数据包,查看其详情:
与文档一致,版本号 X’05’ ,可选方法数目 X’01’
(一种),方法列表此时显然只有一个字节,其值也对应为 X’00’
。根据文档显然其对应为 SOCKS 5 版本,客户端只有一种协商方法 ——
无需认证。其实仔细看上图, Wireshark
这些信息都有对应翻译成英文。紧随其后的第 5 个数据包为 TCP 的 ACK
包,跳过,第 6 个数据包如下:
这一步也与文档一致,为协商认证方式的回复数据包,版本号 X’05’
,选择的认证方式为 X’00’
。可见无需认证,后续直接进行指令交互就可以了。同样的 Wireshark
也已经对应翻译了。
2、TCP 请求流程
第 7 个数据包为应用发送到 SOCKS 服务端的 TCP ACK 包,第 8
个数据包正如协议描述,为指令请求,抓包样例为 CONNECT 指令,具体如下图:
只需要注意一下新碰到的字段: CMD 字段为 X’01’ 表示 CONNECT
指令,保留字段确实为 X’00’ ,地址类型为 X’03’ 也就是域名,域名开头为
X’0d’ 也就是十三,数一数 www.googoe.cm 确实长度为 13
,是的,测试的时候把 com 打错成了 cm ,不过该域名也是合法的。所有内容
Wireshark 依旧有翻译。
第 9 个数据包为第八个数据包的 TCP ACK 数据包,同时稍带了数据,也就是
SOCKS 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 中继指令开始,数据包如下:
可以看到其 CMD 字段为 X’03’ , 即 UDP ASSOCIATE
指令,执行UDP关联操作,需要中继 UDP 数据包到 8.8.8.8 地址的 53 端口,
SOCKS 5 服务端收到该信息后,应答报文如下:
可见 SOCKS 服务端建立关联完毕,根据标准文档可见,中继服务的地址为
127.0.0.1 ,中继服务端口为 9050 ,那么客户端直接把 UDP
数据包按照文档要求添加相应包头后,即可直接发送到这个目标进行中继,如下:
前面部分 10 个字节为 UDP
包头,包含了中继信息(与前面关联信息一致),后面从 X’6d’
开始为具体数据,转换为 ASCII 码即为 “mierhuo”
。可见前面的部分基本符合实验预期。那么 SOCKS 5
服务端收到该信息后会有什么动作呢?我所使用的服务和标准文档说的一样,是个”多机系统”,显然可以猜测,其会通过”内部数据沟通方式”,会发送到”出口端”机器,如下:
显然在其”内部数据沟通方式”中,其数据经过了加密,这部分不属于 SOCKS 5
标准,具体提供 SOCKS 5 服务的应用可自行根据情况去实现。
至此,标准文档的整个流程基本分析完毕。
十三.用SOCKS5实现proxy功能。
以上关于SOCKS协议的介绍均来自维基百科连接为:https://zh.wikipedia.org/wiki/SOCKS。知道这个协议的工作原理,那么我们看关于用Golang实现的代理就会很简单了,具体代码如下:
1 /*
2 #!/usr/bin/env gorun
3 @author :yinzhengjie
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 协议 ,转载请注明出处!