Add support for IPv6 within VPN (#1061)

* add flake.nix with nix based dev shell
* add support for IPv6
* update thunk

---------

Co-authored-by: sijie.sun <sijie.sun@smartx.com>
This commit is contained in:
DavHau
2025-07-04 22:43:30 +07:00
committed by GitHub
parent 01e491ec07
commit d0cfc49806
32 changed files with 893 additions and 70 deletions
+21
View File
@@ -64,6 +64,9 @@ pub trait ConfigLoader: Send + Sync {
fn get_ipv4(&self) -> Option<cidr::Ipv4Inet>;
fn set_ipv4(&self, addr: Option<cidr::Ipv4Inet>);
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet>;
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>);
fn get_dhcp(&self) -> bool;
fn set_dhcp(&self, dhcp: bool);
@@ -259,6 +262,7 @@ struct Config {
instance_name: Option<String>,
instance_id: Option<uuid::Uuid>,
ipv4: Option<String>,
ipv6: Option<String>,
dhcp: Option<bool>,
network_identity: Option<NetworkIdentity>,
listeners: Option<Vec<url::Url>>,
@@ -416,6 +420,23 @@ impl ConfigLoader for TomlConfigLoader {
};
}
fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
let locked_config = self.config.lock().unwrap();
locked_config
.ipv6
.as_ref()
.map(|s| s.parse().ok())
.flatten()
}
fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
self.config.lock().unwrap().ipv6 = if let Some(addr) = addr {
Some(addr.to_string())
} else {
None
};
}
fn get_dhcp(&self) -> bool {
self.config.lock().unwrap().dhcp.unwrap_or_default()
}
+16
View File
@@ -61,6 +61,7 @@ pub struct GlobalCtx {
event_bus: EventBus,
cached_ipv4: AtomicCell<Option<cidr::Ipv4Inet>>,
cached_ipv6: AtomicCell<Option<cidr::Ipv6Inet>>,
cached_proxy_cidrs: AtomicCell<Option<Vec<ProxyNetworkConfig>>>,
ip_collector: Mutex<Option<Arc<IPCollector>>>,
@@ -124,6 +125,7 @@ impl GlobalCtx {
event_bus,
cached_ipv4: AtomicCell::new(None),
cached_ipv6: AtomicCell::new(None),
cached_proxy_cidrs: AtomicCell::new(None),
ip_collector: Mutex::new(Some(Arc::new(IPCollector::new(
@@ -191,6 +193,20 @@ impl GlobalCtx {
self.cached_ipv4.store(None);
}
pub fn get_ipv6(&self) -> Option<cidr::Ipv6Inet> {
if let Some(ret) = self.cached_ipv6.load() {
return Some(ret);
}
let addr = self.config.get_ipv6();
self.cached_ipv6.store(addr.clone());
return addr;
}
pub fn set_ipv6(&self, addr: Option<cidr::Ipv6Inet>) {
self.config.set_ipv6(addr);
self.cached_ipv6.store(None);
}
pub fn get_id(&self) -> uuid::Uuid {
self.config.get_id()
}
+58
View File
@@ -80,4 +80,62 @@ impl IfConfiguerTrait for MacIfConfiger {
async fn set_mtu(&self, name: &str, mtu: u32) -> Result<(), Error> {
run_shell_cmd(format!("ifconfig {} mtu {}", name, mtu).as_str()).await
}
async fn add_ipv6_ip(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
run_shell_cmd(
format!("ifconfig {} inet6 {}/{} add", name, address, cidr_prefix).as_str(),
)
.await
}
async fn remove_ipv6(&self, name: &str, ip: Option<std::net::Ipv6Addr>) -> Result<(), Error> {
if let Some(ip) = ip {
run_shell_cmd(format!("ifconfig {} inet6 {} delete", name, ip).as_str()).await
} else {
// Remove all IPv6 addresses is more complex on macOS, just succeed
Ok(())
}
}
async fn add_ipv6_route(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
cost: Option<i32>,
) -> Result<(), Error> {
let cmd = if let Some(cost) = cost {
format!(
"route -n add -inet6 {}/{} -interface {} -hopcount {}",
address, cidr_prefix, name, cost
)
} else {
format!(
"route -n add -inet6 {}/{} -interface {}",
address, cidr_prefix, name
)
};
run_shell_cmd(cmd.as_str()).await
}
async fn remove_ipv6_route(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
run_shell_cmd(
format!(
"route -n delete -inet6 {}/{} -interface {}",
address, cidr_prefix, name
)
.as_str(),
)
.await
}
}
+29 -1
View File
@@ -7,7 +7,7 @@ mod windows;
mod route;
use std::net::Ipv4Addr;
use std::net::{Ipv4Addr, Ipv6Addr};
use async_trait::async_trait;
use tokio::process::Command;
@@ -41,12 +41,40 @@ pub trait IfConfiguerTrait: Send + Sync {
) -> Result<(), Error> {
Ok(())
}
async fn add_ipv6_route(
&self,
_name: &str,
_address: Ipv6Addr,
_cidr_prefix: u8,
_cost: Option<i32>,
) -> Result<(), Error> {
Ok(())
}
async fn remove_ipv6_route(
&self,
_name: &str,
_address: Ipv6Addr,
_cidr_prefix: u8,
) -> Result<(), Error> {
Ok(())
}
async fn add_ipv6_ip(
&self,
_name: &str,
_address: Ipv6Addr,
_cidr_prefix: u8,
) -> Result<(), Error> {
Ok(())
}
async fn set_link_status(&self, _name: &str, _up: bool) -> Result<(), Error> {
Ok(())
}
async fn remove_ip(&self, _name: &str, _ip: Option<Ipv4Addr>) -> Result<(), Error> {
Ok(())
}
async fn remove_ipv6(&self, _name: &str, _ip: Option<Ipv6Addr>) -> Result<(), Error> {
Ok(())
}
async fn wait_interface_show(&self, _name: &str) -> Result<(), Error> {
return Ok(());
}
+126
View File
@@ -194,6 +194,32 @@ impl NetlinkIfConfiger {
)
}
fn get_prefix_len_ipv6(name: &str, ip: Ipv6Addr) -> Result<u8, Error> {
let addrs = Self::list_addresses(name)?;
for addr in addrs {
if addr.address() == IpAddr::V6(ip) {
return Ok(addr.network_length());
}
}
Err(Error::NotFound)
}
fn remove_one_ipv6(name: &str, ip: Ipv6Addr, prefix_len: u8) -> Result<(), Error> {
let mut message = AddressMessage::default();
message.header.prefix_len = prefix_len;
message.header.index = NetlinkIfConfiger::get_interface_index(name)?;
message.header.family = AddressFamily::Inet6;
message
.attributes
.push(AddressAttribute::Address(std::net::IpAddr::V6(ip)));
send_netlink_req_and_wait_one_resp::<RouteNetlinkMessage>(
RouteNetlinkMessage::DelAddress(message),
true,
)
}
pub(crate) fn mtu_op<T: TryInto<Ioctl>>(
name: &str,
op: T,
@@ -469,6 +495,106 @@ impl IfConfiguerTrait for NetlinkIfConfiger {
Ok(())
}
async fn add_ipv6_ip(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
let mut message = AddressMessage::default();
message.header.prefix_len = cidr_prefix;
message.header.index = NetlinkIfConfiger::get_interface_index(name)?;
message.header.family = AddressFamily::Inet6;
message
.attributes
.push(AddressAttribute::Address(std::net::IpAddr::V6(address)));
// For IPv6, we don't need IFA_LOCAL or IFA_BROADCAST
send_netlink_req_and_wait_one_resp::<RouteNetlinkMessage>(
RouteNetlinkMessage::NewAddress(message),
false,
)
}
async fn remove_ipv6(&self, name: &str, ip: Option<std::net::Ipv6Addr>) -> Result<(), Error> {
if ip.is_none() {
let addrs = Self::list_addresses(name)?;
for addr in addrs {
if let IpAddr::V6(ipv6) = addr.address() {
let prefix_len = addr.network_length();
Self::remove_one_ipv6(name, ipv6, prefix_len)?;
}
}
} else {
let ipv6 = ip.unwrap();
let prefix_len = Self::get_prefix_len_ipv6(name, ipv6)?;
Self::remove_one_ipv6(name, ipv6, prefix_len)?;
}
Ok(())
}
async fn add_ipv6_route(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
cost: Option<i32>,
) -> Result<(), Error> {
let mut message = RouteMessage::default();
message.header.address_family = AddressFamily::Inet6;
message.header.destination_prefix_length = cidr_prefix;
message.header.table = RouteHeader::RT_TABLE_MAIN;
message.header.protocol = RouteProtocol::Static;
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::Oif(NetlinkIfConfiger::get_interface_index(
name,
)?));
message
.attributes
.push(RouteAttribute::Destination(RouteAddress::Inet6(address)));
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::NewRoute(message), false)
}
async fn remove_ipv6_route(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
let routes = Self::list_routes()?;
let ifidx = NetlinkIfConfiger::get_interface_index(name)?;
for msg in routes {
let other_route: Route = msg.clone().into();
if other_route.destination == std::net::IpAddr::V6(address)
&& other_route.prefix == cidr_prefix
&& other_route.ifindex == Some(ifidx)
{
send_netlink_req_and_wait_one_resp(RouteNetlinkMessage::DelRoute(msg), true)?;
return Ok(());
}
}
Ok(())
}
}
#[cfg(test)]
+68
View File
@@ -169,6 +169,74 @@ impl IfConfiguerTrait for WindowsIfConfiger {
)
.await
}
async fn add_ipv6_ip(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
run_shell_cmd(
format!(
"netsh interface ipv6 add address {} {}/{}",
name, address, cidr_prefix
)
.as_str(),
)
.await
}
async fn remove_ipv6(&self, name: &str, ip: Option<std::net::Ipv6Addr>) -> Result<(), Error> {
if let Some(ip) = ip {
run_shell_cmd(
format!("netsh interface ipv6 delete address {} {}", name, ip).as_str(),
)
.await
} else {
// Remove all IPv6 addresses
run_shell_cmd(
format!("netsh interface ipv6 delete address {} all", name).as_str(),
)
.await
}
}
async fn add_ipv6_route(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
cost: Option<i32>,
) -> Result<(), Error> {
let cmd = if let Some(cost) = cost {
format!(
"netsh interface ipv6 add route {}/{} {} metric={}",
address, cidr_prefix, name, cost
)
} else {
format!(
"netsh interface ipv6 add route {}/{} {}",
address, cidr_prefix, name
)
};
run_shell_cmd(cmd.as_str()).await
}
async fn remove_ipv6_route(
&self,
name: &str,
address: std::net::Ipv6Addr,
cidr_prefix: u8,
) -> Result<(), Error> {
run_shell_cmd(
format!(
"netsh interface ipv6 delete route {}/{} {}",
address, cidr_prefix, name
)
.as_str(),
)
.await
}
}
pub struct RegistryManager;