summaryrefslogtreecommitdiff
path: root/src/linux/mod.rs
blob: 68bb3a8476840a3db33e20c1d88ee658d06dbae1 (plain)
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
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"
            );
        }
    }
}