NAT 种类

NAT有4个类型,它们分别是:NAT1、NAT2、NAT3、NAT4。从 NAT1 至 NAT4 限制越来越多。

NAT1 完全圆锥形NAT(Full Cone NAT)

完全圆锥型NAT把一个来自内部IP地址和端口的所有请求,始终映射到相同的外网IP地址和端口;同时,任意外部主机向该映射的外网IP地址和端口发送报文,都可以实现和内网主机进行通信,就像一个向外开口的圆锥形一样,故得名。

这种模式很宽松,限制小,只要内网主机的IP地址和端口与公网IP地址和端口建立映射关系,所有互联网上的主机都可以访问该NAT之后的内网主机。

  • 映射特性:内网地址端口唯一映射为固定公网地址端口
  • 访问规则:允许任意外部主机通过该映射地址发起通信
  • 网络宽容度:高

NAT2 地址限制式圆锥形NAT(Address Restricted Cone NAT)

地址限制式圆锥形NAT同样把一个来自内部IP地址和端口的所有请求,始终映射到相同的外网IP地址和端口;与完全圆锥型NAT不同的是,当内网主机向某公网主机发送过报文后,只有该公网主机才能向内网主机发送报文,故得名。相比NAT1,NAT2 增加了地址限制,也就是IP受限,而端口不受限。

  • 映射特性:保持固定地址端口映射
  • 访问规则:仅允许曾接收过本端流量的外部IP发起通信,需要基于源IP地址进行访问控制
  • 网络宽容度:中

NAT3 端口限制式圆锥形NAT(Port Restricted Cone NAT)

端口限制式圆锥形NAT更加严格,在上述条件下,只有该公网主机该端口才能向内网主机发送报文,故得名。相比NAT2,NAT3 又增加了端口限制,也就是说IP、端口都受限。

  • 映射特性:维持固定地址端口绑定
  • 访问规则:仅允许特定IP地址和端口的组合进行通信
  • 网络宽容度:中

NAT4 对称式NAT(Symmetric NAT)

对称式NAT把内网IP和端口到相同目的地址和端口的所有请求,都映射到同一个公网地址和端口;同一个内网主机,用相同的内网IP和端口向另外一个目的地址发送报文,则会用不同的映射(比如映射到不同的端口)。

  • 动态映射:针对不同目标生成独立映射关系
  • 会话特性:每个外部连接使用独立端口映射,需要端口预测
  • 网络宽容度:低

NAT打洞

所谓NAT打洞就是在通信双方都处于NAT后面时候,使用某种方式使得通信双方可以正常端到端通信。 基于TCP的NAT打洞基本放弃,TCP协议本是有状态的,NAT设备一般只允许outbound发起syn,所以 NAT 打洞技术基本都是基于UDP协议的。

STUN

STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间建立UDP通信。

主要作用于获取公网地址以及NAT类型

打洞策略

基础型穿透(NAT1/NAT2/NAT3):利用STUN获取对端公网地址后直接建立连接

对称型应对(NAT4):采用端口预测算法(如±2规则)尝试建立连接

混合场景处理:不同类型NAT组合需要采用差异化连接策略

NAT1/NAT2/NAT3 打洞

  • NAT1: 对回包没有安全限制,所以其NAT会话中只包含转换后的源IP和源端口,且转换后端口仅和内网源IP+源端口有关;

  • NAT2: 需要注意回包仅限制IP,所以其NAT会话中只包含转换后的源IP、源端口、目的IP,且转换后端口仅和内网源IP+源端口有关;

  • NAT3: 需要注意回包仅限制IP和端口,所以其NAT会话中只包含转换后的源IP、源端口、目的IP、目的端口,且转换后端口仅和内网源IP+源端口有关;

通过 STUN 获取地址端口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use std::sync::Arc;

use stun::{
    agent::TransactionId,
    client::ClientBuilder,
    message::{BINDING_REQUEST, Getter, Message},
    xoraddr::XorMappedAddress,
};
use tokio::net::UdpSocket;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?);

    socket.connect("stun.miwifi.com:3478").await?;
    let mut client = ClientBuilder::new().with_conn(socket).build()?;

    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();

    let mut msg = Message::new();
    msg.build(&[Box::<TransactionId>::default(), Box::new(BINDING_REQUEST)])?;
    client.send(&msg, Some(Arc::new(tx))).await?;

    let event = rx.recv().await.ok_or("rx recv error")?;
    let msg = event.event_body?;
    let mut xor_addr = XorMappedAddress::default();
    xor_addr.get_from(&msg)?;

    println!("{}:{}", xor_addr.ip, xor_addr.port);

    Ok(())
}

连接到对端:

1
socket.connect(peer_addr).await?;

发送以及读取消息:

1
2
socket.send(msg)
socket.recv_from(buf)

NAT4 打洞

  • NAT4: 需要注意回包仅限制IP和端口,所以其NAT会话中只包含转换后的源IP、源端口、目的IP、目的端口,转换后端口仅和上述均有关;

