vopono_core 1.0.1

Library code for running VPN connections in network namespaces
Documentation
use std::io::Write;
use std::path::Path;

use anyhow::Context;
use log::warn;
use serde::{Deserialize, Serialize};
use which::which;

use crate::network::{netns::NetworkNamespace, wireguard_config::WireguardPeer};

use super::{TrojanHost, get_cert, trojan_config::TrojanConfig};

#[derive(Debug, Serialize, Deserialize)]
pub struct Trojan {
    pub(crate) pid: u32,
    pub config: TrojanConfig,
    #[serde(skip)]
    _temporary_files: Vec<tempfile::NamedTempFile>,
    #[serde(skip)]
    cleanup_enabled: bool,
}

impl Trojan {
    pub fn run_in_netns(
        netns: &NetworkNamespace,
        host: Option<TrojanHost>,
        password: Option<&str>,
        config_path: Option<&Path>,
        no_verify: bool,
        peer: Option<WireguardPeer>,
    ) -> anyhow::Result<Trojan> {
        let mut config = TrojanConfig::new(config_path)?;
        let config_path_buf;
        let mut temporary_files = Vec::new();

        if let Some(cpath) = config_path {
            warn!("Using custom Trojan config file: {cpath:?}");
            config_path_buf = config_path.unwrap().to_path_buf();
        } else {
            let h = host
                .clone()
                .expect("Host must be provided if no config path is given");
            if h.is_ip() {
                log::error!(
                    "IP address provided for trojan host, but SSL verification is not disabled. Disabling SSL verification."
                );
            } else {
                let cert = get_cert::get_cert(h.host(), h.port())?;
                temporary_files.push(config.set_cert(&cert)?);
            }
            config.set_verify_fields(!no_verify);
            config.set_remote_fields(&host.unwrap());
            config.set_password(password.unwrap());
            config.set_wg_forwarding_fields(peer.as_ref().unwrap());

            let mut config_file = tempfile::NamedTempFile::new()
                .context("Failed to create temporary Trojan config")?;
            config_file.write_all(serde_json::to_string(&config)?.as_bytes())?;
            config_path_buf = config_file.path().to_path_buf();
            temporary_files.push(config_file);
        }

        let trojan_exec = which("trojan").with_context(|| {
                "trojan executable not found in PATH. Please install trojan and ensure it is in your PATH."
                    })?;

        let handle = NetworkNamespace::exec_no_block(
            &netns.name,
            &[
                trojan_exec.to_str().unwrap(),
                "-c",
                config_path_buf.to_str().unwrap(),
            ],
            None, // Need to pass root?
            None,
            false,
            false,
            false,
            None,
        )?;

        Ok(Trojan {
            pid: handle.id(),
            config,
            _temporary_files: temporary_files,
            cleanup_enabled: true,
        })
    }
}

impl Drop for Trojan {
    fn drop(&mut self) {
        if !self.cleanup_enabled {
            return;
        }
        nix::sys::signal::kill(
            nix::unistd::Pid::from_raw(self.pid as i32),
            nix::sys::signal::Signal::SIGTERM,
        )
        .expect("Failed to kill trojan process");
    }
}

impl Trojan {
    pub(crate) fn set_cleanup_enabled(&mut self, enabled: bool) {
        self.cleanup_enabled = enabled;
    }
}