本文内容:

  • 本文主要分析Socks5的TCP连接过程,对官方RFC文档进行翻译和解读

  • 然后简单用Go语言实现了一下Socks5的TCP连接转发

  • 最后对数据包进行分析,分析握手连接过程中的字节信息变化

RFC文档解读

RFC1928:https://tools.ietf.org/html/rfc1928

客户端第一次请求

当客户端向服务端发送连接请求的时候,会发送如下信息:

VER NMETHODS METHODS
0x05 1 1 TO 255

VER的值当被设置为0x05,标明当前Socks的版本信息
NMETHODS(方法选择)中包含在METHODS中出现的方法标识的数据(用字节表示)

服务端第一次回复

VER METHOD
0x05 1
  • VAR被设置为0x05,标明所选Socks的版本
  • METHOD参数如下:
  • 0x00,说明服务端连接无需经过验证(NO AUTHENTICATION REQUIRED)
    • 0x01,通用安全服务应用程序接口(GSSAPI)
    • 0x02,要求用户名/密码认证(USERNAME/PASSWORD)
    • 0x03 to 0x7F,IANA 分配(IANA ASSIGNED)
    • 0x80 TO 0xFE,私人方法保留(RESERVED FOR PRIVATE METHODS)
    • 0xFF,则说明服务端不支持客户端列出的所选方法,此时客服端应该关闭连接(NO ACCEPTABLE METHODS)

客户端第二次请求

当客户端与服务端关于方法的协商完毕后,客户端将发送完整的详细请求信息

VER CMD RSV ATYP DST.ADDR DST.PORT
0x05 1 0x00 1 Variable 2
  • VER的值当被设置为0x05,标明当前Socks的版本信息

  • CMD参数如下:

    • 0x01,建立连接(CONNECT)
    • 0x02,绑定(Bind)
    • 0x03,使用UDP(UDP ASSOCIATE)
  • RSV,保留字段,被设置为0x00

  • ATYP,目标地址类型,参数如下:

    • 0x01,IPV4地址,长度为4个字节
    • 0x03,域名,其中第一个字节用来标识域名的长度
    • 0x04,IPV6地址,长度为16个字节
  • DST.ADDR,目标地址(长度可变)

  • DST.PORT,目标端口

服务端第二次回复

客户端与服务端建立连接并完成请求后,便立即发送SOCKS请求信息认证协商

服务器评估该请求,并返回如下形式的回复:

VER REP PSV ATYP BND.ADDR BND.PORT
0x05 1 0x00 1 Variable 2
  • VER的值当被设置为0x05,标明当前Socks的版本信息
  • REP,回复字段,参数如下:
    • 0x00, 连接成功(succeeded)
    • 0x01,服务器发生故障(general SOCKS server failure)
    • 0x02,由于规则连接不容许(connection not allowed by ruleset)
    • 0x03,网络不可达(Network unreachable)
    • 0x04,主机不可达(Host unreachable)
    • 0x05,链接被拒绝(ConnectiSSon refused)
    • 0x06,TTL终止失效(TTL expired)
    • 0x07,命令不被支持(Command not supported)
    • 0x08,地址类型不被支持(Address type not supported)
    • 0x09 to 0xFF,没有被设计(unassigned)
  • PSV,保留字段,设置为0x00
  • AYTP,目标地址,参数如下:
    • 0x01,IPV4地址,长度为4个字节
    • 0x03,域名,其中第一个字节用来标识域名的长度
    • 0x04,IPV6地址,长度为16个字节
  • BND.ADDR,服务器绑定地址
  • BND.PORT,服务器绑定端口

对于不同CMD的请求,BND.ADDRBND.PORT有如下区别:

  • CONNECT

    在对CONNECT的答复中

    BND.PORT包含服务器分配用于连接到目标主机的端口号,而BND.ADDR包含关联的IP地址

    服务端提供的BND.ADDR通常不同于客户端用于访问*SOCKS *服务器的IP地址,因为此类服务器通常是多宿主的

    SOCKS服务器将使用DST.ADDRDST.PORT以及客户端源地址和端口来评估CONNECT请求。

  • BIND(这个好像没啥用)

    BIND请求用于要求客户端接受来自服务器的连接的协议中。
    FTP是一个众所周知的示例,它使用主要的客户端到服务器连接来发送命令和状态报告
    但可以使用服务器到客户端连接来按需传输数据(例如LS,GET,PUT)。

  • UDP ASSOCIATE

    用于处理UDP请求报文
    DST.ADDRDST.PORT字段包含客户端期望用于在其上发送UDP数据报以进行关联的地址和端口。
    服务器可以使用该信息来限制对关联的访问
    如果客户端在UDP ASSOCIATE时没有发送信息,则客户端必须使用全零的端口号和地址。
    UDP ASSOCIATE请求到达的TCP连接终止时,UDP关联终止。
    在对UDP ASSOCIATE请求的回复中,BND.PORTBND.ADDR字段指示客户端必须在其中发送要中继的UDP请求消息的端口号/地址。

回复处理

当回复(REP值不是0x00)表示失败时,
SOCKS服务器务必在发送回复后不久终止TCP连接。在检测到导致故障的情况后,该时间不得超过10秒。
如果回复(REP值为0x00)表示成功,并且请求是BINDCONNECT
则客户端现在可以开始传递数据

