为什么我在公司里访问不了家里的电脑?
为什么我在公司里访问不了家里的电脑?
上篇文章「为什么我们家里的 IP 都是 192.168 开头的?」提到,因为 IPv4 地址有限,最大 42 亿个。为了更好的利用这有限的 IP 数量,网络分为局域网和广域网,将 IP 分为了私有 IP 和公网 IP,一个局域网里的 N 多台机器都可以共用一个公网 IP,从而大大增加了"可用 IP 数量"。

当我们需要发送网络包的时候,在 IP 层,需要填入源 IP 地址,和目的 IP 地址,也就是对应快递的发货地址和收货地址。

但是我们家里的局域网内,基本上都用192.168.xx.xx
这样的私有 IP。
如果我们在发送网络包的时候,这么填。对方在回数据包的时候该怎么回?毕竟千家万户人用的都是192.168.0.1
,网络怎么知道该发给谁?
所以肯定需要将这个192.168.xx
私有 IP 转换成公有 IP。
因此在上篇文章最后,留了这么个问题。局域网内用的是私有 IP,公网用的都是公有 IP。一个局域网里的私有 IP 想访问局域网外的公有 IP,必然要做个 IP 转换,这是在哪里做的转换呢?

答案是NAT 设备,全称Network Address Translation,网络地址转换。基本上家用路由器都支持这功能。
我们来聊下它是怎么工作的。
NAT 的工作原理
为了简单,我们假设你很富,你家里分到了一个公网 IP 地址 20.20.20.20
,对应配到了你家自带 NAT 功能的家用路由器上,你家里需要上网的设备有很多,比如你的手机,电脑都需要上网,他们构成了一个局域网,用的都是私有 IP,比如192.168.xx
。其中你在电脑上执行ifconfig
命令,发现家里的电脑 IP 是192.168.30.5
。 你要访问的公网 IP 地址是30.30.30.30
。
于是就有下面这样一张图

当你准备发送数据包的时候,你的电脑内核协议栈就会构造一个 IP 数据包。这个 IP 数据包报头里的发送端IP 地址填的就是192.168.30.5
,接收端IP 地址就是30.30.30.30
。将数据包发到 NAT 路由器中。
此时 NAT 路由器会将 IP 数据包里的源 IP 地址修改一下,私有 IP 地址192.168.30.5
改写为公网 IP 地址20.20.20.20
,这叫SNAT(Source Network Address Translation,源地址转换)。并且还会在 NAT 路由器内部留下一条 192.168.30.5 -> 20.20.20.20
的映射记录,这个信息会在后面用到。之后 IP 数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30
,到这里发送流程结束。

如果接收端处理完数据了,需要发一个响应给你的电脑,那就需要将发送端IP 地址填上自己的30.30.30.30
,将接收端地址填为你的公网 IP 地址20.20.20.20
,发往 NAT 路由器。NAT 路由器收到公网来的消息之后,会检查下自己之前留下的映射信息,发现之前留下了这么一条 192.168.30.5 -> 20.20.20.20
记录,就会将这个数据包的目的 IP 地址修改一下,变成内网 IP 地址192.168.30.5
, 这也叫DNAT
(Destination Network Address Translation,目的地址转换)。 之后将其转发给你的电脑上。

整个过程下来,NAT 悄悄的改了 IP 数据包的发送和接收端 IP 地址,但对真正的发送方和接收方来说,他们却对这件事情,一无所知。
这就是 NAT 的工作原理。
NAPT 的原理
到这里,相信大家都有一个很大的疑问。
局域网里并不只有一台机器,局域网内 每台机器都在 NAT 下留下的映射信息都会是 192.168.xx.xx -> 20.20.20.20
,发送消息是没啥事,但接收消息的时候就不知道该回给谁了。

这问题相当致命,因此实际上大部分时候不会使用普通的 NAT。
那怎么办呢?
问题出在我们没办法区分内网里的多个网络连接。
于是乎。
我们可以加入其他信息去区分内网里的各个网络连接,很自然就能想到端口。
但 IP 数据包(网络层)本身是没有端口信息的。常见的传输层协议 TCP 和 UDP 数据报文里才有端口的信息。


于是流程就变成了下面这样子。
当你准备发送数据包的时候,你的电脑内核协议栈就会先构造一个 TCP 或者 UDP 数据报头,里面写入端口号,比如发送端口是5000
,接收端口是3000
,然后在这个基础上,加入 IP 数据报头,填入发送端和接收端的 IP 地址。
那数据包长这样。

