go版本shadowsocks源码(一)

shadowsocks简介

Shadowsocks(简称SS)是一种基于Socks5代理方式的加密传输协议,也可以指实现这个协议的各种开发包。当前包使用Python、C、C++、C#、Go语言等编程语言开发,大部分主要实现(iOS平台的除外)采用Apache许可证、GPL、MIT许可证等多种自由软件许可协议开放源代码。Shadowsocks分为服务器端和客户端,在使用之前,需要先将服务器端部署到服务器上面,然后通过客户端连接并创建本地代理。

而socks5协议则是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。根据OSI七层模型来划分,SOCKS属于会话层协议,位于表示层与传输层之间。

当防火墙后的客户端要访问外部的服务器时,就跟socks代理服务器连接。该协议设计之初是为了让有权限的用户可以穿过过防火墙的限制,使得高权限用户可以访问外部资源。经过10余年的时间,大量的网络应用程序都支持socks5代理。


客户端

首先我们来看下客户端的代码。

main函数

go语言和大多数语言一样都是从main函数开始的,在源码阅读的过程我们也从main函数开始逐步往下分析。
mian函数整体我认为可以分为三个部分,第一部分读取相关的配置信息,第二部分处理服务器配置信息,第三部分启动客户端,监听本地端口。

读取客户端启动的配置信息

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
                       ......
flag.BoolVar(&printVer, "version", false, "print version")
flag.StringVar(&configFile, "c", "config.json", "specify config file")
flag.StringVar(&cmdServer, "s", "", "server address")
flag.StringVar(&cmdConfig.LocalAddress, "b", "", "local address, listen only to this address if specified")
flag.StringVar(&cmdConfig.Password, "k", "", "password")
flag.IntVar(&cmdConfig.ServerPort, "p", 0, "server port")
flag.IntVar(&cmdConfig.Timeout, "t", 300, "timeout in seconds")
flag.IntVar(&cmdConfig.LocalPort, "l", 0, "local socks5 proxy port")
flag.StringVar(&cmdConfig.Method, "m", "", "encryption method, default: aes-256-cfb")
flag.BoolVar((*bool)(&debug), "d", false, "print debug message")
flag.StringVar(&cmdURI, "u", "", "shadowsocks URI")
......

config, err := ss.ParseConfig(configFile)
if err != nil {
config = &cmdConfig
if !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "error reading %s: %v\n", configFile, err)
os.Exit(1)
}
} else {
ss.UpdateConfig(config, &cmdConfig)
}
if config.Method == "" {
config.Method = "aes-256-cfb"
}

从上述代码中我们可以看到,除了从命令行读取配置信息外,还可以指定json文件读取配置信息。ss的配置信息包括这样几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   Server       interface{} `json:"server"`
ServerPort int `json:"server_port"`
LocalPort int `json:"local_port"`
LocalAddress string `json:"local_address"`
Password string `json:"password"`
Method string `json:"method"` // encryption method

// following options are only used by server
PortPassword map[string]string `json:"port_password"`
Timeout int `json:"timeout"`

// following options are only used by client

// The order of servers in the client config is significant, so use array
// instead of map to preserve the order.
ServerPassword [][]string `json:"server_password"`

从上往下分为客户端配置和服务端配置,其中客户端配置包括远程服务器ip,服务器端口,本地端口,本地地址访问密码,加密方法,服务端配置包括设定端口密码等。
对于go语言来说从命令行读取相应的参数感觉是一件十分方便的事情
,只要调用flag包相关函数即可读取并规定想要的格式参数。

处理服务器配置信息

服务器配置信息这里使用了两个结构体来存储读取的配置信息。

1
2
3
4
5
6
7
8
9
type ServerCipher struct {
server string
cipher *ss.Cipher
}

var servers struct {
srvCipher []*ServerCipher
failCnt []int // failed connection count
}

其实阅读这里我是不太理解,为什么要在用一个匿名结构体将server密码信息包起来的,而且servercipher这个名字也怪怪的,因为里面不仅仅包含server的密码信息呀。可能是因为单独记录shi失败次数有利于后续扩展?这个后面想到在补吧。使用数组就很显然了,为了支持配置多个服务。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
func parseServerConfig(config *ss.Config) {
hasPort := func(s string) bool {
_, port, err := net.SplitHostPort(s)
if err != nil {
return false
}
return port != ""
}

if len(config.ServerPassword) == 0 {
// only one encryption table
cipher, err := ss.NewCipher(config.Method, config.Password)
if err != nil {
log.Fatal("Failed generating ciphers:", err)
}
srvPort := strconv.Itoa(config.ServerPort)
srvArr := config.GetServerArray()
n := len(srvArr)
servers.srvCipher = make([]*ServerCipher, n)

for i, s := range srvArr {
if hasPort(s) {
log.Println("ignore server_port option for server", s)
servers.srvCipher[i] = &ServerCipher{s, cipher}
} else {
servers.srvCipher[i] = &ServerCipher{net.JoinHostPort(s, srvPort), cipher}
}
}
} else {
// multiple servers
n := len(config.ServerPassword)
servers.srvCipher = make([]*ServerCipher, n)

cipherCache := make(map[string]*ss.Cipher)
i := 0
for _, serverInfo := range config.ServerPassword {
if len(serverInfo) < 2 || len(serverInfo) > 3 {
log.Fatalf("server %v syntax error\n", serverInfo)
}
server := serverInfo[0]
passwd := serverInfo[1]
encmethod := ""
if len(serverInfo) == 3 {
encmethod = serverInfo[2]
}
if !hasPort(server) {
log.Fatalf("no port for server %s\n", server)
}
// Using "|" as delimiter is safe here, since no encryption
// method contains it in the name.
cacheKey := encmethod + "|" + passwd
cipher, ok := cipherCache[cacheKey]
if !ok {
var err error
cipher, err = ss.NewCipher(encmethod, passwd)
if err != nil {
log.Fatal("Failed generating ciphers:", err)
}
cipherCache[cacheKey] = cipher
}
servers.srvCipher[i] = &ServerCipher{server, cipher}
i++
}
}
servers.failCnt = make([]int, len(servers.srvCipher))
for _, se := range servers.srvCipher {
log.Println("available remote server", se.server)
}
return
}

处理服务器配置信息主要由parseServerConfig函数完成,老实说我不太喜欢这个函数,写的又臭又长看起来也很费劲,应该很有优化的空间的。
函数整体应该分为两个部分,用一个if分支隔离开来。if直接跟的语句负责处理单个服务器配置的信息,else跟的部分负责多个服务器配置信息的解析。整体流程如下:

开启go携程监听本地端口

run方法是比较简单的,使用tcp协议监听指定地址端口,采用轮询机制等待连接请求,一旦等到连接后开启一个goroutine调用 handleConnection()进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func run(listenAddr string) {
ln, err := net.Listen("tcp", listenAddr)
if err != nil {
log.Fatal(err)
}
log.Printf("starting local socks5 server at %v ...\n", listenAddr)
for {
conn, err := ln.Accept()
if err != nil {
log.Println("accept:", err)
continue
}
go handleConnection(conn)
}
}

##结语
至此,main函数的整体就差不多,接下来我们需要看下在拿到请求后,handleConnection函数是怎样对其进行处理的。