完整的连接流程

  1. 向服务器的端口建立TCP连接。
  2. 向服务器发送 0x05 0x01 0x00
  3. 如果接到 0x05 0x00 则是可以代理
  4. 发送 0x05 0x01 0x00 地址类型 + 目的地址 + 目的端口
  5. 接受服务器返回的自身地址和端口,连接完成以后操作和直接与目的方进行TCP连接相同。

Go语言实现(Demo)

package main

import (
"fmt"
"io"
"net"
"strconv"
)

//Socks5规定版本号
const(
VER = 0X05
)
//Socks5服务端第一次回复METHOD字段
const(
NoAuthValue = 0x00
GSSAPIValue = 0x01
NeedUserAndPasswordValue = 0x02
MethodNotSupportValue = 0xFF
)
//Socks5服务端第二次回复REP字段
const(
SucceededConnectValue = 0x00
SocksServerFailedValue = 0x01
DisallowedByRulesetValue = 0x02
NetworkUnreachableValue = 0x03
HostUnreachableValue = 0x04
ConnectRefusedValue = 0x05
TTLExpiredValue = 0x06
CMDNoSupportedValue = 0x07
AddressTypeNotSupportValue = 0x08
)
//Socks5服务端第二次回复ATYP字段
const(
IPv4AddressValue = 0x01
DomainAddressValue = 0x03
IPv6AddressValue = 0x04
)

//错误处理
func checkError(err error) (e string){
if err != nil {
fmt.Println(err)
return
}
return
}

func main(){
//服务器开始监听8081端口
fmt.Println("Starting!")
l, err := net.Listen("tcp",":8081")
checkError(err)
//开始接受客户端连接
for {
a, err := l.Accept()
checkError(err)
//并发处理
go DealClient(a)
}
}

func DealClient(client net.Conn){
if client == nil{
return
}
defer client.Close()

var buff [1024]byte

length, err := client.Read(buff[:])
checkError(err)
if buff[0] == VER {
//服务器第一次回复,向客户端发送无需认证的消息
client.Write([]byte{VER, NoAuthValue})
//读取客户端第二次请求的数据,同时把数据存入buff中
length, err = client.Read(buff[:])
checkError(err)

var addr, port string
switch buff[3] {
//IPv4
case IPv4AddressValue:
addr = net.IPv4(buff[4], buff[5], buff[6], buff[7]).String()
//域名
case DomainAddressValue:
addr = string(buff[5 : 5+buff[4]])
//IPv6
case IPv6AddressValue:
addr = net.IP{buff[4], buff[5], buff[6], buff[7], buff[8], buff[9], buff[10], buff[11], buff[12], buff[13], buff[15], buff[16], buff[17], buff[18], buff[19]}.String()
}
//处理最后两字节端口
port = strconv.Itoa(int(buff[length-2])<<8 | int(buff[length-1]))
fmt.Println(addr+":"+port)
//请求目的地址
server, err := net.Dial("tcp", net.JoinHostPort(addr, port))
checkError(err)
//服务端第二次回复,向客户端发送握手成功
client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
//TCP转发
go io.Copy(server, client)
io.Copy(client, server)
}
}

这里主要通过对RFC1928中规定的连接过程进行了实现

同时利用Go语言的网络方面的并发高性能进行TCP连接与转发

代码十分简陋,只能处理无需认证的Socks5请求,大概流程都写了注释,方便参考过程

比如著名SSR等工具,会对数据进行加密转发,同时考虑到了UDP转发以及更多的误处理(不可多言-_-)

数据包分析

下载地址:Socks5流量包

以上是我根据Go语言写的代码运行抓包而来

这里分析数据交换过程

由于本代码没有设置KeepLive,所以会重复进行TCP三次握手

实际的Socks5握手在如图的红色框中的数据包[TCP segment of a reassembled PDU]

这里我们追踪其中一条即可

客户端第一次请求

这里客户端会向服务端发送连接请求

由于这里无需验证,所以会请求0x05作为版本号,同时发送0x00表明自己是无需验证的连接

服务端第一次回复

原理一致,表明版本号以及说明自己支持无需验证的连接方式

客户端第二次请求

VER CMD RSV ATYP DST.ADDR DST.PORT
0x05 1 0x00 1 Variable 2
  • VER的值当被设置为0x05,标明当前Socks的版本信息
  • CMD参数:0x01,建立连接(CONNECT)
  • RSV,保留字段,被设置为0x00
  • ATYP,目标地址类型:0x03,域名,其中第一个字节用来标识域名的长度,这里为0x0d
  • DST.ADDR,这里为77 77 77 2e 62 61 69 64 75 2e 63 6f 6d,指www.baidu.com
  • DST.PORT,这里为01 bb,转化为十进制为443端口

服务端第二次回复

VER REP PSV ATYP BND.ADDR BND.PORT
0x05 1 0x00 1 Variable 2
  • VER的值当被设置为0x05,标明当前Socks的版本信息

  • REP,回复字段:0x00, 连接成功(succeeded)

  • PSV,保留字段,设置为0x00

  • AYTP,绑定地址类型:0x01,IPV4地址,长度为4个字节

在对CONNECT的答复中
BND.PORT包含服务器分配用于连接到目标主机的端口号,而BND.ADDR包含关联的IP地址

  • BND.ADDR,这里设置为00 00 00 00

  • BND.PORT,服务器绑定端口,这里为00 00

到此握手结束,可以发送数据了,总体流程如下:

总之,本文只是简单介绍了Socks5的连接过程以及数据包分析

如果对Socks5有更多的应用以及学习兴趣的话

建议学习ShadowSocks(逃…….