diff options
| author | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 04:43:59 +0400 |
|---|---|---|
| committer | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 04:43:59 +0400 |
| commit | 7999fab25c5172207ee41e38732f4923565ff6b6 (patch) | |
| tree | 0e239d6ba65d4babfc0f1007ac350f6f4f9e73d1 /src | |
| parent | fc3c1d51b1be9db6ba6ad286584fbc34bb305f38 (diff) | |
Validate controller diagnostics, reboot protection, and terminal authentication
Diffstat (limited to 'src')
| -rw-r--r-- | src/bus.rs | 13 | ||||
| -rw-r--r-- | src/controller.rs | 70 | ||||
| -rw-r--r-- | src/diagnostics.rs | 17 | ||||
| -rw-r--r-- | src/installation.rs | 6 | ||||
| -rw-r--r-- | src/linux/mod.rs | 8 | ||||
| -rw-r--r-- | src/runtime.rs | 14 | ||||
| -rw-r--r-- | src/xray.rs | 55 |
7 files changed, 135 insertions, 48 deletions
@@ -308,13 +308,22 @@ impl Client { ) -> Result<String> { let proxy = self.proxy().await?; let arguments = (id.unwrap_or(""), speed, interactive); - tokio::select! { - result = proxy.call("Diagnostic", &arguments) => Ok(result?), + let value: String = tokio::select! { + result = proxy.call("Diagnostic", &arguments) => result?, _ = tokio::signal::ctrl_c() => { let _: () = proxy.call("CancelDiagnostic", &(interactive,)).await?; anyhow::bail!("Diagnostic cancelled; proxy intent is unchanged") } + }; + let observation: crate::diagnostics::Observation = serde_json::from_str(&value)?; + if !observation.success { + let delay = observation + .retry_after_seconds + .map(|seconds| format!("; retry after at least {seconds} seconds")) + .unwrap_or_default(); + anyhow::bail!("Diagnostic failed: {}{}", observation.stop_reason, delay); } + Ok(value) } pub async fn change(&self, operation: Operation, interactive: bool) -> Result<()> { let proxy = self.proxy().await?; diff --git a/src/controller.rs b/src/controller.rs index 4686118..ea98a65 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -336,27 +336,32 @@ impl Controller { "The diagnostic provider requested a pause; retry after its Retry-After delay" ); } - let probe = self - .runtime - .probe( - entry.connection.as_ref().unwrap(), - if speed { - settings.maximum_duration_seconds - } else { - 5 - }, + let result = async { + let probe = self + .runtime + .probe( + entry.connection.as_ref().unwrap(), + if speed { + settings.maximum_duration_seconds + } else { + 5 + }, + ) + .await?; + let connection = probe.connection(); + let measured = crate::diagnostics::measure( + &settings, + connection.proxy_url, + connection.certificate, + id.into(), + generation, + speed, ) - .await?; - let result = crate::diagnostics::measure( - &settings, - &probe.proxy, - &probe.certificate, - id.into(), - generation, - speed, - ) + .await; + probe.stop().await; + measured + } .await; - probe.stop().await; match result { Ok(observation) => { if let Some(wait) = observation.retry_after_seconds { @@ -383,19 +388,24 @@ impl Controller { Ok(observation) } Err(error) => { + let observation = crate::diagnostics::Observation { + configuration_id: id.into(), + generation, + kind: if speed { "speed" } else { "ping" }.into(), + success: false, + elapsed_ms: 0, + bytes: 0, + bytes_per_second: None, + stop_reason: error.to_string(), + retry_after_seconds: None, + }; if !speed && Some(id) == selected.as_deref() { - self.observation = Some(crate::diagnostics::Observation { - configuration_id: id.into(), - generation, - kind: "ping".into(), - success: false, - elapsed_ms: 0, - bytes: 0, - bytes_per_second: None, - stop_reason: error.to_string(), - retry_after_seconds: None, - }); + self.observation = Some(observation.clone()); } + self.measurements.retain(|previous| { + previous.configuration_id != id || previous.kind != observation.kind + }); + self.measurements.push(observation); self.record(error.to_string()); Err(error) } diff --git a/src/diagnostics.rs b/src/diagnostics.rs index 432a801..f6ed82a 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -104,6 +104,10 @@ pub async fn measure( !content_type.contains("html"), "Diagnostic endpoint returned HTML" ); + ensure!( + !content_type.contains("application/problem+"), + "Diagnostic endpoint returned an error document" + ); let status = response.status().as_u16(); let stream = response.bytes_stream().map_err(std::io::Error::other); let mut reader = tokio_util::io::StreamReader::new(stream); @@ -154,6 +158,19 @@ pub async fn measure( !prefix.starts_with("<!doctype html") && !prefix.starts_with("<html"), "Diagnostic endpoint returned an HTML document" ); + let json_error = serde_json::from_str::<serde_json::Value>(&prefix) + .ok() + .is_some_and(|body| { + ["error", "errors"].iter().any(|key| { + body.get(key).is_some_and(|value| { + !value.is_null() && value != false && value != 0 && value != "" + }) + }) + }); + ensure!( + !json_error, + "Diagnostic endpoint returned an error document" + ); ensure!(!speed || bytes > 0, "Speed response contained no data"); if !speed && settings.ping_url == Settings::default().ping_url { ensure!( diff --git a/src/installation.rs b/src/installation.rs index bc86db6..19e5018 100644 --- a/src/installation.rs +++ b/src/installation.rs @@ -97,15 +97,15 @@ fn assets() -> [(&'static str, &'static str); 8] { ("/usr/share/polkit-1/actions/org.roentgen.policy", POLKIT), ( "/etc/systemd/system/NetworkManager.service.d/50-roentgen-guard.conf", - "[Unit]\nRequires=roentgen-guard.service\nAfter=roentgen-guard.service\n", + include_str!("../assets/roentgen-network-guard.conf"), ), ( "/etc/systemd/system/systemd-networkd.service.d/50-roentgen-guard.conf", - "[Unit]\nRequires=roentgen-guard.service\nAfter=roentgen-guard.service\n", + include_str!("../assets/roentgen-network-guard.conf"), ), ( "/etc/systemd/system/network.service.d/50-roentgen-guard.conf", - "[Unit]\nRequires=roentgen-guard.service\nAfter=roentgen-guard.service\n", + include_str!("../assets/roentgen-network-guard.conf"), ), ] } diff --git a/src/linux/mod.rs b/src/linux/mod.rs index fd9c471..c6c10bd 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -104,8 +104,12 @@ impl Runtime for LinuxRuntime { } self.apply(connection, generation).await } - async fn probe(&mut self, connection: &Trojan, duration: u64) -> Result<xray::Probe> { - xray::Probe::start(connection, duration).await + async fn probe( + &mut self, + connection: &Trojan, + duration: u64, + ) -> Result<Box<dyn crate::runtime::ProbeSession>> { + Ok(Box::new(xray::Probe::start(connection, duration).await?)) } async fn disable(&mut self) -> Result<()> { xray::systemctl(&["stop", "roentgen-xray.service"]).await?; diff --git a/src/runtime.rs b/src/runtime.rs index 2d5552d..d1b5798 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -2,6 +2,18 @@ use crate::model::Trojan; use anyhow::{Result, bail}; use async_trait::async_trait; +/// A verified local proxy connection owned by a runtime probe session. +pub struct ProbeConnection<'a> { + pub proxy_url: &'a str, + pub certificate: &'a str, +} + +#[async_trait] +pub trait ProbeSession: Send { + fn connection(&self) -> ProbeConnection<'_>; + async fn stop(self: Box<Self>); +} + #[async_trait] pub trait Runtime: Send { /// Protection must be installed before durable enabled intent changes. @@ -14,7 +26,7 @@ pub trait Runtime: Send { async fn maintain(&mut self, connection: Option<&Trojan>, generation: u64) -> Result<()> { self.apply(connection, generation).await } - async fn probe(&mut self, _: &Trojan, _: u64) -> Result<crate::xray::Probe> { + async fn probe(&mut self, _: &Trojan, _: u64) -> Result<Box<dyn ProbeSession>> { bail!("Protected diagnostic probes are unavailable in this runtime") } /// Called only after disabled intent has been durably recorded. diff --git a/src/xray.rs b/src/xray.rs index c57147c..8e2135d 100644 --- a/src/xray.rs +++ b/src/xray.rs @@ -84,6 +84,27 @@ pub fn configuration(connection: &Trojan, tun: bool) -> Value { json!({"log":{"loglevel":"none"},"inbounds":inbounds,"outbounds":[{"tag":"proxy","protocol":"trojan","settings":{"servers":[{"address":connection.host,"port":connection.port,"password":connection.password}]},"streamSettings":{"network":"tcp","security":"tls","tlsSettings":tls}}]}) } +/// Listener policy is supplied by the trusted runtime, never by a subscription. +pub struct ProbeListener<'a> { + pub port: u16, + pub password: &'a str, + pub certificate: &'a str, + pub private_key: &'a str, +} + +pub fn probe_configuration(connection: &Trojan, listener: &ProbeListener<'_>) -> Value { + let mut configuration = configuration(connection, false); + configuration["inbounds"] = json!([{ + "tag":"probe", "listen":"127.0.0.1", "port":listener.port, "protocol":"http", + "settings":{"accounts":[{"user":"roentgen","pass":listener.password}]}, + "streamSettings":{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{ + "certificate":listener.certificate.lines().collect::<Vec<_>>(), + "key":listener.private_key.lines().collect::<Vec<_>>() + }]}} + }]); + configuration +} + pub fn write_config(path: &Path, value: &Value) -> Result<()> { let (_, gid) = identity()?; fs::create_dir_all(RUNTIME_DIR)?; @@ -117,8 +138,8 @@ pub async fn systemctl(args: &[&str]) -> Result<()> { pub struct Probe { unit: String, path: std::path::PathBuf, - pub proxy: String, - pub certificate: String, + proxy: String, + certificate: String, active: bool, } impl Probe { @@ -132,14 +153,15 @@ impl Probe { let certified = rcgen::generate_simple_self_signed(vec!["127.0.0.1".into()])?; let certificate = certified.cert.pem(); let key = certified.key_pair.serialize_pem(); - let mut configuration = configuration(connection, false); - configuration["inbounds"] = json!([{ - "tag":"probe", "listen":"127.0.0.1", "port":port, "protocol":"http", - "settings":{"accounts":[{"user":"roentgen","pass":token}]}, - "streamSettings":{"network":"tcp","security":"tls","tlsSettings":{"certificates":[{ - "certificate":certificate.lines().collect::<Vec<_>>(), "key":key.lines().collect::<Vec<_>>() - }]}} - }]); + let configuration = probe_configuration( + connection, + &ProbeListener { + port, + password: &token, + certificate: &certificate, + private_key: &key, + }, + ); write_config(&path, &configuration)?; drop(listener); let runtime_limit = format!("RuntimeMaxSec={}", duration.saturating_add(10)); @@ -211,3 +233,16 @@ impl Drop for Probe { // systemd RuntimeMaxSec also bounds the process after controller death. } } + +#[async_trait::async_trait] +impl crate::runtime::ProbeSession for Probe { + fn connection(&self) -> crate::runtime::ProbeConnection<'_> { + crate::runtime::ProbeConnection { + proxy_url: &self.proxy, + certificate: &self.certificate, + } + } + async fn stop(self: Box<Self>) { + Probe::stop(*self).await; + } +} |