1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
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>,
armed: bool,
}
impl Default for LinuxRuntime {
fn default() -> Self {
Self {
routing: "unknown".into(),
applied: None,
armed: false,
}
}
}
#[async_trait]
impl Runtime for LinuxRuntime {
async fn arm(&mut self) -> Result<()> {
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 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(());
}
}
self.routing = "blocked".into();
self.applied = None;
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),
)?;
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(());
}
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<()> {
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<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?;
routes::remove().await?;
self.applied = None;
write_guard(false)?;
firewall::apply(false, xray::identity()?.0)?;
self.routing = "direct".into();
self.armed = false;
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"
);
}
}
}
|