关于NAT的一些事儿

关于NAT的一些事儿

从丢包开始

因为经常玩Dota2,然后又是校园网(万恶的ZJUVPN),所以每隔一段时间就会出现持续数十秒的高丢包(40%以上)
甚至掉线。在网上搜索之后发现,处于学校网络或者大型社区之类的网络环境下比较容易出现这种情况,这就是NAT的
原因了。

什么是NAT

大家如果用过路由器(包括无线路由)的话,应该是了解,多台设备使用的是用一个IP地址,想想也是对的,毕竟给你
的ip资源肯定是有限的,尤其是公网ip。那么像学校和大型社区这样的,用户有很多,而我们知道ipv4资源是很紧缺的
,所以一个社区或者学校也可能只有很少的ip资源,即使它有很大的带款。但是我们知道,网络传输是需要ip地址和端
口的,每个ip可以有65536个端口,65536对于一台机器的应用来说,确实是有点多。这样的话,我们可以通过端口映射
的方法来解决ip资源不足的问题。举个例子,2台电脑A,B共用一个ip:192.168.1.100,A要使用他的27765端口与百
度服务器(假设是12.12.12.12)的80端口通信,B要使用他的24578端口与百度服务器的80端口通讯。那么我们可以
映射192.168.1.100:30001 <=> A:27765,192.168.1.100:30002 <=> B:24578,使用这两个端口来收发A、B的数据
包,即可解决ip不足的问题。

NAT怎样处理包

对于NAT内部发送到外部的报文,NAT肯定会发送出去(用户的正常使用嘛),但是对于NAT外部主动发送过来的“不请自来
”包,NAT会主动丢弃,来保护内部安全或是减少处理量。但是用户与服务器通讯,服务器总要回传一些数据的,这样怎么
处理呢?NAT一般采用下面的方式:用户向外部如果发送过一些报文,那么一段时间内,发送报文的目的地,可以主动发送
报文进来。对这种情况的处理,是区分NAT类型的一个标准。

NAT的类型

  1. Full Cone NAT:这种NAT对每个内部地址都会转换到同一个外部地址,不管这些请求是不是属于一个或者多个应用的,
    这种相当于每次对外建立连接都设置了映射表,外部如果知道了存在的映射关系随时也可以无限制的发送数据进来。

  2. Restricted Cone NAT:这种NAT在上一个的基础上,限制只有内部主机发送过报文的ip(注意仅仅判定ip)才能回传
    数据包。

  3. Port Restricted Cone NAT:这种NAT也是限制必须有内部主机发送过报文,外部才可以回传,但是条件更苛刻,需要
    ip和端口都是内部曾经发送过的目标才可以。

  4. Symmetric NAT: 万恶的对称型NAT,他的映射关系不是每次连接就固定建立下来的。举个例子说明:前三种NAT,如果
    内部地址A:15547向外部10.10.10.10:80传送数据,内部地址被映射为192.168.1.100:18000,那么当内部地址A:15547再
    向外部10.10.10.11:80或者其他地址传送数据的时候,因为映射关系A:15547 <=> 192.168.1.100:18000已经建立,所以
    仍然使用这个外部地址进行中转通讯。但是对称型NAT,它的映射关系不仅与源内部地址及端口有关,还与目标ip地址和端口
    有关,也就是说同一个内部端口发送给不同主机或者同一主机不同端口时使用的外部地址和端口都不相同。

NAT打洞

处于NAT内部是无法建立服务器的,而且也没有办法主动和其他NAT之后的机器主动取得联系(NAT丢弃“不请自来”包),但是
事实上我们大多数人使用的网络都是位于NAT内部的,比如学校/家庭路由器等等。但是我们经常也需要进行点对点的通讯(P
2P,例如迅雷等加速下载的工具),这种情况下,我们需要对NAT打洞来实现点对点连接。

实现的方法也是比较简单,因为NAT是允许内部发送过信息的目标主机回传信息的,所以我们利用一台公网ip作为中介,使用
以下方式即可建立p2p连接:
1. 让内部主机A先使用1000端口连接公网服务器,公网服务器这时候知道了A使用的外网地址A外:45123
2. 让内部主机B使用1000端口连接公网服务器,公网服务器知道B使用的外网地址B外:12450
3. 公网服务器告诉A、B他们彼此的外部地址
4. 内部主机A使用1000端口向B的外部地址B外:12450发送数据,但是这对B来说属于“不请自来”,被丢弃
5. 内部主机B使用1000端口向A的外部地址A外:45123发送数据,对A的NAT来说,A已经发送过的目标,允许发送进来,这样A
就成功收到了信息
6. 内部主机A再向B发送信息,B也可以成功接收

这种方法的核心在于使用UDP协议,因为UDP是不需要建立连接即可发送的;另外需要A、B都属于前3种NAT,因为这样他们的外
部地址不会因为连接了不同目标地址而发生改变。以下是一个Go语言版本的简单原理说明:

//Clinet
package main

import (
    "bytes"
    "flag"
    "log"
    "nat/util"
    "net"
    "time"
)

var hostport = flag.String("hostport", "80", "hostpost")
var remoteaddr = flag.String("remoteaddr", "ramnode.inszva.com:3478", "remoteaddr")
var content = flag.String("content", "", "content")
var name = flag.String("name", "88888888", "name(must size of 8)")
var target = flag.String("target", "77777777", "name(must size of 8)")

func main() {
    flag.Parse()
    raddr, err := net.ResolveUDPAddr("udp4", *remoteaddr)
    if err != nil {
        log.Fatalln(err)
    }
    laddr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:"+*hostport)
    if err != nil {
        log.Fatalln(err)
    }
send:
    log.Println("I'm tring to tell the server my address")
    conn, err := net.DialUDP("udp4", laddr, raddr)
    defer conn.Close()
    if err != nil {
        log.Fatalln(err)
    }
    msg := util.Msg{
        Type: util.REGISTER_NAME,
        Name: util.Bytes2array([]byte(*name)),
    }
    conn.SetDeadline(time.Now().Add(time.Second))
    _, err = conn.Write(util.Msg2Bytes(&msg))
    if err != nil {
        log.Printf("send timeout, I'm tring to retry")
        goto send
    }
    conn.SetDeadline(time.Now().Add(time.Second))
    buff := make([]byte, 32)
    _, err = conn.Read(buff)
    if err != nil {
        log.Printf("read timeout, I'm tring to retry")
        goto send
    }
    log.Println("server returns:", string(buff))
    log.Println("I am waiting 5 seconds for his starting...")
    time.Sleep(5 * time.Second)

    //find

    log.Println("I'm tring to question the server his address")
    msg.Type = util.SEARCH_NAME
    msg.Name = util.Bytes2array([]byte(*target))
    conn.SetDeadline(time.Now().Add(time.Second))
    _, err = conn.Write(util.Msg2Bytes(&msg))
    if err != nil {
        log.Printf("send timeout, I'm tring to retry")
        goto send
    }
    conn.SetDeadline(time.Now().Add(time.Second))
    _, err = conn.Read(buff)
    if err != nil {
        log.Printf("read timeout, I'm tring to retry")
        goto send
    }
    log.Println("sever told me his address is:", string(buff))
    buff = bytes.Split(buff, []byte{0})[0]
    //Start

    realAddr, err := net.ResolveUDPAddr("udp4", string(buff))
    if err != nil {
        log.Fatalln(err)
    }
    conn.Close()
    conn2, err := net.DialUDP("udp4", laddr, realAddr)
    defer conn2.Close()
    if err != nil {
        log.Fatalln(err)
    }
    conn2.Write([]byte(*content))
    log.Println("I send a message to him, but it is not sure he can see")
    log.Println("Now I am reading...")
    conn2.Read(buff)
    log.Println("I succeeded reading:", string(buff))
}
//Server
package main

import (
    "log"
    "nat/util"
    "net"
)

func main() {
    nameMap := make(map[string]string)
    laddr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:3478")
    if err != nil {
        log.Fatalln(err)
    }
    conn, err := net.ListenUDP("udp4", laddr)
    if err != nil {
        log.Fatalln(err)
    }
    for {
        buff := make([]byte, 16)
        _, raddr, _ := conn.ReadFromUDP(buff)
        log.Println(raddr)
        msg := util.Bytes2Msg(buff)
        log.Println(msg)
        if msg.Type == util.REGISTER_NAME {
            name := util.Array2bytes(msg.Name)
            nameMap[string(name)] = raddr.String()
            conn.WriteToUDP([]byte(raddr.String()), raddr)
        } else {
            name := util.Array2bytes(msg.Name)
            if addr, ok := nameMap[string(name)]; ok {
                log.Println("found name")
                conn.WriteToUDP([]byte(addr), raddr)
            } else {
                log.Println("not found")
                conn.WriteToUDP([]byte("unkown"), raddr)
            }
        }
    }

}

丢包问题

由于NAT使用端口映射当出现用户使用的端口非常多,而公网ip资源有限,可分配端口也有限的时候,没有分配到端口的应用只能排队
甚至竞争端口,那么肯定会有少量端口刚刚用来映射A连接,而A连接还没有断开的时候却不得不被释放掉去映射B连接。这样的话,A
连接传输的数据就会发生严重的丢包,少量的资源服务于大量的用户,必然会出现轮番供应的现象,有些用户有些时候不得不让出自己
的资源给其他用户使用,于是自己出现大量丢包。如果是浏览网页或者是视频(视频有缓存)那还好,下次轮到自己的时候重发报文就
可以,但是如果是类似dota之类的即时战略游戏,这样的短时间丢包的打击是毁灭性的。

结语

不幸的,经过我的测试,贵浙的网络是对称型NAT。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值