























原版 Clash Meta 运行在普通 Linux (非 OpenWrt 路由器)上时,可以开启 TUN 作为旁路由使用。
但如果想要在不侵入主路由的情况下,接管指定设备,在 IPv4 和 IPv6 下会分别遇到不同的协议问题。
在 IPv4 下,地址分配通常依赖 DHCP 。
DHCP 协议在同一个子网内通常只能存在一个 DHCP Server 。如果强行设置两个 DHCP Server ,最终会变成“谁回复快谁生效”的抢答游戏,容易导致网关、DNS 、地址池混乱。
在 IPv6 下,地址分配、路由宣告和 DNS 宣告主要通过 ICMPv6 Router Advertisement ( RA )完成。
RA 可以指定:
因此,通过控制 RA 的优先级和生存时间,可以实现不侵入主路由的旁路由接管。
IPv4 侧仍然存在 DHCP 无法无侵入接管的问题。
不过好消息是,现在大部分设备,例如 Windows 和 Android ,会优先使用 IPv6 DNS ,并优先解析 IPv6 地址进行外呼。
因此,在接管 IPv6 之后,实测 Android 体验几乎等同于 VPN Service 并且部分场景优于,比如不会被各类金融 APP 检测到代理强制退出。
IPv6 使用 ICMPv6 替代了 IPv4 中的 ARP ,以及部分 DHCP 功能。
RA ( Router Advertisement )是 ICMPv6 Type 134 报文,由路由器定期组播发送到:
ff02::1
即所有节点地址。
当路由器收到主机发送的 RS ( Router Solicitation ,Type 133 )时,也会立即响应 RA 。
| 字段 | 长度 | 含义 |
|---|---|---|
| Router Lifetime | 2 字节 | 宣告自身作为默认路由的有效期,单位为秒;设为0表示撤销 |
| Preference | 2 位 | 路由优先级:01 = high,00 = medium,11 = low |
| Current Hop Limit | 1 字节 | 后续发往互联网的报文使用的默认 Hop Limit |
RA 还可以通过 Option 字段携带附加信息:
| Option 类型 | 编号 | 作用 |
|---|---|---|
| Source Link-Layer Address | 1 | 发送方 MAC 地址 |
| MTU | 5 | 建议链路 MTU |
| RDNSS | 25 | 递归 DNS 服务器地址 |
这是实现旁路由无侵入接入的关键。
假设:
| 设备 | Preference | Lifetime |
|---|---|---|
| 旁路由 | high | 180 秒 |
| 主路由 | medium | 1800 秒 |
此时流程如下:
preference = high 的旁路由作为默认网关。lifetime = 0 的撤销报文。RDNSS 是 IPv6 旁路由接管中的关键设计。
RA 报文中的 RDNSS Option ( Type 25 )可以携带一个或多个 DNS 服务器地址。
与 IPv4 DHCP 不同,RDNSS 与地址分配解耦。旁路由无需参与地址分配,只需要宣告 DNS 即可。
RDNSS Option 格式:
Type: 8 bits ,值为 25
Length: 8 bits ,单位为 8 字节,计算方式为 1 + 2 * address_count
Lifetime: 32 bits ,单位为秒
Addresses: 可变长度,一个或多个 IPv6 地址
Windows 10+ 和 Android 系统会优先使用通过 RDNSS 获取的 DNS 服务器,且优先级通常高于 DHCPv4 分配的 DNS 。
因此实际效果是:
high,则走旁路由。实测 Android:由于大部分 App 会优先通过 IPv6 进行外呼,即使 IPv4 回退,也能正常解析和访问,用户体验基本不受影响。
Linux 内核默认不会主动发送 RA ,需要启用 IPv6 转发。
代码中可以通过写入 sysctl 控制文件实现:
func enableIPv6Forwarding(ifName string) {
writeSysctl("/proc/sys/net/ipv6/conf/all/forwarding", "1")
writeSysctl("/proc/sys/net/ipv6/conf/eth0/forwarding", "1")
writeSysctl("/proc/sys/net/ipv6/conf/eth0/accept_ra", "2")
}
含义如下:
| 配置项 | 作用 |
|---|---|
conf/all/forwarding = 1 |
启用全局 IPv6 转发,是内核允许发送 RA 的前提 |
conf/eth0/forwarding = 1 |
在目标接口上启用 IPv6 转发 |
conf/eth0/accept_ra = 2 |
即使启用了转发,仍然接受其他路由器的 RA |
其中,accept_ra = 2 很关键。它可以确保旁路由本身仍然能从主路由获取 IPv6 路由。
RA 报文可以直接在内存中构造为字节数组,无需依赖外部库。
func buildRouterAdvertisement(
iface *net.Interface,
preference byte,
lifetime uint16,
dnsServers []net.IP,
dnsLifetime uint32,
) []byte {
packet := make([]byte, 16, 32)
packet[0] = icmpv6RouterAdvertisement // Type = 134
packet[4] = raDefaultCurrentHopLimit // Hop Limit = 64
packet[5] = preference // 路由优先级
binary.BigEndian.PutUint16(packet[6:8], lifetime)
// Source Link-Layer Address Option
if len(iface.HardwareAddr) == 6 {
packet = append(packet, 1, 1) // Type = 1, Length = 1
packet = append(packet, iface.HardwareAddr...)
}
// MTU Option
if iface.MTU > 0 {
// Type = 5, Length = 1, MTU value
}
// RDNSS Option
if len(dnsServers) > 0 {
packet = append(packet, buildRDNSSOption(dnsServers, dnsLifetime)...)
}
return packet
}
RDNSS 的 lifetime 可以设置为 router lifetime 的 3 倍:
advertisement := buildRouterAdvertisement(
iface,
preference,
lifetime,
[]net.IP{src},
uint32(lifetime)*raRDNSSLifetimeMultiplier,
)
// raRDNSSLifetimeMultiplier = 3
这样即使路由宣告过期,DNS 信息仍然可以维持一段时间,避免 DNS 抖动。
func (r *routerAdvertiser) loop() {
ticker := time.NewTicker(r.interval) // 默认 30s
// 监听 RS 请求
go func() {
for {
n, cm, _, err := r.packetConn.ReadFrom(buf)
if err != nil {
return
}
if n > 0 && buf[0] == icmpv6RouterSolicitation {
r.send(r.advertisement)
}
_ = cm
}
}()
// 定期发送 RA
for {
select {
case <-ticker.C:
r.send(r.advertisement)
case <-r.done:
return
}
}
}
工作机制:
因此,新设备几乎可以立即感知到旁路由的存在。
Clash Meta 关闭时,可以发送 3 次 lifetime = 0 的撤销 RA:
func (r *routerAdvertiser) Close() error {
r.closeOnce.Do(func() {
close(r.done)
for i := 0; i < 3; i++ {
r.send(r.withdraw)
time.Sleep(100 * time.Millisecond)
}
r.rawConn.Close()
})
return nil
}
这会通知所有客户端:该路由器已经不可用。
客户端随后会自动回切到主路由。
这正是“不侵入”的关键:不修改主路由配置,也不破坏现有网络拓扑。
在 Clash Meta 的 TUN 配置段中启用:
tun:
enable: true
stack: mixed
router-advertise:
enable: true
interface: eth0
default-preference: high
default-lifetime: 180
interval: 30
字段说明:
| 配置项 | 含义 |
|---|---|
enable |
是否启用 TUN |
stack |
TUN 使用的网络栈 |
router-advertise.enable |
是否启用 RA 宣告 |
router-advertise.interface |
发送 RA 的物理接口 |
router-advertise.default-preference |
默认路由优先级,可选high、medium、low |
router-advertise.default-lifetime |
默认路由 lifetime ,单位为秒 |
router-advertise.interval |
RA 发送间隔,单位为秒 |
在同一广播域内抓包,可以看到旁路由定时发出的 RA:
fe80::xxxx:xxxx:xxxx:xxxx > ff02::1:
ICMPv6 Router Advertisement
hop limit 64, Flags [none], pref high
router lifetime 180s
source link-address option: xx:xx:xx:xx:xx:xx
mtu option: 1500
rdnss option, lifetime 540s, addr: fe80::xxxx:xxxx:xxxx:xxxx
也可以主动发送 RS 触发 RA 响应:
python3 -c "
import socket
import struct
sock = socket.socket(socket.AF_INET6, socket.SOCK_RAW, socket.IPPROTO_ICMPV6)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255)
rs = struct.pack('!BBHI', 133, 0, 0, 0)
sock.sendto(rs, ('ff02::2', 0))
"
抓包验证:
tcpdump -i eth0 -vv -n 'icmp6 && ip6[40] == 134'
如果能够看到旁路由立即返回 RA ,即说明“新设备无感接入”能力生效。
通过 IPv6 RA 实现旁路由接管的核心思路是:
lifetime = 0 的撤销 RA 。这种方式可以在不修改主路由配置的情况下,实现对支持 IPv6 设备的无侵入旁路由接管。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。