summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bus.rs13
-rw-r--r--src/controller.rs70
-rw-r--r--src/diagnostics.rs17
-rw-r--r--src/installation.rs6
-rw-r--r--src/linux/mod.rs8
-rw-r--r--src/runtime.rs14
-rw-r--r--src/xray.rs55
7 files changed, 135 insertions, 48 deletions
diff --git a/src/bus.rs b/src/bus.rs
index fd26f1d..68bd316 100644
--- a/src/bus.rs
+++ b/src/bus.rs
@@ -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;
+ }
+}