feat: add interface address fallback for IPv6 prefix detection (DHCPv6 IA_NA / SLAAC) (#2334)

feat: add IPv6 prefix fallback and NDP proxy for SLAAC/IA_NA

Add interface address fallback for IPv6 prefix detection when
route-based detection fails (no delegated prefix on LAN). Covers
DHCPv6 IA_NA / SLAAC where prefix is assigned to WAN without PD.
Uses getifaddrs() and Ipv6Inet to handle host bits.

Add NDP proxy sync for on-link prefixes. ISP router uses NDP to
resolve MACs; EasyTier /128 addresses on tun0 won't answer on
physical WAN. Periodic sync (30s) reads /128 routes via netlink
and manages proxy neigh entries. Auto-enables proxy_ndp sysctl.
Returns WAN ifindex from detection; static NDP_WAN_IFINDEX for
sync task. Spawns from provider reconcile when auto-detection
enabled.

Add tests: fallback default route interface, on-link NDP proxy,
interface prefix selection.

Fixes #2333

Co-authored-by: ririyeye <200610237@qq.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: KKRainbow <443152178@qq.com>
This commit is contained in:
ririyeye
2026-07-01 12:34:46 +08:00
committed by GitHub
co-authored by Claude KKRainbow
parent 741460e1e4
commit 7756a15cbe
8 changed files with 1632 additions and 145 deletions
+72 -9
View File
@@ -65,9 +65,9 @@ use crate::vpn_portal::{self, VpnPortal};
use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
use super::listeners::ListenerManager;
use super::public_ipv6_provider::{
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
should_run_public_ipv6_provider_reconcile, validate_public_ipv6_config,
validate_public_ipv6_config_values,
PublicIpv6ProviderReconcileTask, reconcile_public_ipv6_provider_runtime,
run_public_ipv6_provider_reconcile_task, should_run_public_ipv6_provider_reconcile,
validate_public_ipv6_config, validate_public_ipv6_config_values,
};
#[cfg(feature = "socks5")]
@@ -194,6 +194,44 @@ impl NicCtxContainer {
#[cfg(feature = "tun")]
type ArcNicCtx = Arc<Mutex<Option<NicCtxContainer>>>;
type ArcPublicIpv6ProviderTaskSlot = Arc<PublicIpv6ProviderTaskSlot>;
struct PublicIpv6ProviderTaskSlot {
task: Mutex<Option<PublicIpv6ProviderReconcileTask>>,
closing: AtomicBool,
}
impl PublicIpv6ProviderTaskSlot {
fn new() -> Self {
Self {
task: Mutex::new(None),
closing: AtomicBool::new(false),
}
}
async fn ensure_started(&self, global_ctx: &ArcGlobalCtx) {
let mut task = self.task.lock().await;
if self.closing.load(Ordering::Acquire) || task.is_some() {
return;
}
*task = run_public_ipv6_provider_reconcile_task(global_ctx);
}
async fn shutdown(&self) {
self.closing.store(true, Ordering::Release);
let task = self.task.lock().await.take();
if let Some(task) = task {
task.shutdown().await;
}
}
}
async fn ensure_public_ipv6_provider_reconcile_task(
global_ctx: &ArcGlobalCtx,
task_slot: &ArcPublicIpv6ProviderTaskSlot,
) {
task_slot.ensure_started(global_ctx).await;
}
pub struct InstanceRpcServerHook {
rpc_portal_whitelist: Vec<IpCidr>,
@@ -254,6 +292,7 @@ pub struct InstanceConfigPatcher {
socks5_server: Weak<Socks5Server>,
peer_manager: Weak<PeerManager>,
conn_manager: Weak<ManualConnectorManager>,
public_ipv6_provider_task: ArcPublicIpv6ProviderTaskSlot,
}
impl InstanceConfigPatcher {
@@ -324,7 +363,6 @@ impl InstanceConfigPatcher {
self.patch_mapped_listeners(patch.mapped_listeners).await?;
self.patch_connector(patch.connectors).await?;
let provider_reconcile_was_running = should_run_public_ipv6_provider_reconcile(&global_ctx);
let mut provider_config_changed = false;
if let Some(hostname) = patch.hostname {
global_ctx.set_hostname(hostname.clone());
@@ -362,10 +400,12 @@ impl InstanceConfigPatcher {
if provider_config_changed {
reconcile_public_ipv6_provider_runtime(&global_ctx).await;
let provider_reconcile_should_run =
should_run_public_ipv6_provider_reconcile(&global_ctx);
if !provider_reconcile_was_running && provider_reconcile_should_run {
run_public_ipv6_provider_reconcile_task(&global_ctx);
if should_run_public_ipv6_provider_reconcile(&global_ctx) {
ensure_public_ipv6_provider_reconcile_task(
&global_ctx,
&self.public_ipv6_provider_task,
)
.await;
}
}
@@ -647,6 +687,7 @@ pub struct Instance {
socks5_server: Arc<Socks5Server>,
proxy_cidrs_monitor: Option<AbortOnDropHandle<()>>,
public_ipv6_provider_task: ArcPublicIpv6ProviderTaskSlot,
global_ctx: ArcGlobalCtx,
}
@@ -734,6 +775,7 @@ impl Instance {
socks5_server,
proxy_cidrs_monitor: None,
public_ipv6_provider_task: Arc::new(PublicIpv6ProviderTaskSlot::new()),
global_ctx,
}
@@ -1034,7 +1076,11 @@ impl Instance {
.await?;
self.listener_manager.lock().await.run().await?;
self.peer_manager.run().await?;
run_public_ipv6_provider_reconcile_task(&self.global_ctx);
ensure_public_ipv6_provider_reconcile_task(
&self.global_ctx,
&self.public_ipv6_provider_task,
)
.await;
#[cfg(feature = "tun")]
{
@@ -1347,6 +1393,7 @@ impl Instance {
socks5_server: Arc::downgrade(&self.socks5_server),
peer_manager: Arc::downgrade(&self.peer_manager),
conn_manager: Arc::downgrade(&self.conn_manager),
public_ipv6_provider_task: self.public_ipv6_provider_task.clone(),
}
}
@@ -1602,6 +1649,7 @@ impl Instance {
}
pub async fn clear_resources(&mut self) {
self.public_ipv6_provider_task.shutdown().await;
self.peer_manager.clear_resources().await;
#[cfg(feature = "tun")]
let _ = self.nic_ctx.lock().await.take();
@@ -1787,6 +1835,21 @@ mod tests {
);
}
#[tokio::test]
async fn public_ipv6_provider_task_slot_does_not_restart_after_shutdown() {
let global_ctx = get_mock_global_ctx();
let slot = std::sync::Arc::new(super::PublicIpv6ProviderTaskSlot::new());
global_ctx.config.set_ipv6_public_addr_provider(true);
global_ctx
.config
.set_ipv6_public_addr_prefix(Some("2001:db8::/48".parse().unwrap()));
slot.shutdown().await;
super::ensure_public_ipv6_provider_reconcile_task(&global_ctx, &slot).await;
assert!(slot.task.lock().await.is_none());
}
#[tokio::test]
async fn validate_public_ipv6_patch_allows_enabling_auto_with_manual_ipv6() {
let global_ctx = get_mock_global_ctx();
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -1361,12 +1361,11 @@ impl NicCtx {
}
self.global_ctx
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
.set_tun_device_ready(nic.ifname().to_string());
ret
}
Err(err) => {
self.global_ctx
.issue_event(GlobalCtxEvent::TunDeviceError(err.to_string()));
self.global_ctx.set_tun_device_error(err.to_string());
return Err(err);
}
}
@@ -1405,12 +1404,11 @@ impl NicCtx {
match nic.create_dev_for_mobile(tun_fd).await {
Ok(ret) => {
self.global_ctx
.issue_event(GlobalCtxEvent::TunDeviceReady(nic.ifname().to_string()));
.set_tun_device_ready(nic.ifname().to_string());
ret
}
Err(err) => {
self.global_ctx
.issue_event(GlobalCtxEvent::TunDeviceError(err.to_string()));
self.global_ctx.set_tun_device_error(err.to_string());
return Err(err);
}
}