socks5协议原理学习

什么是socks5

socks5的RFC地址:

rfc1928

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

rfc1929

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

socks是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。根据OSI七层模型来划分,SOCKS属于会话层协议,位于表示层与传输层之间。当防火墙后的客户端要访问外部的服务器时,就跟socks代理服务器连接。该协议设计之初是为了让有权限的用户可以穿过过防火墙的限制,使得高权限用户可以访问外部资源。经过10余年的时间,大量的网络应用程序都支持socks5代理。这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到版本4,最新协议是版本5,与前一版本相比,socks5做了以下增强:

增加对UDP协议的支持;

支持多种用户身份验证方式和通信加密方式;

修改了socks服务器进行域名解析的方法,使其更加优雅;

什么场景需要使用socks5

socks协议的设计初衷是在保证网络隔离的情况下,提高部分人员的网络访问权限,但是对应网络安全来讲,更多的是利用socks5这个协议来访问到内部的网络,访问一些访问不到的资源,这也是对于网络攻防层面来讲,但是socks5的用途也是很多的(例如科学上网)。例如一般我们在大学里面,学校内网肯定提供了很多服务资源给学生,我们可以通过校园网在学校内部网络进行访问使用。但是如果我们假期回到家了就很难进入到学校的内网再访问到学校内部的资源,一般我们可以通过在公网上面的一个VPS搭建一个socks代理服务器,并且在内网搭建一台服务器和VPS建立socks通道。然后我们就可以通过连接到VPS提供的某个代理端口来访问到学校的内部资源了。

socks5协议解析之授权认证

如果要与socks5服务器建立TCP连接,客户端需要先发起请求来对协议的版本及其认证方式。这里就是客户端请求服务器的请求格式:

+----+----------+----------+ |VER | NMETHODS | METHODS| +----+----------+----------+ | 1|1 | 1 to 255 | +----+----------+----------+

VER这里指定的就是socks的版本,如果使用的是socks5的话这里的值需要是0x05,其固定长度为1个字节;

第二个字段NMETHODS表示第三个字段METHODS的长度,它的长度也是1个字节;

第三个METHODS表示客户端支持的验证方式,可以有多种,他的尝试是1-255个字节;

目前支持的验证方式一共有:

X’00’ NO AUTHENTICATION REQUIRED(不需要验证)

X’01’ GSSAPI

X’02’ USERNAME/PASSWORD(用户名、密码认证)

X’03’ to X’7F’ IANA ASSIGNED ( 由IANA分配(保留)

X’80’ to X’FE’ RESERVED FOR PRIVATE METHODS 私人方法保留)

X’FF’ NO ACCEPTABLE METHODS(都不支持,没法连接了)

以上的都是十六进制常量,比如X’00’表示十六进制0x00。当服务器端收到了客户端的请求之后,就会在响应客户端的请求,服务端需要客户端提供哪种验证方式的信息。

+----+--------+ |VER | METHOD | +----+--------+ | 1| 1| +----+--------+

第一个字段VER代表Socket的版本,Soket5默认为0x05,其值长度为1个字节

第二个字段METHOD代表需要服务端需要客户端按照此验证方式提供验证信息,其值长度为1个字节,选择为上面的六种验证方式。

这里举一个例子,例如我们这里是socks5的话接受的肯定就是0x05,并且我们设定不需要验证。那么就是0x00不需要验证

+-------+-----------+ |VER| METHOD| +-------+-----------+ | 0x05| 0x00| +-------+-----------+

这里看到 ;项目的代码。这里就是做授权认证的代码。一开始就是先判断三个数据VER ,NMETHODS ,METHODS 。接着如下就没就是获取到客户端发送过来的模式,默认就是不需要密码的。

useMethod := byte(0x00) //默认不需要密码 for _, v := range s.METHODS {if v == METHOD_CODE { useMethod = METHOD_CODE} } if s.VER != SOCKS_VERSION {return nil, errors.New("该协议不是socks5协议") } if useMethod != METHOD_CODE {return nil, errors.New("协议错误, 加密方法不对") } resp := []byte{SOCKS_VERSION, useMethod}

下面就是完整的代码:

