otoroshictl 0.0.17

a CLI to manage your otoroshi clusters with style ;)
use std::sync::Arc;

use crate::cli::cliopts::CliOpts;
use crate::{cli_stderr_printline, cli_stdout_printline};

use super::cache::SidecarCache;
use super::config::OtoroshiSidecarConfig;

use run_script::ScriptOptions;

pub struct Sidecar {}

impl Sidecar {
    fn update_cache(cache: Arc<SidecarCache>) {
        tokio::spawn(async move {
            loop {
                cache.update().await;
                tokio::time::sleep(std::time::Duration::from_secs(60)).await;
            }
        });
    }

    pub fn how_to() {
        let mut logger = paris::Logger::new();

        logger.log("");
        logger.log("In order to run the sidecar with transparent proxying of the mesh calls, you need to create a user. This will be used to avoid being intercepted by the iptables rules.");
        logger.log("");
        logger.log("  <green>useradd -u 5678 -U otoroshictl</>");
        logger.log("");
        logger.log("Then you will have to install iptables rules to route traffic through the sidecar proxies: ");
        logger.log("");
        logger.log(
            "  <green>sudo otoroshictl sidecar install --user otoroshictl -f ./sidecar.yaml</>",
        );
        logger.log("");
        logger.log("This command will add several rules to your local iptables permanently. Remember that you can uninstall the iptables rules at any time with the following command: ");
        logger.log("");
        logger.warn("  <red>sudo otoroshictl sidecar uninstall</>");
        logger.log("");
        logger.log("Then you can run your sidecar with the following command: ");
        logger.log("");
        logger
            .log("  <green>runuser -u otoroshictl -- otoroshictl sidecar run -f ./sidecar.yaml</>");
        logger.log("");
    }

    pub async fn start(
        clip_opts: CliOpts,
        sidecar_config: OtoroshiSidecarConfig,
        dns_port: &Option<u16>,
    ) -> () {
        if sidecar_config
            .clone()
            .spec
            .inbound
            .tls
            .map(|i| i.enabled)
            .unwrap_or(false)
        {
            Self::start_inbound_https(clip_opts, sidecar_config, dns_port).await;
        } else {
            Self::start_inbound_http(clip_opts, sidecar_config, dns_port).await;
        }
    }

    pub async fn start_inbound_http(
        clip_opts: CliOpts,
        sidecar_config: OtoroshiSidecarConfig,
        dns_port: &Option<u16>,
    ) -> () {
        let cache = Arc::new(SidecarCache::new(sidecar_config.clone(), clip_opts.clone()));

        Self::update_cache(cache.clone());

        let dns = crate::sidecar::dns::DnsServer::start(*dns_port, sidecar_config.clone());
        let outbound = crate::sidecar::outboundproxy::OutboundProxy::start(
            clip_opts.clone(),
            sidecar_config.clone(),
            cache.clone(),
        );
        let inbound = crate::sidecar::inboundproxy::InboundProxy::start_http(
            sidecar_config.clone(),
            cache.clone(),
        );

        let _ = futures::join!(dns, outbound, inbound);
    }

    pub async fn start_inbound_https(
        clip_opts: CliOpts,
        sidecar_config: OtoroshiSidecarConfig,
        dns_port: &Option<u16>,
    ) -> () {
        let cache = Arc::new(SidecarCache::new(sidecar_config.clone(), clip_opts.clone()));

        Self::update_cache(cache.clone());

        let dns = crate::sidecar::dns::DnsServer::start(*dns_port, sidecar_config.clone());
        let outbound = crate::sidecar::outboundproxy::OutboundProxy::start(
            clip_opts.clone(),
            sidecar_config.clone(),
            cache.clone(),
        );
        let inbound = crate::sidecar::inboundproxy::InboundProxy::start_https(
            sidecar_config.clone(),
            cache.clone(),
        );

        let _ = futures::join!(dns, outbound, inbound);
    }

    fn get_backup_file_path() -> String {
        confy::get_configuration_file_path("io.otoroshi.otoroshictl", Some("iptables_backup"))
            .unwrap()
            .to_string_lossy()
            .to_string()
    }

