浅谈NPTv6的实现
引入
最近有个需求,在之前的NAT
实现上,实现NPTv6
。这需求我连名字都没有听过啊,直接两眼一黑。但是后面查阅相关资料之后,发现其实也就是ipv6
的一个前缀转换,类似nat66
了吧。实现起来也相对简单了。
RFC
了解NPTv6
,直接看RFC
文档就知道了,RFC6296
根据文档,大概可以知道:
工作原理
- NPTv6 是一种无状态的前缀转换,转换规则是:
内部前缀 ↔ 外部前缀,比如2001:db8:1::/48
↔2001:db8:2::/48
。 - 它通过对地址中主机部分保持不变,仅转换前缀(网络部分);
- 为了保持校验和一致,可能还需要调整 IPv6 报文中的校验和(使用调整和算法)。
🧾 关键特点
特性 | 说明 |
---|---|
无状态 | 不维护每个连接状态,只需要知道前缀映射规则 |
一对一映射 | 避免传统 NAT 的多对一、端口复用问题 |
保留端到端可达性 | 如果两端都知道映射规则,通信仍可达 |
校验和修正 | 必须调整 IPv6 报文的伪首部校验和 |
调整计算
NPTv6
并不是简单的前缀替换,实际还涉及到一个调整/adjustment
值的计算。
详见RFC6296中的定义:
3.4 NPTv6 with a /48 or Shorter Prefix
When an NPTv6 Translator is configured with internal and external prefixes that are 48 bits in length (a /48) or shorter, the adjustment MUST be added to or subtracted from bits 48..63 of the address.
This mapping results in no modification of the Interface Identifier (IID), which is held in the lower half of the IPv6 address, so it will not interfere with future protocols that may use unique IIDs for node identification. NPTv6 Translator implementations MUST implement the /48 mapping.
3.5 NPTv6 with a /49 or Longer Prefix
When an NPTv6 Translator is configured with internal and external prefixes that are longer than 48 bits in length (such as a /52, /56, or /60), the adjustment must be added to or subtracted from one of the words in bits 64..79, 80..95, 96..111, or 112..127 of the address. While the choice of word is immaterial as long as it is consistent, these words MUST be inspected in that sequence and the first that is not initially 0xFFFF chosen, for consistency’s sake.
NPTv6 Translator implementations SHOULD implement the mapping for longer prefixes
实际也就是前缀长度在48
及以下的时候,adjustment
是加到IP第4个16位上(实际就是子网位)的,而大于48
的时候,是找到IID
中第一个不是全F
的十六位。
例子
完整的IPV6
的16位的划分如下
1 | 0 15 16 31 32 47 48 63 64 79 80 95 96 111 112 127 |
按照RFC上给的示范,如下:
For the network shown in Figure 1, the Internal Prefix is FD01:0203:0405:/48, and the External Prefix is 2001:0DB8:0001:/48.
If a node with internal address FD01:0203:0405:0001::1234 sends an outbound datagram through the NPTv6 Translator, the resulting external address will be 2001:0DB8:0001:D550::1234. The resulting address is obtained by calculating the checksum of both the internal and external 48-bit prefixes, subtracting the internal prefix from the external prefix using one’s complement arithmetic to calculate the “adjustment”, and adding the adjustment to the 16-bit subnet field (in this case, 0x0001).
To show the work:
The one’s complement checksum of FD01:0203:0405 is 0xFCF5. The one’s complement checksum of 2001:0DB8:0001 is 0xD245. Using one’s complement arithmetic, 0xD245 - 0xFCF5 = 0xD54F. The subnet in the original datagram is 0x0001. Using one’s complement arithmetic,
0x0001 + 0xD54F = 0xD550. Since 0xD550 != 0xFFFF, it is not changed to 0x0000.
So, the value 0xD550 is written in the 16-bit subnet area, resulting in a mapped external address of 2001:0DB8:0001:D550::1234.
内核实现
实际NPTv6
在内核上就是有实现了的,查看内核实现:ip6t_NPT.c
1 | // SPDX-License-Identifier: GPL-2.0-only |
内核里面是做成驱动了,具体的算法实际也就是RFC
里面说的了,和之前说的一致。
自己实现
先不说如何嵌入到项目已有的NAT
实现,先实现自己的算法再说。直接GO
写个转换demo
吧。
1 | package main |
运行结果:
1 | go run .\main.go src FD01:0203:0405:: -dst 2001:0DB8:0001:: -addr FD01:0203:0405:0001::1234 -plen 48 |
总结
上面只是说了NPTv6
的算法实现,如何嵌入到已有带代码中又是另外一件事情了。参照内核上的,其实也是在Netfilter
上做一层钩子的处理就好了吧。我的实现就更简单了,直接把nptv6
当成特殊的nat66
处理了,但是这样实际应该和RFC
上定义的无状态转换相违背了,但是从业务角度上来说这样应该也没有问题吧。