diff options
| author | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 03:10:41 +0400 |
|---|---|---|
| committer | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 03:10:41 +0400 |
| commit | d608ffeeda1a479c9dc0e5f6ad269ba07a510d7f (patch) | |
| tree | 3824ec9da5ca0adac913cc47e3e20054c3dc843a /src/linux/mod.rs | |
| parent | 40cf3276cdc74bf6f1795990ed600891a637d1fe (diff) | |
Implement Linux proxy controller, subscription state, and CLI/TUI
Diffstat (limited to 'src/linux/mod.rs')
| -rw-r--r-- | src/linux/mod.rs | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/src/linux/mod.rs b/src/linux/mod.rs new file mode 100644 index 0000000..68bb3a8 --- /dev/null +++ b/src/linux/mod.rs @@ -0,0 +1,119 @@ +pub mod firewall; +pub mod routes; + +use crate::{model::Trojan, runtime::Runtime, xray}; +use anyhow::Result; +use async_trait::async_trait; +use std::{path::Path, time::Duration}; + +pub struct LinuxRuntime { + routing: String, + applied: Option<Trojan>, +} +impl Default for LinuxRuntime { + fn default() -> Self { + Self { + routing: "unknown".into(), + applied: None, + } + } +} + +#[async_trait] +impl Runtime for LinuxRuntime { + async fn arm(&mut self) -> Result<()> { + xray::preflight().await?; + firewall::preflight()?; + routes::preflight().await?; + let (uid, _) = xray::identity()?; + firewall::apply(true, uid)?; + // Persist the boot guard input before recording enabled intent in SQLite. + write_guard(true)?; + self.routing = "blocked".into(); + Ok(()) + } + async fn apply(&mut self, connection: Option<&Trojan>, _: u64) -> Result<()> { + if self.applied.as_ref() == connection { + if let Some(index) = routes::tun_index().await? { + routes::capture(index).await?; + self.routing = "tun".into(); + return Ok(()); + } + } + self.routing = "blocked".into(); + self.applied = None; + xray::systemctl(&["stop", "roentgen-xray.service"]).await?; + routes::remove().await?; + if let Some(connection) = connection { + xray::write_config( + Path::new("/run/roentgen/xray.json"), + &xray::configuration(connection, true, None), + )?; + xray::systemctl(&["start", "roentgen-xray.service"]).await?; + for _ in 0..100 { + if let Some(index) = routes::tun_index().await? { + routes::capture(index).await?; + self.routing = "tun".into(); + self.applied = Some(connection.clone()); + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + anyhow::bail!("Xray did not create roentgen0; traffic remains blocked"); + } + Ok(()) + } + async fn maintain(&mut self, connection: Option<&Trojan>, generation: u64) -> Result<()> { + match routes::tun_index().await? { + Some(index) if connection.is_some() => { + routes::capture(index).await?; + self.routing = "tun".into(); + Ok(()) + } + _ => self.apply(connection, generation).await, + } + } + async fn disable(&mut self) -> Result<()> { + xray::systemctl(&["stop", "roentgen-xray.service"]).await?; + routes::remove().await?; + self.applied = None; + write_guard(false)?; + firewall::apply(false, xray::identity()?.0)?; + self.routing = "direct".into(); + Ok(()) + } + fn routing(&self) -> &str { + &self.routing + } +} + +pub fn write_guard(enabled: bool) -> Result<()> { + crate::storage::atomic_private_write( + Path::new("/etc/roentgen/guard.toml"), + format!("version = 1\nenabled = {enabled}\n").as_bytes(), + ) +} + +pub fn restore_guard() -> Result<()> { + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct Guard { + version: u32, + enabled: bool, + } + let uid = xray::identity()?.0; + let input = std::fs::read_to_string("/etc/roentgen/guard.toml"); + let value = input + .ok() + .and_then(|input| toml::from_str::<Guard>(&input).ok()) + .filter(|value| value.version == 1); + match value { + Some(value) => firewall::apply(value.enabled, uid), + None => { + firewall::apply(true, uid)?; + anyhow::bail!( + "Corrupt or missing guard state; protection was armed. Use explicit recover --disable" + ); + } + } +} |