IPv6 网络已经日渐普及,中国的绝大多数家庭宽带或校园网用户均可获得 IPv6 地址。对于手动组网的用户来说,可以使用与 IPv4 一样的方式,配置 IPv6 NAT 来让内网设备接入 IPv6 网络。但我们往往希望不使用 NAT,而是让内网设备可以获得全局可路由的公网 IPv6 地址。本文将从 IPv6 网络的原理入手,介绍如何配置 Linux 路由器,使得内网设备可以直接获得上层网络下发的 IPv6 地址,并同时使自定义路由与防火墙可用。
原理
SLAAC
SLAAC(Stateless Address Auto-Configuration,无状态地址自动配置)是 IPv6 网络中最常见的为单一主机分配地址的方式。一般来说,IPv6 终端网络使用 /64
的前缀长度,并使用 SLAAC 来配置每个主机的 /64
后缀。主机使用其网卡 MAC 地址来生成后缀,确保地址不会冲突。
这种配置方式的存在,得益于 IPv6 地址高达 128 位的地址空间。ISP 能够为每个用户网络分配 /64
乃至更大的地址空间,终端网络无需使用 DHCP 小心翼翼地在有限的地址池中为设备分配地址,也不需要使用 NAT 来让网络中的设备共享一个公网地址。
SLAAC 协议由两种数据包组成,它们都属于 ICMPv6:
- Router Solicitation(路由请求),由需要配置 IPv6 的主机发送的组播包,向网络中的路由器请求路由宣告。
- Router Advertisement(路由宣告),由路由器在收到请求后发送,或配置变更时发送。包含前缀、路由、有效时长等信息,也包含 DNS 服务器地址等一些扩展。
- 一些时候 SLAAC 不足以配置网络中的所有信息,仍然需要 DHCP,这种情况不是本文所介绍的重点,但可以用类似的方法解决。
主机在收到路由宣告后,会使用其中的信息来为自己配置 IPv6 地址和路由表。注意,由于 SLAAC 是无状态的,主机并不需要将选择的地址上报给路由器。
NDP
与 IPv4 的 ARP 协议类似,IPv6 使用 NDP(Neighbor Discovery Protocol)协议来将同一网络中其他主机的 IP 地址对应到 MAC 地址。
这个过程与 ARP 基本一致,一方发送Neighbor Solicitation(邻居请求)组播包,包含所查询的 IPv6 地址,持有该地址主机返回Neighbor Advertisement(邻居宣告)。
PD
PD(Prefix Delegation)是 DHCPv6 的一项扩展,用于 DHCP 服务器将一整段地址分配给 DHCP 客户端。这种情况一般常见于 ISP 为用户分配 IPv6 地址。在客户端获取地址时,DHCPv6 服务器(作为上级路由器)会添加一条路由,将整个被下发的网段路由到客户端。这样一来,整个地址块(一般为 /64
或者 /60
)均可被客户端网络使用。
DHCPv6 客户端收到由 PD 下发的前缀后,即可通过 SLAAC 等方式为整个网络内的所有主机配置 IPv6 地址,这个过程不再需要上级路由的参与。这也是一般家用网络中最常见的 IPv6 地址分配方式。
问题
如果我们的路由器本身处于一个没有 DHCPv6 PD 分配的环境中,只能通过 SLAAC 从上级路由(WAN)获得单个 IPv6 地址,应该如何为网络中(LAN)的主机分配 IPv6 地址?
这种情况常见于校园网环境中,不存在 PD 前缀下发,只有 SLAAC 地址分配。以及,在一些家庭宽带环境下,ISP 提供的光猫完成了 PPPoE 拨号、DHCPv6 PD 获取前缀、SLAAC 下发地址的整个过程,接入光猫(上级路由)的路由器同样无法获得前缀。
中继
在这种情况下,除 NAT 之外,使 LAN 中的终端设备接入 IPv6 的主要思路是,假装这些设备被直接接入到 WAN 中,从 WAN 上的上级路由获得 IPv6 地址,并在 LAN 和 WAN 之间对 SLAAC 和 NDP 协议进行代理 —— 双向转发 SLAAC 与 NDP 包,并将源 MAC 地址改为我们的路由器的 MAC 地址。在 WAN 和 LAN 中的设备看来,对方网络中的 IP 地址由我们的路由器所持有,所有流量均由我们的路由器。
这种实现思路被称为 IPv6 中继(Relay)。OpenWrt 的 6relayd
(早期)与 odhcpd
(目前)实现了这个功能,在一般的 Linux 路由器上,我们也可以通过抓包来手动实现它(见 Menci/magpie),但实现较为复杂,运行效果并不好。
直接桥接
我们可以将 IPv4 和 IPv6 视作独立的网络接口。假设我们有 WAN4、WAN6、LAN4、LAN6,将 WAN6 与 LAN6 桥接,保持 WAN4 与 LAN4 上原有的 NAT 配置。这样一来,桥接会使得 WAN 与 LAN 中主机的 IPv6 流量互通,无需关心地址分配与邻居发现上的任何问题。在 Linux 上,通过 ebtables
来配置:
brctl addif br-lan wan # 将 WAN 与 LAN 桥接
ebtables -t broute -A BROUTING -p ! IPv6 -i wan --logical-in br-lan -j DROP
ebtables -t filter -A OUTPUT -p ! IPv6 --logical-out br-lan -o wan -j DROP
上述命令添加了两条 ebtables
规则。首先在 broute
链上,将 br-lan
从 wan
接口进入的非 IPv6 流量全部 DROP。在 broute
链上 DROP 数据包并不会使得数据包被丢弃,而是会使得该数据包不经过网桥,原样发到物理接口上。这样一来,wan
上进入的 IPv4 流量会在 wan
上被收到,不影响原有的 IPv4 NAT 配置,而 IPv6 流量会经过网桥 br-lan
与 LAN 上的主机互通。
第二条规则在 filter
链上,将 br-lan
上要发往 wan
接口的非 IPv6 流量全部 DROP,这保证了 br-lan
上的 IPv4 数据包在泛洪时不会将 LAN 侧的 IPv4 数据包泄漏到 WAN 上。
这种做法虽然解决了 LAN 上主机接入 IPv6 的问题,但 WAN 与 LAN 之间的 IPv6 流量不会经过路由器的三层,无法为其配置路由规则和防火墙规则。
基于桥接的中继
考虑到桥接的实现方式,内核会在 MAC 表中记录每个 MAC 地址所在的接口,在需要时进行泛洪,所以能够天然地正确转发 NDP 的 NS 与 NA 包。不考虑对数据流量的转发,桥接的机制相当于为我们实现了 NDP 中继。
与手动实现中继有一点不同,桥接会直接双向转发 SLAAC 与 NDP 包,不改变包的 MAC 地址。在 WAN 和 LAN 中的设备看来,他们互相可以直接在同一二层中收到对方的 ICMPv6 消息,但实际上这些数据包由我们的路由器进行转发。WAN 和 LAN 上的交换机会分别记录对方网络中设备的 MAC 地址的所属方为我们的路由器,所以,双方会认为它们在同一个网络中,而双方之间的流量则自然而然地到达了我们的路由器。而我们希望不对数据流量进行二层交换,而是在利用桥接带来的连通性的同时,让数据流量经过三层进行路由。
同样地,使用 ebtables
来实现流量分离:
# br-lan 为上一节中桥接了 WAN 与 LAN 的网桥
# 如果上级路由使用 DHCPv6 下发地址,同样需要将 DHCPv6 数据包跳过处理
ebtables -t nat -A PREROUTING -p IPv6 --logical-in br-lan --ip6-proto ipv6-icmp --ip6-icmp-type router-solicitation -j ACCEPT
ebtables -t nat -A PREROUTING -p IPv6 --logical-in br-lan --ip6-proto ipv6-icmp --ip6-icmp-type router-advertisement -j ACCEPT
ebtables -t nat -A PREROUTING -p IPv6 --logical-in br-lan --ip6-proto ipv6-icmp --ip6-icmp-type neighbour-solicitation -j ACCEPT
ebtables -t nat -A PREROUTING -p IPv6 --logical-in br-lan --ip6-proto ipv6-icmp --ip6-icmp-type neighbour-advertisement -j ACCEPT
ebtables -t nat -A PREROUTING -p IPv6 --logical-in br-lan -j redirect
前四条规则跳过 ICMPv6 的 SLAAC 与 NDP 相关的包,让它们通过网桥,实现 SLAAC 与 NDP 的中继。最后一条规则在 nat
链上让进入 br-lan
的所有 IPv6 数据包(排除了 SLAAC 与 NDP,剩余的均为需要路由的数据流量)停止经过网桥,而是将它们的目标 MAC 地址改为 br-lan
的 MAC 地址,使得这些数据包在路由器上的 br-lan
接口上被收到。对于发往本机的包和从本机发出的包,这些规则没有影响。而对于原本目标非本机 IP 的数据包,在被本机收到后,会被三层路由机制进行转发,由于被转发的两个网络地址范围均在 br-lan
上,所以包会再次从 br-lan
发出,经过网桥的 MAC 表,自动发往正确的接口。这需要开启 IPv6 转发,并允许 br-lan
到 br-lan
的转发:
sysctl -w net.ipv6.conf.all.forwarding=1
ip6tables -t filter -A FORWARD -i br-lan -o br-lan -j ACCEPT
此时使用 ip route
配置额外的 IPv6 路由,它们会对来自 LAN 与 WAN 的流量生效。
同理 ip6tables
的防火墙规则也会生效。如果需要在防火墙中判断连接来自 WAN 或 LAN,可以使用 physdev
模块:
ip6tables -t fitler -A FORWARD \
-i br-lan -o br-lan \
-m physdev --physdev-in wan \
-p tcp --dport 80 \
-j DROP
# 注意此处不要使用 `--physdev-out lan`,因为 `--physdev-out` 有不同的含义
即可将从 WAN 进入的目标端口为 TCP 80 的流量全部阻止,使得 WAN 中的主机无法访问 LAN 中主机的 TCP 80 端口。