mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-06 17:59:11 +00:00
allow provider detect dynamically
This commit is contained in:
@@ -211,6 +211,7 @@ pub struct GlobalCtx {
|
|||||||
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
|
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
|
||||||
|
|
||||||
running_listeners: Mutex<Vec<url::Url>>,
|
running_listeners: Mutex<Vec<url::Url>>,
|
||||||
|
advertised_ipv6_public_addr_prefix: Mutex<Option<cidr::Ipv6Cidr>>,
|
||||||
|
|
||||||
flags: ArcSwap<Flags>,
|
flags: ArcSwap<Flags>,
|
||||||
|
|
||||||
@@ -309,6 +310,7 @@ impl GlobalCtx {
|
|||||||
stun_info_collection: Mutex::new(stun_info_collector),
|
stun_info_collection: Mutex::new(stun_info_collector),
|
||||||
|
|
||||||
running_listeners: Mutex::new(Vec::new()),
|
running_listeners: Mutex::new(Vec::new()),
|
||||||
|
advertised_ipv6_public_addr_prefix: Mutex::new(None),
|
||||||
|
|
||||||
flags: ArcSwap::new(Arc::new(flags)),
|
flags: ArcSwap::new(Arc::new(flags)),
|
||||||
|
|
||||||
@@ -383,6 +385,20 @@ impl GlobalCtx {
|
|||||||
self.cached_ipv6.store(None);
|
self.cached_ipv6.store(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_advertised_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr> {
|
||||||
|
*self.advertised_ipv6_public_addr_prefix.lock().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_advertised_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>) -> bool {
|
||||||
|
let mut guard = self.advertised_ipv6_public_addr_prefix.lock().unwrap();
|
||||||
|
if *guard == prefix {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*guard = prefix;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_id(&self) -> uuid::Uuid {
|
pub fn get_id(&self) -> uuid::Uuid {
|
||||||
self.config.get_id()
|
self.config.get_id()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
#[cfg(feature = "tun")]
|
#[cfg(feature = "tun")]
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use std::net::Ipv6Addr;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
#[cfg(feature = "tun")]
|
#[cfg(feature = "tun")]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use cidr::{IpCidr, Ipv4Inet, Ipv6Cidr};
|
use cidr::{IpCidr, Ipv4Inet};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage, RouteType};
|
|
||||||
use tokio::sync::{Mutex, Notify};
|
use tokio::sync::{Mutex, Notify};
|
||||||
#[cfg(feature = "tun")]
|
#[cfg(feature = "tun")]
|
||||||
use tokio::{sync::oneshot, task::JoinSet};
|
use tokio::{sync::oneshot, task::JoinSet};
|
||||||
@@ -28,8 +22,6 @@ use crate::common::acl_processor::AclRuleBuilder;
|
|||||||
use crate::common::config::ConfigLoader;
|
use crate::common::config::ConfigLoader;
|
||||||
use crate::common::error::Error;
|
use crate::common::error::Error;
|
||||||
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx, GlobalCtxEvent};
|
use crate::common::global_ctx::{ArcGlobalCtx, GlobalCtx, GlobalCtxEvent};
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use crate::common::ifcfg::{get_interface_index, list_ipv6_route_messages};
|
|
||||||
use crate::connector::direct::DirectConnectorManager;
|
use crate::connector::direct::DirectConnectorManager;
|
||||||
use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager};
|
use crate::connector::manual::{ConnectorManagerRpcService, ManualConnectorManager};
|
||||||
use crate::connector::tcp_hole_punch::TcpHolePunchConnector;
|
use crate::connector::tcp_hole_punch::TcpHolePunchConnector;
|
||||||
@@ -72,6 +64,10 @@ use crate::vpn_portal::{self, VpnPortal};
|
|||||||
#[cfg(feature = "magic-dns")]
|
#[cfg(feature = "magic-dns")]
|
||||||
use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
use super::dns_server::{MAGIC_DNS_FAKE_IP, runner::DnsRunner};
|
||||||
use super::listeners::ListenerManager;
|
use super::listeners::ListenerManager;
|
||||||
|
use super::public_ipv6_provider::{
|
||||||
|
reconcile_public_ipv6_provider_runtime, run_public_ipv6_provider_reconcile_task,
|
||||||
|
validate_public_ipv6_config,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "socks5")]
|
#[cfg(feature = "socks5")]
|
||||||
use crate::gateway::socks5::Socks5Server;
|
use crate::gateway::socks5::Socks5Server;
|
||||||
@@ -202,202 +198,6 @@ pub struct InstanceRpcServerHook {
|
|||||||
rpc_portal_whitelist: Vec<IpCidr>,
|
rpc_portal_whitelist: Vec<IpCidr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_global_routable_public_ipv6_prefix(prefix: Ipv6Cidr) -> bool {
|
|
||||||
let addr = prefix.first_address();
|
|
||||||
!addr.is_loopback()
|
|
||||||
&& !addr.is_multicast()
|
|
||||||
&& !addr.is_unicast_link_local()
|
|
||||||
&& !addr.is_unique_local()
|
|
||||||
&& !addr.is_unspecified()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_public_ipv6_provider_supported() -> Result<(), Error> {
|
|
||||||
if cfg!(target_os = "linux") {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(anyhow::anyhow!(
|
|
||||||
"the provider feature requires Linux; run without --ipv6-public-addr-provider on this node, or move the provider role to a Linux node. client mode (--ipv6-public-addr-auto) works on all platforms"
|
|
||||||
)
|
|
||||||
.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn public_ipv6_provider_auto_detect_error() -> Error {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"no public IPv6 prefix found on this system; set --ipv6-public-addr-prefix manually, or check that your ISP has delegated an IPv6 prefix and a default-from route exists in the kernel routing table"
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn read_linux_proc_bool(path: &Path) -> Result<bool, Error> {
|
|
||||||
let value = std::fs::read_to_string(path)
|
|
||||||
.with_context(|| format!("failed to read {}", path.display()))?;
|
|
||||||
match value.trim() {
|
|
||||||
"0" => Ok(false),
|
|
||||||
"1" => Ok(true),
|
|
||||||
other => Err(anyhow::anyhow!("unexpected value '{}' in {}", other, path.display()).into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn write_linux_proc_bool(path: &Path, enabled: bool) -> Result<(), Error> {
|
|
||||||
let value = if enabled { "1\n" } else { "0\n" };
|
|
||||||
std::fs::write(path, value).with_context(|| format!("failed to write {}", path.display()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn ensure_linux_ipv6_forwarding_at_paths(
|
|
||||||
all_path: &Path,
|
|
||||||
default_path: &Path,
|
|
||||||
) -> Result<bool, Error> {
|
|
||||||
let all_enabled = read_linux_proc_bool(all_path)?;
|
|
||||||
let default_enabled = read_linux_proc_bool(default_path)?;
|
|
||||||
let mut changed = false;
|
|
||||||
|
|
||||||
if !all_enabled {
|
|
||||||
write_linux_proc_bool(all_path, true)?;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !default_enabled {
|
|
||||||
write_linux_proc_bool(default_path, true)?;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !read_linux_proc_bool(all_path)? || !read_linux_proc_bool(default_path)? {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"failed to enable Linux IPv6 forwarding in {} and {}",
|
|
||||||
all_path.display(),
|
|
||||||
default_path.display()
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(changed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn ensure_linux_ipv6_forwarding() -> Result<bool, Error> {
|
|
||||||
let all_path = Path::new("/proc/sys/net/ipv6/conf/all/forwarding");
|
|
||||||
let default_path = Path::new("/proc/sys/net/ipv6/conf/default/forwarding");
|
|
||||||
|
|
||||||
ensure_linux_ipv6_forwarding_at_paths(all_path, default_path).map_err(|err| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"public IPv6 provider requires Linux IPv6 forwarding; failed to enable net.ipv6.conf.all.forwarding=1 and net.ipv6.conf.default.forwarding=1 automatically: {}. run with sufficient privileges or set them manually",
|
|
||||||
err
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
struct DetectedIpv6Route {
|
|
||||||
dst: Option<Ipv6Cidr>,
|
|
||||||
src: Option<Ipv6Cidr>,
|
|
||||||
ifindex: Option<u32>,
|
|
||||||
kind: RouteType,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn ipv6_cidr_from_route_addr(addr: RouteAddress, prefix_len: u8) -> Option<Ipv6Cidr> {
|
|
||||||
match addr {
|
|
||||||
RouteAddress::Inet6(addr) => Ipv6Cidr::new(addr, prefix_len).ok(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl TryFrom<RouteMessage> for DetectedIpv6Route {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(message: RouteMessage) -> Result<Self, Self::Error> {
|
|
||||||
let dst = message.attributes.iter().find_map(|attr| match attr {
|
|
||||||
RouteAttribute::Destination(addr) => {
|
|
||||||
ipv6_cidr_from_route_addr(addr.clone(), message.header.destination_prefix_length)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let src = message.attributes.iter().find_map(|attr| match attr {
|
|
||||||
RouteAttribute::Source(addr) => {
|
|
||||||
ipv6_cidr_from_route_addr(addr.clone(), message.header.source_prefix_length)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
let ifindex = message.attributes.iter().find_map(|attr| match attr {
|
|
||||||
RouteAttribute::Oif(index) => Some(*index),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
dst,
|
|
||||||
src,
|
|
||||||
ifindex,
|
|
||||||
kind: message.header.kind,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn is_ipv6_default_route(dst: Option<Ipv6Cidr>) -> bool {
|
|
||||||
dst.is_none() || dst == Some(Ipv6Cidr::new(Ipv6Addr::UNSPECIFIED, 0).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn detect_public_ipv6_prefix_from_routes(
|
|
||||||
routes: &[DetectedIpv6Route],
|
|
||||||
loopback_ifindex: u32,
|
|
||||||
) -> Option<Ipv6Cidr> {
|
|
||||||
routes
|
|
||||||
.iter()
|
|
||||||
.filter_map(|route| {
|
|
||||||
if !is_ipv6_default_route(route.dst) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let prefix = route.src?;
|
|
||||||
let wan_ifindex = route.ifindex?;
|
|
||||||
if !is_global_routable_public_ipv6_prefix(prefix) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delegated = routes.iter().any(|candidate| {
|
|
||||||
candidate.dst == Some(prefix)
|
|
||||||
&& candidate.ifindex.is_some()
|
|
||||||
&& candidate.ifindex != Some(wan_ifindex)
|
|
||||||
&& candidate.ifindex != Some(loopback_ifindex)
|
|
||||||
&& candidate.kind == RouteType::Unicast
|
|
||||||
});
|
|
||||||
|
|
||||||
delegated.then_some(prefix)
|
|
||||||
})
|
|
||||||
.min_by_key(|prefix| prefix.network_length())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
async fn detect_public_ipv6_prefix_linux() -> Result<Option<Ipv6Cidr>, Error> {
|
|
||||||
let routes = list_ipv6_route_messages().with_context(|| "failed to query linux ipv6 routes")?;
|
|
||||||
let routes = routes
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(DetectedIpv6Route::try_from)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
let loopback_ifindex =
|
|
||||||
get_interface_index("lo").with_context(|| "failed to resolve linux loopback ifindex")?;
|
|
||||||
|
|
||||||
Ok(detect_public_ipv6_prefix_from_routes(
|
|
||||||
&routes,
|
|
||||||
loopback_ifindex,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
async fn detect_public_ipv6_prefix_linux() -> Result<Option<Ipv6Cidr>, Error> {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InstanceRpcServerHook {
|
impl InstanceRpcServerHook {
|
||||||
pub fn new(rpc_portal_whitelist: Option<Vec<IpCidr>>) -> Self {
|
pub fn new(rpc_portal_whitelist: Option<Vec<IpCidr>>) -> Self {
|
||||||
let rpc_portal_whitelist = rpc_portal_whitelist
|
let rpc_portal_whitelist = rpc_portal_whitelist
|
||||||
@@ -868,57 +668,8 @@ impl Instance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_public_ipv6_config(&self) -> Result<(), Error> {
|
async fn prepare_public_ipv6_config(&self) -> Result<(), Error> {
|
||||||
if self.global_ctx.config.get_ipv6_public_addr_auto()
|
validate_public_ipv6_config(&self.global_ctx)?;
|
||||||
&& self.global_ctx.get_ipv6().is_some()
|
reconcile_public_ipv6_provider_runtime(&self.global_ctx).await;
|
||||||
{
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"cannot use --ipv6-public-addr-auto together with a manually set --ipv6; pick one or the other"
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.global_ctx.config.get_ipv6_public_addr_provider() {
|
|
||||||
let mut feature_flags = self.global_ctx.get_feature_flags();
|
|
||||||
feature_flags.ipv6_public_addr_provider = false;
|
|
||||||
self.global_ctx.set_feature_flags(feature_flags);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_public_ipv6_provider_supported()?;
|
|
||||||
|
|
||||||
let prefix = if let Some(prefix) = self.global_ctx.config.get_ipv6_public_addr_prefix() {
|
|
||||||
prefix
|
|
||||||
} else {
|
|
||||||
let _g = self.global_ctx.net_ns.guard();
|
|
||||||
detect_public_ipv6_prefix_linux()
|
|
||||||
.await?
|
|
||||||
.ok_or_else(public_ipv6_provider_auto_detect_error)?
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_global_routable_public_ipv6_prefix(prefix) {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"the prefix {} is not a valid global unicast IPv6 prefix; it must be a routable address range, not a private, link-local, or multicast address",
|
|
||||||
prefix
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let _g = self.global_ctx.net_ns.guard();
|
|
||||||
if ensure_linux_ipv6_forwarding()? {
|
|
||||||
tracing::info!(
|
|
||||||
"enabled Linux IPv6 forwarding for public IPv6 provider at runtime; this change is not persisted across reboot"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.global_ctx
|
|
||||||
.config
|
|
||||||
.set_ipv6_public_addr_prefix(Some(prefix));
|
|
||||||
let mut feature_flags = self.global_ctx.get_feature_flags();
|
|
||||||
feature_flags.ipv6_public_addr_provider = true;
|
|
||||||
self.global_ctx.set_feature_flags(feature_flags);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,6 +949,7 @@ impl Instance {
|
|||||||
.await?;
|
.await?;
|
||||||
self.listener_manager.lock().await.run().await?;
|
self.listener_manager.lock().await.run().await?;
|
||||||
self.peer_manager.run().await?;
|
self.peer_manager.run().await?;
|
||||||
|
run_public_ipv6_provider_reconcile_task(&self.global_ctx);
|
||||||
|
|
||||||
#[cfg(feature = "tun")]
|
#[cfg(feature = "tun")]
|
||||||
{
|
{
|
||||||
@@ -1802,88 +1554,10 @@ impl Drop for Instance {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use netlink_packet_route::route::RouteType;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use std::fs;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use std::path::PathBuf;
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook,
|
instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
use super::{
|
|
||||||
DetectedIpv6Route, detect_public_ipv6_prefix_from_routes, detect_public_ipv6_prefix_linux,
|
|
||||||
ensure_linux_ipv6_forwarding_at_paths, ensure_public_ipv6_provider_supported,
|
|
||||||
public_ipv6_provider_auto_detect_error,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
use super::{ensure_public_ipv6_provider_supported, public_ipv6_provider_auto_detect_error};
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn run_ip(args: &[&str]) {
|
|
||||||
let output = Command::new("ip")
|
|
||||||
.args(args)
|
|
||||||
.output()
|
|
||||||
.expect("failed to execute ip process");
|
|
||||||
assert!(
|
|
||||||
output.status.success(),
|
|
||||||
"ip command failed: {:?}\nstdout: {}\nstderr: {}",
|
|
||||||
args,
|
|
||||||
String::from_utf8_lossy(&output.stdout),
|
|
||||||
String::from_utf8_lossy(&output.stderr),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn test_iface_name(tag: &str) -> String {
|
|
||||||
format!("et{}{:x}", tag, std::process::id() & 0xffff)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
struct ScopedDummyLink {
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl ScopedDummyLink {
|
|
||||||
fn new(name: &str) -> Self {
|
|
||||||
let _ = Command::new("ip").args(["link", "del", name]).output();
|
|
||||||
run_ip(&["link", "add", name, "type", "dummy"]);
|
|
||||||
run_ip(&["link", "set", name, "up"]);
|
|
||||||
Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
impl Drop for ScopedDummyLink {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = Command::new("ip")
|
|
||||||
.args(["link", "del", &self.name])
|
|
||||||
.output();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn temp_forwarding_paths(
|
|
||||||
all_value: &str,
|
|
||||||
default_value: &str,
|
|
||||||
) -> (tempfile::TempDir, PathBuf, PathBuf) {
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let all_path = dir.path().join("all_forwarding");
|
|
||||||
let default_path = dir.path().join("default_forwarding");
|
|
||||||
fs::write(&all_path, all_value).unwrap();
|
|
||||||
fs::write(&default_path, default_value).unwrap();
|
|
||||||
(dir, all_path, default_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_rpc_portal_whitelist() {
|
async fn test_rpc_portal_whitelist() {
|
||||||
use cidr::IpCidr;
|
use cidr::IpCidr;
|
||||||
@@ -2002,237 +1676,4 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn route(
|
|
||||||
dst: Option<&str>,
|
|
||||||
src: Option<&str>,
|
|
||||||
ifindex: Option<u32>,
|
|
||||||
kind: RouteType,
|
|
||||||
) -> DetectedIpv6Route {
|
|
||||||
DetectedIpv6Route {
|
|
||||||
dst: dst.map(|cidr| cidr.parse().unwrap()),
|
|
||||||
src: src.map(|cidr| cidr.parse().unwrap()),
|
|
||||||
ifindex,
|
|
||||||
kind,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_detect_public_ipv6_prefix_from_routes_selects_delegated_prefix() {
|
|
||||||
let routes = vec![
|
|
||||||
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
|
||||||
route(Some("2001:db8:1::/56"), None, Some(3), RouteType::Unicast),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
detect_public_ipv6_prefix_from_routes(&routes, 1),
|
|
||||||
Some("2001:db8:1::/56".parse().unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_detect_public_ipv6_prefix_from_routes_rejects_non_public_prefixes() {
|
|
||||||
let routes = vec![
|
|
||||||
route(Some("::/0"), Some("fd00::/48"), Some(2), RouteType::Unicast),
|
|
||||||
route(Some("fd00::/48"), None, Some(3), RouteType::Unicast),
|
|
||||||
route(None, Some("fe80::/64"), Some(4), RouteType::Unicast),
|
|
||||||
route(Some("fe80::/64"), None, Some(5), RouteType::Unicast),
|
|
||||||
route(None, Some("ff00::/8"), Some(6), RouteType::Unicast),
|
|
||||||
route(Some("ff00::/8"), None, Some(7), RouteType::Unicast),
|
|
||||||
route(None, Some("::/0"), Some(8), RouteType::Unicast),
|
|
||||||
route(Some("::/0"), None, Some(9), RouteType::Unicast),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_detect_public_ipv6_prefix_from_routes_requires_delegated_route() {
|
|
||||||
let routes = vec![route(
|
|
||||||
None,
|
|
||||||
Some("2001:db8:1::/56"),
|
|
||||||
Some(2),
|
|
||||||
RouteType::Unicast,
|
|
||||||
)];
|
|
||||||
|
|
||||||
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_detect_public_ipv6_prefix_from_routes_rejects_loopback_delegation() {
|
|
||||||
let routes = vec![
|
|
||||||
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
|
||||||
route(Some("2001:db8:1::/56"), None, Some(1), RouteType::Unicast),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_detect_public_ipv6_prefix_from_routes_prefers_shortest_prefix() {
|
|
||||||
let routes = vec![
|
|
||||||
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
|
||||||
route(Some("2001:db8:1::/56"), None, Some(3), RouteType::Unicast),
|
|
||||||
route(None, Some("2001:db8::/48"), Some(4), RouteType::Unicast),
|
|
||||||
route(Some("2001:db8::/48"), None, Some(5), RouteType::Unicast),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
detect_public_ipv6_prefix_from_routes(&routes, 1),
|
|
||||||
Some("2001:db8::/48".parse().unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_detect_public_ipv6_prefix_from_routes_rejects_non_unicast_delegation() {
|
|
||||||
let routes = vec![
|
|
||||||
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
|
||||||
route(Some("2001:db8:1::/56"), None, Some(3), RouteType::BlackHole),
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_public_ipv6_provider_auto_detect_error_mentions_manual_prefix() {
|
|
||||||
let err = public_ipv6_provider_auto_detect_error();
|
|
||||||
let msg = err.to_string();
|
|
||||||
|
|
||||||
assert!(msg.contains("IPv6 prefix"), "{}", msg);
|
|
||||||
assert!(msg.contains("ipv6-public-addr-prefix"), "{}", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_public_ipv6_provider_platform_check_accepts_linux() {
|
|
||||||
assert!(ensure_public_ipv6_provider_supported().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_ensure_linux_ipv6_forwarding_enables_all_and_default() {
|
|
||||||
let (_dir, all_path, default_path) = temp_forwarding_paths("0\n", "0\n");
|
|
||||||
|
|
||||||
let changed = ensure_linux_ipv6_forwarding_at_paths(&all_path, &default_path).unwrap();
|
|
||||||
|
|
||||||
assert!(changed);
|
|
||||||
assert_eq!(fs::read_to_string(&all_path).unwrap(), "1\n");
|
|
||||||
assert_eq!(fs::read_to_string(&default_path).unwrap(), "1\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[test]
|
|
||||||
fn test_ensure_linux_ipv6_forwarding_is_noop_when_already_enabled() {
|
|
||||||
let (_dir, all_path, default_path) = temp_forwarding_paths("1\n", "1\n");
|
|
||||||
|
|
||||||
let changed = ensure_linux_ipv6_forwarding_at_paths(&all_path, &default_path).unwrap();
|
|
||||||
|
|
||||||
assert!(!changed);
|
|
||||||
assert_eq!(fs::read_to_string(&all_path).unwrap(), "1\n");
|
|
||||||
assert_eq!(fs::read_to_string(&default_path).unwrap(), "1\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
#[test]
|
|
||||||
fn test_public_ipv6_provider_platform_check_reports_linux_only() {
|
|
||||||
let err = ensure_public_ipv6_provider_supported().unwrap_err();
|
|
||||||
let msg = err.to_string();
|
|
||||||
|
|
||||||
assert!(msg.contains("Linux"), "{}", msg);
|
|
||||||
assert!(msg.contains("ipv6-public-addr-auto"), "{}", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[serial_test::serial]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_detect_public_ipv6_prefix_linux_reads_netlink_routes_from_kernel() {
|
|
||||||
let wan_if = test_iface_name("dw");
|
|
||||||
let lan_if = test_iface_name("dl");
|
|
||||||
let _wan = ScopedDummyLink::new(&wan_if);
|
|
||||||
let _lan = ScopedDummyLink::new(&lan_if);
|
|
||||||
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"addr",
|
|
||||||
"add",
|
|
||||||
"2001:db8:100:ffff::1/64",
|
|
||||||
"dev",
|
|
||||||
&wan_if,
|
|
||||||
]);
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"route",
|
|
||||||
"add",
|
|
||||||
"default",
|
|
||||||
"from",
|
|
||||||
"2001:db8:100::/56",
|
|
||||||
"dev",
|
|
||||||
&wan_if,
|
|
||||||
]);
|
|
||||||
run_ip(&["-6", "route", "add", "2001:db8:100::/56", "dev", &lan_if]);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
detect_public_ipv6_prefix_linux().await.unwrap(),
|
|
||||||
Some("2001:db8:100::/56".parse().unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
#[serial_test::serial]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_detect_public_ipv6_prefix_linux_prefers_shortest_prefix_from_kernel() {
|
|
||||||
let wan_if_1 = test_iface_name("sw1");
|
|
||||||
let lan_if_1 = test_iface_name("sl1");
|
|
||||||
let wan_if_2 = test_iface_name("sw2");
|
|
||||||
let lan_if_2 = test_iface_name("sl2");
|
|
||||||
let _wan_1 = ScopedDummyLink::new(&wan_if_1);
|
|
||||||
let _lan_1 = ScopedDummyLink::new(&lan_if_1);
|
|
||||||
let _wan_2 = ScopedDummyLink::new(&wan_if_2);
|
|
||||||
let _lan_2 = ScopedDummyLink::new(&lan_if_2);
|
|
||||||
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"addr",
|
|
||||||
"add",
|
|
||||||
"2001:db8:3000:ffff::1/64",
|
|
||||||
"dev",
|
|
||||||
&wan_if_1,
|
|
||||||
]);
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"route",
|
|
||||||
"add",
|
|
||||||
"default",
|
|
||||||
"from",
|
|
||||||
"2001:db8:3000::/56",
|
|
||||||
"dev",
|
|
||||||
&wan_if_1,
|
|
||||||
]);
|
|
||||||
run_ip(&["-6", "route", "add", "2001:db8:3000::/56", "dev", &lan_if_1]);
|
|
||||||
|
|
||||||
run_ip(&["-6", "addr", "add", "2001:db9:ffff::1/64", "dev", &wan_if_2]);
|
|
||||||
run_ip(&[
|
|
||||||
"-6",
|
|
||||||
"route",
|
|
||||||
"add",
|
|
||||||
"default",
|
|
||||||
"from",
|
|
||||||
"2001:db9::/48",
|
|
||||||
"dev",
|
|
||||||
&wan_if_2,
|
|
||||||
]);
|
|
||||||
run_ip(&["-6", "route", "add", "2001:db9::/48", "dev", &lan_if_2]);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
detect_public_ipv6_prefix_linux().await.unwrap(),
|
|
||||||
Some("2001:db9::/48".parse().unwrap())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ pub mod instance;
|
|||||||
|
|
||||||
pub mod listeners;
|
pub mod listeners;
|
||||||
|
|
||||||
|
mod public_ipv6_provider;
|
||||||
|
|
||||||
pub mod proxy_cidrs_monitor;
|
pub mod proxy_cidrs_monitor;
|
||||||
|
|
||||||
#[cfg(feature = "tun")]
|
#[cfg(feature = "tun")]
|
||||||
|
|||||||
@@ -0,0 +1,905 @@
|
|||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use cidr::Ipv6Cidr;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage, RouteType};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use crate::common::ifcfg::{get_interface_index, list_ipv6_route_messages};
|
||||||
|
use crate::common::{
|
||||||
|
error::Error,
|
||||||
|
global_ctx::{ArcGlobalCtx, GlobalCtxEvent},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PUBLIC_IPV6_PROVIDER_RECONCILE_INTERVAL: std::time::Duration =
|
||||||
|
std::time::Duration::from_secs(5);
|
||||||
|
const PUBLIC_IPV6_PROVIDER_RECONCILE_MAX_RETRIES: usize = 3;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum PublicIpv6ProviderRuntimeState {
|
||||||
|
Disabled,
|
||||||
|
Pending(String),
|
||||||
|
Active(Ipv6Cidr),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: bool,
|
||||||
|
configured_prefix: Option<Ipv6Cidr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_public_ipv6_provider_config_snapshot(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
) -> PublicIpv6ProviderConfigSnapshot {
|
||||||
|
PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: global_ctx.config.get_ipv6_public_addr_provider(),
|
||||||
|
configured_prefix: global_ctx.config.get_ipv6_public_addr_prefix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_run_public_ipv6_provider_reconcile_task(
|
||||||
|
config: PublicIpv6ProviderConfigSnapshot,
|
||||||
|
) -> bool {
|
||||||
|
config.provider_enabled && config.configured_prefix.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_global_routable_public_ipv6_prefix(prefix: Ipv6Cidr) -> bool {
|
||||||
|
let addr = prefix.first_address();
|
||||||
|
!addr.is_loopback()
|
||||||
|
&& !addr.is_multicast()
|
||||||
|
&& !addr.is_unicast_link_local()
|
||||||
|
&& !addr.is_unique_local()
|
||||||
|
&& !addr.is_unspecified()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn validate_public_ipv6_config(global_ctx: &ArcGlobalCtx) -> Result<(), Error> {
|
||||||
|
if global_ctx.config.get_ipv6_public_addr_auto() && global_ctx.get_ipv6().is_some() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"cannot use --ipv6-public-addr-auto together with a manually set --ipv6; pick one or the other"
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !global_ctx.config.get_ipv6_public_addr_provider() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_public_ipv6_provider_supported()?;
|
||||||
|
|
||||||
|
if let Some(prefix) = global_ctx.config.get_ipv6_public_addr_prefix()
|
||||||
|
&& !is_global_routable_public_ipv6_prefix(prefix)
|
||||||
|
{
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"the prefix {} is not a valid global unicast IPv6 prefix; it must be a routable address range, not a private, link-local, or multicast address",
|
||||||
|
prefix
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_public_ipv6_provider_supported() -> Result<(), Error> {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"the provider feature requires Linux; run without --ipv6-public-addr-provider on this node, or move the provider role to a Linux node. client mode (--ipv6-public-addr-auto) works on all platforms"
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn public_ipv6_provider_auto_detect_error() -> Error {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"no public IPv6 prefix found on this system; set --ipv6-public-addr-prefix manually, or check that your ISP has delegated an IPv6 prefix and a default-from route exists in the kernel routing table"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn read_linux_proc_bool(path: &Path) -> Result<bool, Error> {
|
||||||
|
let value = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("failed to read {}", path.display()))?;
|
||||||
|
match value.trim() {
|
||||||
|
"0" => Ok(false),
|
||||||
|
"1" => Ok(true),
|
||||||
|
other => Err(anyhow::anyhow!("unexpected value '{}' in {}", other, path.display()).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn write_linux_proc_bool(path: &Path, enabled: bool) -> Result<(), Error> {
|
||||||
|
let value = if enabled { "1\n" } else { "0\n" };
|
||||||
|
std::fs::write(path, value).with_context(|| format!("failed to write {}", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn ensure_linux_ipv6_forwarding_at_paths(
|
||||||
|
all_path: &Path,
|
||||||
|
default_path: &Path,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let all_enabled = read_linux_proc_bool(all_path)?;
|
||||||
|
let default_enabled = read_linux_proc_bool(default_path)?;
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
if !all_enabled {
|
||||||
|
write_linux_proc_bool(all_path, true)?;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !default_enabled {
|
||||||
|
write_linux_proc_bool(default_path, true)?;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !read_linux_proc_bool(all_path)? || !read_linux_proc_bool(default_path)? {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"failed to enable Linux IPv6 forwarding in {} and {}",
|
||||||
|
all_path.display(),
|
||||||
|
default_path.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(changed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn ensure_linux_ipv6_forwarding() -> Result<bool, Error> {
|
||||||
|
let all_path = Path::new("/proc/sys/net/ipv6/conf/all/forwarding");
|
||||||
|
let default_path = Path::new("/proc/sys/net/ipv6/conf/default/forwarding");
|
||||||
|
|
||||||
|
ensure_linux_ipv6_forwarding_at_paths(all_path, default_path).map_err(|err| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"public IPv6 provider requires Linux IPv6 forwarding; failed to enable net.ipv6.conf.all.forwarding=1 and net.ipv6.conf.default.forwarding=1 automatically: {}. run with sufficient privileges or set them manually",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
struct DetectedIpv6Route {
|
||||||
|
dst: Option<Ipv6Cidr>,
|
||||||
|
src: Option<Ipv6Cidr>,
|
||||||
|
ifindex: Option<u32>,
|
||||||
|
kind: RouteType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn ipv6_cidr_from_route_addr(addr: RouteAddress, prefix_len: u8) -> Option<Ipv6Cidr> {
|
||||||
|
match addr {
|
||||||
|
RouteAddress::Inet6(addr) => Ipv6Cidr::new(addr, prefix_len).ok(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
impl TryFrom<RouteMessage> for DetectedIpv6Route {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(message: RouteMessage) -> Result<Self, Self::Error> {
|
||||||
|
let dst = message.attributes.iter().find_map(|attr| match attr {
|
||||||
|
RouteAttribute::Destination(addr) => {
|
||||||
|
ipv6_cidr_from_route_addr(addr.clone(), message.header.destination_prefix_length)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let src = message.attributes.iter().find_map(|attr| match attr {
|
||||||
|
RouteAttribute::Source(addr) => {
|
||||||
|
ipv6_cidr_from_route_addr(addr.clone(), message.header.source_prefix_length)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let ifindex = message.attributes.iter().find_map(|attr| match attr {
|
||||||
|
RouteAttribute::Oif(index) => Some(*index),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
dst,
|
||||||
|
src,
|
||||||
|
ifindex,
|
||||||
|
kind: message.header.kind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn is_ipv6_default_route(dst: Option<Ipv6Cidr>) -> bool {
|
||||||
|
dst.is_none() || dst == Some(Ipv6Cidr::new(std::net::Ipv6Addr::UNSPECIFIED, 0).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn detect_public_ipv6_prefix_from_routes(
|
||||||
|
routes: &[DetectedIpv6Route],
|
||||||
|
loopback_ifindex: u32,
|
||||||
|
) -> Option<Ipv6Cidr> {
|
||||||
|
routes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|route| {
|
||||||
|
if !is_ipv6_default_route(route.dst) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix = route.src?;
|
||||||
|
let wan_ifindex = route.ifindex?;
|
||||||
|
if !is_global_routable_public_ipv6_prefix(prefix) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delegated = routes.iter().any(|candidate| {
|
||||||
|
candidate.dst == Some(prefix)
|
||||||
|
&& candidate.ifindex.is_some()
|
||||||
|
&& candidate.ifindex != Some(wan_ifindex)
|
||||||
|
&& candidate.ifindex != Some(loopback_ifindex)
|
||||||
|
&& candidate.kind == RouteType::Unicast
|
||||||
|
});
|
||||||
|
|
||||||
|
delegated.then_some(prefix)
|
||||||
|
})
|
||||||
|
.min_by_key(|prefix| prefix.network_length())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn detect_public_ipv6_prefix_linux() -> Result<Option<Ipv6Cidr>, Error> {
|
||||||
|
let routes = list_ipv6_route_messages().with_context(|| "failed to query linux ipv6 routes")?;
|
||||||
|
let routes = routes
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(DetectedIpv6Route::try_from)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let loopback_ifindex =
|
||||||
|
get_interface_index("lo").with_context(|| "failed to resolve linux loopback ifindex")?;
|
||||||
|
|
||||||
|
Ok(detect_public_ipv6_prefix_from_routes(
|
||||||
|
&routes,
|
||||||
|
loopback_ifindex,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
async fn detect_public_ipv6_prefix_linux() -> Result<Option<Ipv6Cidr>, Error> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_public_ipv6_prefix_state(
|
||||||
|
prefix: Ipv6Cidr,
|
||||||
|
source: &str,
|
||||||
|
) -> PublicIpv6ProviderRuntimeState {
|
||||||
|
PublicIpv6ProviderRuntimeState::Pending(format!(
|
||||||
|
"the {} prefix {} is not a valid global unicast IPv6 prefix",
|
||||||
|
source, prefix
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn resolve_public_ipv6_provider_runtime_state_linux(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
configured_prefix: Option<Ipv6Cidr>,
|
||||||
|
) -> PublicIpv6ProviderRuntimeState {
|
||||||
|
let _g = global_ctx.net_ns.guard();
|
||||||
|
|
||||||
|
if let Err(err) = ensure_linux_ipv6_forwarding() {
|
||||||
|
return PublicIpv6ProviderRuntimeState::Pending(err.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(prefix) = configured_prefix {
|
||||||
|
if !is_global_routable_public_ipv6_prefix(prefix) {
|
||||||
|
return invalid_public_ipv6_prefix_state(prefix, "configured");
|
||||||
|
}
|
||||||
|
return PublicIpv6ProviderRuntimeState::Active(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
match detect_public_ipv6_prefix_linux().await {
|
||||||
|
Ok(Some(prefix)) if is_global_routable_public_ipv6_prefix(prefix) => {
|
||||||
|
PublicIpv6ProviderRuntimeState::Active(prefix)
|
||||||
|
}
|
||||||
|
Ok(Some(prefix)) => invalid_public_ipv6_prefix_state(prefix, "detected"),
|
||||||
|
Ok(None) => PublicIpv6ProviderRuntimeState::Pending(
|
||||||
|
public_ipv6_provider_auto_detect_error().to_string(),
|
||||||
|
),
|
||||||
|
Err(err) => PublicIpv6ProviderRuntimeState::Pending(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_public_ipv6_provider_runtime_state(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
config: PublicIpv6ProviderConfigSnapshot,
|
||||||
|
) -> PublicIpv6ProviderRuntimeState {
|
||||||
|
if !config.provider_enabled {
|
||||||
|
return PublicIpv6ProviderRuntimeState::Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
return resolve_public_ipv6_provider_runtime_state_linux(
|
||||||
|
global_ctx,
|
||||||
|
config.configured_prefix,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let _ = config.configured_prefix;
|
||||||
|
PublicIpv6ProviderRuntimeState::Pending(
|
||||||
|
ensure_public_ipv6_provider_supported()
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_public_ipv6_provider_runtime_state(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
state: &PublicIpv6ProviderRuntimeState,
|
||||||
|
) -> bool {
|
||||||
|
let next_prefix = match state {
|
||||||
|
PublicIpv6ProviderRuntimeState::Active(prefix) => Some(*prefix),
|
||||||
|
PublicIpv6ProviderRuntimeState::Disabled | PublicIpv6ProviderRuntimeState::Pending(_) => {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let prefix_changed = global_ctx.set_advertised_ipv6_public_addr_prefix(next_prefix);
|
||||||
|
|
||||||
|
let next_provider_enabled = matches!(state, PublicIpv6ProviderRuntimeState::Active(_));
|
||||||
|
let feature_changed = {
|
||||||
|
let mut feature_flags = global_ctx.get_feature_flags();
|
||||||
|
if feature_flags.ipv6_public_addr_provider == next_provider_enabled {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
feature_flags.ipv6_public_addr_provider = next_provider_enabled;
|
||||||
|
global_ctx.set_feature_flags(feature_flags);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
prefix_changed || feature_changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_apply_public_ipv6_provider_runtime_state(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
config: PublicIpv6ProviderConfigSnapshot,
|
||||||
|
state: &PublicIpv6ProviderRuntimeState,
|
||||||
|
) -> Option<bool> {
|
||||||
|
(read_public_ipv6_provider_config_snapshot(global_ctx) == config)
|
||||||
|
.then(|| apply_public_ipv6_provider_runtime_state(global_ctx, state))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_public_ipv6_provider_runtime_state(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
) -> PublicIpv6ProviderRuntimeState {
|
||||||
|
match (
|
||||||
|
global_ctx.get_feature_flags().ipv6_public_addr_provider,
|
||||||
|
global_ctx.get_advertised_ipv6_public_addr_prefix(),
|
||||||
|
) {
|
||||||
|
(false, _) => PublicIpv6ProviderRuntimeState::Disabled,
|
||||||
|
(true, Some(prefix)) => PublicIpv6ProviderRuntimeState::Active(prefix),
|
||||||
|
(true, None) => PublicIpv6ProviderRuntimeState::Pending(
|
||||||
|
"public IPv6 provider runtime is missing an advertised prefix".to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reconcile_public_ipv6_provider_runtime_with_state(
|
||||||
|
global_ctx: &ArcGlobalCtx,
|
||||||
|
) -> (PublicIpv6ProviderRuntimeState, bool) {
|
||||||
|
for attempt in 0..PUBLIC_IPV6_PROVIDER_RECONCILE_MAX_RETRIES {
|
||||||
|
let config = read_public_ipv6_provider_config_snapshot(global_ctx);
|
||||||
|
let next_state = resolve_public_ipv6_provider_runtime_state(global_ctx, config).await;
|
||||||
|
|
||||||
|
if let Some(changed) =
|
||||||
|
try_apply_public_ipv6_provider_runtime_state(global_ctx, config, &next_state)
|
||||||
|
{
|
||||||
|
return (next_state, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
attempt = attempt + 1,
|
||||||
|
max_retries = PUBLIC_IPV6_PROVIDER_RECONCILE_MAX_RETRIES,
|
||||||
|
"public IPv6 provider config changed during reconcile, retrying"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::warn!(
|
||||||
|
max_retries = PUBLIC_IPV6_PROVIDER_RECONCILE_MAX_RETRIES,
|
||||||
|
"skipping public IPv6 provider reconcile because config kept changing"
|
||||||
|
);
|
||||||
|
(
|
||||||
|
current_public_ipv6_provider_runtime_state(global_ctx),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn reconcile_public_ipv6_provider_runtime(global_ctx: &ArcGlobalCtx) -> bool {
|
||||||
|
reconcile_public_ipv6_provider_runtime_with_state(global_ctx)
|
||||||
|
.await
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn run_public_ipv6_provider_reconcile_task(global_ctx: &ArcGlobalCtx) {
|
||||||
|
if !should_run_public_ipv6_provider_reconcile_task(read_public_ipv6_provider_config_snapshot(
|
||||||
|
global_ctx,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let global_ctx = Arc::downgrade(global_ctx);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let Some(initial_ctx) = global_ctx.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut event_receiver = initial_ctx.subscribe();
|
||||||
|
let mut last_state: Option<PublicIpv6ProviderRuntimeState> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(global_ctx) = global_ctx.upgrade() else {
|
||||||
|
tracing::debug!("global ctx dropped, stopping public ipv6 provider reconcile");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (next_state, changed) =
|
||||||
|
reconcile_public_ipv6_provider_runtime_with_state(&global_ctx).await;
|
||||||
|
if last_state.as_ref() != Some(&next_state) {
|
||||||
|
match &next_state {
|
||||||
|
PublicIpv6ProviderRuntimeState::Disabled if last_state.is_some() => {
|
||||||
|
tracing::info!("public IPv6 provider disabled");
|
||||||
|
}
|
||||||
|
PublicIpv6ProviderRuntimeState::Disabled => {}
|
||||||
|
PublicIpv6ProviderRuntimeState::Pending(reason) => {
|
||||||
|
tracing::warn!(reason = %reason, "public IPv6 provider not ready");
|
||||||
|
}
|
||||||
|
PublicIpv6ProviderRuntimeState::Active(prefix) => {
|
||||||
|
tracing::info!(prefix = %prefix, "public IPv6 provider is active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if changed {
|
||||||
|
tracing::info!("public IPv6 provider runtime state changed");
|
||||||
|
}
|
||||||
|
last_state = Some(next_state);
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
last_state.as_ref(),
|
||||||
|
Some(PublicIpv6ProviderRuntimeState::Disabled)
|
||||||
|
) {
|
||||||
|
match event_receiver.recv().await {
|
||||||
|
Ok(GlobalCtxEvent::ConfigPatched(_)) => {}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||||
|
event_receiver = event_receiver.resubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokio::select! {
|
||||||
|
recv = event_receiver.recv() => match recv {
|
||||||
|
Ok(GlobalCtxEvent::ConfigPatched(_)) => {}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||||
|
event_receiver = event_receiver.resubscribe();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = tokio::time::sleep(PUBLIC_IPV6_PROVIDER_RECONCILE_INTERVAL) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::fs;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::path::PathBuf;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use std::process::Command;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use netlink_packet_route::route::RouteType;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use super::{
|
||||||
|
DetectedIpv6Route, detect_public_ipv6_prefix_from_routes, detect_public_ipv6_prefix_linux,
|
||||||
|
ensure_linux_ipv6_forwarding_at_paths, ensure_public_ipv6_provider_supported,
|
||||||
|
public_ipv6_provider_auto_detect_error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
PublicIpv6ProviderConfigSnapshot, PublicIpv6ProviderRuntimeState,
|
||||||
|
read_public_ipv6_provider_config_snapshot, should_run_public_ipv6_provider_reconcile_task,
|
||||||
|
try_apply_public_ipv6_provider_runtime_state,
|
||||||
|
};
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
use super::{ensure_public_ipv6_provider_supported, public_ipv6_provider_auto_detect_error};
|
||||||
|
use crate::common::{
|
||||||
|
config::{ConfigLoader, TomlConfigLoader},
|
||||||
|
global_ctx::GlobalCtx,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn run_ip(args: &[&str]) {
|
||||||
|
let output = Command::new("ip")
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.expect("failed to execute ip process");
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"ip command failed: {:?}\nstdout: {}\nstderr: {}",
|
||||||
|
args,
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn test_iface_name(tag: &str) -> String {
|
||||||
|
format!("et{}{:x}", tag, std::process::id() & 0xffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
struct ScopedDummyLink {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
impl ScopedDummyLink {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
let _ = Command::new("ip").args(["link", "del", name]).output();
|
||||||
|
run_ip(&["link", "add", name, "type", "dummy"]);
|
||||||
|
run_ip(&["link", "set", name, "up"]);
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
impl Drop for ScopedDummyLink {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = Command::new("ip")
|
||||||
|
.args(["link", "del", &self.name])
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn temp_forwarding_paths(
|
||||||
|
all_value: &str,
|
||||||
|
default_value: &str,
|
||||||
|
) -> (tempfile::TempDir, PathBuf, PathBuf) {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let all_path = dir.path().join("all_forwarding");
|
||||||
|
let default_path = dir.path().join("default_forwarding");
|
||||||
|
fs::write(&all_path, all_value).unwrap();
|
||||||
|
fs::write(&default_path, default_value).unwrap();
|
||||||
|
(dir, all_path, default_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn route(
|
||||||
|
dst: Option<&str>,
|
||||||
|
src: Option<&str>,
|
||||||
|
ifindex: Option<u32>,
|
||||||
|
kind: RouteType,
|
||||||
|
) -> DetectedIpv6Route {
|
||||||
|
DetectedIpv6Route {
|
||||||
|
dst: dst.map(|cidr| cidr.parse().unwrap()),
|
||||||
|
src: src.map(|cidr| cidr.parse().unwrap()),
|
||||||
|
ifindex,
|
||||||
|
kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_detect_public_ipv6_prefix_from_routes_selects_delegated_prefix() {
|
||||||
|
let routes = vec![
|
||||||
|
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
||||||
|
route(Some("2001:db8:1::/56"), None, Some(3), RouteType::Unicast),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
detect_public_ipv6_prefix_from_routes(&routes, 1),
|
||||||
|
Some("2001:db8:1::/56".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_detect_public_ipv6_prefix_from_routes_rejects_non_public_prefixes() {
|
||||||
|
let routes = vec![
|
||||||
|
route(Some("::/0"), Some("fd00::/48"), Some(2), RouteType::Unicast),
|
||||||
|
route(Some("fd00::/48"), None, Some(3), RouteType::Unicast),
|
||||||
|
route(None, Some("fe80::/64"), Some(4), RouteType::Unicast),
|
||||||
|
route(Some("fe80::/64"), None, Some(5), RouteType::Unicast),
|
||||||
|
route(None, Some("ff00::/8"), Some(6), RouteType::Unicast),
|
||||||
|
route(Some("ff00::/8"), None, Some(7), RouteType::Unicast),
|
||||||
|
route(None, Some("::/0"), Some(8), RouteType::Unicast),
|
||||||
|
route(Some("::/0"), None, Some(9), RouteType::Unicast),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_detect_public_ipv6_prefix_from_routes_requires_delegated_route() {
|
||||||
|
let routes = vec![route(
|
||||||
|
None,
|
||||||
|
Some("2001:db8:1::/56"),
|
||||||
|
Some(2),
|
||||||
|
RouteType::Unicast,
|
||||||
|
)];
|
||||||
|
|
||||||
|
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_detect_public_ipv6_prefix_from_routes_rejects_loopback_delegation() {
|
||||||
|
let routes = vec![
|
||||||
|
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
||||||
|
route(Some("2001:db8:1::/56"), None, Some(1), RouteType::Unicast),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_detect_public_ipv6_prefix_from_routes_prefers_shortest_prefix() {
|
||||||
|
let routes = vec![
|
||||||
|
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
||||||
|
route(Some("2001:db8:1::/56"), None, Some(3), RouteType::Unicast),
|
||||||
|
route(None, Some("2001:db8::/48"), Some(4), RouteType::Unicast),
|
||||||
|
route(Some("2001:db8::/48"), None, Some(5), RouteType::Unicast),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
detect_public_ipv6_prefix_from_routes(&routes, 1),
|
||||||
|
Some("2001:db8::/48".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_detect_public_ipv6_prefix_from_routes_rejects_non_unicast_delegation() {
|
||||||
|
let routes = vec![
|
||||||
|
route(None, Some("2001:db8:1::/56"), Some(2), RouteType::Unicast),
|
||||||
|
route(Some("2001:db8:1::/56"), None, Some(3), RouteType::BlackHole),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(detect_public_ipv6_prefix_from_routes(&routes, 1), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_public_ipv6_provider_auto_detect_error_mentions_manual_prefix() {
|
||||||
|
let err = public_ipv6_provider_auto_detect_error();
|
||||||
|
let msg = err.to_string();
|
||||||
|
|
||||||
|
assert!(msg.contains("IPv6 prefix"), "{}", msg);
|
||||||
|
assert!(msg.contains("ipv6-public-addr-prefix"), "{}", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_global_ctx() -> Arc<GlobalCtx> {
|
||||||
|
Arc::new(GlobalCtx::new(TomlConfigLoader::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_read_public_ipv6_provider_config_snapshot_reads_provider_fields() {
|
||||||
|
let global_ctx = test_global_ctx();
|
||||||
|
let prefix = "2001:db8::/48".parse().unwrap();
|
||||||
|
global_ctx.config.set_ipv6_public_addr_provider(true);
|
||||||
|
global_ctx.config.set_ipv6_public_addr_prefix(Some(prefix));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
read_public_ipv6_provider_config_snapshot(&global_ctx),
|
||||||
|
PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: true,
|
||||||
|
configured_prefix: Some(prefix),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reconcile_task_only_runs_for_auto_detect_provider() {
|
||||||
|
assert!(!should_run_public_ipv6_provider_reconcile_task(
|
||||||
|
PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: false,
|
||||||
|
configured_prefix: None,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
assert!(!should_run_public_ipv6_provider_reconcile_task(
|
||||||
|
PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: true,
|
||||||
|
configured_prefix: Some("2001:db8::/48".parse().unwrap()),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
assert!(should_run_public_ipv6_provider_reconcile_task(
|
||||||
|
PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: true,
|
||||||
|
configured_prefix: None,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_try_apply_public_ipv6_provider_runtime_state_rejects_stale_config() {
|
||||||
|
let global_ctx = test_global_ctx();
|
||||||
|
let prefix = "2001:db8::/48".parse().unwrap();
|
||||||
|
let config = PublicIpv6ProviderConfigSnapshot {
|
||||||
|
provider_enabled: true,
|
||||||
|
configured_prefix: Some(prefix),
|
||||||
|
};
|
||||||
|
|
||||||
|
global_ctx.config.set_ipv6_public_addr_provider(false);
|
||||||
|
global_ctx.config.set_ipv6_public_addr_prefix(None);
|
||||||
|
|
||||||
|
let changed = try_apply_public_ipv6_provider_runtime_state(
|
||||||
|
&global_ctx,
|
||||||
|
config,
|
||||||
|
&PublicIpv6ProviderRuntimeState::Active(prefix),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(changed, None);
|
||||||
|
assert_eq!(global_ctx.get_advertised_ipv6_public_addr_prefix(), None);
|
||||||
|
assert!(!global_ctx.get_feature_flags().ipv6_public_addr_provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_try_apply_public_ipv6_provider_runtime_state_applies_matching_config() {
|
||||||
|
let global_ctx = test_global_ctx();
|
||||||
|
let prefix = "2001:db8::/48".parse().unwrap();
|
||||||
|
global_ctx.config.set_ipv6_public_addr_provider(true);
|
||||||
|
global_ctx.config.set_ipv6_public_addr_prefix(Some(prefix));
|
||||||
|
let config = read_public_ipv6_provider_config_snapshot(&global_ctx);
|
||||||
|
|
||||||
|
let changed = try_apply_public_ipv6_provider_runtime_state(
|
||||||
|
&global_ctx,
|
||||||
|
config,
|
||||||
|
&PublicIpv6ProviderRuntimeState::Active(prefix),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(changed, Some(true));
|
||||||
|
assert_eq!(
|
||||||
|
global_ctx.get_advertised_ipv6_public_addr_prefix(),
|
||||||
|
Some(prefix)
|
||||||
|
);
|
||||||
|
assert!(global_ctx.get_feature_flags().ipv6_public_addr_provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_public_ipv6_provider_platform_check_accepts_linux() {
|
||||||
|
assert!(ensure_public_ipv6_provider_supported().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_linux_ipv6_forwarding_enables_all_and_default() {
|
||||||
|
let (_dir, all_path, default_path) = temp_forwarding_paths("0\n", "0\n");
|
||||||
|
|
||||||
|
let changed = ensure_linux_ipv6_forwarding_at_paths(&all_path, &default_path).unwrap();
|
||||||
|
|
||||||
|
assert!(changed);
|
||||||
|
assert_eq!(fs::read_to_string(&all_path).unwrap(), "1\n");
|
||||||
|
assert_eq!(fs::read_to_string(&default_path).unwrap(), "1\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn test_ensure_linux_ipv6_forwarding_is_noop_when_already_enabled() {
|
||||||
|
let (_dir, all_path, default_path) = temp_forwarding_paths("1\n", "1\n");
|
||||||
|
|
||||||
|
let changed = ensure_linux_ipv6_forwarding_at_paths(&all_path, &default_path).unwrap();
|
||||||
|
|
||||||
|
assert!(!changed);
|
||||||
|
assert_eq!(fs::read_to_string(&all_path).unwrap(), "1\n");
|
||||||
|
assert_eq!(fs::read_to_string(&default_path).unwrap(), "1\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
#[test]
|
||||||
|
fn test_public_ipv6_provider_platform_check_reports_linux_only() {
|
||||||
|
let err = ensure_public_ipv6_provider_supported().unwrap_err();
|
||||||
|
let msg = err.to_string();
|
||||||
|
|
||||||
|
assert!(msg.contains("Linux"), "{}", msg);
|
||||||
|
assert!(msg.contains("ipv6-public-addr-auto"), "{}", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[serial_test::serial]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_detect_public_ipv6_prefix_linux_reads_netlink_routes_from_kernel() {
|
||||||
|
let wan_if = test_iface_name("dw");
|
||||||
|
let lan_if = test_iface_name("dl");
|
||||||
|
let _wan = ScopedDummyLink::new(&wan_if);
|
||||||
|
let _lan = ScopedDummyLink::new(&lan_if);
|
||||||
|
|
||||||
|
run_ip(&[
|
||||||
|
"-6",
|
||||||
|
"addr",
|
||||||
|
"add",
|
||||||
|
"2001:db8:100:ffff::1/64",
|
||||||
|
"dev",
|
||||||
|
&wan_if,
|
||||||
|
]);
|
||||||
|
run_ip(&[
|
||||||
|
"-6",
|
||||||
|
"route",
|
||||||
|
"add",
|
||||||
|
"default",
|
||||||
|
"from",
|
||||||
|
"2001:db8:100::/56",
|
||||||
|
"dev",
|
||||||
|
&wan_if,
|
||||||
|
]);
|
||||||
|
run_ip(&["-6", "route", "add", "2001:db8:100::/56", "dev", &lan_if]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
detect_public_ipv6_prefix_linux().await.unwrap(),
|
||||||
|
Some("2001:db8:100::/56".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[serial_test::serial]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_detect_public_ipv6_prefix_linux_prefers_shortest_prefix_from_kernel() {
|
||||||
|
let wan_if_1 = test_iface_name("sw1");
|
||||||
|
let lan_if_1 = test_iface_name("sl1");
|
||||||
|
let wan_if_2 = test_iface_name("sw2");
|
||||||
|
let lan_if_2 = test_iface_name("sl2");
|
||||||
|
let _wan_1 = ScopedDummyLink::new(&wan_if_1);
|
||||||
|
let _lan_1 = ScopedDummyLink::new(&lan_if_1);
|
||||||
|
let _wan_2 = ScopedDummyLink::new(&wan_if_2);
|
||||||
|
let _lan_2 = ScopedDummyLink::new(&lan_if_2);
|
||||||
|
|
||||||
|
run_ip(&[
|
||||||
|
"-6",
|
||||||
|
"addr",
|
||||||
|
"add",
|
||||||
|
"2001:db8:3000:ffff::1/64",
|
||||||
|
"dev",
|
||||||
|
&wan_if_1,
|
||||||
|
]);
|
||||||
|
run_ip(&[
|
||||||
|
"-6",
|
||||||
|
"route",
|
||||||
|
"add",
|
||||||
|
"default",
|
||||||
|
"from",
|
||||||
|
"2001:db8:3000::/56",
|
||||||
|
"dev",
|
||||||
|
&wan_if_1,
|
||||||
|
]);
|
||||||
|
run_ip(&["-6", "route", "add", "2001:db8:3000::/56", "dev", &lan_if_1]);
|
||||||
|
|
||||||
|
run_ip(&["-6", "addr", "add", "2001:db9:ffff::1/64", "dev", &wan_if_2]);
|
||||||
|
run_ip(&[
|
||||||
|
"-6",
|
||||||
|
"route",
|
||||||
|
"add",
|
||||||
|
"default",
|
||||||
|
"from",
|
||||||
|
"2001:db9::/48",
|
||||||
|
"dev",
|
||||||
|
&wan_if_2,
|
||||||
|
]);
|
||||||
|
run_ip(&["-6", "route", "add", "2001:db9::/48", "dev", &lan_if_2]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
detect_public_ipv6_prefix_linux().await.unwrap(),
|
||||||
|
Some("2001:db9::/48".parse().unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1888,6 +1888,14 @@ impl PeerManager {
|
|||||||
feature_flag: Some(self.global_ctx.get_feature_flags()),
|
feature_flag: Some(self.global_ctx.get_feature_flags()),
|
||||||
ip_list: Some(self.global_ctx.get_ip_collector().collect_ip_addrs().await),
|
ip_list: Some(self.global_ctx.get_ip_collector().collect_ip_addrs().await),
|
||||||
public_ipv6_addr: self.get_my_public_ipv6_addr().await.map(Into::into),
|
public_ipv6_addr: self.get_my_public_ipv6_addr().await.map(Into::into),
|
||||||
|
ipv6_public_addr_prefix: self
|
||||||
|
.global_ctx
|
||||||
|
.get_advertised_ipv6_public_addr_prefix()
|
||||||
|
.map(|prefix| {
|
||||||
|
cidr::Ipv6Inet::new(prefix.first_address(), prefix.network_length())
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ impl RoutePeerInfo {
|
|||||||
.unwrap_or(24),
|
.unwrap_or(24),
|
||||||
|
|
||||||
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
|
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
|
||||||
ipv6_public_addr_prefix: global_ctx.config.get_ipv6_public_addr_prefix().map(
|
ipv6_public_addr_prefix: global_ctx.get_advertised_ipv6_public_addr_prefix().map(
|
||||||
|prefix| {
|
|prefix| {
|
||||||
Ipv6Inet::new(prefix.first_address(), prefix.network_length())
|
Ipv6Inet::new(prefix.first_address(), prefix.network_length())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ message NodeInfo {
|
|||||||
common.PeerFeatureFlag feature_flag = 10;
|
common.PeerFeatureFlag feature_flag = 10;
|
||||||
peer_rpc.GetIpListResponse ip_list = 11;
|
peer_rpc.GetIpListResponse ip_list = 11;
|
||||||
common.Ipv6Inet public_ipv6_addr = 12;
|
common.Ipv6Inet public_ipv6_addr = 12;
|
||||||
|
common.Ipv6Inet ipv6_public_addr_prefix = 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ShowNodeInfoRequest { InstanceIdentifier instance = 1; }
|
message ShowNodeInfoRequest { InstanceIdentifier instance = 1; }
|
||||||
|
|||||||
@@ -766,8 +766,7 @@ pub async fn public_ipv6_auto_addr_end_to_end() {
|
|||||||
|| async {
|
|| async {
|
||||||
provider
|
provider
|
||||||
.get_global_ctx()
|
.get_global_ctx()
|
||||||
.config
|
.get_advertised_ipv6_public_addr_prefix()
|
||||||
.get_ipv6_public_addr_prefix()
|
|
||||||
== Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
== Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
||||||
},
|
},
|
||||||
Duration::from_secs(10),
|
Duration::from_secs(10),
|
||||||
@@ -782,8 +781,32 @@ pub async fn public_ipv6_auto_addr_end_to_end() {
|
|||||||
.get_global_ctx()
|
.get_global_ctx()
|
||||||
.config
|
.config
|
||||||
.get_ipv6_public_addr_prefix(),
|
.get_ipv6_public_addr_prefix(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
provider
|
||||||
|
.get_global_ctx()
|
||||||
|
.get_advertised_ipv6_public_addr_prefix(),
|
||||||
Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
|
||||||
);
|
);
|
||||||
|
let provider_prefix = PublicIpv6Lab::PROVIDER_PREFIX
|
||||||
|
.parse::<cidr::Ipv6Cidr>()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
provider
|
||||||
|
.get_peer_manager()
|
||||||
|
.get_my_info()
|
||||||
|
.await
|
||||||
|
.ipv6_public_addr_prefix,
|
||||||
|
Some(
|
||||||
|
cidr::Ipv6Inet::new(
|
||||||
|
provider_prefix.first_address(),
|
||||||
|
provider_prefix.network_length()
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
leased.address().segments()[0] & 0xfe00 != 0xfc00,
|
leased.address().segments()[0] & 0xfe00 != 0xfc00,
|
||||||
"leased address should not be unique-local: {leased}"
|
"leased address should not be unique-local: {leased}"
|
||||||
|
|||||||
Reference in New Issue
Block a user