mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-15 18:35:47 +00:00
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:
@@ -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#"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user