假设,发送端IP 地址填的就是192.168.30.5
,接收端IP 地址就是30.30.30.30
。
将数据包发到 NAT 路由器中。
此时 NAT 路由器会将 IP 数据包里的源 IP 地址和端口号修改一下,从192.168.30.5:5000
改写成20.20.20.20:6000
。并且还会在 NAT 路由器内部留下一条 192.168.30.5:5000 -> 20.20.20.20:6000
的映射记录。之后数据包经过公网里各个路由器的转发,发到了接收端30.30.30.30:3000
,到这里发送流程结束。

接收端响应时,就会在数据包里填入发送端地址是30.30.30.30:3000
,将接收端是20.20.20.20:6000
,发往 NAT 路由器。NAT 路由器发现下自己之前留下过这么一条 192.168.30.5:5000 -> 20.20.20.20:6000
的记录,就会将这个数据包的目的 IP 地址和端口修改一下,变回原来的192.168.30.5:5000
。 之后将其转发给你的电脑上。

如果局域网内有多个设备,他们就会映射到不同的公网端口上,毕竟端口最大可达 65535,完全够用。这样大家都可以相安无事。
像这种同时转换IP 和端口的技术,就是NAPT(Network Address Port Transfer , 网络地址端口转换 )。
看到这里,问题就来了。
那这么说只有用到端口的网络协议才能被 NAT 识别出来并转发?
但这怎么解释ping
命令?ping 基于 ICMP 协议,而 ICMP 协议报文里并不带端口
信息。我依然可以正常的 ping 通公网机器并收到回包。

事实上针对 ICMP 协议,NAT 路由器做了特殊处理。ping 报文头里有个Identifier
的信息,它其实指的是放出 ping 命令的进程 id。
对 NAT 路由器来说,这个Identifier
的作用就跟端口
一样。
另外,当我们去抓包的时候,就会发现有两个Identifier
,一个后面带个BE(Big Endian)
,另一个带个LE(Little Endian)
。
其实他们都是同一个数值,只不过大小端不同,读出来的值不一样。就好像同样的数字 345,反着读就成了 543。这是为了兼容不同操作系统(比如 linux 和 Windows)下大小端不同的情况。

内网穿透是什么
看到这里,我们大概也发现了。使用了 NAT 上网的话,前提得内网机器主动请求公网 IP,这样 NAT 才能将内网的IP 端口转成外网IP 端口。
反过来公网的机器想主动请求内网机器,就会被拦在 NAT 路由器上,此时由于 NAT 路由器并没有任何相关的 IP 端口的映射记录,因此也就不会转发数据给内网里的任何一台机器。
举个现实中的场景就是,你在你家里的电脑上启动了一个 HTTP 服务,地址是 192.168.30.5:5000,此时你在公司办公室里想通过手机去访问一下,却发现访问不了。
那问题就来了,有没有办法让外网机器访问到内网的服务?
有。
大家应该听过一句话叫,"没有什么是加中间层不能解决的,如果有,那就再加一层"。
放在这里,依然适用。
说到底,因为 NAT 的存在,我们只能从内网主动发起连接,否则 NAT 设备不会记录相应的映射关系,没有映射关系也就不能转发数据。
所以我们就在公网上加一台服务器 x,并暴露一个访问域名,再让内网的服务主动连接服务器 x,这样 NAT 路由器上就有对应的映射关系。接着,所有人都去访问服务器 x,服务器 x 将数据转发给内网机器,再原路返回响应,这样数据就都通了。这就是所谓的内网穿透。
像上面提到的服务器 x,你也不需要自己去搭,已经有很多现成的方案,花钱就完事了,比如花某壳。

到这里,我们就可以回答文章标题的问题。
为什么我在公司里访问不了家里的电脑?
那是因为家里的电脑在局域网内,局域网和广域网之间有个 NAT 路由器。由于 NAT 路由器的存在,外网服务无法主动连通局域网内的电脑。
两个内网的聊天软件如何建立通讯
好了,问题就叒来了。
我家机子是在我们小区的局域网里,班花家的机子也是在她们小区的局域网里。都在局域网里,且 NAT 只能从内网连到外网,那我电脑上登录的 QQ 是怎么和班花电脑里的 QQ 连上的呢?

上面这个问法其实是存在个误解,误以为两个 qq 客户端应用是直接建立连接的。
然而实际上并不是,两个 qq 客户端之间还隔了一个服务器。

