2021第一篇,继续写点有意思的小东西。
1. 质疑
上篇《实战:150行Go实现高性能socks5代理》发出来后,有同学提出了一些问题,比如说测试机配置太高,结果“不太具有说服力”、“是在耍赖”,再比如说应该和其他开源 socks 代理对比才比较有说服力。
这些质疑我觉得都非常有道理,经过深刻的反思,我做出一个艰难的决定,那就是不予理会,毕竟有这时间,我还不如另写一篇更有营养的,比如在这篇里,我们将看到,如何使用 150 行 Go 实现一个高性能的加密隧道。
不过有一个质疑值得专门一提:@hjc4869 大佬指出,由于 tcp 是双工通信,而 Socks5Forward 在某个方向结束后就把 src 和 dest 都关闭,不符合 tcp 规范,无法支持 half-closed connection。
这确实是个问题,好在依赖这个特性的场景不多,而且有些网络节点(如部分 NAT 路由器)本身并未完整实现这个特性(遇到fin直接或延迟关闭,可避免一些DoS攻击),因此该特性在实践中并不够可靠;此外,完整实现这个特性,代码会比较啰嗦,所以为了标题的 flag 暂且妥协,感兴趣的同学可以自己试着完善它(提示:可以抄一下 io.Copy 的源码)。
2. 隧道
为了照顾新来的同学,我们可能还应该先介绍一下什么是隧道。
如下图所示,直接访问目标服务时,由于网络上可能存在不安全因素(窃听等),我们会希望采用一个隧道协议,将需要传输的内容封装在协议的负载中,从而保障通信的安全。
一个典型的隧道协议就是 SSL/TLS,通过将 http 封装在 TLS 隧道中,我们就得到了 https,同样我们还可以有 ftps,socks5-over-tls;应用隧道的其他场景还包括需要在不兼容的网络上传输数据等情况。
上图中的“加密设备”并不一定需要是个独立的硬件,在接下来的内容里,我们会看到如何实现一个软件版本。
3. 开挖
饭要一口一口吃,隧道要一点点挖。
所以我们先搞个不加密的、用于传输一个 TCP Stream 的隧道,比如下图所示,将请求先发给中继 A(IP_A:PORT_A),A 转发给 B (IP_B:PORT_B),再由 B 转发到目标节点(IP:PORT)。
对于中继A,实现起来就非常简单了,27行搞定:
func main() {
listenAddr := "IP_A:PORT_A"
remoteAddr := "IP_B:PORT_B"
server, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Printf("Listen failed: %v\n", err)
return
}
for {
client, err := server.Accept()
if err != nil {
fmt.Printf("Accept failed: %v", err)
continue
}
go Relay(client, remoteAddr)
}
}
func Relay(client net.Conn, remoteAddr string) {
remote, err