Golang极简实现WebSocket承载socks5流量

环境准备

假设有一台远程服务器:1.2.3.4,上面运行着一个socks5代理:127.0.0.1:1080。登上这台远程服务器后,可以通过curl验证这个代理:

1
2
3
4
5
6
$ curl --socks5 127.0.0.1:1080 https://httpbin.org/get
{
...
"origin": "1.2.3.4",
...
}

本文的目标是通过WebSocket的方式让本地curl也可以使用这个socks5代理。

服务器运行代码

这段代码运行于远程服务器,代码运行时会在1.2.3.4:5000上开启一个WebSocket服务。每接收到一个WebSocket请求,代码就会连接至本地的socks5代理,使用io.Copy进行数据转发工作。

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

import (
"golang.org/x/net/websocket"
"io"
"log"
"net"
"net/http"
"sync"
)

func ws2socks(ws *websocket.Conn) {
defer ws.Close()
socks, err := net.Dial("tcp", "127.0.0.1:1080")
if err != nil {
log.Println("dial socks error:", err)
return
}
defer socks.Close()

var wg sync.WaitGroup
ioCopy := func(dst io.Writer, src io.Reader) {
defer wg.Done()
io.Copy(dst, src)
}
wg.Add(2)
go ioCopy(socks, ws)
go ioCopy(ws, socks)
wg.Wait()
}

func main() {
http.Handle("/", websocket.Handler(ws2socks))
log.Fatal(http.ListenAndServe("1.2.3.4:5000", nil))
}

本地运行代码

这段代码运行于本地,代码运行时会监听127.0.0.1:8888。每接收到一个TCP连接,代码就会连接至远程服务器的WebSocket服务,同样使用io.Copy进行数据转发工作。

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

import (
"golang.org/x/net/websocket"
"io"
"log"
"net"
"sync"
)

func socks2ws(socks *net.TCPConn) {
defer socks.Close()
ws, err := websocket.Dial("ws://1.2.3.4:5000/", "", "ws://1.2.3.4:5000/")
if err != nil {
log.Println("dial websocket error:", err)
return
}
defer ws.Close()

var wg sync.WaitGroup
ioCopy := func(dst io.Writer, src io.Reader) {
defer wg.Done()
io.Copy(dst, src)
}
wg.Add(2)
go ioCopy(ws, socks)
go ioCopy(socks, ws)
wg.Wait()
}

func main() {
listener, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
log.Fatal(err)
}
log.Println("listen tcp at:", "127.0.0.1:8888")

for {
conn, _ := listener.Accept()
go socks2ws(conn.(*net.TCPConn))
}
}

实际运行

在远程服务器、本地分别运行对应的代码后,就可以在本地用curl测试这个”本地socks5代理”了。

1
2
3
4
5
6
$ curl --socks5 127.0.0.1:8888 https://httpbin.org/get
{
...
"origin": "1.2.3.4",
...
}

TLS加密

如果不使用TLS加密传输,WebSocket中的数据将以明文的方式暴露在互联网上。为了安全性考虑,使用TLS加密传输是很有必要的。

服务器端配置

关于域名和证书的文章有很多,这里就不赘述了。假设有域名test.com的解析指向了这台服务器1.2.3.4,已经生成自签名证书文件于/etc/ssl/1.cert/etc/ssl/2.key。配置了Nginx之后(如下所示),WebSocket服务的地址就会从ws://1.2.3.4:5000/变为wss://test.com/ws

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
listen 443 ssl;
server_name test.com;

ssl_certificate /etc/ssl/1.cert;
ssl_certificate_key /etc/ssl/2.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;

root /var/www/html;
index index.html index.htm index.nginx-debian.html;

location / {
try_files $uri $uri/ =404;
}

location /ws {
proxy_pass http://1.2.3.4:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}

本地代码变动

如果不是自签名证书,那么本地代码只需要更改WebSocket连接地址。但如果是自签名证书,本地代码最好主动接受认证文件(即上文提到的1.cert),因为跳过证书认证会带来额外的风险。于是,本地代码在向WebSocket服务请求之前需要先读取认证文件,如下所示:

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
package main
...
var wsConfig *websocket.Config

func initConfig() {
// load certificate file
content, err := ioutil.ReadFile("1.cert")
if err != nil {
log.Fatal(err)
}
roots := x509.NewCertPool()
if ok := roots.AppendCertsFromPEM(content); ok != true {
log.Fatal("cert parse fail")
}
// generate websocket config
wsConfig, err = websocket.NewConfig("wss://test.com/ws", "wss://test.com/ws")
if err != nil {
log.Fatal(err)
}
wsConfig.TlsConfig = &tls.Config{RootCAs: roots}
}

func socks2ws(socks *net.TCPConn) {
defer socks.Close()
ws, err := websocket.DialConfig(wsConfig)
...
}

func main() {
initConfig()
...
}

总结

思路很简单,实现也简单……就当是Golang的入门练习。总结一下就是,Golang真香。


Golang极简实现WebSocket承载socks5流量
https://www.yooo.ltd/2020/02/20/Golang极简实现WebSocket承载socks5流量/
作者
OrangeWolf
发布于
2020年2月20日
许可协议