本地端口转发和网络隧道是调试、导航基础设施和更多有用的工具。它们经常被系统管理员和运营工程师使用,但也为开发者提供了一些很好的使用案例。
在这篇文章中,我们将看看只用Go标准库就能实现本地端口转发是多么简单,以及这个概念作为更多有趣的使用场景的构建块是多么强大。
下面这个例子的基本思路是在某个本地端口上发送流量,并将其转发到另一台机器上,然后再将其发送到某个服务。
这可能很有用,例如,如果这个服务只能从转发的机器上访问,而不能在本地访问。
当然,这里面有两个部分。一个是local 部分,一个是remote 部分。然而,这两个部分做的是同样的事情,它们在一个端口上接受一些TCP 流量,并将其转发到一个指定的目标。
作为一个例子,我们可以尝试通过我们的工具将HTTP流量从我们的本地浏览器转发到一个远程nginx实例。我们甚至可以在本地进行模拟,不需要远程服务器。
首先,我们在8181端口启动一个nginx实例(例如:用Docker),像这样。
docker run -d -p 8181:80 nginx
然后,我们启动两个版本的程序。
go run main.go --target 127.0.0.1:8181 --port 1337
这是remote 服务器,它接受1337 端口的流量并将其转发到nginx实例。
go run main.go --target 127.0.0.1:1337 --port 1338
这是local 服务,它接受来自1338 的流量,例如来自浏览器的流量,并将其转发到我们的remote 服务。
有了这样的设置,1338 端口的流量应该可以通过这两个应用程序到8181 端口的 nginx,然后再无缝返回。
当然,同样的事情也可以在网络上进行。例如,人们可以在remote 服务上通过SSH连接到一个远程服务器,并在某个给定的TCP端口上向local 服务提供这个服务。
同样的概念适用于任何基于TCP的协议,我们有了所有的构建模块,我们需要建立自己的隧道协议。相当不错!
好了,现在我们对我们要建立的东西有了一个大致的概念,让我们看看一个简单的实现。
例子
首先,我们使用flag 包来解析传入的target 和port 参数。
var (
target string
port int
)
func init() {
flag.StringVar(&target, "target", "", "target (<host>:<port>)")
flag.IntVar(&port, "port", 1337, "port")
}
然后,我们在给定的端口上启动一个服务器,这样客户就可以连接到我们的服务(上述例子中的1337 和1338 )。
incoming, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
log.Fatalf("could not start server on %d: %v", port, err)
}
fmt.Printf("server running on %d\n", port)
我们监听传入的连接并接受第一个连接,创建我们的客户端。这个版本只适用于一个连接的客户端,但通过为每个连接的客户端创建一个goroutine,将其改为多客户端解决方案并不难。
client, err := incoming.Accept()
if err != nil {
log.Fatal("could not accept client connection", err)
}
defer client.Close()
fmt.Printf("client '%v' connected!\n", client.RemoteAddr())
最后,我们连接到指定的目标服务器(nginx 和本例中的remote 服务)。
target, err := net.Dial("tcp", target)
if err != nil {
log.Fatal("could not connect to target", err)
}
defer target.Close()
fmt.Printf("connection to server %v established!\n", target.RemoteAddr())
好了,我们有了一个接受传入连接的服务器,并且连接到了目标服务器。现在我们需要一种方法,在client 和target 之间移动流量。
幸运的是,client 和target 都属于net.Conn 类型,并且满足io.Reader 和io.Writer 接口。因此,使用io.Copy ,将传入的流量传递给传出的连接,反之亦然,这很简单。
go func() { io.Copy(target, client) }()
go func() { io.Copy(client, target) }()
然而,如果我们像这样启动程序,它将立即运行,并在第一个连接后退出。为此,我们引入了一个stop 通道,它在程序结束时进行阻塞,并对一个os.Signal 。
这样,应用程序将继续运行,直到例如:CTRL+C 被按下。
func main() {
...flag code...
signals := make(chan os.Signal, 1)
stop := make(chan bool)
signal.Notify(signals, os.Interrupt)
go func() {
for _ = range signals {
fmt.Println("\nReceived an interrupt, stopping...")
stop <- true
}
}()
...forwarding code...
<-stop
}
就这样了。
如果我们按照文章开头的规定运行这个应用程序,有一个local 和一个remote 部分,并导航到http://localhost:1338 ,我们可以看到流量被成功通过。
虽然这只是一个相当无用的HTTP代理在这种状态下,有很多事情可以通过扩展这个简单的想法完成。
例如,你可以监控、多路复用、过滤、操纵或加密流量......有无限的可能性。:)
总结
这又是一个很好的小例子,通过Go的读写器接口的力量使之成为可能。
另外,这个例子只使用了最低限度的功能,全部来自标准库,没有过于繁琐,这说明Go的网络基元是很好的。
祝你转发愉快!:)