如何使用 NAT64 在 IPv4 网络中处理 IPv6 流量
导语:随着联网设备数量的不断增加,对互联网协议 (IP) 地址的需求已经超过了互联网协议版本 4 (IPv4) 地址的供应,导致采用互联网协议版本 6 (IPv6) 来减少加载 IPv4 地址。
随着联网设备数量的不断增加,对互联网协议 (IP) 地址的需求已经超过了互联网协议版本 4 (IPv4) 地址的供应,导致采用互联网协议版本 6 (IPv6) 来减少加载 IPv4 地址。
在您的虚拟专用网络 (VPN) 服务中使用 IPv6 可以帮助您实现更好的安全性、支持更多功能并访问更大的地址空间。该协议可以让您的解决方案面向未来,使其能够在特定的 5G 网络中运行,并支持支持 IPv6 的企业和专用网络。
在本文中,我们在解释了 IPv4 和 IPv6 协议之间的差异后展示了如何将 IPv6 支持添加到应用程序 VPN。在我们的示例中,即使我们无法直接访问 IPv6 网络,我们也会通过网络地址转换 64 (NAT64) 添加 IPv6 支持,并解释 NAT64 在 IPv6 中的作用。您可以在可能无法对网络进行细粒度控制的受限环境中使用我们在此处介绍的方法。受限环境是指只有 IPv6 或 IPv4 网络可用的环境。在这样的环境中,不可能到达存在于不受支持的地址空间中的某些目标服务器。
虚拟专用网络简介
VPN 技术允许多台计算机通过软件定义的虚拟网络在互联网上安全、私密地连接。这些虚拟网络的创建独立于底层物理网络基础设施的物理拓扑。您可以通过以下步骤实现此目的:
通过物理网络打包和中继 VPN 数据包的虚拟网络接口之间的隧道流量
将整个过程抽象为 VPN 客户端
这是一个简单的 VPN 设置示例:
基本的 VPN 设置
在此设置中,如果客户端设备 1 想要向客户端设备 2 发送数据,则会发生以下情况:
客户端设备 1 可以使用 10.0.0.2 地址通过其 VPN 接口向 VPN 服务器发送数据包。
接口查询其配置信息并确定数据包的下一个目的地。当接口必须将数据包发送到另一台物理主机时,作为 VPN 服务器的网络适配器的物理接口将连同标头一起传输整个数据包。
这个新数据包包含物理网络的路由信息。
目标主机收到新数据包,解包原来的 VPN 数据包,并以同样的方式继续路由。
这是包装后的数据包的样子:
包裹的 VPN 数据包
请注意,VPN 接口的软件实现生成物理接口的数据包,允许它在 VPN 数据包被路由之前执行其他操作。例如,物理接口的数据包可以加密整个有效载荷,这样物理主机就无法访问嵌套的 VPN 数据包,这是一个封装在另一个 VPN 数据包中的数据包。
这种在路由数据包之前嵌套数据包的想法也可以应用于常规数据包。以下是这个想法在这种情况下的工作方式:
VPN 服务要求操作系统通过其虚拟接口路由数据包。
VPN 接口根据其配置文件路由数据包。
例如,VPN 接口可以将数据包发送到 VPN 服务器,VPN 服务器解压缩到达的数据包,将它们代理到原始目的地,然后将响应返回给 VPN 客户端。
大多数人在考虑 VPN 的工作原理时都会想到这种情况。虚拟专用网络允许对客户端的出站流量进行加密和代理,以提供额外的安全级别并向客户端的互联网服务提供商 (ISP) 隐藏信息。
VPN 是在通信协议、加密和身份验证的帮助下实现的,这些协议有助于在 Internet 上安全地加密和传输数据。在下一节中,我们将讨论哪些通信协议对于实施 VPN 解决方案至关重要。
IPv4 和 IPv6 概述及其与 VPN 的连接
大多数 VPN 实施在开放系统互连模型的网络层上运行。根据这个模型,VPN 实现处理 IP 数据包并处理它们的路由。这需要 VPN 网络接口背后的软件来实现 Internet 协议,也可能需要一些传输层协议。
网络协议是一组规则,描述数据的结构以及对等方应如何处理它。互联网协议是一种特定的网络协议,可以使互联网上的设备之间进行通信。使用 VPN 时,您通常需要使用多种协议,例如 Internet 协议或传输控制协议 (TCP),这些协议有助于通过 Internet 在设备之间进行安全通信。VPN 中使用 IPv4 和 IPv6 在设备之间传输数据。此外,已实现的 TCP 可以根据从网络接收到的原始字节重建 TCP 数据包,并创建符合 TCP 规则的新 TCP 数据包。
实现一个网络通信协议通常包括以下步骤:
编写用于创建和解析数据包的函数
实现一个状态机,它根据处理过的数据包的内容而改变
传送数据包的方法不是协议的一部分,可以在协议实现过程之外进行处理。
现在,让我们仔细看看两个特定的协议:IPv4 和 IPv6。这些是主要的互联网协议,其中 IPv4 是最常用的,而 IPv6 是最新的。
IPv6 与 IPv4:有何区别?
IPv4是目前世界上使用最广泛的协议,尽管它不是 Internet 协议的最新版本。IPv4 地址是 32 位数字,以十进制表示法表示为由点分隔的四组数字;例如,192.168.0.1。
IPv4 最多支持大约 43 亿个唯一地址,因为地址字段只有 4 个字节(或 32 位)长。IPv6使用 128 位地址并提供更大的地址空间。这是 IPv6 相对于 IPv4 的主要优势。由于连接互联网的设备数量早已超过 40 亿大关,IPv4 的地址空间已经完全耗尽。在 IPv6 网络中,可能的地址数量为 2^128,或大约 340 六十亿,大约是 43 亿的 79 万亿倍。通过 IPv4 网络传输 IPv6 流量还有几个重要的好处:
IPv6 与 IPv4 相比的优势
基本 IPv6 标头仅包含协议运行的最重要信息。如果对等方需要在标头中携带额外信息,他们可以将各种可选标头链接在一起。这种方法减少了协议最常见用例的开销,例如从 A 向 B 发送数据包。
IPv4 与 IPv6 标头
现在您已经知道切换到 IPv6 协议的主要好处,让我们来看看如何在 IPv4 基础设施上路由 IPv6 流量。
使用 IPv6 提高 VPN 安全性
要介绍任何协议,您需要阅读文档并实现状态机和处理特定于所选协议的数据包的功能。但在此步骤中,您可能还会遇到一些问题。让我们看一下在 VPN 服务中实现 IPv6 支持的标准机制。
当您允许来自 IPv4 的 IPv6 流量时,您可以实现以下目标:
允许客户端应用访问 IPv6 网络上的服务器
支持纯 IPv6 环境中的网络
实施 IPv6 协议的过程很简单。VPN 服务从其由操作系统管理的虚拟网络接口获取所有客户端数据。此数据包括实际的协议标头,直到 VPN 服务必须处理的 IP 标头。VPN 服务还必须能够根据从虚拟网络接口接收到的信息构建响应数据包。
使用 IPv6 协议,处理数据包相当简单:
VPN 服务会存储原始标头,直到它从目标服务器获取响应。
VPN 服务通过交换源地址和目标地址并更新与负载相关的字段来重用标头来构造响应数据包。
新标头添加到响应数据之前,并写回虚拟接口供操作系统处理。
您还可以使用其他编程语言在您的应用程序中实现 VPN 服务,例如 C/C++、Java、Python 和 Rust。在本文中,我们探索了VPN 服务的Kotlin实现。当您需要实施每应用 VPN 时,Kotlin 有一些好处,它允许您为每个应用创建单独的 VPN 连接以隔离网络流量:
假设我们的 VPN 服务可以直接访问虚拟网络接口的文件描述符。该服务通过多个套接字转发数据包的有效负载,将数据包代理到外部世界。套接字本身和相关的元数据存储在会话抽象中。然后,数据包由SessionHandler类处理。
以下是SessionHandler类在处理数据包时所做的事情:
解析数据包
根据存储在相应会话中的信息决定如何处理它们
转发数据包的内容
处理响应
在将响应放回网络接口之前为客户端重新打包响应
由于虚拟网络接口由文件描述符表示,因此从中接收数据包就像从常规文件中读取数据一样容易:
原始字节很难处理,尤其是当您需要将它们解释和操作为复杂的数据结构(如协议标头)时。在 Kotlin 中,可以创建可以解释原始字节并提供用于更改标头字段的简单接口的精简包装器。此类包装器提供与标头中每个字段相对应的函数,提供对它们的轻松读写访问。
您还可以将所有这些函数转换为具有自定义 getter 和 setter 的字段。在这种情况下,使用包装器的客户端代码看起来就像在操作常规数据类。IP 标头的包装器如下所示:
class IPWrapper(bytes: ByteArray) { // wrap the bytes into the ByteBuffer class for easier byte manipulation and extra functionality // be sure to account for the ByteBuffer's statefulness and specify indices explicitly when accessing // the bytes private val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) // the IP version is stored in the first 4 bits of the header var ipVersion // to get it, read the first byte and shift it by 4 bits to the right get() = (buffer.get(0).toInt() shr 4) // and to set it, shift the desired value to the left by 4 bits and perform the bitwise shift OR // on the first byte of the underlying byte array set(value) { buffer.put(0, ((value shl 4) or (buffer.get(0).toInt() and 0x0F)).toByte()) } // IPv4 and IPv6 headers contain different fields, so if client code attempts to access a field that // is not present in the underlying packet, throw an exception var headerLength get() = // check the ip version by calling the ipVersion member declared earlier if (ipVersion == 4) (buffer.get(0) and 0x0F) // IPv6 header does not have a field for the header length, so there is no value this getter can return else throw Exception("IPv6 doesn't have a Header Length field!") set(value) { // similarly, if the field is there, set it if (ipVersion == 4) buffer.put(0, ((value and 0x0F) or (buffer.get(0).toInt() and 0xF0)).toByte()) // if it's not, throw an exception else throw Exception("IPv6 doesn't have a Header Length field!") } // IP headers can be of different versions, and it's convenient to have a single wrapper class // for both IPv4 and IPv6 var srcIp get() = InetAddress.getByAddress( run { val (startPos, len) = if (ipVersion == 4) listOf(SOURCE_IP_POS_IPV4, ADDR_LEN_IPV4) else listOf(SOURCE_IP_POS_IPV6, ADDR_LEN_IPV6) // when getting the IP address, simply copy the bytes that represent it and pass the result // into Java's InetAddress.getByAddress function that will do the rest of the parsing buffer.array().copyOfRange(startPos, startPos + len) }) set(value) { value.address.copyInto( buffer.array(), if (ipVersion == 4) SOURCE_IP_POS_IPV4 else SOURCE_IP_POS_IPV6 ) } // do the same for the destination address var destIp get() = /* ... */ set(value) = /* ... */ // other fields can be implemented in a similar fashion /* ... */ // this wrapper can also have various convenience functions; for example, it can provide // means for easily getting the wrapped packet's headers to quickly create response headers fun copyHeaders() = /* ... */ // or handle the checksum computations for the IP and the nested transport headers fun updateChecksums() = /* ... */ }
一旦SessionHandler 类收到数据包,它就可以将数据包字节放入IPWrapper对象并使用 IPWrapper 类从 IP 标头访问它需要的任何信息。例如,在创建响应数据包时,SessionHandler类可以简单地复制标头并更新字段,而不是创建一个全新的标头:
您可以将生成的响应数据包写回 VPN 的网络接口:
一旦SessionHandler 类将数据包的字节放入IPWrapper 类,路由软件将解析 VPN 服务生成的 IP 标头并将数据包路由到其目的地。在这种情况下,目标是本地应用程序,其出站流量已通过操作系统的路由规则重定向到 VPN 的网络接口。
现在,让我们看看如果您只能访问 IPv4 网络,如何检查支持 IPv6 的 VPN。
使用 NAT64 测试 IPv6 实现
虽然实施协议相对简单,但测试才是真正挑战的开始。那么,NAT64、IPv4、IPv6是如何相互连接的呢?
IPv6 明显优于 IPv4,但支持 IPv6 的基础设施尚不存在。世界上许多 ISP 仍然不支持 IPv6,因此他们无法将 IPv6 转换为 IPv4,反之亦然。因此,他们的客户端无法访问任何使用 IPv6 的服务器。相反的情况也存在:有些网络仅使用 IPv6 运行,不处理 IPv4 数据包。
要解决这些不兼容问题,您可以使用以下转换机制之一:
IPv6 过渡机制
对于下面描述的方法,我们使用了NAT64——一种将所有 40 亿个 IPv4 地址映射到 IPv6 地址空间的保留块的转换机制。我们的客户特别要求使用 NAT64 在 IPv4 地址和 IPv6 地址之间进行转换。
让我们看看这种机制在实践中是如何工作的,以及 NAT64 为 IPv6 做了什么。假设连接使用不同 IP 版本的网络的路由器收到一个 IPv6 数据包,其目标地址来自 NAT64 地址范围。这是接下来发生的事情:
路由器从收到的 IPv6 数据包中删除 96 位长的 NAT64 前缀,留下 32 位的 IPv4 地址。
之后,路由器为数据包创建一个新的 IPv4 标头,以便它可以继续在网络中传输。
当路由器收到 IPv4 数据包并必须通过 IPv6 网络路由它时,也会发生同样的情况:
路由器通过添加 NAT64 前缀将 IPv4 地址转换为 IPv6 地址。
路由器为数据包重新创建 IP 标头,然后通过 IPv6 网络路由数据包。
您可以使用这些转换机制来测试 IPv6 实现,尤其是在 IPv6 网络不可用的地方。要查看您的 VPN 服务如何在纯 IPv4 环境中处理 IPv6 数据包,请在您服务的 VPN 接口上使用 NAT64 范围内的目标地址打开 IPv6 套接字:
套接字传输
如果您的 VPN 服务正常运行,它将接收这些数据包并像处理常规 IPv6 数据包一样处理它们。当这些数据包最终通过物理网络接口进行路由时,它们将到达一个路由器,该路由器会将它们转换为常规的 IPv4 数据包。然后,您的 VPN 服务将能够从目标服务接收响应数据,为客户端创建响应 IPv6 数据包,并通过其虚拟接口发送。
结论
虽然 IPv4 仍然更受欢迎,但 IPv6 为用户和开发人员提供了更多好处。在本文中,我们解释了为什么需要在您的应用程序中将 IPv4 转换为 IPv6,以及如何将对 NAT64 的支持添加到您的应用程序中。在您无法完全控制网络的受限环境中,也允许使用 NAT64 将 IPv6 地址映射到 IPv4 目标。
发表评论