Compare commits

...

4 Commits

Author SHA1 Message Date
Ultradesu
4bf349bfe3 Agent restarts yggdrasil on changes 2025-08-26 15:01:28 +03:00
Ultradesu
4a30b441ca Agent restarts yggdrasil on changes 2025-08-26 14:52:58 +03:00
Ultradesu
16e90ccb4d Agent restarts yggdrasil on changes 2025-08-26 14:17:53 +03:00
Ultradesu
35ec6995a0 Added agent 2025-08-26 14:09:32 +03:00
3 changed files with 305 additions and 24 deletions

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::path::Path; use std::path::Path;
use std::process::Command;
use tokio::time::sleep; use tokio::time::sleep;
use tokio_tungstenite::{connect_async, tungstenite::Message}; use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{error, info, warn, debug}; use tracing::{error, info, warn, debug};
@@ -31,6 +32,14 @@ struct Args {
/// Reconnect interval in seconds /// Reconnect interval in seconds
#[arg(long, default_value = "5")] #[arg(long, default_value = "5")]
reconnect_interval: u64, reconnect_interval: u64,
/// Skip automatic Yggdrasil service restart after config changes
#[arg(long)]
no_restart: bool,
/// Custom command to restart Yggdrasil service (overrides platform detection)
#[arg(long)]
restart_command: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
@@ -57,6 +66,7 @@ enum ServerMessage {
allowed_public_keys: Vec<String>, allowed_public_keys: Vec<String>,
}, },
Update { Update {
listen: Vec<String>,
peers: Vec<String>, peers: Vec<String>,
allowed_public_keys: Vec<String>, allowed_public_keys: Vec<String>,
}, },
@@ -181,7 +191,7 @@ async fn run_agent(args: &Args, ygg_config_path: &str) -> Result<()> {
match msg { match msg {
Some(Ok(Message::Text(text))) => { Some(Ok(Message::Text(text))) => {
match serde_json::from_str::<ServerMessage>(&text) { match serde_json::from_str::<ServerMessage>(&text) {
Ok(server_msg) => handle_server_message(server_msg, ygg_config_path).await?, Ok(server_msg) => handle_server_message(server_msg, ygg_config_path, args.no_restart, &args.restart_command).await?,
Err(e) => warn!("Failed to parse server message: {}", e), Err(e) => warn!("Failed to parse server message: {}", e),
} }
} }
@@ -225,7 +235,7 @@ async fn run_agent(args: &Args, ygg_config_path: &str) -> Result<()> {
Ok(()) Ok(())
} }
async fn handle_server_message(msg: ServerMessage, ygg_config_path: &str) -> Result<()> { async fn handle_server_message(msg: ServerMessage, ygg_config_path: &str, no_restart: bool, restart_command: &Option<String>) -> Result<()> {
match msg { match msg {
ServerMessage::Config { ServerMessage::Config {
node_id, node_id,
@@ -246,25 +256,49 @@ async fn handle_server_message(msg: ServerMessage, ygg_config_path: &str) -> Res
// Apply configuration to Yggdrasil // Apply configuration to Yggdrasil
match write_yggdrasil_config(ygg_config_path, &private_key, &listen, &peers, &allowed_public_keys).await { match write_yggdrasil_config(ygg_config_path, &private_key, &listen, &peers, &allowed_public_keys).await {
Ok(_) => info!("Configuration successfully applied to {}", ygg_config_path), Ok(_) => {
info!("Configuration successfully written to {}", ygg_config_path);
// Restart Yggdrasil service to apply new configuration
if !no_restart {
if let Err(e) = restart_yggdrasil_service(restart_command) {
error!("Failed to restart Yggdrasil service: {}", e);
}
} else {
info!("Skipping service restart (--no-restart flag set)");
}
},
Err(e) => error!("Failed to write Yggdrasil config: {}", e), Err(e) => error!("Failed to write Yggdrasil config: {}", e),
} }
} }
ServerMessage::Update { ServerMessage::Update {
listen,
peers, peers,
allowed_public_keys, allowed_public_keys,
} => { } => {
info!("Received configuration update:"); info!("Received configuration update:");
info!(" Updated listen endpoints: {:?}", listen);
info!(" Updated peers: {} configured", peers.len()); info!(" Updated peers: {} configured", peers.len());
for peer in &peers { for peer in &peers {
debug!(" - {}", peer); debug!(" - {}", peer);
} }
info!(" Updated allowed keys: {} configured", allowed_public_keys.len()); info!(" Updated allowed keys: {} configured", allowed_public_keys.len());
// Apply configuration update to Yggdrasil // Apply full configuration update to Yggdrasil
// For updates we need to read current config and update only peers/allowed keys match update_yggdrasil_config_full(ygg_config_path, &listen, &peers, &allowed_public_keys).await {
match update_yggdrasil_config(ygg_config_path, &peers, &allowed_public_keys).await { Ok(true) => {
Ok(_) => info!("Configuration update successfully applied to {}", ygg_config_path), info!("Configuration update successfully applied to {}", ygg_config_path);
// Restart Yggdrasil service to apply updated configuration
if !no_restart {
if let Err(e) = restart_yggdrasil_service(restart_command) {
error!("Failed to restart Yggdrasil service: {}", e);
}
} else {
info!("Skipping service restart (--no-restart flag set)");
}
},
Ok(false) => {
info!("Configuration unchanged, skipping restart");
},
Err(e) => error!("Failed to update Yggdrasil config: {}", e), Err(e) => error!("Failed to update Yggdrasil config: {}", e),
} }
} }
@@ -346,10 +380,45 @@ async fn write_yggdrasil_config(
}); });
let config_json = serde_json::to_string_pretty(&config)?; let config_json = serde_json::to_string_pretty(&config)?;
tokio::fs::write(config_path, config_json).await?;
info!("Yggdrasil configuration written to {}", config_path); // Try to write directly first
Ok(()) match tokio::fs::write(config_path, &config_json).await {
Ok(_) => {
info!("Yggdrasil configuration written to {}", config_path);
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
// Try with sudo if permission denied
warn!("Permission denied writing to {}, trying with sudo...", config_path);
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new("sudo")
.args(&["-n", "tee", config_path])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(config_json.as_bytes()).await?;
}
let output = child.wait_with_output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("Failed to write config with sudo. Make sure the agent has sudo privileges.");
error!("You may need to add this to sudoers: 'username ALL=(ALL) NOPASSWD: /usr/bin/tee {}'", config_path);
return Err(anyhow!("Failed to write config with sudo: {}", stderr));
}
info!("Yggdrasil configuration written to {} with sudo", config_path);
Ok(())
}
Err(e) => Err(anyhow!("Failed to write configuration: {}", e))
}
} }
async fn update_yggdrasil_config( async fn update_yggdrasil_config(
@@ -370,5 +439,203 @@ async fn update_yggdrasil_config(
tokio::fs::write(config_path, updated_config).await?; tokio::fs::write(config_path, updated_config).await?;
info!("Yggdrasil configuration updated in {}", config_path); info!("Yggdrasil configuration updated in {}", config_path);
Ok(())
}
async fn update_yggdrasil_config_full(
config_path: &str,
listen: &[String],
peers: &[String],
allowed_public_keys: &[String]
) -> Result<bool> { // Returns true if config was updated
// Read current config
let current_config = tokio::fs::read_to_string(config_path).await?;
let mut config: serde_json::Value = serde_json::from_str(&current_config)?;
// Check if config actually changed
let old_listen = config["Listen"].clone();
let old_peers = config["Peers"].clone();
let old_keys = config["AllowedPublicKeys"].clone();
let new_listen = serde_json::json!(listen);
let new_peers = serde_json::json!(peers);
let new_keys = serde_json::json!(allowed_public_keys);
if old_listen == new_listen && old_peers == new_peers && old_keys == new_keys {
debug!("Configuration unchanged, skipping update");
return Ok(false);
}
// Update listen, peers and allowed public keys
config["Listen"] = new_listen;
config["Peers"] = new_peers;
config["AllowedPublicKeys"] = new_keys;
// Write updated config back
let updated_config = serde_json::to_string_pretty(&config)?;
// Try to write directly first
match tokio::fs::write(config_path, &updated_config).await {
Ok(_) => {
info!("Yggdrasil configuration fully updated in {}", config_path);
Ok(true)
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
// Try with sudo if permission denied
warn!("Permission denied writing to {}, trying with sudo...", config_path);
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
let mut child = tokio::process::Command::new("sudo")
.args(&["-n", "tee", config_path])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()?;
if let Some(stdin) = child.stdin.as_mut() {
stdin.write_all(updated_config.as_bytes()).await?;
}
let output = child.wait_with_output().await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
error!("Failed to write config with sudo. Make sure the agent has sudo privileges.");
error!("You may need to add this to sudoers: 'username ALL=(ALL) NOPASSWD: /usr/bin/tee {}'", config_path);
return Err(anyhow!("Failed to write config with sudo: {}", stderr));
}
info!("Yggdrasil configuration fully updated in {} with sudo", config_path);
Ok(true)
}
Err(e) => Err(anyhow!("Failed to write configuration: {}", e))
}
}
fn restart_yggdrasil_service(custom_command: &Option<String>) -> Result<()> {
// If custom command is provided, use it
if let Some(cmd) = custom_command {
info!("Using custom restart command: {}", cmd);
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() {
return Err(anyhow!("Invalid custom restart command"));
}
let output = Command::new(parts[0])
.args(&parts[1..])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
error!("Custom restart command failed. Stderr: {}", stderr);
error!("Stdout: {}", stdout);
return Err(anyhow!("Failed to restart Yggdrasil with custom command: {}", stderr));
}
info!("Yggdrasil service restarted successfully with custom command");
return Ok(());
}
// Detect platform and restart accordingly
#[cfg(target_os = "linux")]
{
info!("Restarting Yggdrasil service on Linux...");
// First try with systemctl directly (in case we're running as root)
let output = Command::new("systemctl")
.args(&["restart", "yggdrasil"])
.output();
match output {
Ok(out) if out.status.success() => {
info!("Yggdrasil service restarted successfully");
return Ok(());
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
debug!("Direct systemctl failed: {}", stderr);
// Try with sudo if direct systemctl failed
info!("Attempting restart with sudo...");
let sudo_output = Command::new("sudo")
.args(&["-n", "systemctl", "restart", "yggdrasil"])
.output()?;
if !sudo_output.status.success() {
let sudo_stderr = String::from_utf8_lossy(&sudo_output.stderr);
error!("Failed to restart Yggdrasil service. Make sure the agent is running as root or has sudo privileges.");
error!("You may need to add this to sudoers: 'username ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart yggdrasil'");
return Err(anyhow!("Failed to restart Yggdrasil service: {}", sudo_stderr));
}
info!("Yggdrasil service restarted successfully with sudo");
}
Err(e) => {
return Err(anyhow!("Failed to execute systemctl: {}", e));
}
}
}
#[cfg(target_os = "macos")]
{
info!("Restarting Yggdrasil service on macOS...");
// First unload the service
let unload = Command::new("launchctl")
.args(&["unload", "/Library/LaunchDaemons/yggdrasil.plist"])
.output()?;
if !unload.status.success() {
let stderr = String::from_utf8_lossy(&unload.stderr);
warn!("Failed to unload Yggdrasil service: {} (continuing anyway)", stderr);
}
// Then load it again
let load = Command::new("launchctl")
.args(&["load", "/Library/LaunchDaemons/yggdrasil.plist"])
.output()?;
if !load.status.success() {
let stderr = String::from_utf8_lossy(&load.stderr);
return Err(anyhow!("Failed to load Yggdrasil service: {}", stderr));
}
info!("Yggdrasil service restarted successfully");
}
#[cfg(target_os = "freebsd")]
{
info!("Restarting Yggdrasil service on FreeBSD...");
let output = Command::new("service")
.args(&["yggdrasil", "restart"])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Failed to restart Yggdrasil service: {}", stderr));
}
info!("Yggdrasil service restarted successfully");
}
#[cfg(target_os = "openbsd")]
{
info!("Restarting Yggdrasil service on OpenBSD...");
let output = Command::new("rcctl")
.args(&["restart", "yggdrasil"])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Failed to restart Yggdrasil service: {}", stderr));
}
info!("Yggdrasil service restarted successfully");
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd")))]
{
warn!("Platform not supported for automatic service restart. Please restart Yggdrasil manually.");
}
Ok(()) Ok(())
} }

