3

让Go的HTTP客户端走socks5代理

 2 years ago
source link: https://allenwind.github.io/blog/5887/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

让Go的HTTP客户端走socks5代理

让Go的HTTP客户端走socks5代理

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

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

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

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

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

为此我们需要做两件事:

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

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

shadowsocks-go

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

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对象的源码

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

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方法:

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)
}

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

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网络请求流程,可以在应用代码中拿来访问被墙的网站、做加密传输等等。

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK