NAT打洞

本文介绍了NAT的四种类型及其判断方式,重点阐述了UDP和TCP打洞的原理和过程。通过示例代码展示了UDP打洞如何实现内网主机间的通信,并探讨了TCP打洞的难点。文章最后提出了TCP打洞的策略,并讨论了在实际应用中的挑战,如文件传输和P2P通信的实现。
摘要由CSDN通过智能技术生成

前言


2024 年的 1024 过来给 2023.07 的这篇文章打几个补丁 ~O(∩_∩)O 哈哈~

如果你是 NAT 的小白,对 NAT 并不了解,这篇文章就很适合你,但其中的代码部分写的很是一般,末尾关于 TCP 部分的描述也不到位,建议仅阅读 初始 部分 以及看完几个图就好,知道大概的打洞流程即可,不建议深究和细看

若你对 NAT 有了一些初步的了解,又或是想知道怎么与 NAT 建立连接的
我强烈建议你直接阅读下一篇文章 TCP打洞,实现P2P(附Node源码)

如果你向来只打逆风局,可以直接跳转到 NAT4 的文章 TCP实现P2P(NAT3-NAT4)



一、初识NAT

1.1 NAT 类型

NAT 分为四种类型,分别为 NAT1234 🙃

  1. NAT1 - 完全锥形NAT(Full Clone NAT)
  2. NAT2 - 限制性锥形NAT(Restricted Clone NAT)
  3. NAT3 - 端口限制性锥形NAT( Port Restricted Clone NAT)
  4. NAT4 - 对称式NAT(Symmetric NAT)

NAT1234
怎么判断呢,先从 绿色 箭头看起。

现在假设

192.168.0.100 通过 8000 端口 访问 114.135.246.909001 端口

经过NAT之后,会形成这样一种结果:

  1. 内网的 192.168.0.100:8000120.230.117.10:9999 NAT公网IP,形成绑定关系
  2. 服务端 114.135.246.90:9001120.230.117.10:9999 NAT公网IP,访问

然后如果:
8000 端口 能收到 紫色 发来的消息,就是 NAT1
8000 端口 收不到 紫色 发来的消息,却能收到 浅蓝色 线发来的消息,就是 NAT2
8000 端口 收不到 紫色浅蓝色 线发来的消息,只能收到 绿色 线返回来的消息,就是 NAT3 | NAT4

NAT3 和 NAT4 的区别:
NAT3 是 8000 端口与 9999 端口形成绑定关系,不管通过 8000 端口访问谁,·都是从 9999 端口出去
NAT4 是 8000 端口与 9999 端口形成的绑定关系是有条件的,8000 端口访问同一 ip:port 时,都会从 9999 端口出去,但通过 8000 端口访问其他 ip:port 时,就会绑定 NAT 其他端口,比如 9990

我这移动大内网,一般都是NAT3,手机开数据流量,基本都是NAT4

2. NAT 打洞原理

打洞,说白了就是让 NAT 留下一条访问记录
谁来打洞,就在谁的 NAT 上,留下一条对方的记录,这样在对方访问自己的时候,不会因为自己的 NAT 因为缺少记录而把数据包丢弃

( 对于NAT来说,因为没记录,所以也不知道转发给内网的谁,就算知道 如果没有记录,则数据包的来源也不一定合法 )



二、UDP打洞

2.1 看图

UDP打洞

这里我们通过一个服务端,然后让A B两个内网主机进行UDP打洞并通信。适用于 NAT123

说明:
A、B 分别为两个不同内网下的主机
NA、NB分别问 A 和 B 的 NAT 出口
中间的 90.20.88.99 为服务端主机,下方称为 S

图解:

  1. 首先我们让 A:8001S:9001 建立UDP通信
    此时 NA 就会留下 内网的 A:8001NA:3333 的绑定,NA:3333S:9001 的访问
    即:A:8001 - NA:3333 - S:9001

  2. 接着让 B 也执行同样的操作
    此时 NB 就会留下 内网的 B:6000NB:4444 的绑定,NB:4444S:9001 的访问
    即:B:6000 - NB:4444 - S:9001

  3. 此时 S 知道 NANB,且还知道 (看着上面的两个‘即’ 继续往下看)
    A 通信可以用 S:9001NA:3333 发送消息
    B 通信可以用 S:9001NB:4444 发送消息
    若让 A - B 直接通信,得让 A 知道 NBB 知道 NA
    所以
    NB:4444 通过 S:9001 发送给 NA:3333A 知道可以通过 NB:4444B 通信
    NA:3333 通过 S:9001 发送给 NB:4444B 知道可以通过 NA:3333A 通信

  4. 若此时 B:6000 - NB:4444 直接向 NA:3333 进行 UDP 通信
    NA 会因为不存在 A:8001 - NA:3333 - NB:4444 这条记录,而将数据包丢弃 (同理 A 奕是如此,此时双方是对等的)
    但此时,在 NB 中则会留下 B:6000 - NB:4444 - NA:3333 这样一条记录,这一步就是所谓的 “打洞”

  5. NB:4444NA:3333 发送完之后,此时 A:8001 - NA:3333 再向 NB:4444 发送 UDP 数据包时
    因为 NB 有了 B:6000 - NB:4444 - NA:3333 这样一条记录,所以会将 NA:3333 发来的数据包转发到 B:6000
    并且此时在 NA 也会留下 A:8001 - NA:3333 - NB:4444 这条记录

  6. 之后 因为
    NA 有:
    A:8001 - NA:3333 - NB:4444
    A:8001 - NA:3333 - S:9001
    NB 有:
    B:6000 - NB:4444 - NA:3333
    B:6000 - NB:4444 - S:9001
    所以 AB 都可以与 S 直接通信,AB 也可以直接通信

2.2 看码

// client.js

const dgram = require("dgram");

const option = process.argv.splice(2)

let localPORT = 6677
const SERVER_PORT = option[0] || 9001
const SERVER_ADDR = option[1] || "114.135.246.90"

let localInfo = ""
let serverInfo = ""

let timeout = setInterval(() => {
    createClient()
}, 1000)

let getConnect = (client) => {
    console.log(`${serverInfo}连接成功。`);

    client.send("")

    // process.stdin.on('data', data => {
    //     let sendData = data.toString().trim()
    //     client.send(sendData);
    // })
}

let getMessage = (client, data, rinfo) => {
    console.log(`[${rinfo.address}:${rinfo.port}]:${data.toString()}`)

    let res = JSON.parse(data.toString())
    if (res.msg == "wait") {

    }
    if (res.msg == "toClient") {
        let second = res.second
        createNewClient(second.addr, second.port)
        client.close()
    }
    if (res.msg == "toServer") {
        let first = res.first
        createServer(first.addr, first.port)
        client.close()
    }

}

function createClient() {

    const client = dgram.createSocket("udp4");
    try {

        client.bind(localPORT, "", () => {
            clearInterval(timeout)
        })

        client.connect(SERVER_PORT, SERVER_ADDR)

        client.on('connect', () => getConnect(client))

        client.on('message', (data, rinfo) => {
            getMessage(client, data, rinfo)
        })

        client.on("listening", (res) => {
            localInfo = `[${client.address().address}:${client.address().port}]`
            serverInfo = `[${SERVER_ADDR}:${SERVER_PORT}]`
            console.log(`${localInfo} - ${serverInfo}`);
            // client.send(`${localInfo}-已连接-serverInfo`,SERVER_PORT,SERVER_ADDR)
        })

        client.on("close", (res) => {
            // console.log("close", "正在关闭");
        })

        client.on("error", (err) => {
            console.log("error", "发生一个错误");
            console.log("正在重连...");
            localPORT++
            // createClient()
        })

    } catch (error) {
        console.log("错啦!!!")
        console.log(error)
    }

}

function createNewClient(serverAddr, serverPort) {

    const client = dgram.createSocket("udp4");

    try {

        client.bind(localPORT)

        client.connect(serverPort, serverAddr)

        client.on('connect', () => {
            console.log(`${serverInfo}连接成功。`);
            client.send(`${localInfo}`)
            console.log("回车发送消息:");

            process.stdin.on('data', data => {
                let sendData = data.toString().trim()
                console.log(`${localInfo}: ${sendData}`);
                client.send(sendData);
            })
        })

        client.on('message', (data, rinfo) => {
            console.log(`[${rinfo.address}:${rinfo.port}]: ${data.toString()}`)
        })

        client.on("listening", (res) => {
            localInfo = `[${client.address().address}:${client.address().port}]`
            serverInfo = `[${serverAddr}:${serverPort}]`
            console.log(`${localInfo} - ${serverInfo}`);
            // client.send(`${localInfo}-已连接-serverInfo`,SERVER_PORT,SERVER_ADDR)
        })

        client.on("close", (res) => {
            console.log("close", res);
        })

        client.on("error", (err) => {
            console.log("error", "发生一个错误");
            console.log("正在重连...");
            localPORT++
            // createClient()
        })

    } catch (error) {
        console.log("错啦!!!")
        console.log(error)
    }
}

function createServer(clientAddr, clientPort) {

    const server = dgram.createSocket('udp4')
    server.bind(localPORT)

    // 开洞
    server.send("", clientPort, clientAddr)
    // 通知服务端准备好了
    server.send("ready", SERVER_PORT, SERVER_ADDR)

    server.on("listening", () => {
        console.log(`start to listening: ${server.address().address}:${server.address().port}`)

        console.log("回车发送消息:");
        process.stdin.on('data', data => {
            let sendData = data.toString().trim()
            console.log(`${localInfo}: ${sendData}`);
            server.send(sendData, clientPort, clientAddr);
        })
    })

    server.on("connect", (res) => {
        console.log("connect:", res);
    })

    server.on("message", (msg, rinfo) => {
        console.log(`[${rinfo.address}:${rinfo.port}]: ${msg.toString()}`)
        let message = msg.toString()
        // server.send(message, clientPort, clientAddr)
    })

    server.on("close", (res) => {
        console.log("close", res);
    })

    server.on("error", (err) => {
        console.log("error", err);
    })
}
// server.js

const dgram = require('dgram')

const server = dgram.createSocket('udp4')

let first = ""
let second = ""
let alive = {}

server.bind(9001)

server.on("listening", () => {
    console.log(`start to listening: ${server.address().address}:${server.address().port}`)
})

server.on("connect", (res) => {
    console.log("connect:", res);
})

server.on("message", (message, rinfo) => {
    let msg = message.toString()
    console.log(`[${rinfo.address}:${rinfo.port}]:${msg.toString()}`)

    let count = Object.keys(alive).length
    if (count == 0) {
        first = {
            addr: rinfo.address,
            port: rinfo.port
        }
        server.send(`{"res": "wait"}`, rinfo.port, rinfo.address)
    }
    else if(count == 1){
        second = {
            addr: rinfo.address,
            port: rinfo.port
        }
        let res = {
            first,
            msg: "toServer"
        }
        server.send(JSON.stringify(res), rinfo.port, rinfo.address)
    }
    alive[`${rinfo.address}:${rinfo.port}`] = {
        addr: rinfo.address,
        port: rinfo.port
    }
    console.log(alive)

    if (msg == "ready") {
        let res = {
            second,
            msg: "toClient"
        }
        server.send(JSON.stringify(res), first.port, first.addr)
        alive = {}
    }
    
    
})

server.on("close", (res) => {
    console.log("close", res);
})

server.on("error", (err) => {
    console.log("error", err);
})

这是个小的 demo,分别对应 client.jsserver.js
A 和 B 通用 client.js,S 部署 server.js 即可

用的时候先在 S 启动 server.js
然后分别在 A B 启动 client.js 即可,连接成功后
若将 S 中的 server.js停掉,A B仍能正常通信,则试验成功


三、TCP打洞

TCP打洞

TCP打洞,打洞的原理其实还是那个原理,就是在NAT上留下记录。

TCP的过程就不细说了,但是TCP打洞会比UDP打洞困难得多 (因为我也没成功)

主要是因为,TCP和UDP不同:

UDP是无链接,同一个端口既可以发送 同时可以监听接收。同一个端口,可以给很多人发送,也可以接收很多人发来的信息

但TCP是套接字,自己的 IP:PORT 是与对方的 IP:PORT 是绑定的,期间端口处于被占用的状态,只能发送或者是收到response回来的信息,不能直接从 发送状态 改为 监听状态,需要先进行断开 或者 设置为端口复用

但在端口复用的过程中,NAT可能会因此而改变端口,其原因可能是因为处于 TIME_WAIT 状态,又或是像 NAT4 一样处理新的TCP 连接

所以 此时想要通过TCP打洞,建立可靠的TCP连接,这时候就要搬出 TCP 状态迁移图
TCP状态迁移图
这个时候,想要提高成功打通的概率,可以让双方互发SYN,以达到 同时打开 的效果
因为一个端口发送失败,可能会导致这个端口在短时间内不能再使用

所以我想到的一个方法就是:
先让 A 向 S 发送正常的 TCP 连接,让 S 知道了 A-NA 的 TCP 端口
然后 S 向 NA 端口 往后的 10个端口 同时循环发送 SYN,A 也向 S 发送10+次 SYN,这样就有极大地概率命中其中一个端口
只要 NA 放行了一个任意一个 S 发来的 SYN,就能进行完整握手,建立起TCP连接通信

当然啦,如果这里的 S 是公网IP,则 A 向 S 建立的连接都可以连上
这里主要是想实现的是 S 向 A 主动建立起的连接
因为如果 S 能主动向A建立连接,S 就可以换成 B-NB
最终的效果其实就是 NA 和 NB 相互不断发送 SYN,以同时打开 NA 和 NB 的通道 建立连接

具体的实现,可以看下一篇:TCP打洞,实现P2P(附Node源码)


四、结语

最后再说两句吧…

后来才知道,原来这就是叫 p2p…好吧是我浅薄了


然后关于UDP打洞的部分,我给我的小伙伴讲,给他讲完了之后
他说了一句:“你这是在建立 UDP 的可靠连接嘛”,然后两个人看着图都沉默了…


然后TCP部分,我确实没能在两个大NAT网上打通,所以我也好奇和请教一下大佬们,有什么好的解决方案实现TCP打洞和建立连接的呢
因为如果光用UDP打洞实现连接之后,简单的通信还好,但如果是用来传输文件就好像不太现实?还要手写校验和重传机制?还要注意MTU、数据包大小?

或者像酷狗、迅雷这种利用客户端的,又是怎么实现的呢

如果可以的话,能不能有个 node.js 的 demo,C语言真的好难顶…


另外:
如果觉得颜色有点花里胡哨的话,之后会在这 NAT打洞 同步更新一篇吧

过来填坑啦!
下一篇:TCP打洞,实现P2P(附Node源码)
再下一篇:TCP实现P2P (NAT3-NAT4)



参考文章

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值