feat: support allocating public IPv6 addresses from a provider (#2162)

* feat: support allocating public IPv6 addresses from a provider

Add a provider/leaser architecture for public IPv6 address allocation
between nodes in the same network:

- A node with `--ipv6-public-addr-provider` advertises a delegable
  public IPv6 prefix (auto-detected from kernel routes or manually
  configured via `--ipv6-public-addr-prefix`).
- Other nodes with `--ipv6-public-addr-auto` request a /128 lease from
  the selected provider via a new RPC service (PublicIpv6AddrRpc).
- Leases have a 30s TTL, renewed every 10s by the client routine.
- The provider allocates addresses deterministically from its prefix
  using instance-UUID-based hashing to prefer stable assignments.
- Routes to peer leases are installed on the TUN device, and each
  client's own /128 is assigned as its IPv6 address.

Also includes netlink IPv6 route table inspection, integration tests,
and event-driven route/address reconciliation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
KKRainbow
2026-04-26 21:37:34 +08:00
committed by GitHub
parent b20075e3dc
commit 8f862997eb
30 changed files with 3973 additions and 69 deletions
+9
View File
@@ -39,6 +39,15 @@ core_clap:
ipv6:
en: "ipv6 address of this vpn node, can be used together with ipv4 for dual-stack operation"
zh-CN: "此VPN节点的IPv6地址,可与IPv4一起使用以进行双栈操作"
ipv6_public_addr_provider:
en: "share this node's public IPv6 subnet with other peers so they can obtain public IPv6 addresses (Linux only)"
zh-CN: "将此节点的公网 IPv6 子网共享给其他节点,使它们也能获得公网 IPv6 地址(仅 Linux 支持)"
ipv6_public_addr_auto:
en: "auto-obtain a public IPv6 address from a peer that shares its IPv6 subnet"
zh-CN: "自动从共享了 IPv6 子网的对等节点获取一个公网 IPv6 地址"
ipv6_public_addr_prefix:
en: "manually specify the public IPv6 subnet to share, instead of auto-detecting from system routes"
zh-CN: "手动指定要共享的公网 IPv6 子网,不自动从系统路由检测"
dhcp:
en: "automatically determine and set IP address by Easytier, and the IP address starts from 10.0.0.1 by default. Warning, if there is an IP conflict in the network when using DHCP, the IP will be automatically changed."
zh-CN: "由Easytier自动确定并设置IP地址,默认从10.0.0.1开始。警告:在使用DHCP时,如果网络中出现IP冲突,IP将自动更改。"
+69
View File
@@ -170,6 +170,15 @@ pub trait ConfigLoader: Send + Sync {
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet>;
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>);
fn get_ipv6_public_addr_provider(&self) -> bool;
fn set_ipv6_public_addr_provider(&self, enabled: bool);
fn get_ipv6_public_addr_auto(&self) -> bool;
fn set_ipv6_public_addr_auto(&self, enabled: bool);
fn get_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr>;
fn set_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>);
fn get_dhcp(&self) -> bool;
fn set_dhcp(&self, dhcp: bool);
@@ -519,6 +528,9 @@ struct Config {
instance_id: Option<uuid::Uuid>,
ipv4: Option<String>,
ipv6: Option<String>,
ipv6_public_addr_provider: Option<bool>,
ipv6_public_addr_auto: Option<bool>,
ipv6_public_addr_prefix: Option<String>,
dhcp: Option<bool>,
network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>,
@@ -700,6 +712,43 @@ impl ConfigLoader for TomlConfigLoader {
self.config.lock().unwrap().ipv6 = addr.map(|addr| addr.to_string());
}
fn get_ipv6_public_addr_provider(&self) -> bool {
self.config
.lock()
.unwrap()
.ipv6_public_addr_provider
.unwrap_or_default()
}
fn set_ipv6_public_addr_provider(&self, enabled: bool) {
self.config.lock().unwrap().ipv6_public_addr_provider = Some(enabled);
}
fn get_ipv6_public_addr_auto(&self) -> bool {
self.config
.lock()
.unwrap()
.ipv6_public_addr_auto
.unwrap_or_default()
}
fn set_ipv6_public_addr_auto(&self, enabled: bool) {
self.config.lock().unwrap().ipv6_public_addr_auto = Some(enabled);
}
fn get_ipv6_public_addr_prefix(&self) -> Option<cidr::Ipv6Cidr> {
let locked_config = self.config.lock().unwrap();
locked_config
.ipv6_public_addr_prefix
.as_ref()
.and_then(|s| s.parse().ok())
}
fn set_ipv6_public_addr_prefix(&self, prefix: Option<cidr::Ipv6Cidr>) {
self.config.lock().unwrap().ipv6_public_addr_prefix =
prefix.map(|prefix| prefix.to_string());
}
fn get_dhcp(&self) -> bool {
self.config.lock().unwrap().dhcp.unwrap_or_default()
}
@@ -1312,6 +1361,26 @@ source = "user"
assert!(!explicit_user.dump().contains("[source]"));
}
#[test]
fn test_ipv6_public_addr_config_roundtrip() {
let config = TomlConfigLoader::default();
let prefix: cidr::Ipv6Cidr = "2001:db8:100::/64".parse().unwrap();
config.set_ipv6_public_addr_provider(true);
config.set_ipv6_public_addr_auto(true);
config.set_ipv6_public_addr_prefix(Some(prefix));
assert!(config.get_ipv6_public_addr_provider());
assert!(config.get_ipv6_public_addr_auto());
assert_eq!(config.get_ipv6_public_addr_prefix(), Some(prefix));
let dumped = config.dump();
let loaded = TomlConfigLoader::new_from_str(&dumped).unwrap();
assert!(loaded.get_ipv6_public_addr_provider());
assert!(loaded.get_ipv6_public_addr_auto());
assert_eq!(loaded.get_ipv6_public_addr_prefix(), Some(prefix));
}
#[tokio::test]
async fn full_example_test() {
let config_str = r#"
+77 -6
View File
@@ -68,6 +68,8 @@ pub enum GlobalCtxEvent {
DhcpIpv4Changed(Option<cidr::Ipv4Inet>, Option<cidr::Ipv4Inet>), // (old, new)
DhcpIpv4Conflicted(Option<cidr::Ipv4Inet>),
PublicIpv6Changed(Option<cidr::Ipv6Inet>, Option<cidr::Ipv6Inet>), // (old, new)
PublicIpv6RoutesUpdated(Vec<cidr::Ipv6Inet>, Vec<cidr::Ipv6Inet>), // (added, removed)
PortForwardAdded(PortForwardConfigPb),
@@ -200,6 +202,7 @@ pub struct GlobalCtx {
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
public_ipv6_lease: AtomicCell<Option<cidr::Ipv6Inet>>,
cached_proxy_cidrs: AtomicCell<Option<Vec<ProxyNetworkConfig>>>,
ip_collector: Mutex<Option<Arc<IPCollector>>>,
@@ -209,6 +212,7 @@ pub struct GlobalCtx {
stun_info_collection: Mutex<Arc<dyn StunInfoCollectorTrait>>,
running_listeners: Mutex<Vec<url::Url>>,
advertised_ipv6_public_addr_prefix: Mutex<Option<cidr::Ipv6Cidr>>,
flags: ArcSwap<Flags>,
@@ -295,6 +299,7 @@ impl GlobalCtx {
event_bus,
cached_ipv4: AtomicCell::new(None),
cached_ipv6: AtomicCell::new(None),
public_ipv6_lease: AtomicCell::new(None),
cached_proxy_cidrs: AtomicCell::new(None),
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
@@ -307,6 +312,7 @@ impl GlobalCtx {
stun_info_collection: Mutex::new(stun_info_collector),
running_listeners: Mutex::new(Vec::new()),
advertised_ipv6_public_addr_prefix: Mutex::new(None),
flags: ArcSwap::new(Arc::new(flags)),
@@ -381,6 +387,36 @@ impl GlobalCtx {
self.cached_ipv6.store(None);
}
pub fn get_public_ipv6_lease(&self) -> Option<cidr::Ipv6Inet> {
self.public_ipv6_lease.load()
}
pub fn set_public_ipv6_lease(&self, addr: Option<cidr::Ipv6Inet>) {
self.public_ipv6_lease.store(addr);
}
pub fn is_ip_local_ipv6(&self, ip: &std::net::Ipv6Addr) -> bool {
self.get_ipv6().map(|x| x.address() == *ip).unwrap_or(false)
|| self
.get_public_ipv6_lease()
.map(|x| x.address() == *ip)
.unwrap_or(false)
}
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 {
self.config.get_id()
}
@@ -395,7 +431,7 @@ impl GlobalCtx {
pub fn is_ip_local_virtual_ip(&self, ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => self.get_ipv4().map(|x| x.address() == *v4).unwrap_or(false),
IpAddr::V6(v6) => self.get_ipv6().map(|x| x.address() == *v6).unwrap_or(false),
IpAddr::V6(v6) => self.is_ip_local_ipv6(v6),
}
}
@@ -645,23 +681,23 @@ impl GlobalCtx {
pub fn should_deny_proxy(&self, dst_addr: &SocketAddr, is_udp: bool) -> bool {
let _g = self.net_ns.guard();
let ip = dst_addr.ip();
// first check if ip is virtual ip
// first check if ip is an EasyTier-managed local address
// then try bind this ip, if succ means it is local ip
let dst_is_local_virtual_ip = self.is_ip_local_virtual_ip(&ip);
let dst_is_local_et_ip = self.is_ip_local_virtual_ip(&ip);
// this is an expensive operation, should be called sparingly
// 1. tcp/kcp/quic call this only after proxy conn is established
// 2. udp cache the result in nat entry
let dst_is_local_phy_ip = std::net::UdpSocket::bind(format!("{}:0", ip)).is_ok();
tracing::trace!(
"check should_deny_proxy: dst_addr={}, dst_is_local_virtual_ip={}, dst_is_local_phy_ip={}, is_udp={}",
"check should_deny_proxy: dst_addr={}, dst_is_local_et_ip={}, dst_is_local_phy_ip={}, is_udp={}",
dst_addr,
dst_is_local_virtual_ip,
dst_is_local_et_ip,
dst_is_local_phy_ip,
is_udp
);
if dst_is_local_virtual_ip || dst_is_local_phy_ip {
if dst_is_local_et_ip || dst_is_local_phy_ip {
// if is local ip, make sure the port is not one of the listening ports
self.is_port_in_running_listeners(dst_addr.port(), is_udp)
|| (!is_udp && protected_port::is_protected_tcp_port(dst_addr.port()))
@@ -770,6 +806,7 @@ pub mod tests {
assert!(feature_flags.support_conn_list_sync);
assert!(feature_flags.avoid_relay_data);
assert!(feature_flags.is_public_server);
assert!(!feature_flags.ipv6_public_addr_provider);
}
#[tokio::test]
@@ -789,6 +826,40 @@ pub mod tests {
protected_port::clear_protected_tcp_ports_for_test();
}
#[tokio::test]
async fn virtual_ipv6_and_public_ipv6_lease_are_stored_separately() {
let config = TomlConfigLoader::default();
let global_ctx = GlobalCtx::new(config);
let virtual_ipv6 = "fd00::1/64".parse().unwrap();
let public_ipv6 = "2001:db8::2/64".parse().unwrap();
global_ctx.set_ipv6(Some(virtual_ipv6));
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
assert_eq!(global_ctx.get_ipv6(), Some(virtual_ipv6));
assert_eq!(global_ctx.get_public_ipv6_lease(), Some(public_ipv6));
}
#[tokio::test]
async fn public_ipv6_lease_is_treated_as_local_ip() {
protected_port::clear_protected_tcp_ports_for_test();
let config = TomlConfigLoader::default();
let global_ctx = GlobalCtx::new(config);
let public_ipv6 = "2001:db8::2/64".parse().unwrap();
let listener: url::Url = "tcp://[2001:db8::2]:11010".parse().unwrap();
global_ctx.set_public_ipv6_lease(Some(public_ipv6));
global_ctx.add_running_listener(listener);
let ip = std::net::IpAddr::V6(public_ipv6.address());
let socket = SocketAddr::from((public_ipv6.address(), 11010));
assert!(global_ctx.is_ip_local_virtual_ip(&ip));
assert!(global_ctx.should_deny_proxy(&socket, false));
protected_port::clear_protected_tcp_ports_for_test();
}
pub fn get_mock_global_ctx_with_network(
network_identy: Option<NetworkIdentity>,
) -> ArcGlobalCtx {
+11
View File
@@ -166,3 +166,14 @@ pub type IfConfiger = DummyIfConfiger;
#[cfg(target_os = "windows")]
pub use windows::RegistryManager;
#[cfg(target_os = "linux")]
pub(crate) fn list_ipv6_route_messages()
-> Result<Vec<netlink_packet_route::route::RouteMessage>, Error> {
netlink::NetlinkIfConfiger::list_ipv6_route_messages()
}
#[cfg(target_os = "linux")]
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
netlink::NetlinkIfConfiger::get_interface_index(name)
}
+200 -16
View File
@@ -160,7 +160,7 @@ impl From<RouteMessage> for Route {
pub struct NetlinkIfConfiger {}
impl NetlinkIfConfiger {
fn get_interface_index(name: &str) -> Result<u32, Error> {
pub(crate) fn get_interface_index(name: &str) -> Result<u32, Error> {
let name = CString::new(name).with_context(|| "failed to convert interface name")?;
match unsafe { libc::if_nametoindex(name.as_ptr()) } {
0 => Err(std::io::Error::last_os_error().into()),
@@ -311,7 +311,7 @@ impl NetlinkIfConfiger {
Self::set_flags_op(name, SIOCGIFFLAGS, InterfaceFlags::empty())
}
fn list_routes() -> Result<Vec<RouteMessage>, Error> {
fn list_route_messages(address_family: AddressFamily) -> Result<Vec<RouteMessage>, Error> {
let mut message = RouteMessage::default();
message.header.table = RouteHeader::RT_TABLE_UNSPEC;
@@ -320,7 +320,7 @@ impl NetlinkIfConfiger {
message.header.scope = RouteScope::Universe;
message.header.kind = RouteType::Unicast;
message.header.address_family = AddressFamily::Inet;
message.header.address_family = address_family;
message.header.destination_prefix_length = 0;
message.header.source_prefix_length = 0;
@@ -367,6 +367,14 @@ impl NetlinkIfConfiger {
Ok(ret_vec)
}
fn list_routes() -> Result<Vec<RouteMessage>, Error> {
Self::list_route_messages(AddressFamily::Inet)
}
pub(crate) fn list_ipv6_route_messages() -> Result<Vec<RouteMessage>, Error> {
Self::list_route_messages(AddressFamily::Inet6)
}
}
#[async_trait]
@@ -551,12 +559,9 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
message.header.scope = RouteScope::Universe;
message.header.kind = RouteType::Unicast;
// Add metric (cost) if specified
if let Some(cost) = cost {
message
.attributes
.push(RouteAttribute::Priority(cost as u32));
}
message
.attributes
.push(RouteAttribute::Priority(cost.unwrap_or(65535) as u32));
message
.attributes
@@ -564,9 +569,11 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
name,
)?));
message
.attributes
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
if cidr_prefix != 0 {
message
.attributes
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
}
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
}
@@ -577,7 +584,7 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
let routes = Self::list_routes()?;
let routes = Self::list_route_messages(AddressFamily::Inet6)?;
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
for msg in routes {
@@ -598,29 +605,82 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
const DUMMY_IFACE_NAME: &str = "dummy";
fn run_cmd(cmd: &str) -> String {
let output = std::process::Command::new("sh")
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()
.expect("failed to execute process");
assert!(
output.status.success(),
"command failed: {cmd}\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
String::from_utf8(output.stdout).unwrap()
}
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),
);
}
fn test_iface_name(tag: &str) -> String {
format!("et{}{:x}", tag, std::process::id() & 0xffff)
}
struct ScopedDummyLink {
name: String,
}
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(),
}
}
}
impl Drop for ScopedDummyLink {
fn drop(&mut self) {
let _ = Command::new("ip")
.args(["link", "del", &self.name])
.output();
}
}
struct PrepareEnv {}
impl PrepareEnv {
fn new() -> Self {
let _ = run_cmd(&format!("sudo ip link add {} type dummy", DUMMY_IFACE_NAME));
let _ = Command::new("ip")
.args(["link", "del", DUMMY_IFACE_NAME])
.output();
let _ = run_cmd(&format!("ip link add {} type dummy", DUMMY_IFACE_NAME));
PrepareEnv {}
}
}
impl Drop for PrepareEnv {
fn drop(&mut self) {
let _ = run_cmd(&format!("sudo ip link del {}", DUMMY_IFACE_NAME));
let _ = Command::new("ip")
.args(["link", "del", DUMMY_IFACE_NAME])
.output();
}
}
@@ -701,4 +761,128 @@ mod tests {
.collect::<Vec<_>>();
assert!(!routes.contains(&IpAddr::V4("10.5.5.0".parse().unwrap())));
}
#[serial_test::serial]
#[tokio::test]
async fn ipv6_addr_readback_test() {
let iface = test_iface_name("a");
let _link = ScopedDummyLink::new(&iface);
run_ip(&["-6", "addr", "add", "2001:db8:1234::2/64", "dev", &iface]);
let addrs = NetlinkIfConfiger::list_addresses(&iface).unwrap();
assert!(addrs.iter().any(|addr| {
addr.address() == IpAddr::V6("2001:db8:1234::2".parse().unwrap())
&& addr.network_length() == 64
}));
}
#[serial_test::serial]
#[tokio::test]
async fn ipv6_route_readback_test() {
let wan_if = test_iface_name("rw");
let lan_if = test_iface_name("rl");
let _wan = ScopedDummyLink::new(&wan_if);
let _lan = ScopedDummyLink::new(&lan_if);
run_ip(&[
"-6",
"addr",
"add",
"2001:db8:100:ffff::2/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]);
let wan_ifindex = NetlinkIfConfiger::get_interface_index(&wan_if).unwrap();
let lan_ifindex = NetlinkIfConfiger::get_interface_index(&lan_if).unwrap();
let routes = NetlinkIfConfiger::list_ipv6_route_messages().unwrap();
assert!(routes.iter().any(|route| {
route.header.kind == RouteType::Unicast
&& route.header.source_prefix_length == 56
&& route.attributes.iter().any(|attr| {
matches!(
attr,
RouteAttribute::Source(RouteAddress::Inet6(addr))
if *addr == "2001:db8:100::".parse::<std::net::Ipv6Addr>().unwrap()
)
})
&& route
.attributes
.iter()
.any(|attr| matches!(attr, RouteAttribute::Oif(index) if *index == wan_ifindex))
&& !route
.attributes
.iter()
.any(|attr| matches!(attr, RouteAttribute::Destination(_)))
}));
assert!(routes.iter().any(|route| {
route.header.kind == RouteType::Unicast
&& route.header.destination_prefix_length == 56
&& route.attributes.iter().any(|attr| {
matches!(
attr,
RouteAttribute::Destination(RouteAddress::Inet6(addr))
if *addr == "2001:db8:100::".parse::<std::net::Ipv6Addr>().unwrap()
)
})
&& route
.attributes
.iter()
.any(|attr| matches!(attr, RouteAttribute::Oif(index) if *index == lan_ifindex))
}));
}
#[serial_test::serial]
#[tokio::test]
async fn ipv6_route_remove_test() {
let iface = test_iface_name("rr");
let _link = ScopedDummyLink::new(&iface);
let ifcfg = NetlinkIfConfiger {};
let route_addr = "2001:db8:200::".parse::<std::net::Ipv6Addr>().unwrap();
ifcfg
.add_ipv6_route(&iface, route_addr, 56, None)
.await
.unwrap();
let ifindex = NetlinkIfConfiger::get_interface_index(&iface).unwrap();
let has_route = |routes: &[RouteMessage]| {
routes.iter().any(|route| {
route.header.destination_prefix_length == 56
&& route.attributes.iter().any(|attr| {
matches!(
attr,
RouteAttribute::Destination(RouteAddress::Inet6(addr)) if *addr == route_addr
)
})
&& route
.attributes
.iter()
.any(|attr| matches!(attr, RouteAttribute::Oif(index) if *index == ifindex))
})
};
let routes = NetlinkIfConfiger::list_ipv6_route_messages().unwrap();
assert!(has_route(&routes));
ifcfg
.remove_ipv6_route(&iface, route_addr, 56)
.await
.unwrap();
let routes = NetlinkIfConfiger::list_ipv6_route_messages().unwrap();
assert!(!has_route(&routes));
}
}
@@ -720,7 +720,7 @@ async fn check_udp_socket_local_addr(
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.connect(remote_mapped_addr).await?;
if let Ok(local_addr) = socket.local_addr() {
// local_addr should not be equal to virtual ipv4 or virtual ipv6
// local_addr should not be equal to an EasyTier-managed virtual/public address.
match local_addr.ip() {
IpAddr::V4(ip) => {
if global_ctx.get_ipv4().map(|ip| ip.address()) == Some(ip) {
@@ -728,8 +728,8 @@ async fn check_udp_socket_local_addr(
}
}
IpAddr::V6(ip) => {
if global_ctx.get_ipv6().map(|ip| ip.address()) == Some(ip) {
return Err(anyhow::anyhow!("local address is virtual ipv6").into());
if global_ctx.is_ip_local_ipv6(&ip) {
return Err(anyhow::anyhow!("local address is easytier-managed ipv6").into());
}
}
}
+39
View File
@@ -171,6 +171,31 @@ struct NetworkOptions {
)]
ipv6: Option<String>,
#[arg(
long,
env = "ET_IPV6_PUBLIC_ADDR_PROVIDER",
help = t!("core_clap.ipv6_public_addr_provider").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
ipv6_public_addr_provider: Option<bool>,
#[arg(
long,
env = "ET_IPV6_PUBLIC_ADDR_AUTO",
help = t!("core_clap.ipv6_public_addr_auto").to_string(),
num_args = 0..=1,
default_missing_value = "true"
)]
ipv6_public_addr_auto: Option<bool>,
#[arg(
long,
env = "ET_IPV6_PUBLIC_ADDR_PREFIX",
help = t!("core_clap.ipv6_public_addr_prefix").to_string()
)]
ipv6_public_addr_prefix: Option<String>,
#[arg(
short,
long,
@@ -875,6 +900,20 @@ impl NetworkOptions {
})?))
}
if let Some(enabled) = self.ipv6_public_addr_provider {
cfg.set_ipv6_public_addr_provider(enabled);
}
if let Some(enabled) = self.ipv6_public_addr_auto {
cfg.set_ipv6_public_addr_auto(enabled);
}
if let Some(prefix) = &self.ipv6_public_addr_prefix {
cfg.set_ipv6_public_addr_prefix(Some(prefix.parse().with_context(|| {
format!("failed to parse ipv6 public address prefix: {}", prefix)
})?));
}
if !self.peers.is_empty() {
let mut peers = cfg.get_peers();
peers.reserve(peers.len() + self.peers.len());
+185 -5
View File
@@ -51,13 +51,14 @@ use easytier::{
ListCredentialsRequest, ListCredentialsResponse, ListForeignNetworkRequest,
ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest,
ListPeerResponse, ListPortForwardRequest, ListPortForwardResponse,
ListRouteRequest, ListRouteResponse, MappedListener, MappedListenerManageRpc,
ListPublicIpv6InfoRequest, ListPublicIpv6InfoResponse, ListRouteRequest,
ListRouteResponse, MappedListener, MappedListenerManageRpc,
MappedListenerManageRpcClientFactory, MetricSnapshot, NodeInfo, PeerManageRpc,
PeerManageRpcClientFactory, PortForwardManageRpc,
PortForwardManageRpcClientFactory, RevokeCredentialRequest, ShowNodeInfoRequest,
StatsRpc, StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType,
TcpProxyRpc, TcpProxyRpcClientFactory, TrustedKeySourcePb, VpnPortalInfo,
VpnPortalRpc, VpnPortalRpcClientFactory,
PortForwardManageRpcClientFactory, RevokeCredentialRequest, Route as ApiRoute,
ShowNodeInfoRequest, StatsRpc, StatsRpcClientFactory, TcpProxyEntryState,
TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory,
TrustedKeySourcePb, VpnPortalInfo, VpnPortalRpc, VpnPortalRpcClientFactory,
instance_identifier::{InstanceSelector, Selector},
list_global_foreign_network_response, list_peer_route_pair,
},
@@ -193,6 +194,7 @@ struct PeerArgs {
#[derive(Subcommand, Debug)]
enum PeerSubCommand {
List,
Ipv6,
ListForeign {
#[arg(
long,
@@ -536,6 +538,12 @@ struct RouteListData {
peer_routes: Vec<PeerRoutePair>,
}
struct PeerIpv6DataRaw {
node_info: NodeInfo,
routes: Vec<ApiRoute>,
provider_info: ListPublicIpv6InfoResponse,
}
#[derive(serde::Serialize)]
struct PeerCenterRowData {
node_id: String,
@@ -963,6 +971,27 @@ impl<'a> CommandHandler<'a> {
})
}
async fn fetch_local_public_ipv6_info(&self) -> Result<ListPublicIpv6InfoResponse, Error> {
Ok(self
.get_peer_manager_client()
.await?
.list_public_ipv6_info(
BaseController::default(),
ListPublicIpv6InfoRequest {
instance: Some(self.instance_selector.clone()),
},
)
.await?)
}
async fn fetch_peer_ipv6_data(&self) -> Result<PeerIpv6DataRaw, Error> {
Ok(PeerIpv6DataRaw {
node_info: self.fetch_node_info().await?,
routes: self.list_routes().await?.routes,
provider_info: self.fetch_local_public_ipv6_info().await?,
})
}
async fn fetch_connector_list(&self) -> Result<Vec<Connector>, Error> {
Ok(self
.get_connector_manager_client()
@@ -1375,6 +1404,154 @@ impl<'a> CommandHandler<'a> {
})
}
async fn handle_peer_ipv6(&self) -> Result<(), Error> {
#[derive(tabled::Tabled, serde::Serialize)]
struct PeerIpv6NodeRow {
peer_id: u32,
hostname: String,
inst_id: String,
ipv4: String,
public_ipv6_addr: String,
provider_prefix: String,
}
#[derive(tabled::Tabled, serde::Serialize)]
struct ProviderLeaseRow {
peer_id: u32,
inst_id: String,
leased_addr: String,
valid_until: String,
reused: bool,
}
#[derive(serde::Serialize)]
struct ProviderLeaseSection {
provider_prefix: String,
leases: Vec<ProviderLeaseRow>,
}
#[derive(serde::Serialize)]
struct PeerIpv6View {
nodes: Vec<PeerIpv6NodeRow>,
local_provider: Option<ProviderLeaseSection>,
}
fn fmt_ipv6_inet(value: Option<easytier::proto::common::Ipv6Inet>) -> String {
value
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string())
}
fn fmt_valid_until(unix_seconds: i64) -> String {
chrono::DateTime::<chrono::Utc>::from_timestamp(unix_seconds, 0)
.map(|ts| {
ts.with_timezone(&chrono::Local)
.format("%Y-%m-%d %H:%M:%S")
.to_string()
})
.unwrap_or_else(|| unix_seconds.to_string())
}
let build_view = |data: &PeerIpv6DataRaw| {
let mut nodes = Vec::with_capacity(data.routes.len() + 1);
nodes.push(PeerIpv6NodeRow {
peer_id: data.node_info.peer_id,
hostname: data.node_info.hostname.clone(),
inst_id: data.node_info.inst_id.clone(),
ipv4: data.node_info.ipv4_addr.clone(),
public_ipv6_addr: fmt_ipv6_inet(data.node_info.public_ipv6_addr),
provider_prefix: fmt_ipv6_inet(data.node_info.ipv6_public_addr_prefix),
});
nodes.extend(data.routes.iter().map(|route| {
PeerIpv6NodeRow {
peer_id: route.peer_id,
hostname: route.hostname.clone(),
inst_id: route.inst_id.clone(),
ipv4: route
.ipv4_addr
.map(|ipv4| ipv4.to_string())
.unwrap_or_else(|| "-".to_string()),
public_ipv6_addr: fmt_ipv6_inet(route.public_ipv6_addr),
provider_prefix: fmt_ipv6_inet(route.ipv6_public_addr_prefix),
}
}));
nodes.sort_by_key(|row| {
(
row.peer_id != data.node_info.peer_id,
row.peer_id,
row.inst_id.clone(),
)
});
let local_provider = data.provider_info.provider_prefix.map(|provider_prefix| {
let mut leases = data
.provider_info
.provider_leases
.iter()
.map(|lease| ProviderLeaseRow {
peer_id: lease.peer_id,
inst_id: lease.inst_id.clone(),
leased_addr: fmt_ipv6_inet(lease.leased_addr),
valid_until: fmt_valid_until(lease.valid_until_unix_seconds),
reused: lease.reused,
})
.collect::<Vec<_>>();
leases.sort_by_key(|lease| {
(
lease.peer_id,
lease.inst_id.clone(),
lease.leased_addr.clone(),
)
});
ProviderLeaseSection {
provider_prefix: provider_prefix.to_string(),
leases,
}
});
PeerIpv6View {
nodes,
local_provider,
}
};
let results = self
.collect_instance_results(|handler| Box::pin(handler.fetch_peer_ipv6_data()))
.await?;
if self.verbose || *self.output_format == OutputFormat::Json {
return self.print_json_results(
results
.into_iter()
.map(|result| result.map(|data| build_view(&data)))
.collect(),
);
}
self.print_results(&results, |data| {
let view = build_view(data);
print_output(&view.nodes, self.output_format, &[], &[], self.no_trunc)?;
if let Some(local_provider) = view.local_provider {
println!();
println!("Local provider prefix: {}", local_provider.provider_prefix);
if local_provider.leases.is_empty() {
println!("No active provider leases");
} else {
print_output(
&local_provider.leases,
self.output_format,
&[],
&[],
self.no_trunc,
)?;
}
}
Ok(())
})
}
async fn handle_route_dump(&self) -> Result<(), Error> {
let results = self
.collect_instance_results(|handler| Box::pin(handler.fetch_route_dump()))
@@ -2652,6 +2829,9 @@ async fn main() -> Result<(), Error> {
Some(PeerSubCommand::List) => {
handler.handle_peer_list().await?;
}
Some(PeerSubCommand::Ipv6) => {
handler.handle_peer_ipv6().await?;
}
Some(PeerSubCommand::ListForeign { trusted_keys }) => {
handler.handle_foreign_network_list(*trusted_keys).await?;
}
+138 -3
View File
@@ -9,7 +9,6 @@ use std::time::Duration;
use anyhow::Context;
use cidr::{IpCidr, Ipv4Inet};
use futures::FutureExt;
use tokio::sync::{Mutex, Notify};
#[cfg(feature = "tun")]
@@ -65,6 +64,11 @@ use crate::vpn_portal::{self, VpnPortal};
#[cfg(feature = "magic-dns")]
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,
};
#[cfg(feature = "socks5")]
use crate::gateway::socks5::Socks5Server;
@@ -253,11 +257,64 @@ pub struct InstanceConfigPatcher {
}
impl InstanceConfigPatcher {
fn parse_ipv6_public_addr_prefix_patch(
prefix: Option<&str>,
) -> Result<Option<Option<cidr::Ipv6Cidr>>, anyhow::Error> {
let Some(prefix) = prefix else {
return Ok(None);
};
let prefix = prefix.trim();
if prefix.is_empty() {
return Ok(Some(None));
}
let parsed = prefix
.parse()
.with_context(|| format!("failed to parse ipv6 public address prefix: {prefix}"))?;
Ok(Some(Some(parsed)))
}
fn effective_ipv6_for_public_ipv6_validation(
global_ctx: &ArcGlobalCtx,
patch: &crate::proto::api::config::InstanceConfigPatch,
_auto_enabled: bool,
) -> Option<cidr::Ipv6Inet> {
if let Some(ipv6) = patch.ipv6 {
return Some(ipv6.into());
}
global_ctx.get_ipv6()
}
fn validate_public_ipv6_patch(
global_ctx: &ArcGlobalCtx,
patch: &crate::proto::api::config::InstanceConfigPatch,
) -> Result<Option<Option<cidr::Ipv6Cidr>>, anyhow::Error> {
let parsed_prefix =
Self::parse_ipv6_public_addr_prefix_patch(patch.ipv6_public_addr_prefix.as_deref())?;
let auto_enabled = patch
.ipv6_public_addr_auto
.unwrap_or(global_ctx.config.get_ipv6_public_addr_auto());
let provider_enabled = patch
.ipv6_public_addr_provider
.unwrap_or(global_ctx.config.get_ipv6_public_addr_provider());
let prefix =
parsed_prefix.unwrap_or_else(|| global_ctx.config.get_ipv6_public_addr_prefix());
let ipv6 = Self::effective_ipv6_for_public_ipv6_validation(global_ctx, patch, auto_enabled);
validate_public_ipv6_config_values(ipv6, provider_enabled, auto_enabled, prefix)?;
Ok(parsed_prefix)
}
pub async fn apply_patch(
&self,
patch: crate::proto::api::config::InstanceConfigPatch,
) -> Result<(), anyhow::Error> {
let patch_for_event = patch.clone();
let global_ctx = weak_upgrade(&self.global_ctx)?;
let parsed_ipv6_public_addr_prefix = Self::validate_public_ipv6_patch(&global_ctx, &patch)?;
self.patch_port_forwards(patch.port_forwards).await?;
self.patch_acl(patch.acl).await?;
@@ -267,7 +324,8 @@ impl InstanceConfigPatcher {
self.patch_mapped_listeners(patch.mapped_listeners).await?;
self.patch_connector(patch.connectors).await?;
let global_ctx = weak_upgrade(&self.global_ctx)?;
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());
global_ctx.config.set_hostname(Some(hostname));
@@ -282,9 +340,30 @@ impl InstanceConfigPatcher {
global_ctx.set_ipv6(Some(ipv6.into()));
global_ctx.config.set_ipv6(Some(ipv6.into()));
}
if let Some(enabled) = patch.ipv6_public_addr_provider {
global_ctx.config.set_ipv6_public_addr_provider(enabled);
provider_config_changed = true;
}
if let Some(enabled) = patch.ipv6_public_addr_auto {
global_ctx.config.set_ipv6_public_addr_auto(enabled);
}
if let Some(prefix) = parsed_ipv6_public_addr_prefix {
global_ctx.config.set_ipv6_public_addr_prefix(prefix);
provider_config_changed = true;
}
global_ctx.issue_event(GlobalCtxEvent::ConfigPatched(patch_for_event));
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);
}
}
Ok(())
}
@@ -664,6 +743,12 @@ impl Instance {
Ok(())
}
async fn prepare_public_ipv6_config(&self) -> Result<(), Error> {
validate_public_ipv6_config(&self.global_ctx)?;
reconcile_public_ipv6_provider_runtime(&self.global_ctx).await;
Ok(())
}
// use a mock nic ctx to consume packets.
#[cfg(feature = "tun")]
async fn clear_nic_ctx(
@@ -932,6 +1017,7 @@ impl Instance {
}
pub async fn run(&mut self) -> Result<(), Error> {
self.prepare_public_ipv6_config().await?;
self.listener_manager
.lock()
.await
@@ -939,6 +1025,7 @@ impl Instance {
.await?;
self.listener_manager.lock().await.run().await?;
self.peer_manager.run().await?;
run_public_ipv6_provider_reconcile_task(&self.global_ctx);
#[cfg(feature = "tun")]
{
@@ -1544,7 +1631,9 @@ impl Drop for Instance {
#[cfg(test)]
mod tests {
use crate::{
instance::instance::InstanceRpcServerHook, proto::rpc_impl::standalone::RpcServerHook,
common::global_ctx::tests::get_mock_global_ctx,
instance::instance::{InstanceConfigPatcher, InstanceRpcServerHook},
proto::{api::config::InstanceConfigPatch, rpc_impl::standalone::RpcServerHook},
};
#[tokio::test]
@@ -1665,4 +1754,50 @@ mod tests {
}
}
}
#[tokio::test]
async fn validate_public_ipv6_patch_rejects_non_global_prefix() {
let global_ctx = get_mock_global_ctx();
let patch = InstanceConfigPatch {
ipv6_public_addr_provider: Some(true),
ipv6_public_addr_prefix: Some("fd00::/64".to_string()),
..Default::default()
};
let err =
InstanceConfigPatcher::validate_public_ipv6_patch(&global_ctx, &patch).unwrap_err();
assert!(
err.to_string()
.contains("not a valid global unicast IPv6 prefix")
);
}
#[tokio::test]
async fn validate_public_ipv6_patch_allows_enabling_auto_with_manual_ipv6() {
let global_ctx = get_mock_global_ctx();
global_ctx.set_ipv6(Some("fd00::1/64".parse().unwrap()));
let patch = InstanceConfigPatch {
ipv6_public_addr_auto: Some(true),
..Default::default()
};
assert!(InstanceConfigPatcher::validate_public_ipv6_patch(&global_ctx, &patch).is_ok());
}
#[tokio::test]
async fn validate_public_ipv6_patch_ignores_runtime_auto_ipv6_cache() {
let global_ctx = get_mock_global_ctx();
global_ctx.config.set_ipv6_public_addr_auto(true);
global_ctx.set_ipv6(Some("2001:db8::10/64".parse().unwrap()));
let patch = InstanceConfigPatch {
ipv6_public_addr_provider: Some(true),
ipv6_public_addr_prefix: Some("2001:db8:100::/64".to_string()),
..Default::default()
};
assert!(InstanceConfigPatcher::validate_public_ipv6_patch(&global_ctx, &patch).is_ok());
}
}
+2
View File
@@ -4,6 +4,8 @@ pub mod instance;
pub mod listeners;
mod public_ipv6_provider;
pub mod proxy_cidrs_monitor;
#[cfg(feature = "tun")]
@@ -0,0 +1,918 @@
use std::{path::Path, sync::Arc};
use anyhow::Context;
use cidr::{Ipv6Cidr, Ipv6Inet};
#[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()
}
pub(super) fn should_run_public_ipv6_provider_reconcile(global_ctx: &ArcGlobalCtx) -> bool {
should_run_public_ipv6_provider_reconcile_task(read_public_ipv6_provider_config_snapshot(
global_ctx,
))
}
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_values(
_ipv6: Option<Ipv6Inet>,
provider_enabled: bool,
_auto_enabled: bool,
prefix: Option<Ipv6Cidr>,
) -> Result<(), Error> {
if !provider_enabled {
return Ok(());
}
ensure_public_ipv6_provider_supported()?;
if let Some(prefix) = 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(())
}
pub(super) fn validate_public_ipv6_config(global_ctx: &ArcGlobalCtx) -> Result<(), Error> {
validate_public_ipv6_config_values(
global_ctx.get_ipv6(),
global_ctx.config.get_ipv6_public_addr_provider(),
global_ctx.config.get_ipv6_public_addr_auto(),
global_ctx.config.get_ipv6_public_addr_prefix(),
)
}
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())
);
}
}
+194 -4
View File
@@ -735,9 +735,26 @@ impl VirtualNic {
}
pub async fn add_ipv6_route(&self, address: Ipv6Addr, cidr: u8) -> Result<(), Error> {
self.add_ipv6_route_with_cost(address, cidr, None).await
}
pub async fn add_ipv6_route_with_cost(
&self,
address: Ipv6Addr,
cidr: u8,
cost: Option<i32>,
) -> Result<(), Error> {
let _g = self.global_ctx.net_ns.guard();
self.ifcfg
.add_ipv6_route(self.ifname(), address, cidr, None)
.add_ipv6_route(self.ifname(), address, cidr, cost)
.await?;
Ok(())
}
pub async fn remove_ipv6_route(&self, address: Ipv6Addr, cidr: u8) -> Result<(), Error> {
let _g = self.global_ctx.net_ns.guard();
self.ifcfg
.remove_ipv6_route(self.ifname(), address, cidr)
.await?;
Ok(())
}
@@ -903,7 +920,7 @@ impl NicCtx {
}
let src_ipv6 = ipv6.get_source();
let dst_ipv6 = ipv6.get_destination();
let my_ipv6 = mgr.get_global_ctx().get_ipv6().map(|x| x.address());
let is_local_src = mgr.get_global_ctx().is_ip_local_ipv6(&src_ipv6);
tracing::trace!(
?ret,
?src_ipv6,
@@ -911,14 +928,14 @@ impl NicCtx {
"[USER_PACKET] recv new packet from tun device and forward to peers."
);
if src_ipv6.is_unicast_link_local() && Some(src_ipv6) != my_ipv6 {
if src_ipv6.is_unicast_link_local() && !is_local_src {
// do not route link local packet to other nodes unless the address is assigned by user
return;
}
// TODO: use zero-copy
let send_ret = mgr
.send_msg_by_ip(ret, IpAddr::V6(dst_ipv6), Some(src_ipv6) == my_ipv6)
.send_msg_by_ip(ret, IpAddr::V6(dst_ipv6), is_local_src)
.await;
if send_ret.is_err() {
tracing::trace!(?send_ret, "[USER_PACKET] send_msg failed")
@@ -1039,6 +1056,44 @@ impl NicCtx {
}
}
async fn apply_public_ipv6_route_changes(
ifcfg: &impl IfConfiguerTrait,
ifname: &str,
net_ns: &crate::common::netns::NetNS,
cur_routes: &mut BTreeSet<cidr::Ipv6Inet>,
added: Vec<cidr::Ipv6Inet>,
removed: Vec<cidr::Ipv6Inet>,
) {
for route in removed {
if !cur_routes.contains(&route) {
continue;
}
let _g = net_ns.guard();
let ret = ifcfg
.remove_ipv6_route(ifname, route.address(), route.network_length())
.await;
if ret.is_err() {
tracing::trace!(route = ?route, err = ?ret, "remove public ipv6 route failed");
}
cur_routes.remove(&route);
}
for route in added {
if cur_routes.contains(&route) {
continue;
}
let _g = net_ns.guard();
let ret = ifcfg
.add_ipv6_route(ifname, route.address(), route.network_length(), None)
.await;
if ret.is_err() {
tracing::trace!(route = ?route, err = ?ret, "add public ipv6 route failed");
} else {
cur_routes.insert(route);
}
}
}
async fn run_proxy_cidrs_route_updater(&mut self) -> Result<(), Error> {
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
return Err(anyhow::anyhow!("peer manager not available").into());
@@ -1114,6 +1169,137 @@ impl NicCtx {
Ok(())
}
async fn run_public_ipv6_route_updater(&mut self) -> Result<(), Error> {
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
return Err(anyhow::anyhow!("peer manager not available").into());
};
let global_ctx = self.global_ctx.clone();
let net_ns = self.global_ctx.net_ns.clone();
let nic = self.nic.lock().await;
let ifcfg = nic.get_ifcfg();
let ifname = nic.ifname().to_owned();
let mut event_receiver = global_ctx.subscribe();
self.tasks.spawn(async move {
let mut cur_routes = BTreeSet::<cidr::Ipv6Inet>::new();
let initial_routes = peer_mgr.list_public_ipv6_routes().await;
let initial_added = initial_routes.iter().copied().collect::<Vec<_>>();
Self::apply_public_ipv6_route_changes(
&ifcfg,
&ifname,
&net_ns,
&mut cur_routes,
initial_added,
Vec::new(),
)
.await;
loop {
let event = match event_receiver.recv().await {
Ok(event) => event,
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
event_receiver = event_receiver.resubscribe();
let latest = peer_mgr.list_public_ipv6_routes().await;
let added = latest.difference(&cur_routes).copied().collect::<Vec<_>>();
let removed = cur_routes.difference(&latest).copied().collect::<Vec<_>>();
GlobalCtxEvent::PublicIpv6RoutesUpdated(added, removed)
}
};
let (added, removed) = match event {
GlobalCtxEvent::PublicIpv6RoutesUpdated(added, removed) => (added, removed),
_ => continue,
};
Self::apply_public_ipv6_route_changes(
&ifcfg,
&ifname,
&net_ns,
&mut cur_routes,
added,
removed,
)
.await;
}
});
Ok(())
}
async fn run_public_ipv6_addr_updater(&mut self) -> Result<(), Error> {
let Some(peer_mgr) = self.peer_mgr.upgrade() else {
return Err(anyhow::anyhow!("peer manager not available").into());
};
let global_ctx = self.global_ctx.clone();
let nic = self.nic.clone();
let mut event_receiver = global_ctx.subscribe();
self.tasks.spawn(async move {
let mut current_addr = peer_mgr.get_my_public_ipv6_addr().await;
if let Some(addr) = current_addr {
let nic = nic.lock().await;
if let Err(err) = nic.link_up().await {
tracing::warn!(?err, "failed to bring public ipv6 nic link up");
}
if let Err(err) = nic.add_ipv6(addr.address(), addr.network_length() as i32).await {
tracing::warn!(addr = ?addr, ?err, "failed to add public ipv6 address");
}
if let Err(err) = nic
.add_ipv6_route_with_cost(Ipv6Addr::UNSPECIFIED, 0, Some(5))
.await
{
tracing::warn!(route = %Ipv6Addr::UNSPECIFIED, prefix = 0, ?err, "failed to add default public ipv6 route");
}
}
loop {
let event = match event_receiver.recv().await {
Ok(event) => event,
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
event_receiver = event_receiver.resubscribe();
let latest = peer_mgr.get_my_public_ipv6_addr().await;
GlobalCtxEvent::PublicIpv6Changed(current_addr, latest)
}
};
let (old, new) = match event {
GlobalCtxEvent::PublicIpv6Changed(old, new) => (old, new),
_ => continue,
};
current_addr = new;
let nic = nic.lock().await;
if let Err(err) = nic.link_up().await {
tracing::warn!(?err, "failed to bring public ipv6 nic link up");
}
if let Some(old) = old {
if let Err(err) = nic.remove_ipv6_route(Ipv6Addr::UNSPECIFIED, 0).await {
tracing::warn!(route = %Ipv6Addr::UNSPECIFIED, prefix = 0, ?err, "failed to remove default public ipv6 route");
}
if let Err(err) = nic.remove_ipv6(Some(old)).await {
tracing::warn!(addr = ?old, ?err, "failed to remove old public ipv6 address");
}
}
if let Some(new) = new {
if let Err(err) = nic.add_ipv6(new.address(), new.network_length() as i32).await
{
tracing::warn!(addr = ?new, ?err, "failed to add public ipv6 address");
}
if let Err(err) = nic
.add_ipv6_route_with_cost(Ipv6Addr::UNSPECIFIED, 0, Some(5))
.await
{
tracing::warn!(route = %Ipv6Addr::UNSPECIFIED, prefix = 0, ?err, "failed to add default public ipv6 route");
}
}
}
});
Ok(())
}
pub async fn run(
&mut self,
ipv4_addr: Option<cidr::Ipv4Inet>,
@@ -1169,6 +1355,10 @@ impl NicCtx {
}
self.run_proxy_cidrs_route_updater().await?;
self.run_public_ipv6_route_updater().await?;
// Keep the updater running so runtime config patches can enable auto mode
// without recreating the NIC.
self.run_public_ipv6_addr_updater().await?;
Ok(())
}
+14
View File
@@ -435,6 +435,20 @@ fn handle_event(
event!(info, ?ip, "[{}] dhcp ip conflict", instance_id);
}
GlobalCtxEvent::PublicIpv6Changed(old, new) => {
event!(info, ?old, ?new, "[{}] public ipv6 changed", instance_id);
}
GlobalCtxEvent::PublicIpv6RoutesUpdated(added, removed) => {
event!(
info,
?added,
?removed,
"[{}] public ipv6 routes updated",
instance_id
);
}
GlobalCtxEvent::PortForwardAdded(cfg) => {
event!(
info,
+29
View File
@@ -714,6 +714,24 @@ impl NetworkConfig {
flags.use_smoltcp = use_smoltcp;
}
if let Some(ipv6_public_addr_provider) = self.ipv6_public_addr_provider {
cfg.set_ipv6_public_addr_provider(ipv6_public_addr_provider);
}
if let Some(ipv6_public_addr_auto) = self.ipv6_public_addr_auto {
cfg.set_ipv6_public_addr_auto(ipv6_public_addr_auto);
}
if let Some(ipv6_public_addr_prefix) = self
.ipv6_public_addr_prefix
.as_ref()
.filter(|prefix| !prefix.is_empty())
{
cfg.set_ipv6_public_addr_prefix(Some(ipv6_public_addr_prefix.parse().with_context(
|| format!("failed to parse ipv6 public address prefix: {ipv6_public_addr_prefix}"),
)?));
}
if let Some(disable_ipv6) = self.disable_ipv6 {
flags.enable_ipv6 = !disable_ipv6;
}
@@ -863,6 +881,17 @@ impl NetworkConfig {
result.network_length = Some(ipv4.network_length() as i32);
}
if config.get_ipv6_public_addr_provider() != default_config.get_ipv6_public_addr_provider()
{
result.ipv6_public_addr_provider = Some(config.get_ipv6_public_addr_provider());
}
if config.get_ipv6_public_addr_auto() != default_config.get_ipv6_public_addr_auto() {
result.ipv6_public_addr_auto = Some(config.get_ipv6_public_addr_auto());
}
result.ipv6_public_addr_prefix = config
.get_ipv6_public_addr_prefix()
.map(|prefix| prefix.to_string());
let peers = config.get_peers();
result.networking_method = Some(NetworkingMethod::Manual as i32);
if !peers.is_empty() {
+74 -12
View File
@@ -292,13 +292,33 @@ impl AclFilter {
processor.increment_stat(AclStatKey::PacketsTotal);
}
fn classify_chain_type(
is_in: bool,
packet_info: &PacketInfo,
my_ipv4: Option<Ipv4Addr>,
is_local_ipv6: impl Fn(Ipv6Addr) -> bool,
) -> ChainType {
if !is_in {
return ChainType::Outbound;
}
let is_local_dst = packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|| matches!(packet_info.dst_ip, IpAddr::V6(dst) if is_local_ipv6(dst));
if is_local_dst {
ChainType::Inbound
} else {
ChainType::Forward
}
}
/// Common ACL processing logic
pub fn process_packet_with_acl(
&self,
packet: &ZCPacket,
is_in: bool,
my_ipv4: Option<Ipv4Addr>,
my_ipv6: Option<Ipv6Addr>,
is_local_ipv6: impl Fn(Ipv6Addr) -> bool,
route: &(dyn super::route_trait::Route + Send + Sync + 'static),
) -> bool {
if !self.acl_enabled.load(Ordering::Relaxed) {
@@ -323,17 +343,7 @@ impl AclFilter {
}
};
let chain_type = if is_in {
if packet_info.dst_ip == my_ipv4.unwrap_or(Ipv4Addr::UNSPECIFIED)
|| packet_info.dst_ip == my_ipv6.unwrap_or(Ipv6Addr::UNSPECIFIED)
{
ChainType::Inbound
} else {
ChainType::Forward
}
} else {
ChainType::Outbound
};
let chain_type = Self::classify_chain_type(is_in, &packet_info, my_ipv4, is_local_ipv6);
// Get current processor atomically
let processor = self.get_processor();
@@ -384,3 +394,55 @@ impl AclFilter {
}
}
}
#[cfg(test)]
mod tests {
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
sync::Arc,
};
use crate::{
common::acl_processor::PacketInfo,
proto::acl::{ChainType, Protocol},
};
use super::AclFilter;
fn packet_info(dst_ip: IpAddr) -> PacketInfo {
PacketInfo {
src_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
dst_ip,
src_port: Some(1234),
dst_port: Some(80),
protocol: Protocol::Tcp,
packet_size: 64,
src_groups: Arc::new(Vec::new()),
dst_groups: Arc::new(Vec::new()),
}
}
#[test]
fn classify_chain_type_treats_public_ipv6_lease_as_inbound() {
let leased_ipv6 = Ipv6Addr::new(0x2001, 0xdb8, 0x100, 0, 0, 0, 0, 0x123);
let packet_info = packet_info(IpAddr::V6(leased_ipv6));
let chain =
AclFilter::classify_chain_type(true, &packet_info, None, |ip| ip == leased_ipv6);
assert_eq!(chain, ChainType::Inbound);
}
#[test]
fn classify_chain_type_keeps_non_local_ipv6_as_forward() {
let leased_ipv6 = Ipv6Addr::new(0x2001, 0xdb8, 0x100, 0, 0, 0, 0, 0x123);
let packet_info = packet_info(IpAddr::V6(Ipv6Addr::new(
0x2001, 0xdb8, 0xffff, 2, 0, 0, 0, 0x100,
)));
let chain =
AclFilter::classify_chain_type(true, &packet_info, None, |ip| ip == leased_ipv6);
assert_eq!(chain, ChainType::Forward);
}
}
+1
View File
@@ -11,6 +11,7 @@ pub mod peer_ospf_route;
pub mod peer_rpc;
pub mod peer_rpc_service;
pub mod peer_session;
pub(crate) mod public_ipv6;
pub mod relay_peer_map;
pub mod route_trait;
pub mod rpc_service;
+28 -3
View File
@@ -1062,7 +1062,7 @@ impl PeerManager {
&ret,
true,
global_ctx.get_ipv4().map(|x| x.address()),
global_ctx.get_ipv6().map(|x| x.address()),
|dst| global_ctx.is_ip_local_ipv6(&dst),
&route,
) {
continue;
@@ -1291,6 +1291,18 @@ impl PeerManager {
self.get_route().list_proxy_cidrs_v6().await
}
pub async fn list_public_ipv6_routes(&self) -> BTreeSet<cidr::Ipv6Inet> {
self.get_route().list_public_ipv6_routes().await
}
pub async fn get_my_public_ipv6_addr(&self) -> Option<cidr::Ipv6Inet> {
self.get_route().get_my_public_ipv6_addr().await
}
pub async fn get_local_public_ipv6_info(&self) -> instance::ListPublicIpv6InfoResponse {
self.get_route().get_local_public_ipv6_info().await
}
pub async fn dump_route(&self) -> String {
self.get_route().dump().await
}
@@ -1330,7 +1342,7 @@ impl PeerManager {
data,
false,
None,
None,
|_| false,
&self.get_route(),
) {
return false;
@@ -1532,6 +1544,10 @@ impl PeerManager {
dst_peers.extend(self.peers.list_routes().await.iter().map(|x| *x.key()));
} else if let Some(peer_id) = self.peers.get_peer_id_by_ipv6(ipv6_addr).await {
dst_peers.push(peer_id);
} else if !ipv6_addr.is_unicast_link_local()
&& let Some(peer_id) = self.get_route().get_public_ipv6_gateway_peer_id().await
{
dst_peers.push(peer_id);
} else if !ipv6_addr.is_unicast_link_local() {
// NOTE: never route link local address to exit node.
for exit_node in self.exit_nodes.read().await.iter() {
@@ -1662,7 +1678,7 @@ impl PeerManager {
&& !self.global_ctx.is_ip_local_virtual_ip(&ip_addr)
{
// Keep the loop-prevention flags for proxy-induced self-delivery where
// the destination is not this node's own virtual IP.
// the destination is not this node's own EasyTier-managed IP.
hdr.set_not_send_to_tun(true);
hdr.set_no_proxy(true);
}
@@ -1879,6 +1895,15 @@ impl PeerManager {
version: EASYTIER_VERSION.to_string(),
feature_flag: Some(self.global_ctx.get_feature_flags()),
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),
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()
}),
}
}
+235 -8
View File
@@ -10,7 +10,7 @@ use std::{
};
use arc_swap::ArcSwap;
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr};
use cidr::{IpCidr, Ipv4Cidr, Ipv6Cidr, Ipv6Inet};
use crossbeam::atomic::AtomicCell;
use dashmap::DashMap;
use ordered_hash_map::OrderedHashMap;
@@ -46,9 +46,10 @@ use crate::{
peer_rpc::{
ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, OspfRouteRpc,
OspfRouteRpcClientFactory, OspfRouteRpcServer, PeerGroupInfo, PeerIdVersion,
PeerIdentityType, RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo,
RoutePeerInfos, SyncRouteInfoError, SyncRouteInfoRequest, SyncRouteInfoResponse,
TrustedCredentialPubkey, TrustedCredentialPubkeyProof, route_foreign_network_infos,
PeerIdentityType, PublicIpv6AddrRpcServer, RouteForeignNetworkInfos,
RouteForeignNetworkSummary, RoutePeerInfo, RoutePeerInfos, SyncRouteInfoError,
SyncRouteInfoRequest, SyncRouteInfoResponse, TrustedCredentialPubkey,
TrustedCredentialPubkeyProof, route_foreign_network_infos,
route_foreign_network_summary, sync_route_info_request::ConnInfo,
},
rpc_types::{
@@ -63,6 +64,9 @@ use super::{
PeerPacketFilter,
graph_algo::dijkstra_with_first_hop,
peer_rpc::PeerRpcManager,
public_ipv6::{
PublicIpv6PeerRouteInfo, PublicIpv6RouteControl, PublicIpv6Service, PublicIpv6SyncTrigger,
},
route_trait::{
DefaultRouteCostCalculator, ForeignNetworkRouteInfoMap, NextHopPolicy, RouteCostCalculator,
RouteCostCalculatorInterface,
@@ -137,6 +141,10 @@ fn raw_credential_bytes_from_route_info(
.map(|credential| credential.encode_to_vec())
}
fn route_peer_inst_id(info: &RoutePeerInfo) -> Option<uuid::Uuid> {
info.inst_id.map(Into::into)
}
#[derive(Debug, Clone)]
struct AtomicVersion(Arc<AtomicU32>);
@@ -205,6 +213,8 @@ impl RoutePeerInfo {
quic_port: None,
noise_static_pubkey: Vec::new(),
trusted_credential_pubkeys: Vec::new(),
ipv6_public_addr_prefix: None,
ipv6_public_addr_lease: None,
}
}
@@ -221,6 +231,7 @@ impl RoutePeerInfo {
my_peer_id: PeerId,
peer_route_id: u64,
global_ctx: &ArcGlobalCtx,
public_ipv6_addr_lease: Option<Ipv6Inet>,
) -> Self {
let stun_info = global_ctx.get_stun_info_collector().get_stun_info();
let noise_static_pubkey = global_ctx
@@ -259,6 +270,14 @@ impl RoutePeerInfo {
.unwrap_or(24),
ipv6_addr: global_ctx.get_ipv6().map(|x| x.into()),
ipv6_public_addr_prefix: global_ctx.get_advertised_ipv6_public_addr_prefix().map(
|prefix| {
Ipv6Inet::new(prefix.first_address(), prefix.network_length())
.unwrap()
.into()
},
),
ipv6_public_addr_lease: public_ipv6_addr_lease.map(Into::into),
groups: global_ctx.get_acl_groups(my_peer_id),
@@ -349,6 +368,8 @@ impl From<RoutePeerInfo> for crate::proto::api::instance::Route {
path_latency_latency_first: None,
ipv6_addr: val.ipv6_addr,
public_ipv6_addr: val.ipv6_public_addr_lease,
ipv6_public_addr_prefix: val.ipv6_public_addr_prefix,
}
}
}
@@ -964,8 +985,14 @@ impl SyncedRouteInfo {
my_peer_id: PeerId,
my_peer_route_id: u64,
global_ctx: &ArcGlobalCtx,
public_ipv6_addr_lease: Option<Ipv6Inet>,
) -> bool {
let mut new = RoutePeerInfo::new_updated_self(my_peer_id, my_peer_route_id, global_ctx);
let mut new = RoutePeerInfo::new_updated_self(
my_peer_id,
my_peer_route_id,
global_ctx,
public_ipv6_addr_lease,
);
let mut guard = self.peer_infos.upgradable_read();
let old = guard.get(&my_peer_id);
let new_version = old.map(|x| x.version).unwrap_or(0) + 1;
@@ -1588,6 +1615,21 @@ impl RouteTable {
.or_insert(peer_id_and_version);
}
if let Some(ipv6_addr) = info
.ipv6_public_addr_lease
.as_ref()
.and_then(|addr| addr.address)
{
self.ipv6_peer_id_map
.entry(ipv6_addr.into())
.and_modify(|v| {
if is_new_peer_better(v) {
*v = peer_id_and_version;
}
})
.or_insert(peer_id_and_version);
}
for cidr in info.proxy_cidrs.iter() {
let Ok(cidr) = cidr.parse::<IpCidr>() else {
tracing::warn!("invalid proxy cidr: {:?}, from peer: {:?}", cidr, peer_id);
@@ -2019,6 +2061,8 @@ struct PeerRouteServiceImpl {
foreign_network_owner_map: DashMap<NetworkIdentity, Vec<PeerId>>,
foreign_network_my_peer_id_map: DashMap<(String, PeerId), PeerId>,
synced_route_info: SyncedRouteInfo,
public_ipv6_service: std::sync::Mutex<Weak<PublicIpv6Service>>,
self_public_ipv6_addr_lease: std::sync::Mutex<Option<Ipv6Inet>>,
cached_local_conn_map: std::sync::Mutex<RouteConnBitmap>,
cached_local_conn_map_version: AtomicVersion,
cached_interface_peer_snapshot: std::sync::Mutex<Arc<InterfacePeerSnapshot>>,
@@ -2081,6 +2125,8 @@ impl PeerRouteServiceImpl {
non_reusable_credential_owners: DashMap::new(),
version: AtomicVersion::new(),
},
public_ipv6_service: std::sync::Mutex::new(Weak::new()),
self_public_ipv6_addr_lease: std::sync::Mutex::new(None),
cached_local_conn_map: std::sync::Mutex::new(RouteConnBitmap::default()),
cached_local_conn_map_version: AtomicVersion::new(),
cached_interface_peer_snapshot: std::sync::Mutex::new(Arc::new(
@@ -2119,6 +2165,20 @@ impl PeerRouteServiceImpl {
.unwrap_or(false)
}
fn set_public_ipv6_service(&self, service: Weak<PublicIpv6Service>) {
*self.public_ipv6_service.lock().unwrap() = service;
}
fn public_ipv6_service(&self) -> Option<Arc<PublicIpv6Service>> {
self.public_ipv6_service.lock().unwrap().upgrade()
}
fn notify_public_ipv6_route_change(&self) -> bool {
self.public_ipv6_service()
.map(|service| service.handle_route_change())
.unwrap_or(false)
}
fn get_or_create_session(&self, dst_peer_id: PeerId) -> Arc<SyncRouteSession> {
self.sessions
.entry(dst_peer_id)
@@ -2230,6 +2290,7 @@ impl PeerRouteServiceImpl {
self.my_peer_id,
self.my_peer_route_id,
&self.global_ctx,
*self.self_public_ipv6_addr_lease.lock().unwrap(),
)
}
@@ -2618,14 +2679,19 @@ impl PeerRouteServiceImpl {
untrusted_changed = self.refresh_credential_trusts_and_disconnect().await;
}
let mut public_ipv6_state_updated = false;
if my_peer_info_updated || my_conn_info_updated || untrusted_changed {
self.update_route_table_and_cached_local_conn_bitmap();
self.update_foreign_network_owner_map();
public_ipv6_state_updated = self.notify_public_ipv6_route_change();
}
if my_peer_info_updated {
self.update_peer_info_last_update();
}
my_peer_info_updated || my_conn_info_updated || my_foreign_network_updated
my_peer_info_updated
|| my_conn_info_updated
|| my_foreign_network_updated
|| public_ipv6_state_updated
}
async fn refresh_acl_groups(&self) -> bool {
@@ -2652,15 +2718,17 @@ impl PeerRouteServiceImpl {
let untrusted = self.refresh_credential_trusts_with_current_topology();
self.disconnect_untrusted_peers(&untrusted).await;
let mut public_ipv6_state_updated = false;
if my_peer_info_updated || !untrusted.is_empty() {
self.update_route_table_and_cached_local_conn_bitmap();
self.update_foreign_network_owner_map();
public_ipv6_state_updated = self.notify_public_ipv6_route_change();
}
if my_peer_info_updated {
self.update_peer_info_last_update();
}
my_peer_info_updated || !untrusted.is_empty()
my_peer_info_updated || !untrusted.is_empty() || public_ipv6_state_updated
}
fn refresh_credential_trusts(&self) -> Vec<PeerId> {
@@ -2968,7 +3036,6 @@ impl PeerRouteServiceImpl {
session
.update_dst_saved_foreign_network_version(foreign_network, dst_peer_id);
}
session.update_last_sync_succ_timestamp(next_last_sync_succ_timestamp);
}
}
@@ -3493,7 +3560,13 @@ impl RouteSessionManager {
}
if need_update_route_table || foreign_network_changed {
service_impl.update_route_table_and_cached_local_conn_bitmap();
service_impl.update_foreign_network_owner_map();
if need_update_route_table
&& let Some(public_ipv6_service) = service_impl.public_ipv6_service()
{
public_ipv6_service.handle_route_change();
}
}
tracing::debug!(
@@ -3534,12 +3607,86 @@ impl RouteSessionManager {
}
}
struct OspfPublicIpv6RouteHandle {
service_impl: Weak<PeerRouteServiceImpl>,
}
impl PublicIpv6RouteControl for OspfPublicIpv6RouteHandle {
fn my_peer_id(&self) -> PeerId {
self.service_impl
.upgrade()
.map(|service_impl| service_impl.my_peer_id)
.unwrap_or_default()
}
fn peer_route_snapshot(&self) -> Vec<PublicIpv6PeerRouteInfo> {
let Some(service_impl) = self.service_impl.upgrade() else {
return Vec::new();
};
service_impl
.synced_route_info
.peer_infos
.read()
.iter()
.map(|(peer_id, info)| PublicIpv6PeerRouteInfo {
peer_id: *peer_id,
inst_id: route_peer_inst_id(info),
is_provider: info
.feature_flag
.as_ref()
.map(|flags| flags.ipv6_public_addr_provider)
.unwrap_or(false),
prefix: info
.ipv6_public_addr_prefix
.map(Into::into)
.map(|prefix: Ipv6Inet| prefix.network()),
lease: info.ipv6_public_addr_lease.map(Into::into),
reachable: *peer_id == service_impl.my_peer_id
|| service_impl.route_table.peer_reachable(*peer_id),
})
.collect()
}
fn publish_self_public_ipv6_lease(&self, lease: Option<Ipv6Inet>) -> bool {
let Some(service_impl) = self.service_impl.upgrade() else {
return false;
};
let mut current = service_impl.self_public_ipv6_addr_lease.lock().unwrap();
if *current == lease {
return false;
}
*current = lease;
drop(current);
let changed = service_impl.update_my_peer_info();
if changed {
service_impl.update_route_table_and_cached_local_conn_bitmap();
service_impl.update_foreign_network_owner_map();
}
changed
}
}
#[derive(Clone)]
struct OspfPublicIpv6SyncTrigger {
session_mgr: RouteSessionManager,
}
impl PublicIpv6SyncTrigger for OspfPublicIpv6SyncTrigger {
fn sync_now(&self, reason: &str) {
self.session_mgr.sync_now(reason);
}
}
pub struct PeerRoute {
my_peer_id: PeerId,
global_ctx: ArcGlobalCtx,
peer_rpc: Weak<PeerRpcManager>,
service_impl: Arc<PeerRouteServiceImpl>,
public_ipv6_service: Arc<PublicIpv6Service>,
session_mgr: RouteSessionManager,
tasks: std::sync::Mutex<JoinSet<()>>,
@@ -3563,6 +3710,17 @@ impl PeerRoute {
) -> Arc<Self> {
let service_impl = Arc::new(PeerRouteServiceImpl::new(my_peer_id, global_ctx.clone()));
let session_mgr = RouteSessionManager::new(service_impl.clone(), peer_rpc.clone());
let public_ipv6_service = Arc::new(PublicIpv6Service::new(
global_ctx.clone(),
Arc::downgrade(&peer_rpc),
Arc::new(OspfPublicIpv6RouteHandle {
service_impl: Arc::downgrade(&service_impl),
}),
Arc::new(OspfPublicIpv6SyncTrigger {
session_mgr: session_mgr.clone(),
}),
));
service_impl.set_public_ipv6_service(Arc::downgrade(&public_ipv6_service));
Arc::new(PeerRoute {
my_peer_id,
@@ -3570,6 +3728,7 @@ impl PeerRoute {
peer_rpc: Arc::downgrade(&peer_rpc),
service_impl,
public_ipv6_service,
session_mgr,
tasks: std::sync::Mutex::new(JoinSet::new()),
@@ -3607,6 +3766,9 @@ impl PeerRoute {
tracing::debug!("cost_calculator_need_update");
service_impl.synced_route_info.version.inc();
service_impl.update_route_table();
if let Some(public_ipv6_service) = service_impl.public_ipv6_service() {
public_ipv6_service.handle_route_change();
}
}
select! {
@@ -3631,11 +3793,16 @@ impl PeerRoute {
// make sure my_peer_id is in the peer_infos.
self.service_impl.update_my_infos().await;
self.public_ipv6_service.handle_route_change();
peer_rpc.rpc_server().registry().register(
OspfRouteRpcServer::new(self.session_mgr.clone()),
&self.global_ctx.get_network_name(),
);
peer_rpc.rpc_server().registry().register(
PublicIpv6AddrRpcServer::new(self.public_ipv6_service.rpc_server()),
&self.global_ctx.get_network_name(),
);
self.tasks
.lock()
@@ -3657,6 +3824,16 @@ impl PeerRoute {
.lock()
.unwrap()
.spawn(Self::clear_expired_peer(self.service_impl.clone()));
self.tasks
.lock()
.unwrap()
.spawn(self.public_ipv6_service.clone().provider_gc_routine());
self.tasks
.lock()
.unwrap()
.spawn(self.public_ipv6_service.clone().client_routine());
}
}
@@ -3677,6 +3854,10 @@ impl Drop for PeerRoute {
OspfRouteRpcServer::new(self.session_mgr.clone()),
&self.global_ctx.get_network_name(),
);
peer_rpc.rpc_server().registry().unregister(
PublicIpv6AddrRpcServer::new(self.public_ipv6_service.rpc_server()),
&self.global_ctx.get_network_name(),
);
}
}
@@ -3765,6 +3946,51 @@ impl Route for PeerRoute {
.collect()
}
async fn list_public_ipv6_routes(&self) -> BTreeSet<Ipv6Inet> {
self.public_ipv6_service.list_routes()
}
async fn get_my_public_ipv6_addr(&self) -> Option<Ipv6Inet> {
self.public_ipv6_service.my_addr()
}
async fn get_public_ipv6_gateway_peer_id(&self) -> Option<PeerId> {
self.public_ipv6_service.provider_peer_id_for_client()
}
async fn get_local_public_ipv6_info(
&self,
) -> crate::proto::api::instance::ListPublicIpv6InfoResponse {
let Some((provider, leases)) = self.public_ipv6_service.local_provider_state() else {
return crate::proto::api::instance::ListPublicIpv6InfoResponse::default();
};
crate::proto::api::instance::ListPublicIpv6InfoResponse {
provider_prefix: Some(
Ipv6Inet::new(
provider.prefix.first_address(),
provider.prefix.network_length(),
)
.unwrap()
.into(),
),
provider_leases: leases
.into_iter()
.map(|lease| crate::proto::api::instance::PublicIpv6LeaseInfo {
peer_id: lease.peer_id,
inst_id: lease.inst_id.to_string(),
leased_addr: Some(lease.addr.into()),
valid_until_unix_seconds: lease
.valid_until
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64,
reused: lease.reused,
})
.collect(),
}
}
async fn get_peer_id_by_ipv4(&self, ipv4_addr: &Ipv4Addr) -> Option<PeerId> {
let route_table = &self.service_impl.route_table;
if let Some(p) = route_table.ipv4_peer_id_map.get(ipv4_addr) {
@@ -5180,6 +5406,7 @@ mod tests {
service_impl.my_peer_id,
service_impl.my_peer_route_id,
&service_impl.global_ctx,
None,
);
let mut self_info = self_info;
self_info.version = 1;
+4
View File
@@ -41,6 +41,10 @@ impl DirectConnectorRpc for DirectConnectorManagerRpcServer {
let et_ipv6: crate::proto::common::Ipv6Addr = et_ipv6.address().into();
ret.interface_ipv6s.retain(|x| *x != et_ipv6);
}
if let Some(public_ipv6) = self.global_ctx.get_public_ipv6_lease() {
let public_ipv6: crate::proto::common::Ipv6Addr = public_ipv6.address().into();
ret.interface_ipv6s.retain(|x| *x != public_ipv6);
}
tracing::trace!(
"get_ip_list: public_ipv4: {:?}, public_ipv6: {:?}, listeners: {:?}",
ret.public_ipv4,
File diff suppressed because it is too large Load Diff
+31 -3
View File
@@ -1,3 +1,4 @@
use cidr::Ipv6Inet;
use cidr::{Ipv4Cidr, Ipv6Cidr};
use dashmap::DashMap;
use std::{
@@ -8,9 +9,12 @@ use std::{
use crate::{
common::{PeerId, global_ctx::NetworkIdentity},
proto::peer_rpc::{
ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, PeerIdentityType,
RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo,
proto::{
api::instance::ListPublicIpv6InfoResponse,
peer_rpc::{
ForeignNetworkRouteInfoEntry, ForeignNetworkRouteInfoKey, PeerIdentityType,
RouteForeignNetworkInfos, RouteForeignNetworkSummary, RoutePeerInfo,
},
},
};
@@ -93,6 +97,22 @@ pub trait Route {
// TODO: rewrite route management, remove this
async fn list_proxy_cidrs_v6(&self) -> BTreeSet<Ipv6Cidr>;
async fn list_public_ipv6_routes(&self) -> BTreeSet<Ipv6Inet> {
BTreeSet::new()
}
async fn get_my_public_ipv6_addr(&self) -> Option<Ipv6Inet> {
None
}
async fn get_public_ipv6_gateway_peer_id(&self) -> Option<PeerId> {
None
}
async fn get_local_public_ipv6_info(&self) -> ListPublicIpv6InfoResponse {
ListPublicIpv6InfoResponse::default()
}
async fn get_peer_id_by_ipv4(&self, _ipv4: &Ipv4Addr) -> Option<PeerId> {
None
}
@@ -194,6 +214,14 @@ impl Route for MockRoute {
unimplemented!()
}
async fn list_public_ipv6_routes(&self) -> BTreeSet<Ipv6Inet> {
unimplemented!()
}
async fn get_my_public_ipv6_addr(&self) -> Option<Ipv6Inet> {
panic!("mock route")
}
async fn get_peer_info(&self, _peer_id: PeerId) -> Option<RoutePeerInfo> {
panic!("mock route")
}
+13 -3
View File
@@ -13,9 +13,9 @@ use crate::{
GetWhitelistRequest, GetWhitelistResponse, ListCredentialsRequest,
ListCredentialsResponse, ListForeignNetworkRequest, ListForeignNetworkResponse,
ListGlobalForeignNetworkRequest, ListGlobalForeignNetworkResponse, ListPeerRequest,
ListPeerResponse, ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc,
RevokeCredentialRequest, RevokeCredentialResponse, ShowNodeInfoRequest,
ShowNodeInfoResponse,
ListPeerResponse, ListPublicIpv6InfoRequest, ListPublicIpv6InfoResponse,
ListRouteRequest, ListRouteResponse, PeerInfo, PeerManageRpc, RevokeCredentialRequest,
RevokeCredentialResponse, ShowNodeInfoRequest, ShowNodeInfoResponse,
},
rpc_types::{self, controller::BaseController},
},
@@ -99,6 +99,16 @@ impl PeerManageRpc for PeerManagerRpcService {
Ok(reply)
}
async fn list_public_ipv6_info(
&self,
_: BaseController,
_request: ListPublicIpv6InfoRequest,
) -> Result<ListPublicIpv6InfoResponse, rpc_types::error::Error> {
Ok(weak_upgrade(&self.peer_manager)?
.get_local_public_ipv6_info()
.await)
}
async fn list_route(
&self,
_: BaseController,
+3
View File
@@ -24,6 +24,9 @@ message InstanceConfigPatch {
repeated ExitNodePatch exit_nodes = 8;
repeated UrlPatch mapped_listeners = 9;
repeated UrlPatch connectors = 10;
optional bool ipv6_public_addr_provider = 11;
optional bool ipv6_public_addr_auto = 12;
optional string ipv6_public_addr_prefix = 13;
}
message PortForwardPatch {
+21
View File
@@ -81,6 +81,8 @@ message Route {
optional int32 path_latency_latency_first = 14;
common.Ipv6Inet ipv6_addr = 15;
common.Ipv6Inet public_ipv6_addr = 16;
common.Ipv6Inet ipv6_public_addr_prefix = 17;
}
message PeerRoutePair {
@@ -100,12 +102,29 @@ message NodeInfo {
string version = 9;
common.PeerFeatureFlag feature_flag = 10;
peer_rpc.GetIpListResponse ip_list = 11;
common.Ipv6Inet public_ipv6_addr = 12;
common.Ipv6Inet ipv6_public_addr_prefix = 13;
}
message ShowNodeInfoRequest { InstanceIdentifier instance = 1; }
message ShowNodeInfoResponse { NodeInfo node_info = 1; }
message PublicIpv6LeaseInfo {
uint32 peer_id = 1;
string inst_id = 2;
common.Ipv6Inet leased_addr = 3;
int64 valid_until_unix_seconds = 4;
bool reused = 5;
}
message ListPublicIpv6InfoRequest { InstanceIdentifier instance = 1; }
message ListPublicIpv6InfoResponse {
common.Ipv6Inet provider_prefix = 1;
repeated PublicIpv6LeaseInfo provider_leases = 2;
}
message ListRouteRequest { InstanceIdentifier instance = 1; }
message ListRouteResponse { repeated Route routes = 1; }
@@ -167,6 +186,8 @@ message GetForeignNetworkSummaryResponse {
service PeerManageRpc {
rpc ListPeer(ListPeerRequest) returns (ListPeerResponse);
rpc ListPublicIpv6Info(ListPublicIpv6InfoRequest)
returns (ListPublicIpv6InfoResponse);
rpc ListRoute(ListRouteRequest) returns (ListRouteResponse);
rpc DumpRoute(DumpRouteRequest) returns (DumpRouteResponse);
rpc ListForeignNetwork(ListForeignNetworkRequest)
+3
View File
@@ -96,6 +96,9 @@ message NetworkConfig {
optional bool need_p2p = 59;
optional uint64 instance_recv_bps_limit = 60;
optional bool disable_upnp = 61;
optional bool ipv6_public_addr_provider = 62;
optional bool ipv6_public_addr_auto = 63;
optional string ipv6_public_addr_prefix = 64;
}
message PortForwardConfig {
+1
View File
@@ -225,6 +225,7 @@ message PeerFeatureFlag {
bool is_credential_peer = 8;
bool need_p2p = 9;
bool disable_p2p = 10;
bool ipv6_public_addr_provider = 11;
}
enum SocketType {
+43
View File
@@ -47,6 +47,9 @@ message RoutePeerInfo {
// Trusted credential public keys published by admin nodes (holding network_secret)
repeated TrustedCredentialPubkeyProof trusted_credential_pubkeys = 19;
optional common.Ipv6Inet ipv6_public_addr_prefix = 22;
optional common.Ipv6Inet ipv6_public_addr_lease = 24;
}
message PeerIdVersion {
@@ -133,6 +136,46 @@ service OspfRouteRpc {
rpc SyncRouteInfo(SyncRouteInfoRequest) returns (SyncRouteInfoResponse);
}
message AcquireIpv6PublicAddrLeaseRequest {
uint32 peer_id = 1;
common.UUID inst_id = 2;
}
message RenewIpv6PublicAddrLeaseRequest {
uint32 peer_id = 1;
common.UUID inst_id = 2;
common.Ipv6Inet leased_addr = 3;
}
message ReleaseIpv6PublicAddrLeaseRequest {
uint32 peer_id = 1;
common.UUID inst_id = 2;
}
message GetIpv6PublicAddrLeaseRequest {
uint32 peer_id = 1;
common.UUID inst_id = 2;
}
message Ipv6PublicAddrLeaseReply {
uint32 provider_peer_id = 1;
common.UUID provider_inst_id = 2;
common.Ipv6Inet provider_prefix = 3;
common.Ipv6Inet leased_addr = 4;
google.protobuf.Timestamp valid_until = 5;
bool reused = 6;
optional string error_msg = 7;
}
service PublicIpv6AddrRpc {
rpc AcquireLease(AcquireIpv6PublicAddrLeaseRequest)
returns (Ipv6PublicAddrLeaseReply);
rpc RenewLease(RenewIpv6PublicAddrLeaseRequest)
returns (Ipv6PublicAddrLeaseReply);
rpc ReleaseLease(ReleaseIpv6PublicAddrLeaseRequest) returns (common.Void);
rpc GetLease(GetIpv6PublicAddrLeaseRequest) returns (Ipv6PublicAddrLeaseReply);
}
message GetIpListRequest {}
message GetIpListResponse {
+15 -1
View File
@@ -3,7 +3,10 @@ use std::sync::Arc;
use crate::{
instance_manager::NetworkInstanceManager,
proto::{
api::instance::{self, ListPeerRequest, ListPeerResponse, PeerManageRpc},
api::instance::{
self, ListPeerRequest, ListPeerResponse, ListPublicIpv6InfoRequest,
ListPublicIpv6InfoResponse, PeerManageRpc,
},
rpc_types::controller::BaseController,
},
};
@@ -34,6 +37,17 @@ impl PeerManageRpc for PeerManageRpcService {
.await
}
async fn list_public_ipv6_info(
&self,
ctrl: Self::Controller,
req: ListPublicIpv6InfoRequest,
) -> crate::proto::rpc_types::error::Result<ListPublicIpv6InfoResponse> {
super::get_instance_service(&self.instance_manager, &req.instance)?
.get_peer_manage_service()
.list_public_ipv6_info(ctrl, req)
.await
}
async fn list_route(
&self,
ctrl: Self::Controller,
+1 -1
View File
@@ -38,7 +38,7 @@ async fn test_route_peer_info_ipv6() {
global_ctx.set_ipv6(Some(ipv6_cidr));
// Create RoutePeerInfo with IPv6 support
let updated_info = RoutePeerInfo::new_updated_self(123, 456, &global_ctx);
let updated_info = RoutePeerInfo::new_updated_self(123, 456, &global_ctx, None);
// Verify IPv6 address is included
assert!(updated_info.ipv6_addr.is_some());
+571 -1
View File
@@ -402,6 +402,528 @@ async fn ping6_test(from_netns: &str, target_ip: &str, payload_size: Option<usiz
code.code().unwrap() == 0
}
fn run_cmd(program: &str, args: &[&str]) {
let output = std::process::Command::new(program)
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"{} {:?} failed: stdout={}, stderr={}",
program,
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn run_cmd_output(program: &str, args: &[&str]) -> String {
let output = std::process::Command::new(program)
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"{} {:?} failed: stdout={}, stderr={}",
program,
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn run_ip(args: &[&str]) {
run_cmd("ip", args);
}
fn run_ip_in_ns(ns: &str, args: &[&str]) {
let mut cmd = vec!["netns", "exec", ns, "ip"];
cmd.extend_from_slice(args);
run_cmd("ip", &cmd);
}
fn run_ip_in_ns_output(ns: &str, args: &[&str]) -> String {
let mut cmd = vec!["netns", "exec", ns, "ip"];
cmd.extend_from_slice(args);
run_cmd_output("ip", &cmd)
}
fn run_sysctl_in_ns(ns: &str, assignment: &str) {
run_cmd("ip", &["netns", "exec", ns, "sysctl", "-qw", assignment]);
}
fn create_empty_netns(name: &str) {
del_netns(name);
run_ip(&["netns", "add", name]);
run_ip(&["netns", "exec", name, "ip", "link", "set", "lo", "up"]);
}
fn connect_ns_to_bridge(ns: &str, guest_if: &str, host_if: &str, bridge: &str) {
let _ = std::process::Command::new("ip")
.args(["link", "del", host_if])
.status();
run_ip(&[
"link", "add", host_if, "type", "veth", "peer", "name", guest_if,
]);
run_ip(&["link", "set", guest_if, "netns", ns]);
run_ip(&["link", "set", host_if, "up"]);
run_cmd("brctl", &["addif", bridge, host_if]);
run_ip(&["netns", "exec", ns, "ip", "link", "set", guest_if, "up"]);
}
struct PublicIpv6Lab {
extra_namespaces: [&'static str; 2],
extra_bridges: [&'static str; 2],
}
impl PublicIpv6Lab {
const PROVIDER_NS: &'static str = "net_a";
const CLIENT_NS: &'static str = "net_b";
const UPSTREAM_NS: &'static str = "net_pubgw";
const SERVER_NS: &'static str = "net_pubsrv";
const WAN_BRIDGE: &'static str = "br_pubwan";
const SERVER_BRIDGE: &'static str = "br_pubsrv";
const PROVIDER_TUN: &'static str = "etpubv6p";
const CLIENT_TUN: &'static str = "etpubv6c";
const PROVIDER_PREFIX: &'static str = "2001:db8:100::/64";
const PROVIDER_DEFAULT_FROM: &'static str = "2001:db8:100::/64";
const PROVIDER_WAN_ADDR: &'static str = "2001:db8:ffff:1::2/64";
const UPSTREAM_WAN_ADDR: &'static str = "2001:db8:ffff:1::1/64";
const UPSTREAM_SERVER_ADDR: &'static str = "2001:db8:ffff:2::1/64";
const SERVER_ADDR: &'static str = "2001:db8:ffff:2::100/64";
const SERVER_IP: &'static str = "2001:db8:ffff:2::100";
fn setup() -> Self {
prepare_linux_namespaces();
del_netns(Self::UPSTREAM_NS);
del_netns(Self::SERVER_NS);
let _ = std::process::Command::new("ip")
.args(["link", "del", Self::WAN_BRIDGE])
.status();
let _ = std::process::Command::new("ip")
.args(["link", "del", Self::SERVER_BRIDGE])
.status();
let _ = std::process::Command::new("brctl")
.args(["delbr", Self::WAN_BRIDGE])
.status();
let _ = std::process::Command::new("brctl")
.args(["delbr", Self::SERVER_BRIDGE])
.status();
create_empty_netns(Self::UPSTREAM_NS);
create_empty_netns(Self::SERVER_NS);
prepare_bridge(Self::WAN_BRIDGE);
prepare_bridge(Self::SERVER_BRIDGE);
run_ip(&["link", "set", Self::WAN_BRIDGE, "up"]);
run_ip(&["link", "set", Self::SERVER_BRIDGE, "up"]);
connect_ns_to_bridge(
Self::PROVIDER_NS,
"pubwan0",
"veth_pubwan_p",
Self::WAN_BRIDGE,
);
connect_ns_to_bridge(
Self::UPSTREAM_NS,
"upwan0",
"veth_pubwan_u",
Self::WAN_BRIDGE,
);
connect_ns_to_bridge(
Self::UPSTREAM_NS,
"upsrv0",
"veth_pubsrv_u",
Self::SERVER_BRIDGE,
);
connect_ns_to_bridge(
Self::SERVER_NS,
"srv0",
"veth_pubsrv_s",
Self::SERVER_BRIDGE,
);
run_ip_in_ns(
Self::PROVIDER_NS,
&["addr", "add", Self::PROVIDER_WAN_ADDR, "dev", "pubwan0"],
);
run_ip_in_ns(
Self::UPSTREAM_NS,
&["addr", "add", Self::UPSTREAM_WAN_ADDR, "dev", "upwan0"],
);
run_ip_in_ns(
Self::UPSTREAM_NS,
&["addr", "add", Self::UPSTREAM_SERVER_ADDR, "dev", "upsrv0"],
);
run_ip_in_ns(
Self::SERVER_NS,
&["addr", "add", Self::SERVER_ADDR, "dev", "srv0"],
);
run_ip_in_ns(
Self::PROVIDER_NS,
&["link", "add", "pubprefix0", "type", "dummy"],
);
run_ip_in_ns(Self::PROVIDER_NS, &["link", "set", "pubprefix0", "up"]);
run_ip_in_ns(
Self::PROVIDER_NS,
&[
"-6",
"route",
"add",
Self::PROVIDER_PREFIX,
"dev",
"pubprefix0",
],
);
run_ip_in_ns(
Self::PROVIDER_NS,
&[
"-6",
"route",
"add",
"default",
"from",
Self::PROVIDER_DEFAULT_FROM,
"via",
"2001:db8:ffff:1::1",
"dev",
"pubwan0",
],
);
run_ip_in_ns(
Self::SERVER_NS,
&[
"-6",
"route",
"add",
"default",
"via",
"2001:db8:ffff:2::1",
"dev",
"srv0",
],
);
run_ip_in_ns(
Self::UPSTREAM_NS,
&[
"-6",
"route",
"add",
Self::PROVIDER_PREFIX,
"via",
"2001:db8:ffff:1::2",
"dev",
"upwan0",
],
);
run_sysctl_in_ns(Self::PROVIDER_NS, "net.ipv6.conf.all.forwarding=1");
run_sysctl_in_ns(Self::UPSTREAM_NS, "net.ipv6.conf.all.forwarding=1");
Self {
extra_namespaces: [Self::UPSTREAM_NS, Self::SERVER_NS],
extra_bridges: [Self::WAN_BRIDGE, Self::SERVER_BRIDGE],
}
}
}
impl Drop for PublicIpv6Lab {
fn drop(&mut self) {
for ns in self.extra_namespaces {
del_netns(ns);
}
for bridge in self.extra_bridges {
let _ = std::process::Command::new("ip")
.args(["link", "del", bridge])
.status();
let _ = std::process::Command::new("brctl")
.args(["delbr", bridge])
.status();
}
}
}
fn get_public_ipv6_config(
inst_name: &str,
netns: &str,
ipv4: &str,
dev_name: &str,
inst_id: uuid::Uuid,
) -> TomlConfigLoader {
let config = get_inst_config(inst_name, Some(netns), ipv4, "fd00::1/64");
config.set_id(inst_id);
config.set_ipv6(None);
config.set_socks5_portal(None);
config.set_network_identity(NetworkIdentity {
network_name: "public_ipv6_auto_addr_test".to_string(),
network_secret: Some("public_ipv6_auto_addr_secret".to_string()),
network_secret_digest: None,
});
config.set_listeners(vec!["tcp://0.0.0.0:11010".parse().unwrap()]);
let mut flags = config.get_flags();
flags.dev_name = dev_name.to_string();
config.set_flags(flags);
config
}
async fn init_public_ipv6_two_node(
client_inst_id: uuid::Uuid,
) -> (PublicIpv6Lab, Instance, Instance) {
let lab = PublicIpv6Lab::setup();
let provider_cfg = get_public_ipv6_config(
"provider_public_ipv6",
PublicIpv6Lab::PROVIDER_NS,
"10.144.144.1",
PublicIpv6Lab::PROVIDER_TUN,
uuid::Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(),
);
provider_cfg.set_ipv6_public_addr_provider(true);
let client_cfg = get_public_ipv6_config(
"client_public_ipv6",
PublicIpv6Lab::CLIENT_NS,
"10.144.144.2",
PublicIpv6Lab::CLIENT_TUN,
client_inst_id,
);
client_cfg.set_ipv6_public_addr_auto(true);
let mut provider = Instance::new(provider_cfg);
let mut client = Instance::new(client_cfg);
provider.run().await.unwrap();
client.run().await.unwrap();
provider
.get_conn_manager()
.add_connector(TcpTunnelConnector::new(
"tcp://10.1.1.2:11010".parse().unwrap(),
));
wait_for_condition(
|| async {
provider.get_peer_manager().list_routes().await.len() == 1
&& client.get_peer_manager().list_routes().await.len() == 1
},
Duration::from_secs(8),
)
.await;
(lab, provider, client)
}
async fn wait_for_public_ipv6_addr(inst: &Instance) -> cidr::Ipv6Inet {
wait_for_condition(
|| async {
inst.get_peer_manager()
.get_my_public_ipv6_addr()
.await
.is_some()
},
Duration::from_secs(10),
)
.await;
inst.get_peer_manager()
.get_my_public_ipv6_addr()
.await
.unwrap()
}
async fn wait_for_public_ipv6_route(inst: &Instance, target: cidr::Ipv6Inet) {
wait_for_condition(
|| async {
inst.get_peer_manager()
.list_public_ipv6_routes()
.await
.contains(&target)
},
Duration::from_secs(10),
)
.await;
}
fn route_exists_in_ns(ns: &str, needle: &str) -> bool {
run_ip_in_ns_output(ns, &["-6", "route", "show"])
.lines()
.any(|line| line.contains(needle))
}
fn addr_exists_in_ns(ns: &str, dev: &str, needle: &str) -> bool {
run_ip_in_ns_output(ns, &["-6", "addr", "show", "dev", dev]).contains(needle)
}
#[tokio::test]
#[serial_test::serial]
pub async fn public_ipv6_auto_addr_end_to_end() {
let client_id = uuid::Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap();
let (_lab, provider, client) = init_public_ipv6_two_node(client_id).await;
wait_for_condition(
|| async {
provider
.get_global_ctx()
.get_advertised_ipv6_public_addr_prefix()
== Some(PublicIpv6Lab::PROVIDER_PREFIX.parse().unwrap())
},
Duration::from_secs(10),
)
.await;
let leased = wait_for_public_ipv6_addr(&client).await;
wait_for_public_ipv6_route(&provider, leased).await;
assert_eq!(
provider
.get_global_ctx()
.config
.get_ipv6_public_addr_prefix(),
None
);
assert_eq!(
provider
.get_global_ctx()
.get_advertised_ipv6_public_addr_prefix(),
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()
)
);
let provider_info = provider
.get_peer_manager()
.get_local_public_ipv6_info()
.await;
let client_peer_id = client.get_peer_manager().get_my_info().await.peer_id;
assert_eq!(
provider_info.provider_prefix,
Some(
cidr::Ipv6Inet::new(
provider_prefix.first_address(),
provider_prefix.network_length()
)
.unwrap()
.into()
)
);
assert_eq!(provider_info.provider_leases.len(), 1);
assert_eq!(provider_info.provider_leases[0].peer_id, client_peer_id);
assert_eq!(
provider_info.provider_leases[0].inst_id,
client_id.to_string()
);
assert_eq!(
provider_info.provider_leases[0].leased_addr,
Some(leased.into())
);
assert!(
leased.address().segments()[0] & 0xfe00 != 0xfc00,
"leased address should not be unique-local: {leased}"
);
wait_for_condition(
|| async {
addr_exists_in_ns(
PublicIpv6Lab::CLIENT_NS,
PublicIpv6Lab::CLIENT_TUN,
&leased.to_string(),
) && route_exists_in_ns(
PublicIpv6Lab::CLIENT_NS,
&format!("default dev {}", PublicIpv6Lab::CLIENT_TUN),
) && route_exists_in_ns(
PublicIpv6Lab::PROVIDER_NS,
&format!("{} dev {}", leased.address(), PublicIpv6Lab::PROVIDER_TUN),
)
},
Duration::from_secs(10),
)
.await;
wait_for_condition(
|| async { ping6_test(PublicIpv6Lab::CLIENT_NS, PublicIpv6Lab::SERVER_IP, None).await },
Duration::from_secs(10),
)
.await;
wait_for_condition(
|| async {
ping6_test(
PublicIpv6Lab::SERVER_NS,
leased.address().to_string().as_str(),
None,
)
.await
},
Duration::from_secs(10),
)
.await;
drop_insts(vec![provider, client]).await;
}
#[tokio::test]
#[serial_test::serial]
pub async fn public_ipv6_auto_addr_reconnect_reuses_same_address() {
let client_id = uuid::Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap();
let (_lab, provider, client) = init_public_ipv6_two_node(client_id).await;
let first = wait_for_public_ipv6_addr(&client).await;
drop_insts(vec![client]).await;
let client_cfg = get_public_ipv6_config(
"client_public_ipv6_reconnect",
PublicIpv6Lab::CLIENT_NS,
"10.144.144.2",
PublicIpv6Lab::CLIENT_TUN,
client_id,
);
client_cfg.set_ipv6_public_addr_auto(true);
let mut client = Instance::new(client_cfg);
client.run().await.unwrap();
provider
.get_conn_manager()
.add_connector(TcpTunnelConnector::new(
"tcp://10.1.1.2:11010".parse().unwrap(),
));
wait_for_condition(
|| async {
provider.get_peer_manager().list_routes().await.len() == 1
&& client.get_peer_manager().list_routes().await.len() == 1
},
Duration::from_secs(8),
)
.await;
let second = wait_for_public_ipv6_addr(&client).await;
assert_eq!(first, second);
wait_for_condition(
|| async { ping6_test(PublicIpv6Lab::CLIENT_NS, PublicIpv6Lab::SERVER_IP, None).await },
Duration::from_secs(10),
)
.await;
drop_insts(vec![provider, client]).await;
}
#[rstest::rstest]
#[tokio::test]
#[serial_test::serial]
@@ -3077,7 +3599,15 @@ pub async fn config_patch_test() {
};
use crate::tunnel::common::tests::_tunnel_pingpong_netns_with_timeout;
let insts = init_three_node("udp").await;
let insts = init_three_node_ex(
"udp",
|cfg| {
cfg.set_ipv6(None);
cfg
},
false,
)
.await;
check_route(
"10.144.144.2/24",
@@ -3124,6 +3654,46 @@ pub async fn config_patch_test() {
},
);
// 测试1.1:修改公网 IPv6 provider 相关配置
let public_prefix = "2001:db8:100::/64";
let patch = InstanceConfigPatch {
ipv6_public_addr_provider: Some(true),
ipv6_public_addr_auto: Some(true),
ipv6_public_addr_prefix: Some(public_prefix.to_string()),
..Default::default()
};
insts[1]
.get_config_patcher()
.apply_patch(patch)
.await
.unwrap();
assert!(
insts[1]
.get_global_ctx()
.config
.get_ipv6_public_addr_provider()
);
assert!(insts[1].get_global_ctx().config.get_ipv6_public_addr_auto());
assert_eq!(
insts[1]
.get_global_ctx()
.config
.get_ipv6_public_addr_prefix(),
Some(public_prefix.parse().unwrap())
);
assert!(
insts[1]
.get_global_ctx()
.get_feature_flags()
.ipv6_public_addr_provider
);
assert_eq!(
insts[1]
.get_global_ctx()
.get_advertised_ipv6_public_addr_prefix(),
Some(public_prefix.parse().unwrap())
);
// 测试2: 端口转发
let patch = InstanceConfigPatch {
port_forwards: vec![PortForwardPatch {