内网穿透(3/4) - 基于UDP的打洞穿透方法

2019 12 8, Sun

打洞

前面提到了有利用转发编写的“穿透”工具,但是实际上我们都是搭建了一个反向代理,把我们的请求转发了出去。

那么比如说我在办公室远程编译了一套东西,打包以后占地面积10GB,如果我通过代理转发这个压缩包,就会在主机上产生10G流量。按照带宽计费的话,带宽低了速度太慢,带宽高了价格又贵,流量计费的话一个月流量的总费用也贵,都不如我去办公室跑一趟复制回来。话说回来了,既然要跑一趟,干嘛还远程。

针对这种情况,我们需要想办法尽可能让两个在不同内网里的主机可以直接通信。这种方法就叫做打洞。

原理

说打洞原理就不得不说到NAT

IPv4、内网和NAT

IPv4是目前来说用的最广的IP协议和地址格式,由32bit组成,通过八位一个数字来表示,比如说1.2.3.4。分成了不同的地址段,刨去内网地址段、环回地址段等等,公网IP数量相对来说很少,前两天最后一个未分配的地址段也分配出去了。

那既然所有地址段都分配出去了,我们是如何让新的设备加入IPv4网络呢?

通过NAT。我们通过俄罗斯套娃NAT,把一批机器套在一个本地的子网里面,通过地址和端口转换,把多个设备套在一个IP上面。而且这种转换可以一层套一层,就可以在一个公网IP下套非常多的设备。

除了可以容纳更多设备以外,nat还有一些其他好处,比如说增加内网机器的安全性,还有共享带宽的同时可以QoS。

nat类型

nat的类型大致来说分为两类:

  • 网络地址转换NAT
  • 网络地址端口转换
    • 完全锥形NAT
    • 受限锥形NAT(端口or地址受限)
    • 对称NAT

网络地址转换

就是简单的翻译一下两个IP地址,不改变端口,但是由于IP变了,checksum什么的也需要改变。

通常外层IP是一个IP池,内网机器和外层网络只有IP的对应关系。

网络地址端口转换

这类地址转换会同时转换地址和端口。

比如说我在家,网络拓扑比较简单,就是几层子网套起来:

电信:
  光猫(....<->192.168.1.1/24):
    CentOS路由器(192.168.1.2/24<->192.168.2.1/24):
      我的设备1(192.168.2.x/24)
      我的设备2(2.x/24)
      虚拟机子网网关(2.30/24<->172.100.0.0/16)
        虚拟机1(172.100.0.2/24)
        ....
      k8s/docker(2.31/24 <->172.200.0.0/16)
      ......

如果我要从192.168.2.100向114.114.114.114:53/udp发送DNS请求。那么路径会是这样的:

  • 我的机器从192.168.2.100:<随机a>发送数据包到1.1.1.1:53
  • 路由器收到这个包发现确实不是内网里的机器
    • 建立临时的映射关系192.168.1.2:<随机b>↔192.168.2.100:<随机a>
    • 修改数据包源地址为自己的出口IP192.168.1.2:<随机b>,修改对应的checksum等信息
    • 按照路由发往出口IP
  • 光猫也发现不是自己内网的
    • 建立映射<光猫>:<随机b>↔192.168.1.2:<随机a>
    • 修改源地址并且发送出去
  • 经历了不知道多少层,假设我们直接到了公网网关,那么最后由公网网关再修改源地址为<公网>:<随机z><-><光猫>:<随机c>
  • 114收到并回复给了源地址<公网>:<随机z>
  • 公网网关按照映射,修改回复数据包的目的地址为<光猫>:<随机c>
  • 光猫按照映射关系,把目的地址修改为192.168.1.2:<随机b>
  • 路由器按照映射关系,修改目的地址为192.168.2.100:<随机a>
  • 我的机器收到回复数据包

看起来挺复杂的,其实每一次路由就涉及这几件事情:

  • 出口流量:
    • 建立内外IP:端口的端口映射
    • 修改源地址
    • 按照路由转发
  • 进口流量:
    • 找到映射的IP端口
      • 找得到:修改目的地址
    • 按照路由转发

这种端口是动态的,当然我们也可以建立静态的端口映射,路由器支持就可以做。

按照映射关系的特点,我们又把NAPT分为了完全锥形、受限锥形和对称NAT。

锥形NAT

锥形NAT就是映射关系会保持一段时间、只在特定的情况下失效的NAT。

比如说上面的例子,就会形成一个映射的链:每一级都建立映射关系,最后变成一个链:192.168.2.100:<a> ↔ 192.168.1.2:<b> ↔ .... ↔ <公网>:<z> ↔ 1.1.1.1:53,在一段时间内,这个映射链都是可靠的,只要我是从192.168.2.100:<a>发给1.1.1.1:53、或者反过来,都会走这条路。

我们家里用的nat一般都是这类。按照一些配置细节不同,还分了完全锥形NAT、地址受限NAT和端口受限NAT。

对称NAT

对称NAT就是映射关系不保持、每次都映射到新的端口上的NAT。

一般企业里面用的多一些。

打洞

上面说NAT的时候,我们提到了锥形NAT会维持一个映射关系的链。

有的时候路由不可靠、或者是IP会浮动,或者有的机房有多个出口IP,偶尔回复数据会换个IP发回来。所以一般回复时不要求数据包源地址在链上,只要目的地址没问题就行。

那么其实换句话说,只要往公网的这个端口发送数据包就可以打洞了。

问题:但是我们要怎么样知道我们的公网IP呢?

我们需要一个或者多个在公网上有IP的服务器,专门用来接收数据包,能收到数据包一般就可以看到公网IP。比如说迅雷的tracker、zerotier的planet/moon

问题:不是映射关系会失效吗

一般来说失效速度不会那么快,我们只要定时发一次心跳包就好了。如果映射关系改变了,我们也可以很快的更新端口信息,之后重发没有收到的数据包就好了

问题:我试了TCP不能打洞

是的,我看到的很老的资料也提到了TCP打洞,但是现在的网关上一般都会解析TCP包头,如果是从外向内建立连接的请求一般都会过滤掉。所以我们现在只能用UDP打洞了。

那如果公司用的是对称NAT怎么办

那只能走转发了。用FRP、ssh或者是类似的工具吧~

实现

自己写这个实现比较麻烦,之后再自己写吧。先列举一下常见的利用了打洞的工具

  • P2P
    • bt/eMule
    • frp的xtcp模式
  • 虚拟网络
    • zerotier