内网穿透原理浅析

2019/05/17 计算机网络

以前用过一下花生壳穿透内网,当时觉得这玩意太神奇了,有了这工具只需要在本地开发完就可以让别人直接访问,不需要烦琐的部署。可惜当初自己能力不够,只有眼馋的份。最近开始钻计算机网络这块知识,仅仅看理论觉得不过瘾,刚好看到 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 呢?答案是肯定的,前题是我们拥有一台自己的服务器。这种方法就是标题所说的内网穿透。内网穿透的原理如下图所示。

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

Search

    Table of Contents