环境准备
假设有一台远程服务器: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() { 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") } 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真香。