use super::RpcHandler; use crate::vpn; use anyhow::Result; use tracing::info; impl RpcHandler { /// vpn.status — Get current VPN connection status. pub(super) async fn handle_vpn_status(&self) -> Result { let status = vpn::get_status().await; let config = vpn::load_config(&self.config.data_dir).await?; Ok(serde_json::json!({ "connected": status.connected, "provider": status.provider, "interface": status.interface, "ip_address": status.ip_address, "hostname": status.hostname, "peers_connected": status.peers_connected, "bytes_in": status.bytes_in, "bytes_out": status.bytes_out, "configured": config.enabled, "configured_provider": format!("{:?}", config.provider).to_lowercase(), })) } /// vpn.configure — Configure VPN (Tailscale or WireGuard). pub(super) async fn handle_vpn_configure( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let provider = params .get("provider") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'provider' (tailscale or wireguard)"))?; match provider { "tailscale" => { let auth_key = params .get("auth_key") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'auth_key' for Tailscale"))?; vpn::configure_tailscale(auth_key, &self.config.data_dir).await?; info!("Tailscale VPN configured"); Ok(serde_json::json!({ "configured": true, "provider": "tailscale", })) } "wireguard" => { let address = params .get("address") .and_then(|v| v.as_str()) .unwrap_or("10.0.0.1/24"); let dns = params .get("dns") .and_then(|v| v.as_str()) .unwrap_or("1.1.1.1"); let peer = if let Some(peer_obj) = params.get("peer") { let public_key = peer_obj .get("public_key") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing peer public_key"))?; let endpoint = peer_obj .get("endpoint") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing peer endpoint"))?; let allowed_ips = peer_obj .get("allowed_ips") .and_then(|v| v.as_str()) .unwrap_or("0.0.0.0/0"); let keepalive = peer_obj .get("persistent_keepalive") .and_then(|v| v.as_u64()) .map(|v| v as u16); Some(vpn::WireGuardPeer { public_key: public_key.to_string(), endpoint: endpoint.to_string(), allowed_ips: allowed_ips.to_string(), persistent_keepalive: keepalive, }) } else { None }; let wg_config = vpn::configure_wireguard( &self.config.data_dir, address, dns, peer, ) .await?; info!("WireGuard VPN configured"); Ok(serde_json::json!({ "configured": true, "provider": "wireguard", "public_key": wg_config.public_key, "address": wg_config.address, })) } _ => { anyhow::bail!("Unknown provider: {} (expected tailscale or wireguard)", provider); } } } /// remote.setup — One-click Tailscale remote access setup. /// Accepts an auth key, configures Tailscale, and restricts access to ports 80/443. pub(super) async fn handle_remote_setup( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let auth_key = params .get("auth_key") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'auth_key' — get one from https://login.tailscale.com/admin/settings/keys"))?; // Configure Tailscale vpn::configure_tailscale(auth_key, &self.config.data_dir).await?; info!("Remote access: Tailscale configured"); // Set ACL-like port restrictions via iptables on tailscale0 // Allow only HTTP (80) and HTTPS (443) on the Tailscale interface let restrict_cmds = [ "sudo iptables -D INPUT -i tailscale0 -p tcp --dport 80 -j ACCEPT 2>/dev/null; true", "sudo iptables -D INPUT -i tailscale0 -p tcp --dport 443 -j ACCEPT 2>/dev/null; true", "sudo iptables -D INPUT -i tailscale0 -j DROP 2>/dev/null; true", "sudo iptables -A INPUT -i tailscale0 -p tcp --dport 80 -j ACCEPT", "sudo iptables -A INPUT -i tailscale0 -p tcp --dport 443 -j ACCEPT", "sudo iptables -A INPUT -i tailscale0 -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT", "sudo iptables -A INPUT -i tailscale0 -j DROP", ]; for cmd in &restrict_cmds { let _ = tokio::process::Command::new("sh") .arg("-c") .arg(cmd) .output() .await; } info!("Remote access: Restricted Tailscale to ports 80/443"); // Get the Tailscale IP for display let status = vpn::get_status().await; let tailscale_ip = status.ip_address.clone().unwrap_or_default(); let hostname = status.hostname.clone().unwrap_or_default(); // Build the remote access URL let remote_url = if !hostname.is_empty() { format!("http://{}", hostname) } else if !tailscale_ip.is_empty() { format!("http://{}", tailscale_ip) } else { String::new() }; Ok(serde_json::json!({ "configured": true, "provider": "tailscale", "tailscale_ip": tailscale_ip, "hostname": hostname, "remote_url": remote_url, "ports_exposed": [80, 443], })) } /// vpn.disconnect — Disable VPN. pub(super) async fn handle_vpn_disconnect(&self) -> Result { let mut config = vpn::load_config(&self.config.data_dir).await?; config.enabled = false; vpn::save_config(&self.config.data_dir, &config).await?; // Try to bring down the interface match config.provider { vpn::VpnProvider::Tailscale => { let _ = tokio::process::Command::new("podman") .args(["exec", "tailscale", "tailscale", "down"]) .output() .await; } vpn::VpnProvider::Wireguard => { let _ = tokio::process::Command::new("wg-quick") .args(["down", "wg0"]) .output() .await; } } info!("VPN disconnected"); Ok(serde_json::json!({ "disconnected": true })) } }