也就是说,两个在内网的客户端登录 qq 时都会主动向公网的聊天服务器建立连接,这时两方的 NAT 路由器中都会记录有相应的映射关系。当在其中一个 qq 上发送消息时,数据会先到服务器,再通过服务器转发到另外一个客户端上。反过来也一样,通过这个方式让两台内网的机子进行数据传输。
两个内网的应用如何直接建立连接
上面的情况,是两个客户端通过第三方服务器进行通讯,但有些场景就是要抛开第三端,直接进行两端通信,比如 P2P 下载,这种该怎么办呢?
这种情况下,其实也还是离不开第三方服务器的帮助。
假设还是 A 和 B 两个局域网内的机子,A 内网对应的 NAT 设备叫NAT_A
,B 内网里的 NAT 设备叫NAT_B
,和一个第三方服务器server
。
流程如下。
step1 和 2: A 主动去连 server,此时 A 对应的NAT_A
就会留下 A 的内网地址和外网地址的映射关系,server 也拿到了 A 对应的外网 IP 地址和端口。
step3 和 4: B 的操作和 A 一样,主动连第三方 server,NAT_B
内留下 B 的内网地址和外网地址的映射关系,然后 server 也拿到了 B 对应的外网 IP 地址和端口。
step5 和 step6 以及 step7: 重点来了。此时 server 发消息给 A,让 A 主动发UDP
消息到 B 的外网 IP 地址和端口。此时 NAT_B 收到这个 A 的 UDP 数据包时,这时候根据 NAT_B 的设置不同,导致这时候有可能NAT_B 能直接转发数据到 B,那此时 A 和 B 就通了。但也有可能不通,直接丢包,不过丢包没关系,这个操作的目的是给 NAT_A 上留下有关 B 的映射关系。
step8 和 step9 以及 step10: 跟 step5 一样熟悉的配方,此时 server 再发消息给 B,让 B 主动发UDP
消息到 A 的外网 IP 地址和端口。NAT_B 上也留下了关于 A 到映射关系,这时候由于之前 NAT_A 上有过关于 B 的映射关系,此时 NAT_A 就能正常接受 B 的数据包,并将其转发给 A。到这里 A 和 B 就能正常进行数据通信了。这就是所谓的NAT 打洞。
step11: 注意,之前我们都是用的UDP 数据包,目的只是为了在两个局域网的 NAT 上打个洞出来,实际上大部分应用用的都是 TCP 连接,所以,这时候我们还需要在 A 主动向 B 发起 TCP 连接。到此,我们就完成了两端之间的通信。

这里估计大家会有疑惑。
端口已经被 udp 用过了,TCP 再用,那岂不是端口重复占用(address already in use)?
其实并不会,端口重复占用的报错常见于两个 TCP 连接在不使用SO_REUSEADDR
的情况下,重复使用了某个 IP 端口。而 UDP 和 TCP 之间却不会报这个错。之所以会有这个错,主要是因为在一个 linux 内核中,内核收到网络数据时,会通过五元组(传输协议,源IP,目的IP,源端口,目的端口
)去唯一确定数据接受者。当五元组都一模一样的时候,内核就不知道该把数据发给谁。而 UDP 和 TCP 之间"传输协议"不同,因此五元组也不同,所以也就不会有上面的问题。
NAPT 还分为好多种类型,上面的 nat 打洞方案,都能成功吗?
关于 NAPT,确实还细分为好几种类型,比如完全锥形 NAT 和限制型 NAT 啥的,但这并不是本文的重点。所以我就略过了。我们现在常见的都是锥形 NAT。上面的打洞方案适用于大部分场景,这其中包括限制最多的端口受限锥形 NAT。

总结
- IPV4 地址有限,但通过 NAT 路由器,可以使得整个内网 N 多台机器,对外只使用一个公网 IP,大大节省了 IP 资源。
- 内网机子主动连接公网 IP,中间的 NAT 会将内网机子的内网 IP 转换为公网 IP,从而实现内网和外网的数据交互。
- 普通的 NAT 技术,只会修改网络包中的发送端和接收端 IP 地址,当内网设备较多时,将有可能导致冲突。因此一般都会使用 NAPT 技术,同时修改发送端和接收端的IP 地址和端口。
- 由于 NAT 的存在,公网 IP 是无法访问内网服务的,但通过内网穿透技术,就可以让公网 IP 访问内网服务。一波操作下来,就可以在公司的网络里访问家里的电脑。
最后留个问题,有了 NAT 之后,原本并不富裕的 IPv4 地址突然就变得非常够用了。
那我们为什么还需要 IPv6?
另外 IPv6 号称地址多到每粒沙子都能拥有自己的 IP 地址,那我们还需要 NAT 吗?
最后
最近原创更文的阅读量稳步下跌,思前想后,夜里辗转反侧。
我有个不成熟的请求。

离开广东好长时间了,好久没人叫我靓仔了。
大家可以在评论区里,叫我一靓仔吗?
最近评论区里叫我 diao 毛的兄弟越来越多了。
so emo. 哪有什么 diao 毛,在你面前的,不过是一个漂泊在外,思念故乡的可怜打工人而已。
所以。
我这么善良质朴的愿望,能被满足吗?