diff options
| author | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 03:32:00 +0400 |
|---|---|---|
| committer | Andrew Guschin <guschin@altlinux.org> | 2026-10-30 03:32:00 +0400 |
| commit | fc3c1d51b1be9db6ba6ad286584fbc34bb305f38 (patch) | |
| tree | d23628e0534f0a4646c111c2ef6dd1501f18ae17 /src | |
| parent | d608ffeeda1a479c9dc0e5f6ad269ba07a510d7f (diff) | |
Fix protected activation ordering and diagnostic review findings
Diffstat (limited to 'src')
| -rw-r--r-- | src/controller.rs | 97 | ||||
| -rw-r--r-- | src/diagnostics.rs | 8 | ||||
| -rw-r--r-- | src/linux/firewall.rs | 18 | ||||
| -rw-r--r-- | src/linux/mod.rs | 53 | ||||
| -rw-r--r-- | src/linux/routes.rs | 3 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/model.rs | 19 | ||||
| -rw-r--r-- | src/runtime.rs | 7 | ||||
| -rw-r--r-- | src/storage.rs | 65 | ||||
| -rw-r--r-- | src/subscription.rs | 10 | ||||
| -rw-r--r-- | src/tui.rs | 9 | ||||
| -rw-r--r-- | src/xray.rs | 39 |
12 files changed, 266 insertions, 65 deletions
diff --git a/src/controller.rs b/src/controller.rs index 1384ad8..4686118 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -47,6 +47,7 @@ pub struct Controller { logs: std::collections::VecDeque<String>, next_check: Option<tokio::time::Instant>, measurements: Vec<crate::diagnostics::Observation>, + cooldowns: std::collections::HashMap<String, Option<tokio::time::Instant>>, } impl Controller { @@ -73,6 +74,7 @@ impl Controller { logs: std::collections::VecDeque::new(), next_check: Some(tokio::time::Instant::now()), measurements: vec![], + cooldowns: std::collections::HashMap::new(), }) } @@ -145,7 +147,7 @@ impl Controller { name: entry.name, host: entry.connection.as_ref().map(|c| c.host.clone()), port: entry.connection.as_ref().map(|c| c.port), - status: entry.status, + status: entry.status.as_str().into(), explanation: entry.explanation, }); } @@ -154,9 +156,13 @@ impl Controller { } pub async fn restore(&mut self) -> Result<()> { - if self.storage.state()?.0 { - self.runtime.arm().await?; - self.reconcile().await?; + let (enabled, selected, _) = self.storage.state()?; + if enabled { + self.runtime.block().await?; + if selected.is_some() { + self.runtime.arm().await?; + self.reconcile().await?; + } } else { self.runtime.disable().await?; } @@ -211,6 +217,16 @@ impl Controller { } Operation::DeleteSubscription { name } => { let id = self.subscription(&name)?.id.clone(); + let (enabled, selected, _) = self.storage.state()?; + if enabled + && self + .storage + .entries(&id)? + .iter() + .any(|entry| Some(&entry.id) == selected.as_ref()) + { + self.runtime.block().await?; + } let mut definitions = self.storage.definitions.clone(); definitions.subscriptions.retain(|s| s.id != id); self.storage.save_definitions(&definitions)?; @@ -241,10 +257,16 @@ impl Controller { } Operation::Select { id } => { self.usable_entry(&id)?; + if self.storage.state()?.0 { + self.runtime.block().await?; + } self.storage.select(Some(&id))?; self.reconcile().await?; } Operation::Unset => { + if self.storage.state()?.0 { + self.runtime.block().await?; + } self.storage.select(None)?; self.reconcile().await?; } @@ -302,21 +324,48 @@ impl Controller { .ok_or_else(|| anyhow!("Select a configuration or supply its ID before testing"))?; let entry = self.usable_entry(id)?; let settings = self.settings(); - let probe = crate::xray::Probe::start( - entry.connection.as_ref().unwrap(), - if speed { - settings.maximum_duration_seconds - } else { - 5 - }, + let url = if speed { + &settings.speed_url + } else { + &settings.ping_url + }; + let provider = url::Url::parse(url)?.origin().ascii_serialization(); + if let Some(deadline) = self.cooldowns.get(&provider) { + ensure!( + deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline), + "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 + }, + ) + .await?; + let result = crate::diagnostics::measure( + &settings, + &probe.proxy, + &probe.certificate, + id.into(), + generation, + speed, ) - .await?; - let result = - crate::diagnostics::measure(&settings, &probe.proxy, id.into(), generation, speed) - .await; + .await; probe.stop().await; match result { Ok(observation) => { + if let Some(wait) = observation.retry_after_seconds { + self.cooldowns.insert( + provider, + tokio::time::Instant::now() + .checked_add(std::time::Duration::from_secs(wait)), + ); + } if !speed && Some(id) == selected.as_deref() { let wait = observation.retry_after_seconds.unwrap_or(30).max(30); self.next_check = tokio::time::Instant::now() @@ -421,6 +470,19 @@ impl Controller { } } } + let (enabled, selected, _) = self.storage.state()?; + if enabled { + if let Some(id) = selected { + if let Some(previous) = old.iter().find(|entry| entry.id == id) { + let replacement = entries.iter().find(|entry| entry.id == id); + if replacement.and_then(|entry| entry.connection.as_ref()) + != previous.connection.as_ref() + { + self.runtime.block().await?; + } + } + } + } let affected: std::collections::HashSet<_> = old.iter().map(|e| e.id.as_str()).collect(); self.measurements .retain(|o| !affected.contains(o.configuration_id.as_str())); @@ -443,7 +505,7 @@ impl Controller { .find_entry(id)? .ok_or_else(|| anyhow!("Configuration not found; run configuration list"))?; ensure!( - entry.connection.is_some(), + entry.status == EntryStatus::Supported && entry.connection.is_some(), "Configuration is invalid or unsupported; inspect its explanation" ); Ok(entry) @@ -452,6 +514,9 @@ impl Controller { async fn reconcile(&mut self) -> Result<()> { let (enabled, selected, generation) = self.storage.state()?; if enabled { + if selected.is_none() { + return self.runtime.block().await; + } let entry = selected.map(|id| self.usable_entry(&id)).transpose()?; if let Err(error) = self .runtime diff --git a/src/diagnostics.rs b/src/diagnostics.rs index a2052ec..432a801 100644 --- a/src/diagnostics.rs +++ b/src/diagnostics.rs @@ -18,11 +18,12 @@ pub struct Observation { pub retry_after_seconds: Option<u64>, } -/// The caller supplies an authenticated, service-owned SOCKS endpoint. This client +/// The caller supplies an authenticated, service-owned HTTPS proxy endpoint. This client /// never inherits environment proxies or creates a direct route to the target. pub async fn measure( settings: &Settings, proxy: &str, + certificate: &str, id: String, generation: u64, speed: bool, @@ -42,11 +43,14 @@ pub async fn measure( }; let proxy = reqwest::Proxy::all(proxy).map_err(|_| anyhow!("Invalid protected probe endpoint"))?; + let certificate = reqwest::Certificate::from_pem(certificate.as_bytes()) + .map_err(|_| anyhow!("Invalid private probe certificate"))?; let client = reqwest::Client::builder() .no_proxy() .proxy(proxy) .redirect(reqwest::redirect::Policy::none()) - .timeout(Duration::from_secs(duration)) + .tls_certs_merge([certificate]) + .retry(reqwest::retry::never()) .user_agent(concat!("Roentgen/", env!("CARGO_PKG_VERSION"))) .build() .map_err(|_| anyhow!("Could not initialize diagnostic TLS client"))?; diff --git a/src/linux/firewall.rs b/src/linux/firewall.rs index 79731b7..8a0dd43 100644 --- a/src/linux/firewall.rs +++ b/src/linux/firewall.rs @@ -84,6 +84,12 @@ pub fn preflight() -> Result<()> { /// Replacing the table is one kernel transaction. Connection marks survive table replacement. pub fn apply(enabled: bool, xray_uid: u32) -> Result<()> { + apply_policy(enabled, xray_uid, false) +} +pub fn forward(xray_uid: u32) -> Result<()> { + apply_policy(true, xray_uid, true) +} +fn apply_policy(enabled: bool, xray_uid: u32, allow_tun: bool) -> Result<()> { let existing = tables()?; let table = Table::new(TABLE, ProtoFamily::Inet); let mut batch = Batch::new(); @@ -124,11 +130,13 @@ pub fn apply(enabled: bool, xray_uid: u32) -> Result<()> { guard.set_policy(Policy::Drop); batch.add(&guard, MsgType::Add); exceptions(&mut batch, &guard, xray_uid); - let mut tun = Rule::new(&guard); - tun.add_expr(&nft_expr!(meta oifname)); - tun.add_expr(&nft_expr!(cmp == &b"roentgen0\0"[..])); - tun.add_expr(&nft_expr!(verdict accept)); - batch.add(&tun, MsgType::Add); + if allow_tun { + let mut tun = Rule::new(&guard); + tun.add_expr(&nft_expr!(meta oifname)); + tun.add_expr(&nft_expr!(cmp == &b"roentgen0\0"[..])); + tun.add_expr(&nft_expr!(verdict accept)); + batch.add(&tun, MsgType::Add); + } } send(batch.finalize()).context("Could not apply Roentgen firewall transaction") } diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 68bb3a8..fd9c471 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -9,12 +9,14 @@ use std::{path::Path, time::Duration}; pub struct LinuxRuntime { routing: String, applied: Option<Trojan>, + armed: bool, } impl Default for LinuxRuntime { fn default() -> Self { Self { routing: "unknown".into(), applied: None, + armed: false, } } } @@ -25,17 +27,39 @@ impl Runtime for LinuxRuntime { xray::preflight().await?; firewall::preflight()?; routes::preflight().await?; + anyhow::ensure!( + self.applied.is_some() || routes::tun_index().await?.is_none(), + "roentgen0 already exists outside the active owned configuration; resolve the interface conflict before enabling" + ); 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(); + self.armed = true; + Ok(()) + } + async fn block(&mut self) -> Result<()> { + firewall::apply(true, xray::identity()?.0)?; + write_guard(true)?; + self.routing = "blocked".into(); + self.applied = None; + xray::systemctl(&["stop", "roentgen-xray.service"]).await?; + routes::remove().await?; Ok(()) } async fn apply(&mut self, connection: Option<&Trojan>, _: u64) -> Result<()> { - if self.applied.as_ref() == connection { + if connection.is_none() { + return self.block().await; + } + anyhow::ensure!( + self.armed, + "Activation requires successful compatibility checks and an armed guard" + ); + if connection.is_some() && self.applied.as_ref() == connection { if let Some(index) = routes::tun_index().await? { routes::capture(index).await?; + firewall::forward(xray::identity()?.0)?; self.routing = "tun".into(); return Ok(()); } @@ -45,14 +69,22 @@ impl Runtime for LinuxRuntime { xray::systemctl(&["stop", "roentgen-xray.service"]).await?; routes::remove().await?; if let Some(connection) = connection { + anyhow::ensure!( + routes::tun_index().await?.is_none(), + "roentgen0 remained after stopping the owned Xray unit; refusing to take over another interface" + ); + xray::preflight().await?; + firewall::preflight()?; + routes::preflight().await?; xray::write_config( Path::new("/run/roentgen/xray.json"), - &xray::configuration(connection, true, None), + &xray::configuration(connection, true), )?; xray::systemctl(&["start", "roentgen-xray.service"]).await?; for _ in 0..100 { if let Some(index) = routes::tun_index().await? { routes::capture(index).await?; + firewall::forward(xray::identity()?.0)?; self.routing = "tun".into(); self.applied = Some(connection.clone()); return Ok(()); @@ -64,14 +96,16 @@ impl Runtime for LinuxRuntime { 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, + if connection.is_none() { + return self.block().await; + } + if !self.armed { + self.arm().await?; } + 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 disable(&mut self) -> Result<()> { xray::systemctl(&["stop", "roentgen-xray.service"]).await?; @@ -80,6 +114,7 @@ impl Runtime for LinuxRuntime { write_guard(false)?; firewall::apply(false, xray::identity()?.0)?; self.routing = "direct".into(); + self.armed = false; Ok(()) } fn routing(&self) -> &str { diff --git a/src/linux/routes.rs b/src/linux/routes.rs index 07083bb..4212183 100644 --- a/src/linux/routes.rs +++ b/src/linux/routes.rs @@ -84,6 +84,9 @@ fn owned_rule(rule: &rtnetlink::packet_route::rule::RuleMessage) -> bool { } pub async fn capture(interface: u32) -> Result<()> { + // Only the product-owned TUN changes this setting. Loose reverse-path + // validation accepts proxied replies whose ordinary reverse route is uplink. + std::fs::write("/proc/sys/net/ipv4/conf/roentgen0/rp_filter", "2")?; let handle = connection()?; for route in [ RouteMessageBuilder::<Ipv4Addr>::new() diff --git a/src/main.rs b/src/main.rs index 7ad2d81..f4c5373 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,6 +59,8 @@ enum Command { Daemon, #[command(hide = true)] Guard, + #[command(hide = true)] + XrayPreflight, } #[derive(Subcommand)] @@ -114,6 +116,7 @@ async fn run(cli: Cli) -> Result<()> { Command::Recover { .. } => return roentgen::installation::recover().await, Command::Daemon => return roentgen::daemon::run().await, Command::Guard => return roentgen::daemon::guard().await, + Command::XrayPreflight => return roentgen::xray::preflight().await, _ => {} } let client = Client::system().await?; diff --git a/src/model.rs b/src/model.rs index 8966956..8229207 100644 --- a/src/model.rs +++ b/src/model.rs @@ -16,11 +16,28 @@ impl Trojan { } } +#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum EntryStatus { + Supported, + Unsupported, + Invalid, +} +impl EntryStatus { + pub fn as_str(self) -> &'static str { + match self { + Self::Supported => "supported", + Self::Unsupported => "unsupported", + Self::Invalid => "invalid", + } + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct Entry { pub id: String, pub name: String, - pub status: String, + pub status: EntryStatus, pub explanation: Option<String>, pub connection: Option<Trojan>, } diff --git a/src/runtime.rs b/src/runtime.rs index 96ffd0d..2d5552d 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -7,9 +7,16 @@ pub trait Runtime: Send { /// Protection must be installed before durable enabled intent changes. async fn arm(&mut self) -> Result<()>; async fn apply(&mut self, connection: Option<&Trojan>, generation: u64) -> Result<()>; + async fn block(&mut self) -> Result<()> { + self.arm().await?; + self.apply(None, 0).await + } 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> { + bail!("Protected diagnostic probes are unavailable in this runtime") + } /// Called only after disabled intent has been durably recorded. async fn disable(&mut self) -> Result<()>; fn routing(&self) -> &str; diff --git a/src/storage.rs b/src/storage.rs index cb7fcd9..93cb98c 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,4 +1,4 @@ -use crate::model::{Definitions, Entry}; +use crate::model::{Definitions, Entry, EntryStatus}; use anyhow::{Context, Result, ensure}; use rusqlite::{Connection, OptionalExtension, params}; use std::{ @@ -70,7 +70,30 @@ impl Storage { ); crate::subscription::subscription_url(&subscription.url)?; } + #[derive(serde::Deserialize)] + #[serde(deny_unknown_fields)] + struct GuardInput { + version: u32, + enabled: bool, + } + let guard_path = config_dir.join("guard.toml"); + let guard_enabled = if guard_path.exists() { + let guard: GuardInput = toml::from_str(&fs::read_to_string(&guard_path)?) + .context("Corrupt guard state; use explicit recover --disable")?; + ensure!( + guard.version == 1, + "Unsupported guard state; use explicit recover --disable" + ); + guard.enabled + } else { + false + }; let path = data_dir.join("state.sqlite3"); + let database_existed = path.exists(); + ensure!( + database_existed || !guard_enabled, + "Durable state is missing while enabled protection is requested; use explicit recover --disable" + ); if path.exists() { ensure!( fs::symlink_metadata(&path)?.is_file(), @@ -84,6 +107,10 @@ impl Storage { let version: u32 = db.query_row("PRAGMA user_version", [], |r| r.get(0))?; ensure!(version <= 1, "Unsupported database version"); if version == 0 { + ensure!( + !database_existed, + "Existing database has no state schema; use explicit recover --disable" + ); db.execute_batch("BEGIN IMMEDIATE; CREATE TABLE entries (subscription_id TEXT NOT NULL, ordinal INTEGER NOT NULL, id TEXT PRIMARY KEY, data TEXT NOT NULL); CREATE TABLE state (singleton INTEGER PRIMARY KEY CHECK(singleton=1), enabled INTEGER NOT NULL CHECK(enabled IN (0,1)), selected TEXT, generation INTEGER NOT NULL); @@ -156,13 +183,7 @@ impl Storage { let values = query .query_map([subscription_id], |r| r.get::<_, String>(0))? .collect::<rusqlite::Result<Vec<_>>>()?; - values - .iter() - .map(|v| { - serde_json::from_str(v) - .context("Corrupt saved configuration; explicit recovery is required") - }) - .collect() + values.iter().map(|v| decode_entry(v)).collect() } pub fn find_entry(&self, id: &str) -> Result<Option<Entry>> { @@ -171,7 +192,7 @@ impl Storage { r.get::<_, String>(0) }) .optional()? - .map(|v| serde_json::from_str(&v).context("Corrupt saved configuration")) + .map(|v| decode_entry(&v)) .transpose() } @@ -254,7 +275,17 @@ pub fn persist_recovery_disabled(data_dir: &Path) -> Result<()> { let db = Connection::open(&path)?; fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; db.busy_timeout(std::time::Duration::from_secs(5))?; - db.execute_batch("PRAGMA synchronous=FULL; BEGIN EXCLUSIVE;")?; + db.execute_batch("PRAGMA synchronous=FULL;")?; + let version: u32 = db.query_row("PRAGMA user_version", [], |row| row.get(0))?; + ensure!( + version <= 1, + "Unknown database schema; repair it before explicit recovery" + ); + db.execute_batch("BEGIN EXCLUSIVE; + CREATE TABLE IF NOT EXISTS entries (subscription_id TEXT NOT NULL, ordinal INTEGER NOT NULL, id TEXT PRIMARY KEY, data TEXT NOT NULL); + CREATE TABLE IF NOT EXISTS state (singleton INTEGER PRIMARY KEY CHECK(singleton=1), enabled INTEGER NOT NULL CHECK(enabled IN (0,1)), selected TEXT, generation INTEGER NOT NULL); + INSERT OR IGNORE INTO state VALUES (1,0,NULL,0); + PRAGMA user_version=1;")?; db.execute( "UPDATE state SET enabled=0,generation=generation+1 WHERE singleton=1", [], @@ -262,3 +293,17 @@ pub fn persist_recovery_disabled(data_dir: &Path) -> Result<()> { db.execute_batch("COMMIT")?; Ok(()) } + +fn decode_entry(value: &str) -> Result<Entry> { + let entry: Entry = serde_json::from_str(value).context("Corrupt saved configuration")?; + ensure!( + entry.status == EntryStatus::Supported + && entry.connection.is_some() + && entry.explanation.is_none() + || entry.status != EntryStatus::Supported + && entry.connection.is_none() + && entry.explanation.is_some(), + "Saved entry has contradictory validation state; explicit recovery is required" + ); + Ok(entry) +} diff --git a/src/subscription.rs b/src/subscription.rs index df5b318..8abcb1c 100644 --- a/src/subscription.rs +++ b/src/subscription.rs @@ -1,4 +1,4 @@ -use crate::model::{Entry, Trojan, display_text}; +use crate::model::{Entry, EntryStatus, Trojan, display_text}; use anyhow::{Result, anyhow, bail, ensure}; use async_trait::async_trait; use base64::{Engine, engine::general_purpose}; @@ -191,7 +191,7 @@ fn parse_entry(raw: &str) -> Entry { let mut entry = Entry { id: Uuid::new_v4().to_string(), name: "Invalid entry".into(), - status: "invalid".into(), + status: EntryStatus::Invalid, explanation: None, connection: None, }; @@ -205,7 +205,7 @@ fn parse_entry(raw: &str) -> Entry { entry.name = display_text(&decode(label)?); } if url.scheme() != "trojan" { - entry.status = "unsupported".into(); + entry.status = EntryStatus::Unsupported; bail!("Only Trojan TCP + TLS connections are supported"); } ensure!( @@ -276,7 +276,7 @@ fn parse_entry(raw: &str) -> Entry { "allowInsecure" if value == "0" || value == "false" => {} "headerType" if value == "none" => {} _ => { - entry.status = "unsupported".into(); + entry.status = EntryStatus::Unsupported; bail!("Unsupported transport, TLS, or connection option"); } } @@ -296,7 +296,7 @@ fn parse_entry(raw: &str) -> Entry { })(); match result { Ok(connection) => { - entry.status = "supported".into(); + entry.status = EntryStatus::Supported; entry.connection = Some(connection); } Err(error) => entry.explanation = Some(error.to_string()), @@ -65,7 +65,14 @@ impl Ui { self.entries = client.configurations(self.viewed.as_deref()).await?; self.settings = client.read("Settings").await?; self.logs = client.read("Logs").await?; - self.row = self.row.min(self.visible().len().saturating_sub(1)); + self.row = self.row.min( + if self.tab == 1 { + self.subscriptions.len() + } else { + self.visible().len() + } + .saturating_sub(1), + ); Ok(()) } fn visible(&self) -> Vec<&ConfigurationSummary> { diff --git a/src/xray.rs b/src/xray.rs index 5ac8b62..c57147c 100644 --- a/src/xray.rs +++ b/src/xray.rs @@ -69,14 +69,11 @@ pub async fn preflight() -> Result<()> { Ok(()) } -pub fn configuration(connection: &Trojan, tun: bool, probe: Option<(u16, &str)>) -> Value { +pub fn configuration(connection: &Trojan, tun: bool) -> Value { let mut inbounds = vec![]; if tun { inbounds.push(json!({"tag":"host-tun","protocol":"tun","settings":{"name":"roentgen0","MTU":1500,"gateway":["198.18.0.1/30","fdfe:726f:656e::1/126"],"autoSystemRoutingTable":[],"autoOutboundsInterface":"","autoSystemDnsToGateway":false}})); } - if let Some((port, password)) = probe { - inbounds.push(json!({"tag":"probe","listen":"127.0.0.1","port":port,"protocol":"socks","settings":{"auth":"password","accounts":[{"user":"roentgen","pass":password}],"udp":false}})); - } let mut tls = json!({"serverName":connection.server_name,"allowInsecure":false}); if !connection.alpn.is_empty() { tls["alpn"] = json!(connection.alpn); @@ -121,6 +118,7 @@ pub struct Probe { unit: String, path: std::path::PathBuf, pub proxy: String, + pub certificate: String, active: bool, } impl Probe { @@ -131,18 +129,33 @@ impl Probe { let token = uuid::Uuid::new_v4().simple().to_string(); let unit = format!("roentgen-probe-{token}"); let path = Path::new(RUNTIME_DIR).join(format!("{unit}.json")); - write_config( - &path, - &configuration(connection, false, Some((port, &token))), - )?; + 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<_>>() + }]}} + }]); + write_config(&path, &configuration)?; drop(listener); let runtime_limit = format!("RuntimeMaxSec={}", duration.saturating_add(10)); + let probe = Self { + unit, + path, + proxy: format!("https://roentgen:{token}@127.0.0.1:{port}"), + certificate, + active: true, + }; let result = Command::new("/usr/bin/systemd-run") .args([ "--quiet", "--collect", "--unit", - &unit, + &probe.unit, "--uid=roentgen-xray", "--property=NoNewPrivileges=yes", "--property=PrivateTmp=yes", @@ -154,17 +167,11 @@ impl Probe { "run", "-config", ]) - .arg(&path) + .arg(&probe.path) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .await; - let probe = Self { - unit, - path, - proxy: format!("socks5h://roentgen:{token}@127.0.0.1:{port}"), - active: true, - }; if !result?.success() { probe.stop().await; anyhow::bail!("Could not launch protected Xray probe"); |