让Go的HTTP客户端走socks5代理

链路

现在通常使用Socks5协议的实现shadowsocks翻墙,再配合Privoxy HTTP代理把HTTP转化成Socks5协议。具体过程如下:

1
app -HTTP-> Privoxy -socks5-> Socks5Client -encryption-> Socks5Server -HTTP-> Server

现在我们希望app也能直接Socks5代理,即:

1
app -socks5-encryption-data-> Socks5Server -HTTP-> Server

这样的好处是只要Socks5Server服务搭建好了,就可以让我们的应用代码直接翻墙,而无需考虑操作系统环境。

为此我们需要做两件事:

  1. 实现socks5协议
  2. HTTPsocks5通道

第一点我们直接使用开源模块shadowsocks-go,第二点通过GoHTTP包的底层实现原理,让网络请求走shadowsocks-gopipe。接下来分别讲这两点。

shadowsocks-go

shadowsocks-go是实现socks5协议的开源软件,除了用于翻墙还可以当做普通模块使用。本文需要使用到它的TCP加密通道。使用很简单,以一段代码为示例:

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
package main

import (
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
"log"
"net"
"strconv"
)

var Config struct {
server string
port int
password string
method string
}

func Socks5Conn(addr string, config *Config) (net.Conn, error) {
rawAddr, err := ss.RawAddr(addr)
if err != nil {
return nil, err
}
serverAddr := net.JoinHostPort(config.server, strconv.Itoa(config.port))
cipher, err := ss.NewCipher(config.method, config.password)
if err != nil {
return nil, err
}
return ss.DialWithRawAddr(rawAddr, serverAddr, cipher)
}

Socks5Conn的作用是通过输入访问目标服务器的地址(IP,Port组合),返回经过socks5加密的网络连接对象net.Conn(其实是一个接口interface)。这个函数的核心是ss.DialWithRawAddr,它通过目标服务器的地址和socks5服务器以及加密模块即可返回经过socks5加密的网络连接对象net.Conn。这个网络连接对象供HTTP连接使用。

Go HTTP 模块中的 Transport 对象

GoHTTP标准库的实现原理分为两个层次。一层负责HTTP语义的处理包括HeaderURL表单,另外一层负责网络连接。前者的相关对象包括RequestResponse,后者则由Transport对象复杂处理。

通过Transport对象的源码

1
2
3
4
5
6
7
8
9
10
type Transport struct {
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)

// Dial specifies the dial function for creating unencrypted TCP connections.
//
// Deprecated: Use DialContext instead, which allows the transport
// to cancel dials as soon as they are no longer needed.
// If both are set, DialContext takes priority.
Dial func(network, addr string) (net.Conn, error)
}

可知,负责底层网络连接的函数为:DialContextDial,后者已经弃用。由于shadowsocks-go实现原因和出于化简实现的目的,本文依旧Dial函数实现。

指定Transport使用socks5的网络连接对象和设置Client对象使用该Transport

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func HTTPClientBySocks5(addr string) *http.Client {
//....
//...
rawAddr, err := ss.RawAddr(addr)
handleError(err)

serverAddr := net.JoinHostPort(config.server, strconv.Itoa(config.port))
cipher, err := ss.NewCipher(config.method, config.password)
handleError(err)

dailFunc := func(network, addr string) (net.Conn, error) {
return ss.DialWithRawAddr(rawAddr, serverAddr, cipher.Copy())
}
tr := &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
tr.Dial = dailFunc
return &http.Client{Transport: tr}
}

此时,就可以直接使用*http.Client对象做网络请求了。例如实现GETPOST方法:

1
2
3
4
5
6
7
8
9
func Get(uri string) (resp *http.Response, err error) {
client := HTTPClientBySocks5(uri)
return client.Get(uri)
}

func Post(uri string, contentType string, body io.Reader) (resp *http.Response, err error) {
client := HTTPClientBySocks5(uri)
return client.Post(uri, contentType, body)
}

上述为原理介绍,完整代码如下:

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package main

import (
"fmt"
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"strconv"
"time"
)

var config struct {
server string
port int
password string
method string
}

func handleError(err error) {
if err != nil {
log.Fatal(err)
}
}

func HTTPClientBySocks5(uri string) *http.Client {
parsedURL, err := url.Parse(uri)
handleError(err)

host, _, err := net.SplitHostPort(parsedURL.Host)
if err != nil {
if parsedURL.Scheme == "https" {
host = net.JoinHostPort(parsedURL.Host, "443")
} else {
host = net.JoinHostPort(parsedURL.Host, "80")
}
} else {
host = parsedURL.Host
}

rawAddr, err := ss.RawAddr(host)
handleError(err)

serverAddr := net.JoinHostPort(config.server, strconv.Itoa(config.port))
cipher, err := ss.NewCipher(config.method, config.password)
handleError(err)

dailFunc := func(network, addr string) (net.Conn, error) {
return ss.DialWithRawAddr(rawAddr, serverAddr, cipher.Copy())
}
//dailContext := func(ctx context.Context, network, addr string) (net.Conn, error) {}

tr := &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
tr.Dial = dailFunc
return &http.Client{Transport: tr}
}

func Get(uri string) (resp *http.Response, err error) {
client := HTTPClientBySocks5(uri)
return client.Get(uri)
}

func Post(uri string, contentType string, body io.Reader) (resp *http.Response, err error) {
client := HTTPClientBySocks5(uri)
return client.Post(uri, contentType, body)
}

func PostForm(uri string, data url.Values) (resp *http.Response, err error) {
client := HTTPClientBySocks5(uri)
return client.PostForm(uri, data)
}

func Head(uri string) (resp *http.Response, err error) {
client := HTTPClientBySocks5(uri)
return client.Head(uri)
}

func main() {
// for testing
config.method = "aes-256-cfb" // default method
config.password = "your socks pw"
config.port = 0 // your port
config.server = "your socks ip"
var uri string = "https://www.google.com.hk/?gws_rd=ssl"
resp, err := Get(uri)
handleError(err)

defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
handleError(err)
fmt.Println(string(body))
}

上面的代码实现下面的app网络请求流程,可以在应用代码中拿来访问被墙的网站、做加密传输等等。

1
app -socks5-encryption-data-> Socks5Server -HTTP-> Server

事实上,利用上述方法和结合上一篇文章为Redis编写安全通道,可以让数据库走socks5代理。有空详细写写。