use argh::FromArgs;
use miette::{Result, miette};
use crate::{config::Profile, r#const::WIREGUARD_PORT, machine::corrosion, ssh::SshSession, ui};
#[derive(FromArgs)]
#[argh(subcommand, name = "add")]
pub struct AddExternalPeer {
#[argh(positional)]
pub name: String,
#[argh(option, long = "wg-pubkey")]
pub wg_pubkey: String,
#[argh(option, long = "endpoint")]
pub endpoint: String,
#[argh(option, long = "wg-address")]
pub wg_address: Option<String>,
#[argh(option, long = "ssh-priv-key")]
pub key_path: Option<std::path::PathBuf>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "wg-config")]
pub struct WgConfig {
#[argh(positional)]
pub name: String,
#[argh(option, long = "ssh-priv-key")]
pub key_path: Option<std::path::PathBuf>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "remove")]
pub struct RemoveExternalPeer {
#[argh(positional)]
pub name: String,
#[argh(switch, long = "force")]
pub force: bool,
#[argh(option, long = "ssh-priv-key")]
pub key_path: Option<std::path::PathBuf>,
}
fn validate_peer_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(miette!("Peer name cannot be empty"));
}
if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(miette!(
"Peer name '{name}' contains invalid characters. Only A-Z, a-z, 0-9, underscores (_), and dashes (-) are allowed",
));
}
if name.len() > 63 {
return Err(miette!(
"Peer name '{name}' is too long. Maximum length is 63 characters",
));
}
Ok(())
}
fn assign_wireguard_address(ssh: &SshSession) -> Result<String> {
let used_ips: std::collections::HashSet<String> = corrosion::query_peers(ssh)
.unwrap_or_default()
.into_iter()
.map(|p| p.wg_address)
.collect();
for i in 1..=254 {
let candidate = format!("10.44.44.{i}");
if !used_ips.contains(&candidate) {
return Ok(candidate);
}
}
Err(miette!(
"No available WireGuard IP addresses in the 10.44.44.0/24 range"
))
}
pub fn add_external_peer(request: &AddExternalPeer, profile: &Profile) -> Result<()> {
validate_peer_name(&request.name)?;
if profile.machines.is_empty() {
return Err(miette!(
"No machines configured. Initialise at least one machine first with `maki machine init`"
));
}
let machine = &profile.machines[0];
ui::status(&format!("Connecting to {} to add peer...", machine.name));
let ssh = SshSession::new(&machine.ssh_target, machine.port, request.key_path.as_ref())?;
if let Ok(Some(_)) = corrosion::query_peer(&ssh, &request.name) {
return Err(miette!(
"Peer '{}' already exists in the cluster",
request.name
));
}
let wg_address = match &request.wg_address {
Some(addr) => addr.clone(),
None => assign_wireguard_address(&ssh)?,
};
ui::header("Adding external peer:");
ui::field("Name", &request.name);
ui::field("WireGuard pubkey", &request.wg_pubkey);
ui::field("WireGuard address", &wg_address);
ui::field("Endpoint", &request.endpoint);
let sql = corrosion::Statement::with_params(
"INSERT INTO peers (name, latitude, longitude, ipv4, ipv6, wg_public_key, wg_address, is_nameserver, is_external) VALUES (?, 0.0, 0.0, ?, NULL, ?, ?, 0, 1)",
vec![
serde_json::json!(request.name),
serde_json::json!(request.endpoint),
serde_json::json!(request.wg_pubkey),
serde_json::json!(wg_address),
],
);
corrosion::execute_transactions(&ssh, &[sql])?;
ui::status("External peer added successfully");
ui::info(&format!(
"Run `maki peer wg-config {}` to see the WireGuard configuration for this peer",
request.name
));
Ok(())
}
pub fn show_wg_config(request: &WgConfig, profile: &Profile) -> Result<()> {
if profile.machines.is_empty() {
return Err(miette!(
"No machines configured. Initialise at least one machine first with `maki machine init`"
));
}
let machine = &profile.machines[0];
let ssh = SshSession::new(&machine.ssh_target, machine.port, request.key_path.as_ref())?;
let peer = corrosion::query_peer(&ssh, &request.name)?
.ok_or_else(|| miette!("Peer '{}' not found in the cluster", request.name))?;
ui::header(&format!("WireGuard configuration for '{}'", request.name));
println!();
println!("[Interface]");
println!("PrivateKey = <private-key>");
println!("Address = {}/32", peer.wg_address);
println!("ListenPort = {WIREGUARD_PORT}");
println!();
let all_peers = corrosion::query_machines(&ssh)?;
for peer in &all_peers {
println!("[Peer] # {}", peer.name);
println!("PublicKey = {}", peer.wg_public_key);
println!("Endpoint = {}:{WIREGUARD_PORT}", peer.ipv4);
println!("AllowedIPs = {}/32", peer.wg_address);
println!("PersistentKeepalive = 25");
println!();
}
Ok(())
}
pub fn remove_external_peer(request: &RemoveExternalPeer, profile: &Profile) -> Result<()> {
if profile.machines.is_empty() {
return Err(miette!("No machines configured"));
}
let machine = &profile.machines[0];
let ssh = SshSession::new(&machine.ssh_target, machine.port, request.key_path.as_ref())?;
let _peer = corrosion::query_peer(&ssh, &request.name)?
.ok_or_else(|| miette!("Peer '{}' not found in the cluster", request.name))?;
if !request.force {
ui::warn(&format!(
"About to remove external peer '{}' from the cluster.",
request.name
));
let confirm = dialoguer::Confirm::new()
.with_prompt("Do you want to continue?")
.default(false)
.interact()
.map_err(|e| miette!("Failed to read confirmation: {e}"))?;
if !confirm {
ui::info("Removal cancelled");
return Ok(());
}
}
ui::status(&format!("Removing external peer '{}'", request.name));
corrosion::delete_peer(&ssh, &request.name)?;
ui::info(&format!(
"External peer '{}' removed successfully",
request.name
));
Ok(())
}