kapiti 0.0.3

The Kapiti DNS Server
Documentation
use std::env;
use std::ffi::CString;
use std::sync::Arc;
use std::thread;

use anyhow::{bail, Context, Result};
use async_lock::Barrier;
use git_testament::{git_testament, render_testament};
use libc;
use nix::unistd;
use signal_hook::{consts, iterator};
use tracing::{self, debug, info};

use kapiti::runner::Runner;
use kapiti::{config, logging, panic};

git_testament!(TESTAMENT);

fn print_syntax() {
    println!(
        "Kapiti DNS ({})

Syntax: {} </path/to/config.toml>

Environment variables:
  CONFIG_PATH=/path/to/config.toml
  LOG_LEVEL=error/warn/info/debug/trace/off",
        render_testament!(TESTAMENT),
        env::args().nth(0).unwrap()
    );
}

fn main() -> Result<()> {
    logging::init_logging();
    panic::init_hook();

    // Try to get path via commandline, then fall back to envvar
    let config_path = match env::args().nth(1) {
        Some(arg) => {
            if arg == "-v" || arg == "--version" {
                // Special case to just print the version and exit successfully.
                // Mainly useful for checking that the executable is valid in CI builds.
                println!("Kapiti DNS ({})", render_testament!(TESTAMENT));
                return Ok(());
            }
            if arg.starts_with('-') {
                // Probably a commandline argument like '-h'/'--help', avoid parsing as a path
                print_syntax();
                bail!("Unrecognized TOML config path argument: {}", arg);
            }
            arg
        }
        None => {
            if let Ok(var) = env::var("CONFIG_PATH") {
                var
            } else {
                print_syntax();
                bail!("Missing required TOML config path argument");
            }
        }
    };

    // Got past logging/args, print version message before getting into config parsing
    info!("Kapiti DNS version {}", render_testament!(TESTAMENT));

    let config = config::parse_config_file(&config_path)
        .with_context(|| format!("Failed to parse TOML config: {}", config_path))?;
    debug!("config: {:?}", config);

    // Get downgrade user before passing ownership of config
    let downgrade_user_orig = config.user.clone();
    let downgrade_user = downgrade_user_orig.trim();

    // Needs to run async since it sets up the sockets internally
    let runner = smol::block_on(Runner::new(config_path, config))?;

    // If currently root, downgrade to specified non-root user after Runner has set up any sockets
    if !downgrade_user.is_empty() && unistd::geteuid().is_root() {
        chuser(downgrade_user)
            .with_context(|| format!("Failed to change to user: {}", downgrade_user))?;
        info!("Changed to user: {}", downgrade_user);
    }

    // Run the service indefinitely, it will exit when the stop barrier returns.
    let barrier = Arc::new(Barrier::new(2));
    let barrier_copy = barrier.clone();
    let mut signals = iterator::Signals::new(&[consts::SIGINT, consts::SIGTERM])?;
    thread::spawn(move || {
        // Clean shutdown on Ctrl+C or TERM
        for sig in signals.forever() {
            info!("Received signal: {:?}", sig);
            smol::block_on(barrier_copy.wait());
        }
    });
    smol::block_on(runner.run(barrier))
}

/// Downgrades the user running the process to the provided username
/// This allows binding to port 53 as root, then running the daemon as a user
fn chuser(username: &str) -> Result<()> {
    let mut result = std::ptr::null_mut::<libc::passwd>();
    let ret = unsafe {
        let mut pwd = std::mem::zeroed::<libc::passwd>();
        let mut buf = vec![0; 4096];
        libc::getpwnam_r(
            CString::new(username.as_bytes())?.as_ptr(),
            &mut pwd,
            buf.as_mut_ptr(),
            buf.len(),
            &mut result,
        )
    };
    if ret != 0 || result.is_null() {
        bail!("User not found: {:?}", username);
    }

    let uid: u32;
    let gid: u32;
    unsafe {
        uid = (*result).pw_uid;
        gid = (*result).pw_gid;
    }

    if unsafe { libc::setgroups(1, &gid) } != 0 {
        bail!("Unable to revoke supplementary groups");
    }
    unistd::setgid(unistd::Gid::from_raw(gid))
        .with_context(|| format!("Unable to set process gid to {}/{}", username, gid))?;
    unistd::setuid(unistd::Uid::from_raw(uid))
        .with_context(|| format!("Unable to set process uid to {}/{}", username, uid))?;

    Ok(())
}