1. Socks5 协议简介
1.1 什么是Socks代理
Socks代理是基于Socks协议的一种代理,也叫全能代理,SOCKS代理与其他类型的代理不同,它只是简单地传递数据包,而并不关心是何种应用协议,既可以是HTTP请求,所以SOCKS代理服务器比其他类型的代理服务器速度要快得多。
1.2 SOCKS代理又分为SOCKS4和SOCKS5
SOCK4只支持TCP协议;SOCK5支持TCP和UDP协议,还支持身份验证、服务器端域名解释等。
SOCKS5代理则既支持TCP协议又支持UDP协议(即用户数据包协议),还支持各种身份验证机制、服务器端域名解析等。
1.3 为什么要用socks代理?
更少的限制
有的代理可以方规避暗示的封锁,然后许多阻止是使用DPI(深度数据包检测)技术实现的,并且流量在到达被阻止的网站之前就在ISP端被阻止,代理也无济于事。HTTP代理只能处理网页,但SOCKS5可以处理任何类型的流量。
传输协议自由度高
使用 SOCKS5 代理,用户可以自由选择 TCP 或 UDP 传输数据。TCP 连接更可靠,因为连接在整个数据传输过程中都保持不变。UDP 更快、更高效,使其成为涉及大量数据的用例的绝佳选择。
提高性能和速度
SOCKS5的前身使用TCP协议,而较新的代理可以处理UDP数据包。TCP是一种可以保证交付的协议,这意味着在客户端和目的地之间传输的数据包会被交付。为了确保这种可传递性,用户需要彻底格式化消息。
更高的性能
SOCKS5 代理不会重写数据包标头,因此用户的数据被错误标记或错误路由的可能性较小。此外SOCKS5代理还可以发送传输速度更快的较小数据包。这种改进的性能让 SOCKS5 代理可以很好地与 P2P 网站和平台配合使用。
与 HTTP 代理相比,Socks5 可以很好地处理不同的协议。
此外,SOCKS 服务器不会以任何方式解释设备和服务器之间的流量。这就是为什么对于防火墙后面的客户端无法与防火墙外部的服务器建立 TCP 连接的一个很好的解决方案的原因。
2. 协议详解
Socks5代理的实现流程
实现socks5代理主要有三个步骤,分别为三次握手建立连接,协商认证和请求代理
三次握手建立链接
连接
客户端首先和代理服务器进行三次握手建立连接
协商认证
1. TCP/UDP连接成功后进行协商认证,客户端发送一个数据包:
+-----+----------+----------+
| VER | NMETHODS | METHODS |
+-----+----------+----------+
| 1 | 1 | 1 to 255 |
+-----+----------+----------+
VER : (1 bytes) 协议版本号
NMETHODS : (1 bytes) 客户端支持的认证方法数量(这个的大小决定了METHODS的数量)
METHODS : (1 bytes) 每个byte对应一个认证方法
X’00’ 不需要身份验证(NO AUTHENTICATION REQUIRED)
X’01’ GSSAPI
X’02’ 用户密码认证(USERNAME/PASSWORD)
X’03’ to X’7F’ IANA ASSIGNED
X’80’ to X’FE’ RESERVED FOR PRIVATE METHODS
X’FF’ NO ACCEPTABLE METHODS
比如当客户端发送一个数据包 {0x05,0x02,0x00,0x02},则表明这是这是一个socks5协议,并且客户端支持无密码认证和用户密码认证两种认证方式。
2. 代理服务器返回一个应答数据包:
+-----+--------+
| VER | METHOD |
+-----+--------+
| 1 | 1 |
+-----+--------+
VER : (1 bytes) socks 版本,这里用的是 socks5,所以是0x05。
METHOD : (1 bytes) 代表代理服务器选择了一种握手(认证)方式。该方法应从客户端提供的认证方法中挑选一个,或者是X’FF’用以拒绝认证
X’00’ 不需要身份验证(NO AUTHENTICATION REQUIRED)
X’01’ GSSAPI
X’02’ 用户密码认证(USERNAME/PASSWORD)
X’03’ to X’7F’ IANA ASSIGNED
X’80’ to X’FE’ RESERVED FOR PRIVATE METHODS
X’FF’ NO ACCEPTABLE METHODS
例如,代理服务器发送的 5 0,代表 版本5 选择了“不加密”的握手方式。如果客户端的所有握手方式代理服务器都不满足,直接断开连接。
用户认证请求
如果代理服务器发送{0x05,0x02},代表socks版本5 选择了“用户名、密码认证”的握手方式。此时客户端会发送账号密码数据给代理服务器,再由代理服务器检验,并返回结果。格式如下:
+-----+-----------------+----------+-----------------+----------+
| VER | USERNAME_LENGTH | USERNAME | PASSWORD_LENGTH | PASSWORD |
+-----+-----------------+----------+-----------------+----------+
| 1 | 1 | 1 to 255 | 1 | 1 to 255 |
+-----+-----------------+----------+-----------------+----------+
VERSION:认证子协商版本(与 SOCKS 协议版本的0x05无关系)
USERNAME_LENGTH:用户名长度
USERNAME:用户名字节数组,长度为 USERNAME_LENGTH
PASSWORD_LENGTH:密码长度
PASSWORD:密码字节数组,长度为 PASSWORD_LENGTH
用户认证请求的响应
+-----+--------+
| VER | STATUS |
+-----+--------+
|0x01 | 0x01 |
+-----+--------+
VERSION:认证子协商版本,与客户端 VERSION 字段一致
STATUS:认证结果(0x00 认证成功 / 大于0x00 认证失败)
请求代理
1. 客户端在认证成功之后,需要发送一个数据包来请求服务端 :
+-----+-----+-------+------+----------+----------+
| VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+-----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+-----+-----+-------+------+----------+----------+
VER : (1 bytes) 协议版本号,此处为X’05’
CMD : (1 bytes) 指定代理方式
CONNECT X’01’
BIND X’02’
UDP ASSOCIATE X’03’
RSV : (1 bytes) 预留位置,标准Socks5应为X’00’
ATYP : (1 bytes) 指定DST.ADDR的类型
IPV4地址 : X’01’
Domain Name : X’03’
IPV6地址 : X’04’
DST.ADDR : 目标的地址,长度由ATYP指定
IPV4 : (4 bytes)
域名 : 第一个byte指定长度n,其后跟着n个byte标识域名
IPV6 : (16 bytes)
DST.PORT : (2 bytes)目标的端口
CONNECT 方法:
这个时最常用的代理方法,当服务端接收到的数据包中 CMD 为 0x01 时,服务器使用 Connect 方法进行代理。
此时,服务端尝试使用TCP协议连接对应的 (DST.ADDR, DST.PORT),根据连接的情况,决定 REP 的值。
如果连接成功,回复的数据包中的 BND.ADDR,BND.PORT 没有太大的意义,象征性的填写Socks 服务端在此次连接中使用的 ADDR 和 PORT 即可。
BIND 方法
Bind方法使用于目标主机需要主动连接客户机的情况(如 FTP 协议)
当服务端接收到的数据包中 CMD 为 0x02 时,服务器使用 Bind 方法进行代理。使用 - Bind 方法代理时服务端需要回复客户端至多两次数据包。
服务端使用 TCP 协议连接对应的 (DST.ADDR, DST.PORT),如果失败则返回失败状态的数据包并且关闭此次会话。如果成功,则监听 (BND.ADDR, BND.PORT) 来接受请求的主机的请求,然后返回第一次数据包,该数据包用以让客户机发送指定目标主机连接客户机地址和端口的数据包。
在目标主机连接服务端指定的地址和端口成功或失败之后,回复第二次数据包。此时的(BND.ADDR, BND.PORT) 应该为目标主机与服务端建立的连接的地址和端口。
2. 客户端发送了socks请求信息,就立即发送它建立socks服务器的连接,并完成身份验证。服务器验证请求,然后回复如下格式的回复 :
+-----+-----+-------+------+----------+----------+
| VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+-----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+-----+-----+-------+------+----------+----------+
VER : (1 bytes)协议版本号 X’05’
REP : (1 bytes)回复请求的状态
X’00’ 成功代理
X’01’ SOCKS服务器出现了错误
X’02’ 不允许的连接
X’03’ 找不到网络
X’04’ 找不到主机
X’05’ 连接被拒
X’06’ TTL超时
X’07’ 不支持的CMD
X’08’ 不支持的ATYP
X’09’ to X’FF’ Socks5标准中没有分配对应的状态
RSV : (1 bytes)预留位
ATYP : (1 bytes)BND.ADDR的类型
IPV4: X’01’
域名: X’03’
IPV6: X’04’
BND.ADDR : 服务端指定的地址,长度由ATYP指定
IPV4: (4 bytes)
域名: 第一个byte指定长度n,其后跟着n个byte标识域名
IPV6: (16 bytes)
BND.PORT : (2 bytes)服务端指定的端口
socks5的实现
Go实现socks5编程之需要两个步骤:
监听端口 1080(socks5的默认端口)
每收到一个请求,启动一个 goroutine 来处理它
func main() {
server, err := net.Listen("tcp", ":1080")
if err != nil {
fmt.Printf("Listen failed: %v\n", err)
return
}
for {
client, err := server.Accept()
if err != nil {
fmt.Printf("Accept failed: %v", err)
continue
}
go process(client)
}
}
func process(client net.Conn) {
remoteAddr := client.RemoteAddr().String()
fmt.Printf("Connection from %s\n", remoteAddr)
client.Write([]byte("Hello world!\n"))
client.Close()
}
socks5是个二进制协议,不那么直观,不过实际上非常简单,主要分成三个步骤:
认证
建立连接
转发数据
我们只需 16 行就能把 socks5 的架子搭起来:
func process(client net.Conn) {
// 认证
if err := Socks5Auth(client); err != nil {
fmt.Println("auth error:", err)
client.Close()
return
}
// 建立连接
target, err := Socks5Connect(client)
if err != nil {
fmt.Println("connect error:", err)
client.Close()
return
}
// 转发数据
Socks5Forward(client, target)
}
然后只要把 Socks5Auth、Socks5Connect 和 Socks5Forward 给补上,一个完整的 socks5 代理就完成啦!
1. Socks5Auth
客户端首先会向socks5服务器发送一个请求认证包:
+-----+----------+----------+
| VER | NMETHODS | METHODS |
+-----+----------+----------+
| 1 | 1 | 1 to 255 |
+-----+----------+----------+
我们用如下代码来读取客户端的请求认证包:
func Socks5Auth(client net.Conn) (err error) {
buf := make([]byte, 256)
// 读取 VER 和 NMETHODS
n, err := io.ReadFull(client, buf[:2])
if n != 2 {
return errors.New("reading header: " + err.Error())
}
ver, nMethods := int(buf[0]), int(buf[1])
if ver != 5 {
return errors.New("invalid version")
}
// 读取 METHODS 列表
n, err = io.ReadFull(client, buf[:nMethods])
if n != nMethods {
return errors.New("reading methods: " + err.Error())
}
//TO BE CONTINUED...
Go语言中的ReadFull()函数用于从指定的读取器“r”读取到指定的缓冲区“buf”,并且复制的字节恰好等于指定缓冲区的长度
ReadFull函数原型:func ReadFull(r Reader, buf []byte) (n int, err error)
然后服务端得选择一种认证方式,告诉客户端:
+-----+--------+
| VER | METHOD |
+-----+--------+
| 1 | 1 |
+-----+--------+
简单起见我们就不认证了,给客户端回复 0x05、0x00 即可:
//无需认证
n, err = client.Write([]byte{0x05, 0x00})
if n != 2 || err != nil {
return errors.New("write rsp err: " + err.Error())
}
return nil
}
2. Socks5Connect
在完成认证以后,客户端需要告知服务端它的目标地址,协议具体要求为:
+-----+-----+-------+------+----------+----------+
| VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+-----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+-----+-----+-------+------+----------+----------+
先读四个字段
func Socks5Connect(client net.Conn) (net.Conn, error) {
// 创建一个缓冲区
buf := make([]byte, 256)
// 从请求流中读取到缓冲区中,只取前四个字节
n, err := io.ReadFull(client, buf[:4])
if n != 4 {
return nil, errors.New("read header: " + err.Error())
}
ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3]
// 检查socks5协议
if ver != 5 || cmd != 1 {
return nil, errors.New("invalid ver/cmd")
}
//TO BE CONTINUED...
注:BIND 和 UDP ASSOCIATE 这两个 cmd 我们这里就先偷懒不支持了。
接下来问题是如何读取 DST.ADDR 和 DST.PORT。
如前所述,ADDR 的格式取决于 ATYP:
0x01:4个字节,对应 IPv4 地址
0x02:先来一个字节 n 表示域名长度,然后跟着 n 个字节。注意这里不是 NUL 结尾的。
0x03:16个字节,对应 IPv6 地址
addr := ""
switch atyp {
case 1:
n, err = io.ReadFull(client, buf[:4])
if n != 4 {
return nil, errors.New("invalid IPv4: " + err.Error())
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case 3:
n, err = io.ReadFull(client, buf[:1])
if n != 1 {
return nil, errors.New("invalid hostname: " + err.Error())
}
addrLen := int(buf[0])
n, err = io.ReadFull(client, buf[:addrLen])
if n != addrLen {
return nil, errors.New("invalid hostname: " + err.Error())
}
// 获取ipv4地址
addr = string(buf[:addrLen])
case 4:
return nil, errors.New("IPv6: no supported yet")
default:
return nil, errors.New("invalid atyp")
}
注:这里再偷个懒,IPv6 也不管了。
接着要读取的 PORT 是一个 2 字节的无符号整数。
n, err = io.ReadFull(client, buf[:2])
if n != 2 {
return nil, errors.New("read port: " + err.Error())
}
port := binary.BigEndian.Uint16(buf[:2])
既然 ADDR 和 PORT 都就位了,我们马上创建一个到 dst 的连接:
destAddrPort := fmt.Sprintf("%s:%d", addr, port)
dest, err := net.Dial("tcp", destAddrPort)
if err != nil {
return nil, errors.New("dial dst: " + err.Error())
}
最后一步是告诉客户端,我们已经准备好了,协议要求是:
+-----+-----+-------+------+----------+----------+
| VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+-----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+-----+-----+-------+------+----------+----------+
BND.ADDR/PORT 本应填入 dest.LocalAddr(),但因为基本上也没什么用,我们就直接用 0 填充了:
n, err = client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
dest.Close()
return nil, errors.New("write rsp: " + err.Error())
}
return dest, nil
}
注: ATYP = 0x01 表示 IPv4,所以需要填充 6 个 0 —— 4 for ADDR, 2 for PORT。
3. Socks5Forward
万事俱备,剩下的事情就是转发。
所谓“转发”,其实就是从一头读,往另一头写。
需要注意的是,由于 TCP 连接是双工通信,我们需要创建两个 goroutine,用于完成“双工转发”。
由于 golang 有一个 io.Copy 用来做转发的事情,代码只要 9 行,简单到难以形容:
func Socks5Forward(client, target net.Conn) {
forward := func(src, dest net.Conn) {
defer src.Close()
defer dest.Close()
io.Copy(src, dest)
}
go forward(client, target)
go forward(target, client)
}
注意:在发送完以后需要关闭连接。
代码
以上原理的实现
package main
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
)
func main() {
server, err := net.Listen("tcp", ":1080")
if err != nil {
fmt.Printf("Listen failed: %v\n", err)
return
}
for {
client, err := server.Accept()
if err != nil {
fmt.Printf("Accept failed: %v", err)
continue
}
go process(client)
}
}
func process(client net.Conn) {
if err := Socks5Auth(client); err != nil {
fmt.Println("auth error:", err)
client.Close()
return
}
target, err := Socks5Connect(client)
if err != nil {
fmt.Println("connect error:", err)
client.Close()
return
}
Socks5Forward(client, target)
}
func Socks5Auth(client net.Conn) (err error) {
buf := make([]byte, 256)
// 读取 VER 和 NMETHODS
n, err := io.ReadFull(client, buf[:2])
if n != 2 {
return errors.New("reading header: " + err.Error())
}
ver, nMethods := int(buf[0]), int(buf[1])
if ver != 5 {
return errors.New("invalid version")
}
// 读取 METHODS 列表
n, err = io.ReadFull(client, buf[:nMethods])
if n != nMethods {
return errors.New("reading methods: " + err.Error())
}
// 通知客户端无需认证
n, err = client.Write([]byte{0x05, 0x00})
if n != 2 || err != nil {
return errors.New("write rsp err: " + err.Error())
}
return nil
}
func Socks5Connect(client net.Conn) (net.Conn, error) {
buf := make([]byte, 256)
n, err := io.ReadFull(client, buf[:4])
if n != 4 {
return nil, errors.New("read header: " + err.Error())
}
ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3]
if ver != 5 || cmd != 1 {
return nil, errors.New("invalid ver/cmd")
}
addr := ""
switch atyp {
case 1:
n, err = io.ReadFull(client, buf[:4])
if n != 4 {
return nil, errors.New("invalid IPv4: " + err.Error())
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case 3:
n, err = io.ReadFull(client, buf[:1])
if n != 1 {
return nil, errors.New("invalid hostname: " + err.Error())
}
addrLen := int(buf[0])
n, err = io.ReadFull(client, buf[:addrLen])
if n != addrLen {
return nil, errors.New("invalid hostname: " + err.Error())
}
addr = string(buf[:addrLen])
case 4:
return nil, errors.New("IPv6: no supported yet")
default:
return nil, errors.New("invalid atyp")
}
n, err = io.ReadFull(client, buf[:2])
if n != 2 {
return nil, errors.New("read port: " + err.Error())
}
port := binary.BigEndian.Uint16(buf[:2])
destAddrPort := fmt.Sprintf("%s:%d", addr, port)
dest, err := net.Dial("tcp", destAddrPort)
if err != nil {
return nil, errors.New("dial dst: " + err.Error())
}
n, err = client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
dest.Close()
return nil, errors.New("write rsp: " + err.Error())
}
return dest, nil
}
// socks转发
func Socks5Forward(client, target net.Conn) {
forward := func(src, dest net.Conn) {
defer src.Close()
defer dest.Close()
io.Copy(src, dest)
}
go forward(client, target)
go forward(target, client)
}
带有认证的socks5的实现
package main
import (
"encoding/binary"
"errors"
"fmt"
"io"
"net"
)
func main() {
//
server, err := net.Listen("tcp", ":1080")
if err != nil {
fmt.Printf("Listen failed: %v\n", err)
return
}
fmt.Println("Socks5 create success!")
fmt.Println("Listen Port:1080")
for {
client, err := server.Accept()
if err != nil {
fmt.Printf("Accept failed: %v", err)
continue
}
go process(client)
}
}
func process(client net.Conn) {
if err := Socks5Auth(client); err != nil {
fmt.Println("auth error:", err)
client.Close()
return
}
target, err := Socks5Connect(client)
if err != nil {
fmt.Println("connect error:", err)
client.Close()
return
}
Socks5Forward(client, target)
}
func Socks5Auth(client net.Conn) (err error) {
user := "user"
passwd := "passwd"
buf := make([]byte, 256)
// 读取 VER 和 NMETHODS
n, err := io.ReadFull(client, buf[:2])
if n != 2 {
return errors.New("reading header: " + err.Error())
}
ver, nMethods := int(buf[0]), int(buf[1])
if ver != 5 {
return errors.New("invalid version")
}
// 读取 METHODS 列表
n, err = io.ReadFull(client, buf[:nMethods])
if n != nMethods {
return errors.New("reading methods: " + err.Error())
}
//fmt.Println(client.Read())
fmt.Println(client.RemoteAddr(), "methods:", buf[:n])
// 无认证的socks5
if buf[0] == 0x00 {
n, err = client.Write([]byte{0x05, 0x00})
if n != 2 || err != nil {
return errors.New("write rsp err: " + err.Error())
}
// 允许无认证
//return nil
return errors.New("no auth")
}
// 带认证的socks5
if buf[0] == 0x02 {
n, err = client.Write([]byte{0x05, 0x02})
if n != 2 {
return errors.New("reading methods: " + err.Error())
}
//fmt.Println("已经选择带用户名/密码认证方式的socks5")
// 检查请求头版本
n, err = io.ReadFull(client, buf[:1])
//fmt.Println(buf[0])
//认证子协商版本(与 SOCKS 协议版本的0x05无关系)
if buf[0] != 0x01 {
return errors.New("reading methods: " + err.Error())
}
// 从请求中获取用户名
n, err = io.ReadFull(client, buf[:1])
userLen := buf[0]
n, err = io.ReadFull(client, buf[:userLen])
bufUsername := string(buf[:userLen])
n, err = io.ReadFull(client, buf[:1])
passwdLen := buf[0]
n, err = io.ReadFull(client, buf[:passwdLen])
bufPassword := string(buf[:passwdLen])
//fmt.Println("user:", bufUsername, "passwd:", bufPassword)
// 验证用户名和密码
if string(bufUsername) == user && string(bufPassword) == passwd {
//STATUS:认证结果(0x00 认证成功 / 大于0x00 认证失败)
n, err = client.Write([]byte{0x05, 0x00})
return nil
} else {
n, err = client.Write([]byte{0x05, 0x01})
return errors.New("auth error")
}
}
fmt.Println("无法通过socks5认证!")
return nil
}
// 在完成认证以后,客户端需要告知服务端它的目标地址
func Socks5Connect(client net.Conn) (net.Conn, error) {
// 定义一个数组,用来xxx
buf := make([]byte, 256)
n, err := io.ReadFull(client, buf[:4])
// 检查是否为ipv4协议
if n != 4 {
return nil, errors.New("read header: " + err.Error())
}
//
ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3]
if ver != 5 || cmd != 1 {
return nil, errors.New("invalid ver/cmd")
}
addr := ""
switch atyp {
case 1:
n, err = io.ReadFull(client, buf[:4])
if n != 4 {
return nil, errors.New("invalid IPv4: " + err.Error())
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case 3:
n, err = io.ReadFull(client, buf[:1])
if n != 1 {
return nil, errors.New("invalid hostname: " + err.Error())
}
addrLen := int(buf[0])
n, err = io.ReadFull(client, buf[:addrLen])
if n != addrLen {
return nil, errors.New("invalid hostname: " + err.Error())
}
addr = string(buf[:addrLen])
case 4:
return nil, errors.New("IPv6: no supported yet")
default:
return nil, errors.New("invalid atyp")
}
n, err = io.ReadFull(client, buf[:2])
if n != 2 {
return nil, errors.New("read port: " + err.Error())
}
port := binary.BigEndian.Uint16(buf[:2])
destAddrPort := fmt.Sprintf("%s:%d", addr, port)
dest, err := net.Dial("tcp", destAddrPort)
if err != nil {
return nil, errors.New("dial dst: " + err.Error())
}
n, err = client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
dest.Close()
return nil, errors.New("write rsp: " + err.Error())
}
return dest, nil
}
评论区