以前用过一下花生壳穿透内网,当时觉得这玩意太神奇了,有了这工具只需要在本地开发完就可以让别人直接访问,不需要烦琐的部署。可惜当初自己能力不够,只有眼馋的份。最近开始钻计算机网络这块知识,仅仅看理论觉得不过瘾,刚好看到 NAT 这块,找到了关于内网穿透的一些文章,希望自己写代码实现一个内网穿透工具,加深这方面的理解。
有时候都觉得自己有点走火入魔,总喜欢折腾些很多人都不怎么爱折腾的事,给人的感觉就是浪费时间,重复造轮子,何必呢?但个人还挺喜欢这种入迷的感觉,心怀好奇心,总是好事吧~极客精神?然而我的技术离极客还差远了。闲话就不多说了,下面进入正题。由于技术所限,难免出错。如果有心且发现本文的错误,可以到lwlwilliam.github.io给我提 issue。
由于 IP 地址(本文指的都是 IPv4)的紧缺,同一局域网内的所有主机一般都共用一个公网 IP 来访问互联网,在 IP 层中必须标明源地址和目标地址,如果局域网内多台主机同时上网会出现什么情况?这多台主机的源地址都是这同一个公网 IP,服务器的响应分组到了拥有这个 IP 的路由器应该把分组传给哪台主机?NAT 路由器都有一个 NAT 转发表,在主机到服务器的分组经过路由器时,路由器会对在 IP 层对其进行解开封装,其实现在很多 NAT 还会处理运输层,把分组的源地址和端口更换为路由器的接口的 IP 和端口,然后重新封装,把处理前后的 IP+端口对应关系记录到 NAT 转发表中,这样就可以对分组进行正确的传输。
现在如果我们想把本地的 HTTP 服务分享给其他人,但是其他人并不知道我们的本机地址,即使知道了也没有用,因为 NAT 转发表并没有记录我们 HTTP 服务的 IP 和端口。当然,这可以通过路由器对 NAT 转发表进行配置来实现(这个我没有亲自实践过),这种方法也稍显麻烦,还可能会被运营商封(这个我也没试过,道听途说的,哈)。还有没有其它办法呢?
通过以上的说明,我们知道了 NAT,那么可不可以利用 NAT 呢?答案是肯定的,前题是我们拥有一台自己的服务器。这种方法就是标题所说的内网穿透
。内网穿透的原理如下图所示。
客户端 A 想要访问内网主机 A 的 web 服务,必须通过服务器作为中转。首先,服务器的穿透软件需要监听两个 socket,一个用来监听客户端 A 的请求,一个用来监听内网主机 B 的连接;接着内网主机 B 要通过内网穿透软件和服务器建立永久连接;当客户端 A 向服务器请求时,服务器的穿透软件就通过已有的两个 socket 把请求转发到内网主机 B 中,内网主机 B 读取到这个请求,再把这个请求发到 web 服务处进行处理,最后把 web 响应反向传给客户端 A。这就是内网穿透的大概原理。以下是示例代码。
server.go:
package main
import (
"net"
"log"
"time"
"fmt"
)
func main() {
listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 9981})
if err != nil {
log.Fatal(err)
}
log.Printf("local: <%s> \n", listener.LocalAddr().String())
peers := make([]net.UDPAddr, 0, 2)
data := make([]byte, 1024)
for {
n, remoteAddr, err := listener.ReadFromUDP(data)
if err != nil {
fmt.Printf("error during read: %s", err)
}
log.Printf("<%s> %s\n", remoteAddr.String(), data[:n])
peers = append(peers, *remoteAddr)
if len(peers) == 2 {
log.Printf("punch hole, %s <--> %s\n", peers[0].String(), peers[1].String())
listener.WriteToUDP([]byte(peers[1].String()), &peers[0])
listener.WriteToUDP([]byte(peers[0].String()), &peers[1])
time.Sleep(time.Second * 8)
log.Println("the exit of the transit server will not affect the communication between peers")
return
}
}
}
peer.go:
// p2p peers
package main
import (
"net"
"log"
"fmt"
"strings"
"strconv"
"time"
"flag"
)
const HandShakeMsg = "udp hole message"
var (
tag, ip *string
)
func main() {
tag = flag.String("t", "mac", "the process tag")
ip = flag.String("i", "", "the server ip")
flag.Parse()
if *ip == "" {
log.Fatal("IP address can not be empty.")
}
srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 9982}
dstAddr := &net.UDPAddr{IP: net.ParseIP(*ip), Port: 9981}
conn, err := net.DialUDP("udp", srcAddr, dstAddr)
if err != nil {
log.Fatal(err)
}
if _, err = conn.Write([]byte("Hello, I'm new peer:" + *tag)); err != nil {
log.Fatal(err)
}
data := make([]byte, 1024)
n, remoteAddr, err := conn.ReadFromUDP(data)
if err != nil {
log.Fatal(err)
}
conn.Close()
anotherPeer := parseAddr(string(data[:n]))
fmt.Printf("local: %s server: %s another: %s\n", srcAddr, remoteAddr, anotherPeer.String())
// start to punch hole
bidirectionHole(srcAddr, &anotherPeer)
}
func parseAddr(addr string) net.UDPAddr {
t := strings.Split(addr, ":")
port, _ := strconv.Atoi(t[1])
return net.UDPAddr{
IP: net.ParseIP(t[0]),
Port:port,
}
}
func bidirectionHole(srcAddr *net.UDPAddr, anotherAddr *net.UDPAddr) {
conn, err := net.DialUDP("udp", srcAddr, anotherAddr)
if err != nil {
fmt.Println(err)
}
defer conn.Close()
// send message to another peer (the nat device of another peer will discard the message, because of the invalid origin of the it),
// to punch a hole between the peers
if _, err = conn.Write([]byte(HandShakeMsg)); err != nil {
log.Println("send handshake:", err)
}
go func() {
for {
time.Sleep(1 * time.Second)
if _, err = conn.Write([]byte("from [" + *tag + "]")); err != nil {
log.Println("send msg fail", err)
}
}
}()
for {
data := make([]byte, 1024)
n, _, err := conn.ReadFromUDP(data)
if err != nil {
log.Printf("error during read: %s\n", err)
} else {
log.Printf("received: %s\n", data[:n])
}
}
}
先在服务器中运行 server.go:
$ go build server.go
$ ./server
然后分别在两个在不同内网的主机上运行 peer.go,参数-t
是主机的标记,两个主机使用不同的标记以示区分:
$ go build peer.go
$ ./peer -t tag -i serverIP