release: NX9-WG v1.0.0
This commit is contained in:
1 parent
c8a9b7cde6
commit
4dfe42fe68
42 files changed
+4689
-336
No files matched your search
@@ -79,18 +79,31 @@ impl ClientConfigBuilder {
|
||||
lines.push(format!("PresharedKey = {}", psk.as_str()));
|
||||
}
|
||||
|
||||
let host_trimmed = server_host_or_ip.trim();
|
||||
if host_trimmed.is_empty() {
|
||||
return Err(WireGuardError::Config(
|
||||
"No reachable WireGuard server endpoint is configured. Configure 'server_endpoint' in settings or provide --endpoint.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Endpoint
|
||||
let endpoint = if server_host_or_ip.contains(':') && !server_host_or_ip.starts_with('[') {
|
||||
let endpoint = if host_trimmed.contains(':') && !host_trimmed.starts_with('[') {
|
||||
// Check if already contains port
|
||||
server_host_or_ip.to_string()
|
||||
host_trimmed.to_string()
|
||||
} else {
|
||||
format!("{}:{}", server_host_or_ip, interface.listen_port)
|
||||
format!("{}:{}", host_trimmed, interface.listen_port)
|
||||
};
|
||||
lines.push(format!("Endpoint = {endpoint}"));
|
||||
|
||||
// AllowedIPs based on Peer Profile
|
||||
let allowed_ips = match peer.profile {
|
||||
PeerProfile::FullTunnel => "0.0.0.0/0, ::/0".to_string(),
|
||||
PeerProfile::FullTunnel => {
|
||||
if interface.address_v6.is_some() || peer.address_v6.is_some() {
|
||||
"0.0.0.0/0, ::/0".to_string()
|
||||
} else {
|
||||
"0.0.0.0/0".to_string()
|
||||
}
|
||||
}
|
||||
PeerProfile::SplitTunnel => {
|
||||
let mut subnets = Vec::new();
|
||||
subnets.push(interface.address_v4.to_string());
|
||||
@@ -101,7 +114,11 @@ impl ClientConfigBuilder {
|
||||
}
|
||||
PeerProfile::Custom => {
|
||||
if peer.allowed_ips.trim().is_empty() {
|
||||
"0.0.0.0/0, ::/0".to_string()
|
||||
if interface.address_v6.is_some() || peer.address_v6.is_some() {
|
||||
"0.0.0.0/0, ::/0".to_string()
|
||||
} else {
|
||||
"0.0.0.0/0".to_string()
|
||||
}
|
||||
} else {
|
||||
peer.allowed_ips.clone()
|
||||
}
|
||||
@@ -181,7 +198,7 @@ mod tests {
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
// Full Tunnel
|
||||
// Full Tunnel (IPv4-only interface -> 0.0.0.0/0 to prevent silent IPv6 blackhole)
|
||||
let full_conf = ClientConfigBuilder::build(&peer, &iface, "vpn.example.com").unwrap();
|
||||
assert!(full_conf.contains(&format!("PrivateKey = {}", peer_priv.as_str())));
|
||||
assert!(full_conf.contains("Address = 10.0.0.2/32"));
|
||||
@@ -190,9 +207,15 @@ mod tests {
|
||||
assert!(full_conf.contains(&format!("PublicKey = {}", srv_pub.as_str())));
|
||||
assert!(full_conf.contains(&format!("PresharedKey = {}", psk.as_str())));
|
||||
assert!(full_conf.contains("Endpoint = vpn.example.com:51820"));
|
||||
assert!(full_conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
assert!(full_conf.contains("AllowedIPs = 0.0.0.0/0"));
|
||||
assert!(full_conf.contains("PersistentKeepalive = 25"));
|
||||
|
||||
// Full Tunnel (Dual-stack interface -> 0.0.0.0/0, ::/0)
|
||||
let mut dual_iface = iface.clone();
|
||||
dual_iface.address_v6 = Some(IpNet::from_str("fd00::1/64").unwrap());
|
||||
let dual_conf = ClientConfigBuilder::build(&peer, &dual_iface, "vpn.example.com").unwrap();
|
||||
assert!(dual_conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
|
||||
// Split Tunnel
|
||||
peer.profile = PeerProfile::SplitTunnel;
|
||||
let split_conf = ClientConfigBuilder::build(&peer, &iface, "vpn.example.com").unwrap();
|
||||
@@ -274,6 +297,6 @@ mod tests {
|
||||
assert!(conf.contains("PersistentKeepalive = 20"));
|
||||
assert!(conf.contains("DNS = 9.9.9.9"));
|
||||
assert!(conf.contains("Address = 10.0.0.5/32"));
|
||||
assert!(conf.contains("AllowedIPs = 0.0.0.0/0, ::/0"));
|
||||
assert!(conf.contains("AllowedIPs = 0.0.0.0/0"));
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,12 @@ pub struct LiveInterfaceStats {
|
||||
pub listen_port: u16,
|
||||
pub fwmark: u32,
|
||||
pub peers: Vec<LivePeerStats>,
|
||||
#[serde(default)]
|
||||
pub addresses: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mtu: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub is_up: bool,
|
||||
}
|
||||
|
||||
/// Abstract WireGuard Engine interface for kernel netlink and simulated environments.
|
||||
@@ -82,6 +88,12 @@ impl SimulatedWireGuardEngine {
|
||||
"Peer '{peer_public_key}' on interface '{interface_name}' not found"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Directly inject live interface stats (for testing drift and telemetry scenarios).
|
||||
pub async fn inject_interface_stats(&self, stats: LiveInterfaceStats) {
|
||||
let mut map = self.state.write().await;
|
||||
map.insert(stats.name.clone(), stats);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -94,7 +106,7 @@ impl WireGuardEngine for SimulatedWireGuardEngine {
|
||||
.filter(|p| p.state == PeerState::Active)
|
||||
.map(|p| {
|
||||
let allowed_ips: Vec<String> = p
|
||||
.allowed_ips
|
||||
.server_wireguard_allowed_ips()
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
@@ -112,12 +124,20 @@ impl WireGuardEngine for SimulatedWireGuardEngine {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut addresses = vec![interface.address_v4.to_string()];
|
||||
if let Some(ref v6) = interface.address_v6 {
|
||||
addresses.push(v6.to_string());
|
||||
}
|
||||
|
||||
let stats = LiveInterfaceStats {
|
||||
name: interface.name.clone(),
|
||||
public_key: interface.public_key.as_str().to_string(),
|
||||
listen_port: interface.listen_port,
|
||||
fwmark: 0,
|
||||
peers: live_peers,
|
||||
addresses,
|
||||
mtu: interface.mtu.map(|m| m as u32),
|
||||
is_up: true,
|
||||
};
|
||||
|
||||
map.insert(interface.name.clone(), stats);
|
||||
|
||||
@@ -24,7 +24,8 @@ use netlink_packet_wireguard::{
|
||||
};
|
||||
use nx9_wg_core::types::wireguard::{Interface, Peer, PeerState};
|
||||
use rtnetlink::LinkWireguard;
|
||||
use rtnetlink::packet_route::link::{InfoKind, LinkAttribute, LinkInfo};
|
||||
use rtnetlink::packet_route::address::{AddressAttribute, AddressMessage};
|
||||
use rtnetlink::packet_route::link::{InfoKind, LinkAttribute, LinkFlags, LinkInfo};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
/// Linux Native WireGuard Engine using kernel RTNETLINK and Generic Netlink.
|
||||
@@ -55,42 +56,53 @@ async fn rtnetlink_handle() -> Result<(rtnetlink::Handle, tokio::task::JoinHandl
|
||||
Ok((handle, join))
|
||||
}
|
||||
|
||||
/// Ensure a WireGuard interface exists with the given name.
|
||||
/// Ensure a WireGuard interface exists with the given name and addresses.
|
||||
///
|
||||
/// - If the interface already exists and is a WireGuard link, this is a no-op.
|
||||
/// - If the interface already exists but is NOT a WireGuard link, returns an error.
|
||||
/// - If the interface does not exist, it is created as a WireGuard link and brought up.
|
||||
async fn ensure_link(name: &str) -> Result<()> {
|
||||
/// - Sets MTU if configured.
|
||||
/// - Assigns IPv4 and IPv6 addresses via RTNETLINK if not already assigned.
|
||||
/// - Removes stale IP addresses on the managed interface that do not match desired state.
|
||||
async fn ensure_link_and_addresses(interface: &Interface) -> Result<()> {
|
||||
let (handle, _conn_task) = rtnetlink_handle().await?;
|
||||
|
||||
// Try to find existing interface by name
|
||||
let mut links = handle.link().get().match_name(name.to_string()).execute();
|
||||
let mut links = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(interface.name.to_string())
|
||||
.execute();
|
||||
|
||||
match links.try_next().await {
|
||||
let link_index = match links.try_next().await {
|
||||
Ok(Some(link)) => {
|
||||
let mut is_wireguard = false;
|
||||
let mut is_other_type = false;
|
||||
for nla in &link.attributes {
|
||||
if let LinkAttribute::LinkInfo(infos) = nla {
|
||||
for info in infos {
|
||||
if let LinkInfo::Kind(InfoKind::Wireguard) = info {
|
||||
is_wireguard = true;
|
||||
match info {
|
||||
LinkInfo::Kind(InfoKind::Wireguard) => {}
|
||||
LinkInfo::Kind(InfoKind::Other(k))
|
||||
if k.eq_ignore_ascii_case("wireguard") => {}
|
||||
LinkInfo::Kind(_) => {
|
||||
is_other_type = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if is_wireguard {
|
||||
tracing::debug!(interface = %name, "WireGuard interface already exists");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(WireGuardError::WrongInterfaceType(format!(
|
||||
"interface '{name}' exists but is not a WireGuard interface"
|
||||
)))
|
||||
if is_other_type {
|
||||
return Err(WireGuardError::WrongInterfaceType(format!(
|
||||
"interface '{}' exists but is not a WireGuard interface",
|
||||
interface.name
|
||||
)));
|
||||
}
|
||||
tracing::debug!(interface = %interface.name, index = link.header.index, "WireGuard interface found");
|
||||
link.header.index
|
||||
}
|
||||
Ok(None) | Err(_) => {
|
||||
// Interface does not exist — create it and bring it up
|
||||
tracing::info!(interface = %name, "Creating WireGuard interface via RTNETLINK");
|
||||
let add_msg = LinkWireguard::new(name).up().build();
|
||||
tracing::info!(interface = %interface.name, "Creating WireGuard interface via RTNETLINK");
|
||||
let add_msg = LinkWireguard::new(&interface.name).up().build();
|
||||
|
||||
handle.link().add(add_msg).execute().await.map_err(|e| {
|
||||
let msg = format!("{e}");
|
||||
@@ -99,19 +111,208 @@ async fn ensure_link(name: &str) -> Result<()> {
|
||||
|| msg.contains("Operation not permitted")
|
||||
{
|
||||
WireGuardError::PermissionDenied(format!(
|
||||
"insufficient privileges to create WireGuard interface '{name}': {e}"
|
||||
"insufficient privileges to create WireGuard interface '{}': {e}",
|
||||
interface.name
|
||||
))
|
||||
} else {
|
||||
WireGuardError::Netlink(format!(
|
||||
"failed to create WireGuard interface '{name}': {e}"
|
||||
"failed to create WireGuard interface '{}': {e}",
|
||||
interface.name
|
||||
))
|
||||
}
|
||||
})?;
|
||||
|
||||
tracing::info!(interface = %name, "WireGuard interface created and brought up");
|
||||
Ok(())
|
||||
// Retrieve newly created link to get its index
|
||||
let mut new_links = handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(interface.name.to_string())
|
||||
.execute();
|
||||
match new_links.try_next().await {
|
||||
Ok(Some(nl)) => {
|
||||
tracing::info!(interface = %interface.name, "WireGuard interface created and brought up");
|
||||
nl.header.index
|
||||
}
|
||||
_ => {
|
||||
return Err(WireGuardError::Netlink(format!(
|
||||
"failed to retrieve newly created interface '{}'",
|
||||
interface.name
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set MTU and ensure UP
|
||||
let mut link_builder = rtnetlink::LinkUnspec::new_with_index(link_index).up();
|
||||
if let Some(mtu) = interface.mtu {
|
||||
link_builder = link_builder.mtu(mtu as u32);
|
||||
}
|
||||
let msg = link_builder.build();
|
||||
if let Err(e) = handle.link().change(msg).execute().await {
|
||||
let msg_str = format!("{e}");
|
||||
if msg_str.contains("permission")
|
||||
|| msg_str.contains("EPERM")
|
||||
|| msg_str.contains("Operation not permitted")
|
||||
{
|
||||
return Err(WireGuardError::PermissionDenied(format!(
|
||||
"insufficient privileges to set link UP/MTU for '{}': {e}",
|
||||
interface.name
|
||||
)));
|
||||
}
|
||||
tracing::warn!(interface = %interface.name, "Failed to set link UP/MTU: {e}");
|
||||
}
|
||||
|
||||
// Read existing addresses on link
|
||||
let mut existing_addrs: Vec<(IpAddr, u8)> = Vec::new();
|
||||
let mut stale_addr_msgs: Vec<AddressMessage> = Vec::new();
|
||||
|
||||
let mut addr_stream = handle
|
||||
.address()
|
||||
.get()
|
||||
.set_link_index_filter(link_index)
|
||||
.execute();
|
||||
|
||||
let desired_v4_ip = interface.address_v4.addr();
|
||||
let desired_v4_prefix = interface.address_v4.prefix_len();
|
||||
let desired_v6 = interface.address_v6.as_ref();
|
||||
|
||||
while let Ok(Some(addr_msg)) = addr_stream.try_next().await {
|
||||
let prefix = addr_msg.header.prefix_len;
|
||||
let mut msg_ip: Option<IpAddr> = None;
|
||||
|
||||
for attr in &addr_msg.attributes {
|
||||
match attr {
|
||||
AddressAttribute::Address(ip) | AddressAttribute::Local(ip) => {
|
||||
if !existing_addrs.contains(&(*ip, prefix)) {
|
||||
existing_addrs.push((*ip, prefix));
|
||||
}
|
||||
msg_ip = Some(*ip);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ip) = msg_ip {
|
||||
let is_desired = match ip {
|
||||
IpAddr::V4(v4) => v4 == desired_v4_ip && prefix == desired_v4_prefix,
|
||||
IpAddr::V6(v6) => {
|
||||
if v6.is_unicast_link_local() {
|
||||
true // preserve IPv6 link-local fe80::/10
|
||||
} else if let Some(v6_desired) = desired_v6 {
|
||||
v6 == v6_desired.addr() && prefix == v6_desired.prefix_len()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !is_desired {
|
||||
stale_addr_msgs.push(addr_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any stale addresses
|
||||
for stale_msg in stale_addr_msgs {
|
||||
if let Err(e) = handle.address().del(stale_msg).execute().await {
|
||||
tracing::warn!(interface = %interface.name, "Failed to delete stale address: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Add desired IPv4 address if not already present
|
||||
if !existing_addrs
|
||||
.iter()
|
||||
.any(|(ip, p)| *ip == desired_v4_ip && *p == desired_v4_prefix)
|
||||
{
|
||||
handle
|
||||
.address()
|
||||
.add(link_index, desired_v4_ip, desired_v4_prefix)
|
||||
.execute()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = format!("{e}");
|
||||
if msg.contains("permission")
|
||||
|| msg.contains("EPERM")
|
||||
|| msg.contains("Operation not permitted")
|
||||
{
|
||||
WireGuardError::PermissionDenied(format!(
|
||||
"insufficient privileges to assign IPv4 address '{}/{}' to interface '{}': {e}",
|
||||
desired_v4_ip, desired_v4_prefix, interface.name
|
||||
))
|
||||
} else if msg.contains("File exists") || msg.contains("EEXIST") {
|
||||
WireGuardError::Netlink(e.to_string())
|
||||
} else {
|
||||
WireGuardError::Netlink(format!(
|
||||
"failed to assign IPv4 address '{}/{}' to interface '{}': {e}",
|
||||
desired_v4_ip, desired_v4_prefix, interface.name
|
||||
))
|
||||
}
|
||||
})
|
||||
.or_else(|e| {
|
||||
if e.to_string().contains("File exists") || e.to_string().contains("EEXIST") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})?;
|
||||
tracing::info!(
|
||||
interface = %interface.name,
|
||||
ip = %desired_v4_ip,
|
||||
prefix = desired_v4_prefix,
|
||||
"Assigned IPv4 address to WireGuard interface via RTNETLINK"
|
||||
);
|
||||
}
|
||||
|
||||
// Add desired IPv6 address if present and not already configured
|
||||
if let Some(v6) = desired_v6 {
|
||||
let v6_ip = v6.addr();
|
||||
let v6_prefix = v6.prefix_len();
|
||||
if !existing_addrs
|
||||
.iter()
|
||||
.any(|(ip, p)| *ip == v6_ip && *p == v6_prefix)
|
||||
{
|
||||
handle
|
||||
.address()
|
||||
.add(link_index, v6_ip, v6_prefix)
|
||||
.execute()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let msg = format!("{e}");
|
||||
if msg.contains("permission")
|
||||
|| msg.contains("EPERM")
|
||||
|| msg.contains("Operation not permitted")
|
||||
{
|
||||
WireGuardError::PermissionDenied(format!(
|
||||
"insufficient privileges to assign IPv6 address '{}/{}' to interface '{}': {e}",
|
||||
v6_ip, v6_prefix, interface.name
|
||||
))
|
||||
} else if msg.contains("File exists") || msg.contains("EEXIST") {
|
||||
WireGuardError::Netlink(e.to_string())
|
||||
} else {
|
||||
WireGuardError::Netlink(format!(
|
||||
"failed to assign IPv6 address '{}/{}' to interface '{}': {e}",
|
||||
v6_ip, v6_prefix, interface.name
|
||||
))
|
||||
}
|
||||
})
|
||||
.or_else(|e| {
|
||||
if e.to_string().contains("File exists") || e.to_string().contains("EEXIST") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})?;
|
||||
tracing::info!(
|
||||
interface = %interface.name,
|
||||
ip = %v6_ip,
|
||||
prefix = v6_prefix,
|
||||
"Assigned IPv6 address to WireGuard interface via RTNETLINK"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a WireGuard interface by name.
|
||||
@@ -140,17 +341,18 @@ async fn delete_link(name: &str) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// List all WireGuard interface names using RTNETLINK link dump.
|
||||
/// List all WireGuard interface names using RTNETLINK link dump and Generic Netlink enumeration.
|
||||
async fn list_wireguard_links() -> Result<Vec<String>> {
|
||||
let (handle, _conn_task) = rtnetlink_handle().await?;
|
||||
|
||||
let mut links = handle.link().get().execute();
|
||||
let mut wg_names = Vec::new();
|
||||
|
||||
// 1. Primary discovery: RTNETLINK link dump
|
||||
let (handle, _conn_task) = rtnetlink_handle().await?;
|
||||
let mut links = handle.link().get().execute();
|
||||
|
||||
while let Some(link) = links
|
||||
.try_next()
|
||||
.await
|
||||
.map_err(|e| WireGuardError::Netlink(format!("failed to dump links: {e}")))?
|
||||
.map_err(|e| WireGuardError::Netlink(format!("failed to dump links via rtnetlink: {e}")))?
|
||||
{
|
||||
let mut name = None;
|
||||
let mut is_wireguard = false;
|
||||
@@ -160,8 +362,14 @@ async fn list_wireguard_links() -> Result<Vec<String>> {
|
||||
LinkAttribute::IfName(n) => name = Some(n.clone()),
|
||||
LinkAttribute::LinkInfo(infos) => {
|
||||
for info in infos {
|
||||
if let LinkInfo::Kind(InfoKind::Wireguard) = info {
|
||||
is_wireguard = true;
|
||||
match info {
|
||||
LinkInfo::Kind(InfoKind::Wireguard) => is_wireguard = true,
|
||||
LinkInfo::Kind(InfoKind::Other(k))
|
||||
if k.eq_ignore_ascii_case("wireguard") =>
|
||||
{
|
||||
is_wireguard = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,11 +377,44 @@ async fn list_wireguard_links() -> Result<Vec<String>> {
|
||||
}
|
||||
}
|
||||
|
||||
if let (true, Some(n)) = (is_wireguard, name) {
|
||||
if let (true, Some(n)) = (is_wireguard, name)
|
||||
&& !wg_names.contains(&n)
|
||||
{
|
||||
wg_names.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Secondary discovery: WireGuard Generic Netlink dump
|
||||
if let Ok((mut genl_handle, _)) = wireguard_genl_handle().await {
|
||||
let genlmsg = GenlMessage::from_payload(WireguardMessage {
|
||||
cmd: WireguardCmd::GetDevice,
|
||||
attributes: Vec::new(),
|
||||
});
|
||||
let mut nlmsg = NetlinkMessage::from(genlmsg);
|
||||
nlmsg.header.flags = NLM_F_REQUEST | NLM_F_DUMP;
|
||||
nlmsg.finalize();
|
||||
|
||||
if let Ok(mut response) = genl_handle.request(nlmsg).await {
|
||||
while let Some(Ok(msg)) = response.next().await {
|
||||
if let NetlinkPayload::InnerMessage(genl) = msg.payload {
|
||||
for attr in genl.payload.attributes {
|
||||
if let WireguardAttribute::IfName(ifname) = attr
|
||||
&& !wg_names.contains(&ifname)
|
||||
{
|
||||
wg_names.push(ifname);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
discovered_count = wg_names.len(),
|
||||
interfaces = ?wg_names,
|
||||
"Discovered WireGuard interfaces in kernel"
|
||||
);
|
||||
|
||||
Ok(wg_names)
|
||||
}
|
||||
|
||||
@@ -236,8 +477,9 @@ async fn configure_device(interface: &Interface, peers: &[Peer]) -> Result<()> {
|
||||
peer_attrs.push(WireguardPeerAttribute::PersistentKeepalive(keepalive));
|
||||
}
|
||||
|
||||
// Allowed IPs
|
||||
let allowed_ips = parse_allowed_ips(&peer.allowed_ips)?;
|
||||
// Server-side Allowed IPs (cryptokey routing in Linux kernel)
|
||||
let server_allowed_str = peer.server_wireguard_allowed_ips();
|
||||
let allowed_ips = parse_allowed_ips(&server_allowed_str)?;
|
||||
if !allowed_ips.is_empty() {
|
||||
peer_attrs.push(WireguardPeerAttribute::Flags(
|
||||
WireguardPeerFlags::ReplaceAllowedIps,
|
||||
@@ -299,6 +541,51 @@ async fn configure_device(interface: &Interface, peers: &[Peer]) -> Result<()> {
|
||||
|
||||
/// Query a WireGuard device via Generic Netlink GET_DEVICE and return live stats.
|
||||
async fn query_device(name: &str) -> Result<Option<LiveInterfaceStats>> {
|
||||
// 1. Query RTNETLINK for link existence, MTU, is_up, and assigned addresses
|
||||
let mut link_exists = false;
|
||||
let mut addresses = Vec::new();
|
||||
let mut mtu = None;
|
||||
let mut is_up = false;
|
||||
|
||||
if let Ok((rt_handle, _)) = rtnetlink_handle().await {
|
||||
let mut links = rt_handle
|
||||
.link()
|
||||
.get()
|
||||
.match_name(name.to_string())
|
||||
.execute();
|
||||
if let Ok(Some(link)) = links.try_next().await {
|
||||
link_exists = true;
|
||||
let index = link.header.index;
|
||||
is_up = link.header.flags.contains(LinkFlags::Up);
|
||||
for attr in link.attributes {
|
||||
if let LinkAttribute::Mtu(m) = attr {
|
||||
mtu = Some(m);
|
||||
}
|
||||
}
|
||||
|
||||
let mut addr_stream = rt_handle
|
||||
.address()
|
||||
.get()
|
||||
.set_link_index_filter(index)
|
||||
.execute();
|
||||
while let Ok(Some(addr_msg)) = addr_stream.try_next().await {
|
||||
let prefix = addr_msg.header.prefix_len;
|
||||
for attr in addr_msg.attributes {
|
||||
match attr {
|
||||
AddressAttribute::Address(ip) | AddressAttribute::Local(ip) => {
|
||||
let cidr = format!("{}/{}", ip, prefix);
|
||||
if !addresses.contains(&cidr) {
|
||||
addresses.push(cidr);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Query Generic Netlink for WireGuard keys, port, fwmark, and peers
|
||||
let (mut handle, _conn_task) = wireguard_genl_handle().await?;
|
||||
|
||||
let genlmsg = GenlMessage::from_payload(WireguardMessage {
|
||||
@@ -310,18 +597,35 @@ async fn query_device(name: &str) -> Result<Option<LiveInterfaceStats>> {
|
||||
nlmsg.header.flags = NLM_F_REQUEST | NLM_F_DUMP;
|
||||
nlmsg.finalize();
|
||||
|
||||
let mut response = handle.request(nlmsg).await.map_err(|e| {
|
||||
let msg = format!("{e}");
|
||||
if msg.contains("No such device") || msg.contains("ENODEV") {
|
||||
WireGuardError::InterfaceNotFound(format!("interface '{name}' not found"))
|
||||
} else if msg.contains("not found") || msg.contains("No such") {
|
||||
WireGuardError::Unsupported(
|
||||
"WireGuard Generic Netlink family not available — is the wireguard kernel module loaded?".to_string(),
|
||||
)
|
||||
} else {
|
||||
WireGuardError::Netlink(format!("failed to query WireGuard device '{name}': {e}"))
|
||||
let mut response = match handle.request(nlmsg).await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
let msg = format!("{e}");
|
||||
if msg.contains("No such device") || msg.contains("ENODEV") {
|
||||
if link_exists {
|
||||
return Ok(Some(LiveInterfaceStats {
|
||||
name: name.to_string(),
|
||||
public_key: String::new(),
|
||||
listen_port: 0,
|
||||
fwmark: 0,
|
||||
peers: Vec::new(),
|
||||
addresses,
|
||||
mtu,
|
||||
is_up,
|
||||
}));
|
||||
}
|
||||
return Ok(None);
|
||||
} else if msg.contains("not found") || msg.contains("No such") {
|
||||
return Err(WireGuardError::Unsupported(
|
||||
"WireGuard Generic Netlink family not available — is the wireguard kernel module loaded?".to_string(),
|
||||
));
|
||||
} else {
|
||||
return Err(WireGuardError::Netlink(format!(
|
||||
"failed to query WireGuard device '{name}': {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
};
|
||||
|
||||
let mut public_key = String::new();
|
||||
let mut listen_port: u16 = 0;
|
||||
@@ -335,11 +639,40 @@ async fn query_device(name: &str) -> Result<Option<LiveInterfaceStats>> {
|
||||
NetlinkPayload::Error(err) => {
|
||||
if let Some(code) = err.code {
|
||||
let code_val = code.get();
|
||||
// ENODEV = -19 means device not found
|
||||
if code_val == -19 {
|
||||
// ENODEV
|
||||
if link_exists {
|
||||
return Ok(Some(LiveInterfaceStats {
|
||||
name: name.to_string(),
|
||||
public_key: String::new(),
|
||||
listen_port: 0,
|
||||
fwmark: 0,
|
||||
peers: Vec::new(),
|
||||
addresses,
|
||||
mtu,
|
||||
is_up,
|
||||
}));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
if code_val == -1 {
|
||||
// EPERM
|
||||
if link_exists {
|
||||
tracing::warn!(
|
||||
interface = %name,
|
||||
"Permission denied reading WireGuard keys via Generic Netlink; returning RTNETLINK link info"
|
||||
);
|
||||
return Ok(Some(LiveInterfaceStats {
|
||||
name: name.to_string(),
|
||||
public_key: String::new(),
|
||||
listen_port: 0,
|
||||
fwmark: 0,
|
||||
peers: Vec::new(),
|
||||
addresses,
|
||||
mtu,
|
||||
is_up,
|
||||
}));
|
||||
}
|
||||
return Err(WireGuardError::PermissionDenied(
|
||||
"insufficient privileges to query WireGuard device".to_string(),
|
||||
));
|
||||
@@ -438,16 +771,28 @@ async fn query_device(name: &str) -> Result<Option<LiveInterfaceStats>> {
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
if !found && !link_exists {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
interface = %name,
|
||||
listen_port,
|
||||
peer_count = live_peers.len(),
|
||||
addresses_count = addresses.len(),
|
||||
is_up,
|
||||
"Retrieved live WireGuard interface telemetry"
|
||||
);
|
||||
|
||||
Ok(Some(LiveInterfaceStats {
|
||||
name: name.to_string(),
|
||||
public_key,
|
||||
listen_port,
|
||||
fwmark,
|
||||
peers: live_peers,
|
||||
addresses,
|
||||
mtu,
|
||||
is_up,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -512,8 +857,8 @@ fn parse_endpoint(s: &str) -> Result<SocketAddr> {
|
||||
#[async_trait::async_trait]
|
||||
impl WireGuardEngine for NativeLinuxWireGuardEngine {
|
||||
async fn sync_interface(&self, interface: &Interface, peers: &[Peer]) -> Result<()> {
|
||||
// 1. Ensure the WireGuard link exists
|
||||
ensure_link(&interface.name).await?;
|
||||
// 1. Ensure the WireGuard link exists and addresses/MTU are configured
|
||||
ensure_link_and_addresses(interface).await?;
|
||||
|
||||
// 2. Configure the WireGuard device (private key, listen port, peers)
|
||||
configure_device(interface, peers).await?;
|
||||
|
||||
Reference in new issue
Block a user