refactor!: use rtnetlink for client interfaces, add deregistration

This commit is contained in:
2026-02-17 22:59:50 -08:00
parent a022c18ff9
commit 03f38b9ee3
20 changed files with 975 additions and 167 deletions

View File

@@ -21,6 +21,7 @@ tracing-actix-web = "0.7.21"
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread", "sync"] }
thiserror = "2.0.18"
thiserror-ext = "0.3.0"
wireguard-control = "1.7.1"
[lib]
name = "registry"

View File

@@ -0,0 +1,27 @@
use actix_web::{HttpResponse, web};
use serde::Deserialize;
use wireguard_control::Key;
use crate::{
AppState,
error::{Error, Result},
storage::StorageImpl,
};
#[derive(Deserialize)]
pub struct DeregisterRequest {
public_key: String,
}
pub async fn deregister(
app_state: web::Data<AppState>,
query: web::Query<DeregisterRequest>,
) -> Result<HttpResponse> {
Key::from_base64(&query.public_key).map_err(|_| Error::invalid_key(&query.public_key))?;
app_state
.storage
.deregister_device(&query.public_key)
.await?;
Ok(HttpResponse::Ok().finish())
}

View File

@@ -1,3 +1,4 @@
pub mod deregister;
pub mod peers;
pub mod register;
pub mod ws;

View File

@@ -4,13 +4,14 @@ use crate::{
storage::{RegisterRequest, StorageImpl},
};
use actix_web::{HttpResponse, web};
use registry::Peer;
use registry::{Peer, RegisterResponse};
pub async fn register_peer(
app_state: web::Data<AppState>,
request: web::Json<RegisterRequest>,
) -> Result<HttpResponse> {
app_state.storage.register_device(&request).await?;
let mesh_ip = app_state.storage.register_device(&request).await?;
app_state
.peer_updates
.send(PeerUpdate {
@@ -18,10 +19,11 @@ pub async fn register_peer(
public_key: request.public_key.as_str().to_string(),
public_ip: request.public_ip.to_string(),
port: request.port.clone(),
mesh_ip,
allowed_ips: request.allowed_ips.clone(),
},
})
.unwrap();
.ok();
Ok(HttpResponse::Ok().finish())
Ok(HttpResponse::Ok().json(RegisterResponse { mesh_ip }))
}

View File

@@ -1,4 +1,5 @@
use actix_web::{HttpResponse, ResponseError};
use serde::Serialize;
use thiserror::Error;
use thiserror_ext::{Box, Construct};
@@ -32,12 +33,30 @@ pub enum ErrorKind {
},
#[error("error handling ws")]
Ws(#[source] actix_web::Error),
#[error("IP pool exhausted: no available addresses in {pool}")]
IpPoolExhausted { pool: String },
#[error("error deregistering device {public_key}")]
DeregisterDevice {
public_key: String,
#[source]
source: redis::RedisError,
},
#[error("error invalid key")]
InvalidKey(String),
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
}
impl ResponseError for Error {
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
match self.inner() {
ErrorKind::Ws(e) => e.error_response(),
ErrorKind::InvalidKey(key) => HttpResponse::BadRequest().json(ErrorResponse {
error: format!("error invalid key: {key}"),
}),
_ => HttpResponse::InternalServerError().finish(),
}
}

View File

@@ -1,4 +1,4 @@
mod types;
mod utils;
pub use types::peer_message::*;
pub use types::peer_message::{Peer, PeerMessage, RegisterResponse};

View File

@@ -47,6 +47,10 @@ async fn run() -> crate::error::Result<()> {
"/register",
web::post().to(endpoints::register::register_peer),
)
.route(
"/deregister",
web::delete().to(endpoints::deregister::deregister),
)
.route("/peers", web::get().to(endpoints::peers::get_peers))
.route("/ws/peers", web::get().to(endpoints::ws::peers::peers))
})

View File