/**The localConn connects to the dstServer, and sends a veridentifier/method selection message:+----+----------+----------+|VER | NMETHODS | METHODS|+----+----------+----------+| 1|1 | 1 to 255 |+----+----------+----------+The VER field is set to X05 for this ver of the protocol.TheNMETHODS field contains the number of method identifier octets thatappear in the METHODS field.METHODS常见的几种方式如下:1>.数字“0”:表示不需要用户名或者密码验证;2>.数字“1”:GSSAPI是SSH支持的一种验证方式;3>.数字“2”:表示需要用户名和密码进行验证;4>.数字“3”至“7F”:表示用于IANA 分配(IANA ASSIGNED)5>.数字“80”至“FE”表示私人方法保留(RESERVED FOR PRIVATE METHODS)4>.数字“FF”:不支持所有的验证方式,无法进行连接**/type ProtocolVersion struct { VERuint8 NMETHODS uint8 METHODS[]uint8}func (s *ProtocolVersion) HandleHandshake(b []byte) ([]byte, error) { n := len(b) if n < 3 {return nil, errors.New("协议错误, sNMETHODS不对") } s.VER = b[0] //ReadByte reads and returns a single byte,第一个参数为socks的版本号 if s.VER != 0x05 {return nil, errors.New("协议错误, version版本不为5!") } s.NMETHODS = b[1] //nmethods是记录methods的长度的。nmethods的长度是1个字节 if n != int(2+s.NMETHODS) {return nil, errors.New("协议错误, sNMETHODS不对") } s.METHODS = b[2 : 2+s.NMETHODS] //读取指定长度信息,读取正好len(buf)长度的字节。如果字节数不是指定长度,则返回错误信息和正确的字节数 useMethod := byte(0x00) //默认不需要密码 for _, v := range s.METHODS {if v == METHOD_CODE { useMethod = METHOD_CODE} } if s.VER != SOCKS_VERSION {return nil, errors.New("该协议不是socks5协议") } //服务器回应客户端消息: //第一个参数表示版本号为5,即socks5协议, // 第二个参数表示服务端选中的认证方法,0即无需密码访问, 2表示需要用户名和密码进行验证。 // 88是一种私有的加密协议 if useMethod != METHOD_CODE {return nil, errors.New("协议错误, 加密方法不对") } resp := []byte{SOCKS_VERSION, useMethod} return resp, nil}

如果客户端请求发送过去的信息是表明是请求到是0x02用户名、密码认证。下面就是认证的格式。

+----+------+----------+------+----------+ |VER | ULEN |UNAME | PLEN |PASSWD| +----+------+----------+------+----------+ | 1|1 | 1 to 255 |1 | 1 to 255 | +----+------+----------+------+----------+

VER就是鉴定协议的版本。

ULEN就是用户名的长度

UNAME就是用户名

PLEN就是密码的长度

PASSWD就是密码

客户端发送请求过去之后,服务器鉴定之后就会发出如下回应。

+----+--------+|VER | STATUS |+----+--------+| 1| 1|+----+--------+

VER 依然就是鉴定协议的版本。

STATUS 就是鉴定的状态,也就是鉴定是否通过验证。其中鉴定状态 0x00 表示成功,0x01 表示失败。

下面代码就是实现了socks5的认证。一开始依然是判断socks协议版本,如果不正确的话依然会返回错误。接着在获取到对应的ULEN UNAME PLE PASSWD。可以看到如果验证成功就会返回如下数据给客户端。

resp := []byte{SOCKS_VERSION, 0x00}/* This begins with the client producing a Username/Password request: +----+------+----------+------+----------+ |VER | ULEN |UNAME | PLEN |PASSWD| +----+------+----------+------+----------+ | 1|1 | 1 to 255 |1 | 1 to 255 | +----+------+----------+------+----------+*/type Socks5AuthUPasswd struct { VERuint8 ULEN uint8 UNAMEstring PLEN uint8 PASSWD string}func (s *Socks5AuthUPasswd) HandleAuth(b []byte) ([]byte, error) { // b := make([]byte, 128) // n, err := conn.Read(b) // if err != nil{ // log.Println(err) // return err // } n := len(b) s.VER = b[0] if s.VER != 5 {return nil, errors.New("该协议不是socks5协议") } s.ULEN = b[1] s.UNAME = string(b[2 : 2+s.ULEN]) s.PLEN = b[2+s.ULEN+1] s.PASSWD = string(b[n-int(s.PLEN) : n]) log.Println(s.UNAME, s.PASSWD) /** 回应客户端,响应客户端连接成功 The server verifies the supplied UNAME and PASSWD, and sends the following response: +----+--------+ |VER | STATUS | +----+--------+ | 1| 1| +----+--------+ A STATUS field of X00 indicates success. If the server returns a `failure (STATUS value other than X00) status, it MUST close the connection. */ resp := []byte{SOCKS_VERSION, 0x00} // conn.Write(resp) return resp, nil}

socks5协议解析之建立连接。

Socket5的客户端和服务端进行双方授权验证通过之后,就开始建立连接了。连接由客户端发起,告诉Sokcet服务端客户端需要访问哪个远程服务器,其中包含,远程服务器的地址和端口,地址可以是IP4,IP6,也可以是域名。

+----+-----+-------+------+----------+----------+|VER | CMD |RSV| ATYP | DST.ADDR | DST.PORT |+----+-----+-------+------+----------+----------+| 1|1| X00 |1 | Variable |2 |+----+-----+-------+------+----------+----------+

VER代表Socket协议的版本,Soket5默认为0x05,其值长度为1个字节

CMD代表客户端请求的类型,值长度也是1个字节,有三种类型

CONNECT X’01’

BIND X’02’

UDP ASSOCIATE X’03’

RSV保留字,值长度为1个字节

ATYP代表请求的远程服务器地址类型,值长度1个字节,有三种类型

IP V4 address: X’01’

DOMAINNAME: X’03’

IP V6 address: X’04’

DST.ADDR代表远程服务器的地址,根据ATYP进行解析,值长度不定。

DST.PORT代表远程服务器的端口,要访问哪个端口的意思,值长度2个字节

接着客户端把要请求的远程服务器的信息都告诉Socket5代理服务器了,那么Socket5代理服务器就可以和远程服务器建立连接了,不管连接是否成功等,都要给客户端回应,其回应格式为:

+----+-----+-------+------+----------+----------+|VER | REP |RSV| ATYP | BND.ADDR | BND.PORT |+----+-----+-------+------+----------+----------+| 1|1| X00 |1 | Variable |2 |+----+-----+-------+------+----------+----------+

VER代表Socket协议的版本,Soket5默认为0x05,其值长度为1个字节

REP代表响应状态码,值长度也是1个字节,有以下几种类型

X’00’ succeeded

X’01’ general SOCKS server failure

X’02’ connection not allowed by ruleset

X’03’ Network unreachable

X’04’ Host unreachable

X’05’ Connection refused

X’06’ TTL expired

X’07’ Command not supported

X’08’ Address type not supported

X’09’ to X’FF’ unassigned

RSV保留字,值长度为1个字节

ATYP代表请求的远程服务器地址类型,值长度1个字节,有三种类型

IP V4 address: X’01’

DOMAINNAME: X’03’

IP V6 address: X’04’

BND.ADDR表示绑定地址,值长度不定。

BND.PORT表示绑定端口,值长度2个字节

服务端响应客户端连接成功就会返回如下的数据给客户端。

resp := []byte{SOCKS_VERSION, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}

下面就是实现的完整代码:

/**结构:+----+-----+-------+------+----------+----------+|VER | CMD |RSV| ATYP | DST.ADDR | DST.PORT |+----+-----+-------+------+----------+----------+| 1|1| X00 |1 | Variable |2 |+----+-----+-------+------+----------+----------+cmd代表客户端请求的类型,值长度也是1个字节,有三种类型:1>.数字“1”:表示客户端需要你帮忙代理连接,即CONNECT ;2>.数字“2”:表示让你代理服务器,帮他建立端口,即BIND ;3>.数字“3”:表示UDP连接请求用来建立一个在UDP延迟过程中操作UDP数据报的连接,即UDP ASSOCIATE;ATYP代表请求的远程服务器地址类型,它是一个可变参数,但是它值的长度1个字节,有三种类型:1>.数字“1”:表示是一个IPV4地址(IP V4 address);2>.数字“3”:表示是一个域名(DOMAINNAME);3>.数字“4”:表示是一个IPV6地址(IP V6 address);**/type Socks5Resolution struct { VER uint8 CMD uint8 RSV uint8 ATYPuint8 DSTADDR []byte DSTPORT uint16 DSTDOMAIN string RAWADDR *net.TCPAddr}func (s *Socks5Resolution) LSTRequest(b []byte) ([]byte, error) { // b := make([]byte, 128) // n, err := conn.Read(b) n := len(b) if n < 7 {return nil, errors.New("请求协议错误") } s.VER = b[0] if s.VER != SOCKS_VERSION {return nil, errors.New("该协议不是socks5协议") } s.CMD = b[1] if s.CMD != 1 {return nil, errors.New("客户端请求类型不为代理连接, 其他功能暂时不支持.") } s.RSV = b[2] //RSV保留字端,值长度为1个字节 s.ATYP = b[3] switch s.ATYP { case 1:// IP V4 address: X01s.DSTADDR = b[4 : 4+net.IPv4len] case 3:// DOMAINNAME: X03s.DSTDOMAIN = string(b[5 : n-2])ipAddr, err := net.ResolveIPAddr("ip", s.DSTDOMAIN)if err != nil { return nil, err}s.DSTADDR = ipAddr.IP case 4:// IP V6 address: X04s.DSTADDR = b[4 : 4+net.IPv6len] default:return nil, errors.New("IP地址错误") } s.DSTPORT = binary.BigEndian.Uint16(b[n-2 : n]) // DSTADDR全部换成IP地址,可以防止DNS污染和封杀 s.RAWADDR = &net.TCPAddr{IP: s.DSTADDR,Port: int(s.DSTPORT), } /** 回应客户端,响应客户端连接成功 +----+-----+-------+------+----------+----------+ |VER | REP |RSV| ATYP | BND.ADDR | BND.PORT | +----+-----+-------+------+----------+----------+ | 1|1| X00 |1 | Variable |2 | +----+-----+-------+------+----------+----------+ */ resp := []byte{SOCKS_VERSION, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // conn.Write(resp) return resp, nil}

建立链接之后的数据转发

最后完成链接之后就是需要进行数据转发了,这个操作和写端口转发的一样的,通过io.Copy()把两个端口的的流量进行转发即可

go func() {defer wg.Done()defer dstServer.Close()io.Copy(dstServer, client)}()go func() {defer wg.Done()defer client.Close()io.Copy(client, dstServer)}()

rsocks-socks5代理工具

这个项目的地址如下,是一个小型的sock5代理工具,这个工具在socks5协议中调用了armon的go-socks5项目和调用了多路复用yamux项目。

https://github.com/brimstone/rsockshttps://github.com/armon/go-socks5https://github.com/hashicorp/yamux

这里调用了go-socks5这个包,这是一个socks5协议的api,是使用Go写的,所有可以直接调用即可原理和上面介绍的一样。这个包结构我修改了一下,这里是我的包结构

- rsock- client-client.go- cmd - main.go- server-server.go

首先看一下服务端的代码,服务端的代码实现了一个方法ConnectForSocks()。首先一开始就创建一个SOCKS5服务器,socks5.Config{} 适用于配置 Config的。用于设置和配置服务器

//创建一个SOCKS5服务器// // socks5.New 就是新建一个socks5服务器server,err := socks5.New(&socks5.Config{})if err != nil{ returnerr}

接着创建一个建立,用于连接客户端监听的端口,也就是对应客户端的 -listen 参数给的端口

var conn net.Connlog.Println("Connecting to far end")conn,err = net.Dial("tcp",address)if err != nil{ return err}log.Println("Starting server")

接着就是通过把conn创建一个yamux多路复用的session。

session,err := yamux.Server(conn,nil)if err != nil{ return err}

接着就会用这个session来等待 yamux client 连接。在通过匿名函数利用yamux连接到的stream 来连接socks。

for{// 等待 yamux client 连接stream,err := session.Accept()log.Println("Acceping stream")if err != nil{ return err}log.Println("Passing off to socks5")go func() { // 接着利用yamux连接到的stream 来连接socks。 err = server.ServeConn(stream) if err != nil{log.Println(err) }}() }

全部代码如下:

package serverimport ( "github.com/armon/go-socks5" "github.com/hashicorp/yamux" "log" "net")//这个方法用来连接客户端开启的Listen端口。用来把流量传输到客户端的Listen端口//客户端的Listen端口接受到了流量之后再通过端口转发把流量转到socks端口func ConnectForSocks(address string) error { //Example /*创建一个SOCKS5服务器conf := &socks5.Config{}server, err := socks5.New(conf)if err != nil {panic(err)}在本地主机端口8000上创建SOCKS5代理if err := server.ListenAndServe("tcp", "127.0.0.1:8000"); err != nil {panic(err)} */ //创建一个SOCKS5服务器 // socks5.Config{} Config用于设置和配置服务器 // socks5.New 就是新建一个socks5服务器 server,err := socks5.New(&socks5.Config{}) if err != nil{returnerr } //创建一个建立,用于连接客户端监听的端口,也就是对应客户端的 -listen 参数给的端口 var conn net.Conn log.Println("Connecting to far end") conn,err = net.Dial("tcp",address) if err != nil{return err } log.Println("Starting server") //启动 yamux server 端 /*func server() {// 接受TCP连接conn, err := listener.Accept()if err != nil {panic(err)}// yamux的设置服务器端session, err := yamux.Server(conn, nil)if err != nil {panic(err)}// Accept a streamstream, err := session.Accept()if err != nil {panic(err)}// Listen for a messagebuf := make([]byte, 4)stream.Read(buf)} */ session,err := yamux.Server(conn,nil) if err != nil{return err } for{// 等待 yamux client 连接stream,err := session.Accept()log.Println("Acceping stream")if err != nil{ return err}log.Println("Passing off to socks5")go func() { // 接着利用yamux连接到的stream 来连接socks。 err = server.ServeConn(stream) if err != nil{log.Println(err) }}() }}

接着就是客户端的代码,客户端的代码实现了两个方法ListenForSocks()和ListenForClients()方法ListenForSocks()这个方法是用来被服务端连接的方法,该方法监听一个端口用于服务端连接也就是-listen 参数的端口。他调用net.Listen()等待服务端的连接。接着等待到连接之后会创建yamux的客户端

func ListenForSocks(address string) { log.Println("Listening for the far end") ln,err := net.Listen("tcp",address) if err != nil{return } for{conn,err := ln.Accept()log.Println("Got a client")if err != nil{ _, _ = fmt.Fprintf(os.Stderr, "Errors accepting!")}session,err = yamux.Client(conn,nil) }}

ListenForClients()该方法开启一个socks5端口,等待用户使用socks5协议连接。一开始net.Listen的就是一般使用的1080的那个端口,客户连接过来进行流量代理的端口。最后会使用io.copy将conn的流量和stream(也就是多路服用创建的stream)进行流量copy转发过去。

func ListenForClients(address string) error { log.Println("Waiting for clients") ln,err := net.Listen("tcp",address) if err != nil{return err } for{conn,err := ln.Accept()if err != nil{ return err}if session == nil{ conn.Close() continue}log.Println("Got a client")log.Println("Opening a stream")//Open is used to create a new stream as a net.Conn//用ListenForSocks 连接到的session开启一个streamstream,err := session.Open()if err != nil{ return err}//接着这就有点端口转发的意思了,两个端口的流量传递,利用的是io.copy来实现go func() { log.Println("Starting to copy conn to stream") _, _ = io.Copy(conn, stream) _ = conn.Close()}()go func() { log.Println("Starting to copy stream to conn") _, _ = io.Copy(stream, conn) _ = stream.Close() log.Println("Done copying stream to conn")}() }

完整的代码如下:

package clientimport ( "fmt" "github.com/hashicorp/yamux" "io" "log" "net" "os")// 定义一个yamux的Session//在 yamux 中,一条连接称为 session,一个 session 可以开启多个数据流,数据流称为 stream。var session *yamux.Session//这个方法是用来被服务端连接的方法//该方法监听一个端口用于服务端连接也就是-listen 参数的端口func ListenForSocks(address string) { log.Println("Listening for the far end") ln,err := net.Listen("tcp",address) if err != nil{return } for{conn,err := ln.Accept()log.Println("Got a client")if err != nil{ _, _ = fmt.Fprintf(os.Stderr, "Errors accepting!")}/*func client() { // 获取TCP连接 conn, err := net.Dial(...) if err != nil { panic(err) } // 设置yamux的客户端 session, err := yamux.Client(conn, nil) if err != nil { panic(err) } // 开启新stream stream, err := session.Open() if err != nil { panic(err) } // Stream 实现 net.Conn stream.Write([]byte("ping")) } */session,err = yamux.Client(conn,nil) }}//该方法开启一个socks5端口,等待用户使用socks5协议连接。func ListenForClients(address string) error { log.Println("Waiting for clients") ln,err := net.Listen("tcp",address) if err != nil{return err } for{conn,err := ln.Accept()if err != nil{ return err}if session == nil{ conn.Close() continue}log.Println("Got a client")log.Println("Opening a stream")//Open is used to create a new stream as a net.Conn//用ListenForSocks 连接到的session开启一个streamstream,err := session.Open()if err != nil{ return err}//接着这就有有点端口转发的意思了,两个端口的流量传递,利用的是io.copy来实现go func() { log.Println("Starting to copy conn to stream") _, _ = io.Copy(conn, stream) _ = conn.Close()}()go func() { log.Println("Starting to copy stream to conn") _, _ = io.Copy(stream, conn) _ = stream.Close() log.Println("Done copying stream to conn")}() }}

主函数这里就是定义一些命令行的参数。并且在对个个函数进行调用

package mainimport ( "flag" "fmt" "log" "os" "rsocks/client" "rsocks/server")func main() { listen := flag.String("listen", "", "listen port for receiver address:port") socks := flag.String("socks", "127.0.0.1:1080", "socks address:port") connect := flag.String("connect", "", "connect address:port") flag.Usage = func() {fmt.Println("")fmt.Println("Usage:")fmt.Println("1) 客户端操作:rsocks -listen :8080 -socks 客户端ip:1080<=====>注意如果客户端ip写环回地址的话别人连接不到!")fmt.Println("2) 客户端操作:rsocks -connect 客户端ip:8080 <=====> 服务端,也就是被控端连接客户端的监听地址" )fmt.Println("3) 接着你连接socks 客户端ip:1080 即可访问到对方内网")fmt.Println("4) Enjoy. :]")fmt.Println("")fmt.Println("") } flag.Parse() if *listen != "" {log.Println("Starting to listen for clients")go client.ListenForSocks(*listen)log.Fatal(client.ListenForClients(*socks)) } if *connect != "" {log.Println("Connecting to the far end")log.Fatal(server.ConnectForSocks(*connect)) } _, _ = fmt.Fprintf(os.Stderr, "You must specify a listen port or a connect address") os.Exit(1)}

场景使用

这里我假设一个场景如下。A可以连接B,B也可以连接A,B可以连接C但是C不能连接A。也就是说我们需要通过B作为代理的中间人来对流量进行转发,中间通过的就是socks5协议。我这里的环境是两台电脑。C是在第一台电脑,A,B是二台电脑。A是VMware虚拟机,使用的NAT进行转换IP。并且B和C是在同一个wifi下。所以C不能通过VMware的路由访问到虚拟机A。

A - ip address:192.168.8.221B - ip address:192.168.1.106C - ip address:192.168.1.105

一开始我们需要在B机器上面进行操作,设置一点监听和一个socks。这里需要注意的是 -socks 参数后面带的IP地址不要换回地址 127.0.0.1 ,写本机地址即可,不然连接不到socks地址。

.\rsocks.exe -listen :8080 -socks 192.168.1.106:1080

接着在内网机器中使用命令连接 listen :8080 端口即可。

.\rsocks.exe -connect 192.168.1.106:8080

接着就可以通过Proxifier连接到192.168.1.106:1080来访问到对方的内网了。