例如如果是 NAT1 与 NAT4 打洞,得到NAT4方的地址后,需要预测端口,例如有一种很简单的规则是,使用上一次使用的端口号+2或-2。

1
port.wrapping_add(2)

如果是 NAT4对NAT4 则双方都需要预测,成功率极低。

中继服务

中继一般用于交换客户端之间的信令,一般就是公网地址+NAT类型以及某些附带信息,可以服务器直接获取也可以通过 STUN 获取,并且在打洞失败的情况下提供中转。

交换地址信令的简单例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let socket = UdpSocket::bind("0.0.0.0:7899").await?;

    let clients = Arc::new(Mutex::new(Vec::new()));

    let mut buf = [0u8; 1024];
    loop {
        let (len, addr) = socket.recv_from(&mut buf).await?;
        println!("registr from: {}", addr);

        let data = String::from_utf8_lossy(&buf[..len]);
        let peer_addr = SocketAddr::from_str(&data)?;
        let mut clients = clients.lock().unwrap();
        clients.push((addr, peer_addr));

        if clients.len() == 2 {
            let (addr1, peer1) = clients[0];
            let (addr2, peer2) = clients[1];

            tokio::time::sleep(Duration::from_secs(2)).await;

            socket.send_to(&peer2.to_string().as_bytes(), addr1).await?;
            socket.send_to(&peer1.to_string().as_bytes(), addr2).await?;
            println!("{addr1}={peer1} <-------> {addr2}={peer2}");

            clients.clear();
        }
    }
}

NAT1-NAT1 的 Demo

双方需要手动输入对方地址

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use std::{
    net::SocketAddr,
    sync::Arc,
    time::{Duration, Instant},
};

use tokio::{
    io::{self, AsyncBufReadExt, BufReader},
    net::UdpSocket,
    sync::Mutex,
};

struct Peer {
    addr: SocketAddr,
    last_heartbeat: Instant,
}

use stun::{
    agent::TransactionId,
    client::ClientBuilder,
    message::{BINDING_REQUEST, Getter, Message},
    xoraddr::XorMappedAddress,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?);

    socket.connect("stun.miwifi.com:3478").await?;
    let mut client = ClientBuilder::new().with_conn(socket.clone()).build()?;

    let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();

    let mut msg = Message::new();
    msg.build(&[Box::<TransactionId>::default(), Box::new(BINDING_REQUEST)])?;
    client.send(&msg, Some(Arc::new(tx))).await?;

    let event = rx.recv().await.ok_or("rx recv error")?;
    let msg = event.event_body?;
    let mut xor_addr = XorMappedAddress::default();
    xor_addr.get_from(&msg)?;

    println!("本地公网地址: {xor_addr}");
    println!("请输入对方公网地址 (格式: IP:Port):");
    let mut peer_addr_str = String::new();
    BufReader::new(io::stdin())
        .read_line(&mut peer_addr_str)
        .await?;
    let peer_addr: SocketAddr = peer_addr_str.trim().parse()?;

    let peer = Arc::new(Mutex::new(Peer {
        addr: peer_addr,
        last_heartbeat: Instant::now(),
    }));

    socket.connect(peer_addr).await?;

    let socket = Arc::new(socket);
    let socket_heartbeat = socket.clone();
    let peer_heartbeat = peer.clone();
    let socket_receive = socket.clone();
    let peer_receive = peer.clone();

    tokio::spawn(async move {
        loop {
            tokio::time::sleep(Duration::from_secs(10)).await;
            let addr = peer_heartbeat.lock().await.addr;
            if let Err(e) = socket_heartbeat.send(b"PING").await {
                eprintln!("心跳发送失败: {}", e);
            } else {
                println!("心跳已发送至 {}", addr);
            }
        }
    });

    tokio::spawn(async move {
        let mut buf = [0u8; 1024];
        loop {
            match socket_receive.recv_from(&mut buf).await {
                Ok((size, src)) => {
                    let msg = String::from_utf8_lossy(&buf[..size]);
                    println!("收到来自 {} 的消息: {}", src, msg);

                    if msg == "PING" {
                        let mut peer = peer_receive.lock().await;
                        if peer.addr == src {
                            peer.last_heartbeat = Instant::now();
                        }
                    }
                }
                Err(e) => eprintln!("接收错误: {}", e),
            }
        }
    });

    println!("P2P 连接已启动,输入消息发送(输入 'exit' 退出):");
    let stdin = BufReader::new(io::stdin());
    let mut lines = stdin.lines();

    while let Some(line) = lines.next_line().await? {
        let msg = line.trim();
        if msg == "exit" {
            break;
        }

        if let Err(e) = socket.send(msg.as_bytes()).await {
            eprintln!("消息发送失败: {}", e);
        } else {
            println!("消息已发送: {}", msg);
        }
    }

    Ok(())
}