go版本shadowsocks源码(二)

本节来分析下,在上次开启本地服务等待连接后,handleConnection()函数是怎样处理拿到的请求的。

socks5协议客户端连接要求

在看具体的代码之前,我们首先来看看官方标准中是如何规定本地与客户端之间的交互的。
协议规定当客户端连到服务器后,然后就发送请求来协商版本和认证方法:
|VER|NMETHODS|METHODS|
:-:|:-:
|1|1|1 to 255|
其中ver表示协议版本(固定长度为一个字节),nmethods表示第三个字段的长度(即有几种认证方法),methods表示客户端支持的验证方式,长度1-255字节。
支持的验证方式官方制定了以下几种:

  • 0x00:NO AUTHENTICATION REQUIRED(不需要验证)
  • 0x01:GSSAPI (通用安全服务应用程序接口)
  • 0x02:USERNAME/PASSWD(用户名密码)
  • 0x03:IANA ASSIGNED(至 0x’7F’ IANA 分配)
  • 0x80:RESERVED FOR PRIVATE METHODS(至 0x’FE’ 私人方法保留)
  • 0xff:NO ACCEPTABLE METHODS(没有可接受的方法)

当服务端收到客户端的验证信息后,就要回应客户端提供哪种验证方式的信息。回应格式如下:
|VER|METHOD|
:-:|:-:
|1|1|

handleConnect函数

现在我们来看hendleConnect函数是如何处理�连接的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
func handleConnection(conn net.Conn) {
if debug {
debug.Printf("socks connect from %s\n", conn.RemoteAddr().String())
}
closed := false
defer func() {
if !closed {
conn.Close()
}
}()

var err error = nil
if err = handShake(conn); err != nil {
log.Println("socks handshake:", err)
return
}
rawaddr, addr, err := getRequest(conn)
if err != nil {
log.Println("error getting request:", err)
return
}
// Sending connection established message immediately to client.
// This some round trip time for creating socks connection with the client.
// But if connection failed, the client will get connection reset error.
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
if err != nil {
debug.Println("send connection confirmation:", err)
return
}

remote, err := createServerConn(rawaddr, addr)
if err != nil {
if len(servers.srvCipher) > 1 {
log.Println("Failed connect to all available shadowsocks server")
}
return
}
defer func() {
if !closed {
remote.Close()
}
}()

go ss.PipeThenClose(conn, remote, nil)
ss.PipeThenClose(remote, conn, nil)
closed = true
debug.Println("closed connection to", addr)
}

首先我们可以看到在代码13行-16行,对连接进行请求的协商,也就是第一部分介绍的交互协商认证的流程。
handShake代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func handShake(conn net.Conn) (err error) {
const (
idVer = 0
idNmethod = 1
)

buf := make([]byte, 258)

var n int
ss.SetReadTimeout(conn)
if n, err = io.ReadAtLeast(conn, buf, idNmethod+1); err != nil {
return
}
if buf[idVer] != socksVer5 {
return errVer
}
nmethod := int(buf[idNmethod])
msgLen := nmethod + 2
if n == msgLen {
} else if n < msgLen {
if _, err = io.ReadFull(conn, buf[n:msgLen]); err != nil {
return
}
} else {
return errAuthExtraData
}
_, err = conn.Write([]byte{socksVer5, 0})
return
}

在代码11行读取了本地客户端发来的版本信息以及认证方法信息,如果协议版本不是socks5则立马返回。由于socks规定的字段ver与nmethods都是两个字节所以最后一个字段加上2个字节就是需要读取的信息总长度,最后第27行代码返回给客户端协议版本信息与选择的方法(此处不需要验证,所以传递0)。


1
2
3
4
5
rawaddr, addr, err := getRequest(conn)
if err != nil {
log.Println("error getting request:", err)
return
}

然后让我们把视角在回到handleConnection函数,在完成handshake的认证后客户端会向local发送一个带有目的地址和端口的请求包,由request函数完成获取操作。以下是发送的包内容:
|VER|CMD|RSV|ATYP|DST.ADDR|DST.PORT|
:-:|:-:|:-:|
|1|1|0x00|1|Variable|2|

  • VER:socks的版本
  • CMD:代表客户端请求的类型,值长度1个字节,有三种类型:
    • Connect:0x01
    • Bind:0x02
    • UDP:0x03
  • RSV:保留字段,默认0x00,长度1个字节
  • ATYP:代表请求的远程服务器地址类型,长度1个字节,三种类型:
    • IPV4:0X01
    • IPV6:0X04
    • DOMAINNAME:0x03
  • DST.ADDR:代表远程服务器的地址,根据ATYP进行解析,值长度不定
  • DST.PORT:代表远程服务器的端口,值长度2个字节

当loca接收到该信息,接下来应当向客户端返回一个结果,在代码中默认返回了success。如下所示:

1
2
3
4
5
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
if err != nil {
debug.Println("send connection confirmation:", err)
return
}

当这些步骤完成后,接下来local端和server端建立连接。然后local负责把client的数据包加密后发送给ss-server。把收到的server数据包在发回给client。完成这些操作的函数就是PipeThenClose。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func PipeThenClose(src, dst net.Conn, addTraffic func(int)) {
defer dst.Close()
buf := leakyBuf.Get()
defer leakyBuf.Put(buf)
for {
SetReadTimeout(src)
n, err := src.Read(buf)
if addTraffic != nil {
addTraffic(n)
}
// read may return EOF with n > 0
// should always process n > 0 bytes before handling error
if n > 0 {
// Note: avoid overwrite err returned by Read.
if _, err := dst.Write(buf[0:n]); err != nil {
Debug.Println("write:", err)
break
}
}
if err != nil {
// Always "use of closed network connection", but no easy way to
// identify this specific error. So just leave the error along for now.
// More info here: https://code.google.com/p/go/issues/detail?id=4373
/*
if bool(Debug) && err != io.EOF {
Debug.Println("read:", err)
}
*/
break
}
}
return
}

在PipeThenClose代码中,其申请一块缓冲区然后不停的从src中读取数据,将数据转发到dst中。当然,读取的时候需要对数据进行解密,写的时候需要加密。解密加密操作分别由项目中的Conn负责,其重写了read与write函数,在read时会根据加密方法对数据进行解密。write时则会进行加密。

1
2
3
4
go ss.PipeThenClose(conn, remote, nil)
ss.PipeThenClose(remote, conn, nil)
closed = true
debug.Println("closed connection to", addr)

在handleConnection代码的最后,其使用两个PipeThenClose,新开启的goroutine和本身的goroutine病发的从本地到远程、远程到本地的上下行进行数据传输。

综上,handleConnection函数整体流程如下:

结语

local端的代码到此就算结束了,由于是按照整体流程进行分析,有些过于细节的地方几句就略过去了,重点还是在学习整体的流程以及socks协议的使用。server端的思路其实和local端比较类似,除了没有local中握手(handShake)的步骤。整体看来这个还是不太复杂的,整体代码也就2000行左右。