feat: initial commit, add register and peers endpoints
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
2021
Cargo.lock
generated
Normal file
2021
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal 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
6
bacon.toml
Normal 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
9
docker-compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey:latest
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
volumes:
|
||||||
|
- valkey-data:/data
|
||||||
|
volumes:
|
||||||
|
valkey-data:
|
||||||
2
src/endpoints/mod.rs
Normal file
2
src/endpoints/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod peers;
|
||||||
|
pub mod register;
|
||||||
36
src/endpoints/peers.rs
Normal file
36
src/endpoints/peers.rs
Normal 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
59
src/endpoints/register.rs
Normal 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
43
src/error.rs
Normal 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
67
src/main.rs
Normal 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
3
src/utils/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod wg;
|
||||||
|
|
||||||
|
pub use wg::WireguardPublicKey;
|
||||||
33
src/utils/wg.rs
Normal file
33
src/utils/wg.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user