    pub fn install(sidecar_config: OtoroshiSidecarConfig, user: &String, dry: &Option<bool>) -> () {
        let outbound_port = sidecar_config.spec.outbounds.port.unwrap_or(15001);
        let inbound_port = sidecar_config.spec.inbound.port.unwrap_or(15000);
        let target_port = sidecar_config.spec.inbound.target_port.unwrap_or(8080);
        let dns_port = sidecar_config.spec.dns_port.unwrap_or(15053);

        let out = std::process::Command::new("id")
            .arg("-u")
            .arg(user)
            .output()
            .unwrap()
            .stdout;
        let uid: i32 = String::from_utf8(out)
            .unwrap()
            .trim_end_matches('\n')
            .to_string()
            .parse()
            .unwrap();

        // --------- cli_stdout_printline!("useradd -u 5678 -U otoroshictl");
        // --------- cli_stdout_printline!("iptables -t nat -A OUTPUT -p tcp --dport {} -j DNAT --to-destination 127.0.0.1:{}", 80, outbound_port);
        // --------- cli_stdout_printline!("iptables -t nat -A INPUT -p tcp --dport {} -j DNAT --to-destination 127.0.0.1:{}", target_port, inbound_port);
        // --------- cli_stdout_printline!("iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:{}", dns_port);

        let iptables_backup = format!("iptables-save -f '{}'", Self::get_backup_file_path());

        // create user
        // let user_add = format!("useradd -u 5678 -U otoroshictl");

        // outbound rules: call to anything 80 to local outbound proxy but not for otoroshictl user
        let outbound_rules = format!(
            "
iptables -t nat -N OTOCTL_OUTBOUND_REDIRECT
iptables -t nat -I OUTPUT 1 -p tcp --dport 80 -m owner --uid-owner {} -j RETURN  
iptables -t nat -A OUTPUT -p tcp --dport 80 -j OTOCTL_OUTBOUND_REDIRECT 
iptables -t nat -A OTOCTL_OUTBOUND_REDIRECT -p tcp -j REDIRECT --to-ports {}",
            uid, outbound_port
        );

        // inbound rules: call to backend port goes to local inbound proxy
        let inbound_rule = format!(
            "
iptables -t nat -N OTOCTL_INBOUND_REDIRECT
iptables -t nat -A INPUT -p tcp --dport {} -j OTOCTL_INBOUND_REDIRECT
iptables -t nat -A OTOCTL_INBOUND_REDIRECT -p tcp -j REDIRECT --to-ports {}",
            target_port, inbound_port
        );

        // dns rules, call udp to 53 goes to local dns server but not for the otoroshictl user
        let dns_rule = format!(
            "
iptables -t nat -N OTOCTL_DNS_REDIRECT
iptables -t nat -I OUTPUT 1 -p udp --dport 53 -m owner --uid-owner {} -j RETURN
iptables -t nat -A OUTPUT -p udp --dport 53 -j OTOCTL_DNS_REDIRECT
iptables -t nat -A OTOCTL_DNS_REDIRECT -p udp -j REDIRECT --to-ports {}",
            uid, dns_port
        );

        let script = format!(
            "{}\n{}\n{}\n{}\niptables -t nat --list\n",
            iptables_backup, outbound_rules, inbound_rule, dns_rule
        );

        if (*dry).unwrap_or(false) {
            cli_stdout_printline!("{}", script);
            std::process::exit(0);
        } else {
            let options = ScriptOptions::new();
            let args = vec![];
            let (code, output, error) = run_script::run(&script, &args, &options).unwrap();
            if code == 0 {
                cli_stdout_printline!("{}", output);
                std::process::exit(0);
            } else {
                cli_stdout_printline!("{}", output);
                cli_stderr_printline!("{}", error);
                std::process::exit(-1);
            }
        }
    }

    pub fn uninstall(dry: &Option<bool>) -> () {
        let backup = Self::get_backup_file_path();
        let script = format!(
            "set +e\n\nif [ -f '{path}' ]; then\n  echo 'restoring iptables from backup: {path}'\n  iptables-restore '{path}'\nelse\n  echo 'no iptables backup found at {path}, performing selective cleanup'\n  CHAINS=(\n    OTOCTL_OUTBOUND_REDIRECT\n    OTOCTL_INBOUND_REDIRECT\n    OTOCTL_DNS_REDIRECT\n    OTOROSHICTL_SIDECAR_OUTBOUND_REDIRECT\n    OTOROSHICTL_SIDECAR_INBOUND_REDIRECT\n    OTOROSHICTL_SIDECAR_DNS_REDIRECT\n  )\n\n  # delete jump rules to our chains in OUTPUT/INPUT for nat table\n  for TABLE in nat; do\n    for HOOK in OUTPUT INPUT PREROUTING; do\n      iptables -t \"$TABLE\" -S \"$HOOK\" 2>/dev/null | while read -r LINE; do\n        for CH in \"${{CHAINS[@]}}\"; do\n          echo \"$LINE\" | grep -q \" -j $CH\" && {{\n            CMD=$(echo \"$LINE\" | sed -E 's/^-A /-D /')\n            echo \"iptables -t $TABLE $CMD\"\n            iptables -t \"$TABLE\" $CMD || true\n          }}\n        done\n      done\n    done\n  done\n\n  # flush and delete our custom chains if they exist\n  for CH in \"${{CHAINS[@]}}\"; do\n    iptables -t nat -F \"$CH\" 2>/dev/null || true\n    iptables -t nat -X \"$CH\" 2>/dev/null || true\n  done\nfi\n\niptables -t nat --list\n",
            path = backup
        );

        if (*dry).unwrap_or(false) {
            cli_stdout_printline!("{}", script);
            std::process::exit(0);
        } else {
            let options = ScriptOptions::new();
            let args = vec![];
            let (code, output, error) = run_script::run(&script, &args, &options).unwrap();
            if code == 0 {
                cli_stdout_printline!("{}", output);
                std::process::exit(0);
            } else {
                cli_stdout_printline!("{}", output);
                cli_stderr_printline!("{}", error);
                std::process::exit(-1);
            }
        }
    }
}