feat: initial commit, add register and peers endpoints

This commit is contained in:
2026-02-09 00:25:17 -08:00
commit 15fc72d5c9
13 changed files with 2301 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

2021
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "wg-mesh"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = "4.12.1"
base64 = "0.22.1"
console = "0.16.2"
ipnetwork = { version = "0.21.1", features = ["serde"] }
redis = { version = "=1.0.2", features = ["connection-manager", "tokio-comp"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
thiserror = "2.0.18"
thiserror-ext = "0.3.0"
tokio = "1.49.0"
tracing = "0.1.44"
tracing-actix-web = "0.7.21"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }

6
bacon.toml Normal file
View File

@@ -0,0 +1,6 @@
[jobs.dev]
command = ["cargo", "run"]
need_stdout = true
background = false
on_change_strategy = "kill_then_restart"
kill = ["kill", "-s", "INT"]

9
docker-compose.yaml Normal file
View File

@@ -0,0 +1,9 @@
services:
valkey:
image: valkey/valkey:latest
ports:
- "6379:6379"
volumes:
- valkey-data:/data
volumes:
valkey-data:

2
justfile Normal file
View File

@@ -0,0 +1,2 @@
dev:
bacon dev

2
src/endpoints/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod peers;
pub mod register;

36
src/endpoints/peers.rs Normal file
View File

@@ -0,0 +1,36 @@
use std::collections::HashMap;
use actix_web::{HttpResponse, web};
use redis::AsyncTypedCommands;
use crate::{
AppState,
error::{Error, Result},
};
pub async fn get_peers(app_state: web::Data<AppState>) -> Result<HttpResponse> {
let mut conn = app_state
.valkey_client
.get_multiplexed_async_connection()
.await
.map_err(|e| Error::valkey_get_connection(e))?;
let keys = conn
.smembers("peers")
.await
.map_err(|e| Error::get_peer(e))?;
let mut pipe = redis::pipe();
for key in keys.iter() {
pipe.hgetall(format!("peer:{key}"));
}
let mut peers = pipe
.query_async::<Vec<HashMap<String, String>>>(&mut conn)
.await
.map_err(|e| Error::get_peer(e))?;
for (key, peer) in keys.iter().zip(peers.iter_mut()) {
peer.insert("public_key".to_string(), key.clone());
}
Ok(HttpResponse::Ok().json(peers))
}

59
src/endpoints/register.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::net::IpAddr;
use crate::{
AppState,
error::{Error, Result},
utils::WireguardPublicKey,
};
use actix_web::{HttpResponse, web};
use ipnetwork::IpNetwork;
use redis::AsyncTypedCommands;
use serde::Deserialize;
#[derive(Deserialize, Clone)]
pub struct RegisterRequest {
public_ip: IpAddr,
public_key: WireguardPublicKey,
allowed_ips: Vec<IpNetwork>,
}
pub async fn register_peer(
app_state: web::Data<AppState>,
request: web::Json<RegisterRequest>,
) -> Result<HttpResponse> {
let mut conn = app_state
.valkey_client
.get_multiplexed_async_connection()
.await
.map_err(|e| Error::valkey_get_connection(e))?;
conn.hset_multiple::<_, _, _>(
format!("peer:{}", request.public_key.as_str()),
&[
("public_ip", &request.public_ip.to_string()),
(
"allowed_ips",
&serde_json::to_string(&request.allowed_ips)
.map_err(|e| Error::serialize_json(e, "serializing allowed_ips"))?,
),
],
)
.await
.map_err(|e| {
Error::add_peer(
e,
request.public_key.as_str(),
request.public_ip.to_string(),
)
})?;
conn.sadd("peers", &request.public_key.as_str())
.await
.map_err(|e| {
Error::add_peer(
e,
request.public_key.as_str(),
request.public_ip.to_string(),
)
})?;
Ok(HttpResponse::Ok().finish())
}

43
src/error.rs Normal file
View File

@@ -0,0 +1,43 @@
use actix_web::{HttpResponse, ResponseError};
use thiserror::Error;
use thiserror_ext::{Box, Construct};
#[derive(Error, Debug, Box, Construct)]
#[thiserror_ext(newtype(name = Error))]
pub enum ErrorKind {
#[error("error connecting to valkey at {address}")]
ValkeyConnect {
address: String,
#[source]
source: redis::RedisError,
},
#[error("error getting valkey connection")]
ValkeyGetConnection(#[source] redis::RedisError),
#[error("error adding peer")]
AddPeer {
public_key: String,
public_ip: String,
#[source]
source: redis::RedisError,
},
#[error("error getting peers")]
GetPeer(#[source] redis::RedisError),
#[error("io error")]
Io(#[from] std::io::Error),
#[error("error serializing json: {context}")]
SerializeJson {
context: String,
#[source]
source: serde_json::Error,
},
}
impl ResponseError for Error {
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
match self.inner() {
_ => HttpResponse::InternalServerError().finish(),
}
}
}
pub type Result<T> = core::result::Result<T, Error>;

67
src/main.rs Normal file
View File

@@ -0,0 +1,67 @@
mod endpoints;
mod error;
mod utils;
use actix_web::{App, HttpServer, web};
use console::style;
use thiserror_ext::AsReport;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{
EnvFilter, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt,
};
use crate::error::Error;
struct AppState {
valkey_client: redis::Client,
}
async fn run() -> crate::error::Result<()> {
let app_state = web::Data::new(AppState {
valkey_client: redis::Client::open("redis://127.0.0.1:6379/")
.map_err(|e| Error::valkey_connect(e, "127.0.0.1:6379/".to_string()))?,
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone())
.wrap(tracing_actix_web::TracingLogger::default())
.route(
"/",
web::get()
.to(async || concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))),
)
.route(
"/register",
web::post().to(endpoints::register::register_peer),
)
.route("/peers", web::get().to(endpoints::peers::get_peers))
})
.bind(("0.0.0.0", 8080))?
.run()
.await?;
Ok(())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let tracing_env_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy();
tracing_subscriber::registry()
.with(tracing_env_filter)
.with(
tracing_subscriber::fmt::layer()
.compact()
.with_span_events(FmtSpan::CLOSE),
)
.init();
if let Err(e) = run().await {
eprintln!("{}: {}", style("error").red(), e.as_report());
std::process::exit(1)
}
Ok(())
}

3
src/utils/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod wg;
pub use wg::WireguardPublicKey;

33
src/utils/wg.rs Normal file
View File

@@ -0,0 +1,33 @@
use base64::Engine;
use serde::{Deserialize, de};
#[derive(Clone)]
pub struct WireguardPublicKey(String);
impl<'de> Deserialize<'de> for WireguardPublicKey {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(&s)
.map_err(|_| de::Error::custom("invalid base64 in public key"))?;
if bytes.len() != 32 {
return Err(de::Error::invalid_length(
bytes.len(),
&"exactly 32 bytes for a Wireguard public key",
));
}
Ok(WireguardPublicKey(s))
}
}
impl WireguardPublicKey {
pub fn as_str(&self) -> &str {
&self.0
}
}