@@ -1,3 +1,5 @@
use std::net::Ipv4Addr;
use crate::error::{Error, Result};
mod valkey;
@@ -10,12 +12,13 @@ pub enum Storage {
}
pub trait StorageImpl {
async fn register_device(&self, request: &RegisterRequest) -> Result<()>;
async fn register_device(&self, request: &RegisterRequest) -> Result<Ipv4Addr>;
async fn deregister_device(&self, public_key: &str) -> Result<()>;
async fn get_peers(&self) -> Result<Vec<Peer>>;
}
impl StorageImpl for Storage {
async fn register_device(&self, request: &RegisterRequest) -> Result<()> {
async fn register_device(&self, request: &RegisterRequest) -> Result<Ipv4Addr> {
match self {
Self::Valkey(storage) => storage.register_device(request).await,
}
@@ -26,6 +29,12 @@ impl StorageImpl for Storage {
Self::Valkey(storage) => storage.get_peers().await,
}
}
async fn deregister_device(&self, public_key: &str) -> Result<()> {
match self {
Self::Valkey(storage) => storage.deregister_device(public_key).await,
}
}
}
pub fn get_storage_from_env() -> Result<Storage> {

View File

@@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::net::{IpAddr, Ipv4Addr};
use futures::TryFutureExt;
use ipnetwork::IpNetwork;
use redis::AsyncTypedCommands;
use registry::Peer;
@@ -10,6 +11,10 @@ use crate::error::Result;
use crate::utils::WireguardPublicKey;
use crate::{error::Error, storage::StorageImpl};
const MESH_NETWORK_BASE: [u8; 4] = [10, 100, 0, 0];
const MESH_POOL_START: u8 = 1;
const MESH_POOL_END: u8 = 254;
pub struct ValkeyStorage {
pub valkey_client: redis::Client,
}
@@ -23,22 +28,38 @@ pub struct RegisterRequest {
}
impl StorageImpl for ValkeyStorage {
async fn register_device(&self, request: &RegisterRequest) -> Result<()> {
async fn register_device(&self, request: &RegisterRequest) -> Result<Ipv4Addr> {
let mut conn = self
.valkey_client
.get_multiplexed_async_connection()
.await
.map_err(|e| Error::valkey_get_connection(e))?;
let peer_key = format!("peer:{}", request.public_key.as_str());
let existing_mesh_ip: Option<String> = conn
.hget(&peer_key, "mesh_ip")
.await
.map_err(|e| Error::get_peer(e))?;
let mesh_ip = if let Some(ip_str) = existing_mesh_ip {
ip_str.parse::<Ipv4Addr>().unwrap()
} else {
let allocated_ip = self.allocate_mesh_ip(&mut conn).await?;
allocated_ip
};
conn.hset_multiple::<_, _, _>(
format!("peer:{}", request.public_key.as_str()),
&peer_key,
&[
("public_ip", &request.public_ip.to_string()),
("public_ip", request.public_ip.to_string()),
(
"allowed_ips",
&serde_json::to_string(&request.allowed_ips)
serde_json::to_string(&request.allowed_ips)
.map_err(|e| Error::serialize_json(e, "serializing allowed_ips"))?,
),
("port", &request.port),
("port", request.port.clone()),
("mesh_ip", mesh_ip.to_string()),
],
)
.await
@@ -49,7 +70,8 @@ impl StorageImpl for ValkeyStorage {
request.public_ip.to_string(),
)
})?;
conn.sadd("peers", &request.public_key.as_str())
conn.sadd("peers", request.public_key.as_str())
.await
.map_err(|e| {
Error::add_peer(
@@ -59,6 +81,25 @@ impl StorageImpl for ValkeyStorage {
)
})?;
Ok(mesh_ip)
}
async fn deregister_device(&self, public_key: &str) -> Result<()> {
let mut conn = self
.valkey_client
.get_multiplexed_async_connection()
.await
.map_err(|e| Error::valkey_get_connection(e))?;
let hash_key = format!("peer:{public_key}");
conn.srem("peers", public_key)
.map_err(|e| Error::deregister_device(e, public_key))
.await?;
let response = conn
.del(hash_key)
.await
.map_err(|e| Error::deregister_device(e, public_key))?;
tracing::debug!("deleted hash {keys} key(s) removed", keys = response);
Ok(())
}
@@ -89,21 +130,72 @@ impl StorageImpl for ValkeyStorage {
.map_err(|e| Error::get_peer(e))?
.into_iter()
.zip(keys.iter())
.map(|(peer, key): (HashMap<String, String>, &String)| {
.filter_map(|(peer, key): (HashMap<String, String>, &String)| {
let allowed_ips: Vec<IpNetwork> = peer
.get("allowed_ips")
.map(|s| serde_json::from_str(s).unwrap_or_default())
.unwrap_or_default();
Peer {
let mesh_ip: Ipv4Addr = peer.get("mesh_ip")?.parse().ok()?;
Some(Peer {
public_key: key.clone(),
public_ip: peer.get("public_ip").unwrap().to_string(),
port: peer.get("port").unwrap().to_string(),
public_ip: peer.get("public_ip")?.to_string(),
port: peer.get("port")?.to_string(),
mesh_ip,
allowed_ips,
}
})
})
.collect();
Ok(peers)
}
}
impl ValkeyStorage {
async fn allocate_mesh_ip(
&self,
conn: &mut redis::aio::MultiplexedConnection,
) -> Result<Ipv4Addr> {
let keys: HashSet<String> = conn
.smembers("peers")
.await
.map_err(|e| Error::get_peer(e))?;
let mut assigned_ips: HashSet<Ipv4Addr> = HashSet::new();
if !keys.is_empty() {
let mut pipe = redis::pipe();
for key in keys.iter() {
pipe.hget(format!("peer:{key}"), "mesh_ip");
}
let ips: Vec<Option<String>> = pipe
.query_async(conn)
.await
.map_err(|e| Error::get_peer(e))?;
for ip_opt in ips {
if let Some(ip_str) = ip_opt {
if let Ok(ip) = ip_str.parse::<Ipv4Addr>() {
assigned_ips.insert(ip);
}
}
}
}
for last_octet in MESH_POOL_START..=MESH_POOL_END {
let candidate = Ipv4Addr::new(
MESH_NETWORK_BASE[0],
MESH_NETWORK_BASE[1],
MESH_NETWORK_BASE[2],
last_octet,
);
if !assigned_ips.contains(&candidate) {
return Ok(candidate);
}
}
Err(Error::ip_pool_exhausted("10.100.0.0/24".to_string()))
}
}

View File

@@ -1,3 +1,5 @@
use std::net::Ipv4Addr;
use serde::{Deserialize, Serialize};
use ipnetwork::IpNetwork;
@@ -7,9 +9,15 @@ pub struct Peer {
pub public_key: String,
pub public_ip: String,
pub port: String,
pub mesh_ip: Ipv4Addr,
pub allowed_ips: Vec<IpNetwork>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RegisterResponse {
pub mesh_ip: Ipv4Addr,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum PeerMessage {

View File

@@ -1,5 +1,5 @@
use base64::Engine;
use serde::{Deserialize, de};
use serde::{de, Deserialize};
#[derive(Clone)]
pub struct WireguardPublicKey(String);