View File

@@ -31,6 +31,7 @@ pub enum ServerMessage {
allowed_public_keys: Vec<String>, allowed_public_keys: Vec<String>,
}, },
Update { Update {
listen: Vec<String>,
peers: Vec<String>, peers: Vec<String>,
allowed_public_keys: Vec<String>, allowed_public_keys: Vec<String>,
}, },
@@ -149,21 +150,32 @@ pub async fn handle_agent_socket(
// Get current node information // Get current node information
if let Some(current_node) = node_manager.get_node_by_id(id).await { if let Some(current_node) = node_manager.get_node_by_id(id).await {
// Update node with new addresses // Sort addresses for comparison to avoid false positives
match node_manager.update_node( let mut new_addresses = addresses.clone();
id, new_addresses.sort();
current_node.name.clone(), let mut current_addresses = current_node.addresses.clone();
current_node.listen.clone(), current_addresses.sort();
addresses
).await { // Only update if addresses actually changed
Ok(_) => { if new_addresses != current_addresses {
info!("Updated addresses for node {}", id); // Update node with new addresses
// Broadcast configuration update to all agents match node_manager.update_node(
crate::websocket_state::broadcast_configuration_update(&node_manager).await; id,
} current_node.name.clone(),
Err(e) => { current_node.listen.clone(),
error!("Failed to update addresses for node {}: {}", id, e); addresses
).await {
Ok(_) => {
info!("Updated addresses for node {}", id);
// Broadcast configuration update to all agents
crate::websocket_state::broadcast_configuration_update(&node_manager).await;
}
Err(e) => {
error!("Failed to update addresses for node {}: {}", id, e);
}
} }
} else {
debug!("Address list unchanged for node {}, skipping update", id);
} }
} else { } else {
warn!("Cannot update addresses for unknown node: {}", id); warn!("Cannot update addresses for unknown node: {}", id);

View File

@@ -35,6 +35,7 @@ pub async fn broadcast_configuration_update(node_manager: &Arc<NodeManager>) {
for (node_id, tx) in connections.iter() { for (node_id, tx) in connections.iter() {
if let Some(config) = configs.get(node_id) { if let Some(config) = configs.get(node_id) {
let update = ServerMessage::Update { let update = ServerMessage::Update {
listen: config.listen.clone(),
peers: config.peers.clone(), peers: config.peers.clone(),
allowed_public_keys: config.allowed_public_keys.clone(), allowed_public_keys: config.allowed_public_keys.clone(),
}; };
@@ -46,6 +47,7 @@ pub async fn broadcast_configuration_update(node_manager: &Arc<NodeManager>) {
} else { } else {
// Node was deleted, send empty configuration to disconnect agent gracefully // Node was deleted, send empty configuration to disconnect agent gracefully
let update = ServerMessage::Update { let update = ServerMessage::Update {
listen: vec![],
peers: vec![], peers: vec![],
allowed_public_keys: vec![], allowed_public_keys: vec![],
}; };