3、IPv6 的 web 服务
复用 2 的架构,在服务器端部署一个 web 服务,在客户端访问该 web 服务。web 服务没有选择像 apache 或者 nginx 这样的庞然大物,而选择了很轻量的 boa。原因是 boa 虽然原始支持 IPv6,但是我想粗暴的把所有 IPv4 的 socket 套接字都替换成 IPv6 版本,尝试做一个自定义的升级。结果需要改动的代码非常少,不超过 20 行,boa 就能完全支持 IPv6。
配合实验,写了一个简单的 CGI,只是在版面 echo 字符串。如下图:
图 20 浏览器使用 IPv6 地址访问网络资源
这里值得注意的是,在浏览器中使用 IPv6 的地址访问 web 资源,IPv6 的地址必须要使用中括号“[]”包起来。
图 21 IPv6 下的 http 报文
从 Server 端抓包看,IPv6 下的 Web 服务 http 报文,除了网络层 L3 的报文头部不一样之外,其余的都和 IPv4 版本的没有太大差别差别。
4、IPv6 的过渡技术实验
这部分将在过渡技术介绍中一起实验。
IPv6 的过渡技术
IPv6 的提出,最重要的目的就是解决公网 IPv4 耗尽的问题,而且 IPv6 协议的设计就考虑到了更加好的效率、安全、扩展等方面,可以那么说,IPv6 是未来网络发展的大趋势。但为什么 IPv6 已经发展了十几年了,目前在我们的工作和生活中还是比较少接触和使用。这里的原因是非常的复杂,有技术上障碍,因为 IPv6 和 IPv4 是两个完全不兼容的协议(在极少数的特定场景可以实现兼容),如果要从支持 IPv4 升级到 IPv6,无论是应用程序用客户端、服务器程序端、路由器等等,都要同时支持 IPv6 才能解决问题,这个的升级改造需要花费的成本是巨大的。而且,正是由于技术上的升级花费大量的人力物力,无论是运营商还是互联网服务商,一方面要重视用户的体验问题,这个肯定不能强制客户更新换代硬件设备和软件,另一方面也要维护自身的投资和利益,更愿意去选择利用现有技术降低 IPv4 地址耗尽带来的压力,例如 NAT 的广泛应用,就是 IPv6 推广使用的一个重要的“障碍”。
由上所述,IPv4 升级到 IPv6 肯定不会是一蹴而就的,是需要经历一个十分漫长的过渡阶段(用我厂通用的术语说,就是 IPv4 升级 IPv6 这个灰度的时间非常长),要数十年的时间都不为过。现阶段,就出现了 IPv4 慢慢过渡到 IPv6 的技术(或者叫过渡时期的技术)。过渡技术要解决最重要的问题就是,如何利用现在大规模的 IPv4 网络进行 IPv6 的通信。
要解决上面的问题,这里主要介绍 3 种过渡技术:
1、双栈技术
2、隧道技术
3、转换技术(有一些文献叫做翻译技术)
本章节会对以上的过渡技术,选取几个典型的、我们未来最有机会接触到的具体的过渡技术结合实验观察过渡技术的具体实现和数据包的表现形式。
双栈技术
这种技术其实很好理解,就是通信节点同时支持 IPv4 和 IPv6 双栈。例如在同一个交换机下面有 2 个 Linux 的节点,2 个节点都是 IPv4/IPv6 双栈,节点间原来使用 IPv4 上的 UDP 协议通信传输,现在需要升级为 IPv6 上的 UDP 传输。由于 2 个节点都支持 IPv6,那只要修改应用程序为 IPv6 的 socket 通信基本达到目的了。
上面的例子在局域网通信的改造是很容易的。但是在广域网,问题就变得十分复杂了。因为主要问题是在广域网上的 2 个节点间往往经过多个路由器,按照双栈技术的部署要求,之间的所有节点都要支持 IPv4/IPv6 双栈,并且都要配置了 IPv4 的公网 IP 才能正常工作,这里就无法解决 IPv4 公网地址匮乏的问题。因此,双栈技术一般不会直接部署到网络中,而是配合其他过渡技术一起使用,例如在隧道技术中,在隧道的边界路由器就是双栈的,其他参与通信的节点不要求是双栈的。
隧道技术
当前的网络是以 IPv4 为主,因此尽可能地充分利用 IPv4 网络进行 IPv6 通信是十分好的手段之一。隧道技术就是这样子的一种过渡技术。
隧道将 IPv6 的数据报文封装在 IPv4 的报文头部后面(IPv6 的数据报文是 IPv4 的载荷部分),IPv6 通信节点之间传输的 IPv6 数据包就可以穿越 IPv4 网络进行传输。隧道技术的一个很重要的优点是透明性,通过隧道进行通信的两个 IPv6 节点(或者节点上的应用程序)几乎感觉不到隧道的存在。
图 22 IPv6 典型的隧道
上图是一种典型的隧道技术:路由器-路由器隧道,两个 IPv6 网络中的主机通过隧道方式穿越了 IPv4 进行通信。其中 C 节点和 D 节点被称为边界路由器,边界路由器必须要支持 IPv4-IPv6 双栈。当 IPv6 网络 1 的主机 A 将 IPv6 数据包发给边界路由器 C,C 对 IPv6 数据包进行 IPv4 封装,然后在 IPv4 网络上进行传输,发送到边界路由器 D,D 收到 IPv4 的数据包后剥掉 IPv4 的包头,还原 IPv6 的数据包,发送到 IPv6 网络 2 的主机 B。
根据隧道的出口入口的构成,隧道可以分为路由器-路由器,主机-路由器隧道、路由器-主机、主机-主机隧道等类型。
隧道的类型也分为手动配置类型和自动配置类型两种,手动配置是指点对点的隧道是手动加以配置,例如手动配置点对点隧道外层的 IPv4 地址才能建立起隧道;自动配置是指隧道的建立和卸载是动态的,一般会把隧道外层的 IPv4 地址内嵌到数据包的目的 IPv6 地址里面,在隧道路由器获取该 IPv6 地址时候取出内嵌 IPv4 地址从而使用该 IPv4 地址作为隧道的对端来建立隧道。
下面就介绍几种我们很可能会接触到的具体的隧道技术。
在介绍具体的隧道技术前,特别要说明一下,Linux 内核原生支持一种叫做 sit(Simple Internet Transition)隧道。这个隧道专门用于 IPv6-in-IPv4 的数据封装解封和传输,应用十分之广泛,现在很多主流的 IPv6 隧道技术都能基于 sit 隧道实现。关于 sit 隧道的技术实现,可以查阅 Linux 内核源码 net/ipv6/sit.c 。
1、6to4 隧道
6to4 是当前使用得比较广泛的一种自动配置隧道技术,这种技术采用特殊的 IPv6 地址,称为 6to4 地址,这种地址是以 2002 开头,接着后面的 32 位就是内嵌的隧道对端的 IPv4 地址。当边界路由器收到这类目的地址,取出 IPv4 地址建立隧道。
6to4 隧道一般用在路由器-路由器、主机-路由器、路由器-主机场景,典型的应用场景是两个 IPv6 的站点内主机通过 6to4 隧道进行相互访问。
6to4 隧道的一个限制是内嵌的 IPv4 地址必须是公网地址。
6to4 隧道实验
如下图,就是本次 6to4 实验中使用的隧道架构,该架构是典型的路由器-路由器隧道,隧道两侧的 IPv6 网络对隧道的存在无感知。
图 23 6to4 路由器-路由器隧道
在 Linux 下的 sit 隧道可以自适应为 6to4 隧道。
图 24 Linux 下配置 sit 隧道(6to4)
上图就是在路由器上配置 sit 隧道的命令,因为是使用 6to4 隧道,隧道的目的端点地址是从目的地址中获取,因此只需要配置本地端点即可。
图 25 浏览器通过隧道访问 web 服务
配置完隧道后,使用客户端访问 web 服务,可以正常访问。
图 26 web 服务器端抓取 http 报文
在 web 服务端抓取 http 报文,可以看到,web 服务获取到就是一个普通的 http 请问报文。
图 27 隧道内抓取 http 报文
在隧道内抓取 http 报文,可以看到里面的乾坤。这个不是一般的 http 报文,它比服务端抓取到的多了一层 IPv4 报文头部,是隧道的外出通信协议,隧道内层 IPv6 才是真正的数据。IPv4 报文头部中的协议字段,不是我们熟悉的 TCP(6)/UDP(17)协议,而是 IPv6-in-IPv4 专属的隧道协议类型。
可以看到,经过隧道的数据报文,在隧道两端的边界路由器分别完成了隧道协议的封包和解包,在真正获取到数据的节点看来,几乎不感知隧道的存在。
2、ISATAP 隧道
ISATAP 全称是站点内自动隧道寻址协议(Intra-Site Automatic Tunnel Addressing Protocol),用来为 IPv4 网络中的 IPv6 双栈节点可以跨越 IPv4 网络访问外部的 IPv6 节点。
ISATAP 隧道一般用于主机-主机、主机-路由器的场景。
ISATAP 隧道实验
如下图就是本次实验使用的架构,是一种典型的主机-路由器场景。实验中需要在路由器 2 上部署 radvd 服务,用于客户端进行无状态自动配置地址。Linux 下的 ISATAP 隧道也是可以使用 sit 隧道实现。
图 28 ISATAP 主机-路由器隧道
图 29 Windows 下配置 ISATAP 隧道
实验用的客户端使用 windows 7,原生支持 ISATAP 隧道,如上图,需要进入 netsh 开启并且设置 ISATAP 的路由器地址(支持域名)。
图 30 ISATAP 隧道中的无状态自动配置
当客户端设置完 router 后,隧道已经建立,客户端便发起了无状态自动配置流程,可以看到上面的截图路由器通过隧道将前缀信息下发给客户端,客户端完成无状态自动配置,获取到公网 IP 地址。
图 31 ISATAP 隧道接口地址
在 windows 7 上查看 ISATAP 接口,获取到公网地址。这个地址类型是 ISATAP 专用的地址结构,由 64 位全球单播路由前缀:200(0):5e5f:w.x.y.z 组成(w.x.y.z 是客户端的 IPv4 地址)。
图 32 使用 ISATAP 隧道访问 web 服务
如上图,使用 ISATAP 隧道访问 web 服务,在隧道内的数据抓包,可以看到和 6to4 的类似,这里就不再深入阐述。
3、Teredo 隧道
前面的隧道技术,主要是在 IPv4 的数据报文承载着 IPv6 的数据报文,这是一种特殊的数据包格式(IPV6-in-IPv4),不同于我们熟悉的 TCP、UDP 等传输层协议。而我们平常接触到的网络都存在于 NAT 架构中(例如我们的办公网络和家庭网络),在这种网络架构中,路由器仅对于 TCP、UDP 等传输层协议做 NAT 处理,而无法正确处理 IPv6-in-IPv4 这种报文,例如使用 ISATAP 隧道,IPv6 双栈节点与 ISATAP 路由器之前如果存在 NAT,ISATAP 建立隧道失败;6to4 隧道也会遇到同样的问题。
Teredo 隧道是有微软公司主导的一项隧道技术,主要用于在 NAT 网络架构下建立穿越 NAT 的隧道。
Teredo 隧道的核心思路,是将 IPv6 的数据封装成 IPv4 的 UDP 数据包,利用 NAT 对 IPv4 的 UDP 支持进行穿越 NAT 的传输,当 UDP 包到达隧道的另外一端后,再把 IPv4 的包头、UDP 包头剥离,还原 IPv6 的数据包,再进行下一步的 IPv6 数据通信转发。Teredo 节点会分配一个以 2001::/32 的前缀,而且地址中还包含 Teredo 的服务器、标志位和客户端外部映射模糊地址和端口号等信息。
Teredo 的实现还会遇到 NAT 的类型不同而被限制的问题。NAT 的类型有锥形 NAT、受限制的 NAT、对称 NAT 几种,Teredo 只能在锥形 NAT 和受限制的 NAT 的环境下正常工作,而且在这两种 NAT 需要处理的逻辑又是不一样的。因此 Teredo 整体的实现会比较复杂。
实验环境搭建:
在 Linux 平台下有开源的 Teredo 实现版本:miredo。由于时间和文章篇幅的原因,而且部署 miredo 比较复杂,因此这里的实验等以后有机会再补充。:(
转换技术(有一些文献叫做:翻译技术)
隧道技术是比较好地解决了在很长期一段时间内还是 IPv4 网络是主流的情况下 IPv6 节点(或者双栈节点)间的通信问题。但是由于 IPv4 到 IPv6 的过渡是十分漫长的,因此也需要解决 IPv6 节点与 IPv4 节点通信的问题。协议转换技术可以用来解决这个问题。
协议转换技术根据协议在网络中位置的不同,分为网络层协议转换、传输层协议转换和应用层协议转换等。协议转换技术的核心思路就是在 IPv4 和 IPv6 通信节点之间部署中间层,将 IPv4 和 IPv6 相互映射转换。
我们非常熟悉的 NAT 也是一种典型的协议转换技术,是将私网 IPv4 地址映射转换为公网 IPv4 地址,这种转换技术又称为 NAT44。而我们接着要重点介绍的名为 NAT64/DNS64 的协议转换技术。
NAT64/DNS64
提到 NAT64/DNS64,相信做 iOS 客户端开发的同学一定非常熟悉。在 2016 年中开始,苹果要求 app 必须支持 IPv6 网络。而苹果官方提供的过渡解决方案正是 NAT64/DNS64。
以下是苹果提供的技术图:
图 33 苹果提供的过渡技术解决方案
NAT64/DNS64 分为 NAT64、DNS64 两大方面,两者需要结合使用。
DNS64 在 RFC6147 中明确定义,将 IPv6 的地址记录 AAAA DNS 查询消息转换为 IPv4 的地址记录查询。当 IPv6 节点发起 DNS 请求,NAT64/DNS64 中间层同时发起 A 域名查询和 AAAA 域名查询。如果仅有 A 域名查询的 IPv4 地址响应,表明 IPv6 节点需要访问一个 IPv4 的节点,NAT64/DNS64 中间层将回应的 IPv4 地址转换为 IPv6 地址,返回给 IPv6 节点。
IPv6 节点使用获取到的 IPv6 服务端地址进行访问,数据包会经过 NAT64/DNS64 中间层,中间层将 IPv6 地址映射转换为 IPv4 的地址进行访问。
实验环境搭建:
Linux 平台下有多个 NAT64 的开源软件,实现方式各有不同,有纯内核态实现的 ecdysis,也有用户态实现的 tayga。
DNS64 的实现可以使用著名的开源 DNS 服务 BIND 就可以很好地支持,详细可以查看上面 2 个开源软件的搭建说明。
时间的原因,还没有把 NAT64/DNS64 的开源软件研究透彻,因此这里的实践等以后有机会再补上。
PS:在研究 tayga 和 miredo 源码的时候,发现了在 Linux 平台上面有一些有趣的东西,如下图,是 tayga 的软件实现框架。
图 34 Linux 下的一个有趣的虚拟设备
Linux 内核自带了一个软件虚拟设备,也是一种隧道的实现(/dev/net/tun),该设备可以实现将内核态的网络数据发送到用户态,用户态修改后再返回给内核态,用户态的进程负责完成 NAT64 这一次“偷龙转凤”操作。
关于/dev/net/tun 设备的实现,可以查阅 Linux 内核源码 drivers/net/tun.c,一些著名的 VPN 软件例如 openvpn 等,都是以它作为实现基础。
本章只介绍了一些典型的过渡技术,其实过渡技术种类还有很多,有一些在实验室阶段,有一些已经商用,有一些已经被废弃,但是总的来说,每一种过渡技术都是在解决特定时期特定场景下的过渡问题。
IPv6 编程应该注意的问题
在《IPv6 Socket 编程》一文中,ray 已经很详细介绍了 IPv6 下的 socket 编程细节和应该注意的问题。本章作为一个补充,介绍一下 IPv6 socket 编程可能还会遇到的问题。
1、IPv6 地址编码
IPv4 地址本质是一个 32 位整数,因此一般无论是存储层还是逻辑层,都经常将点分制的 IPv4 字符串地址转为 32 位整数使用。而在 IPv6,情况就复杂多了(可能也有同学就想到,光是原子性就很难保证了)。
举一个典型的例子,现在有个需求,分别统计每个 IP 的访问频次。
在 IPv4 的情况下,最简单就是 STL 用 std::map 搞定(单线程),土豪一点的可以开个 16G 的数组用空间换时间。
但是在 IPv6 的场景下,那就尴尬了,IPv6 可是个 128 位整数,可以用 map 吗?可能会有人直接将原始的字符串类型的 IPv6 地址作为 key 来累计。一旦那么用,就要十分注意了。由于 IPv6 是支持前导 0 和连续 0 的压缩表示方式,而且支持英文字母大小写,例如:
2001:db8:4::41
2001:db8:4:0:0:0:0:0:41
2001:0db8:4::41
2001:DB8:4::41
这 4 个都是合法的 IPv6 地址,如果将输入毫无修改地作为 key 来累计,那必须会将累计逻辑分散了,最终得不到正确的频率结果。类似的问题也在 MAC 地址(BSSID)上面,由于 MAC 地址分号间的数字前导 0 可以省略,并且也是支持大小写英文字母,所以也是会同样的问题。在微信安全中心,MAC 地址的逻辑统一转为 64 位整数处理,情况相对还好。
但是到了 IPv6 有木有更好的解决办法呢?答案是肯定的,但是需要具体问题具体分析。
在上面的频率例子比较优雅的做法,依然用 map 的话,可以利用自定义 key 类型解决,这个方法需要重载自定义类型的比较符号’<’:
图 35 自定义 IPv6 地址结构
其中 struct in6_addr 就是一个 128 位的 IPv6 地址结构体。
图 36 使用 std::map 实现 IPv6 频率
其实还有更优雅的方式,直接将 IPv6 的地址强制转为 2 个 64 位整数来比较,if else 会写得更少一些,效率更高一些。
上面说到 2 个 64 位整数,微信安全中心有一些静态的 key-value 数据查询(批量写,多次读),其中 key 是 MD5,我们将 MD5 也是作为 2 个 64 位整数来对待,将 2 个 64 位整数联合排序,写入内存,然后使用两次二分查找的方式搜索,效率非常高。在这种场景下面,IPv6 也是可以用类似的方法处理。
IPv6 地址结构,以后很可能会给我们的编程或多或少带来一些“未知”的坑-_-||。
2、IPv6 socket“兼容”IPv4 的情况
在 IPv4 和 IPv6 共存的一个很长的时间里,在 socket 编程上不得不面对的就是 IPv6 和 IPv4 一定程度的“兼容问题”。而在文章前面有提到,IPv6 和 IPv4 和完全不兼容的两种协议,但是 IPv6 协议的地址空间更大,是可以使用 IPv6 的地址表示 IPv4 地址,例如 IPv4 映射地址,因此,在很特殊的情况下,IPv4 和 IPv6 可以实现“兼容”,但是这种兼容是很有限的。在 Linux 平台下,这种“兼容性”是如何表现的,我们这里来分析一下。
在 Linux 下面,以 IPv6 下的 UDP Socket 举例:
有个 UDP 协议的 Server 改造 IPv6,该 Server 机器上有一个网卡并且同时配置 IPv6 和 IPv4 地址,支持双栈。Server 进程创建 IPv6 UDP socket 套接字,绑定 Server 本地任意地址(IPv4 和 IPv6 都是以全 0 地址为绑定任意地址)。客户端是 IPv4,向这个 Server 发送 UDP 请求数据包。
图 37 IPv6 服务收到 IPv4 报文
可以看到的是,IPv6 的 socket 会正常收到客户端的数据报文,并且会将 IPv4 地址转化为映射地址,为了明确这个逻辑,我们分析 Linux 内核的实现。
图 38 IPv6 下 UDP socket 收到 IPv4 数据包内核实现
IPv6 的 socket 收到数据包,如果是 IPv4 协议,则将来源 IPv4 的 IP 地址转为 IPv6 的 IPv4 映射地址。与实验的结果很一致。
如果 Server 的 IPv6 socket 按照这个来源地址返回数据包,那么内核又是如何处理的呢?
图 39 IPv6 下 UDP socket 发送 IPv4 数据包内核实现
首先内核会判断目的地址是否为 IPv6 的 IPv4 映射地址,如果是映射地址,那么要发送的数据是 IPv4 数据,直接以 IPv4 协议栈的形式发送该数据(udp_sendmsg 是 IPv4 udp 发送接口)。
可以看到,Linux 内核本身对这类双栈上的改造做了一定的适配,我们可以根据内核的这种特性去进行改造工作。
3、使用链路本地地址
从前面的章节可以知道,IPv6 具有自动配置地址的能力。链路本地地址是 IPv6 要求在每个接口默认自动配置生成的地址,用于链路上的通信,路由器不能转发链路本地地址。除了以上提到的特征外,链路本地地址就是一个普通的 IPv6 地址,我们可以使用这类地址做 socket 编程通信。
但是我们在 IPv6 Socket 编程的时候使用链路本地地址,有一个细节需要注意。
图 40 IPv6 地址结构
在 IPv6 地址结构中(对应于 IPv4 的 struct sockaddr_in),有一个我们非常陌生的字段 scope_id,这个字段在我们使用链路本地地址来编程的时候是必须要使用的,这个字段表示我们需要选择接口 ID。为什么需要需要有这么一个字段,那是因为链路本地地址的特殊性,一个网络节点可以有多个网络接口,多个网络接口可以有相同的链路本地地址,例如我们需要 bind 一个本地链路地址,这个时候就会有冲突,操作系统无法决策需要绑定的是哪个接口的本地链路地址。
又例如,如果我们在直连的 2 个主机之间直接用链路本地地址 ping 的话,会 ping 失败。
因此 IPv6 引入了 scope_id 来解决这个问题,scope_id 指定了使用哪个网络接口。
如何查看这个网络接口(网卡)的 scope_id 是多少?
一、在 Linux 下查看网络接口的 scope_id:
图 41 Linux 下查看网络接口 scope id
使用 ip addr 命令可以查看每个接口的 scope_id,如图第一列的数字就是 scope_id。
二、在 windows 下查看 scope_id:
图 42 Windows 下查看网路接口 scope id
最后的百分号 %后面的数字就是该网络接口的 scope_id。
Windows 下也可以使用 route print -6 查看接口列表,列表第一列数字就是 scope_id。
因此,在使用链路本地地址编程的时候,需要把这个 scope_id 赋值到 sin6_scope_id 字段。
而在使用 ping 命令的时候,需要在地址后面加上 %和 scope_id 才能 ping 成功,如图:
图 43 使用链路本地地址 ping
关于这个 scope id,详细可以查看 RFC2553
总结
本文主要科普介绍了 IPv6 的基本内容,配合各种实验分析比较清晰认识了 IPv6 的各种基本概念;也介绍一些“超纲”的内容(我们的工作中很可能不会接触到),但是我觉得这类内容在技术实现上十分有趣,可以在一些技术的方法和思路上面可能会给我们一些通用的启示,例如 NAT64/DNS64 就是使用中间层来处理 IPv4 和 IPv6 互通的问题,我们的工作中也确实经常遇到类似的技术问题。
IPv6 本身是一个很庞大的体系,还有很多高级内容没有介绍(IPv6-IPSec、移动 IPv6 等等)。而且查看和 IPv6 相关的 RFC,不断在做修正,Linux 内核的 IPv6 模块代码也不断有配合新的 RFC 修改来做调整,引入新的逻辑,以适应各种场景的实际需求。有兴趣的同学可以一直留意 RFC 的变化和紧跟 Linux 内核的版本发布。
本文是我在结合各种文献和实验对 IPv6 理解的一个总结归纳,难免会有理解偏差和手抖的地方,希望各位同学熟悉的话能帮忙指出其中的错误,并且提供修改建议和意见,谢谢:)。
本文转载自公众号云加社区(ID:QcloudCommunity)。
原文链接:
https://mp.weixin.qq.com/s/XaTijoNnUBmWNwUZwm8eGQ
评论