aboutsummaryrefslogtreecommitdiff
#!/bin/sh

# SPDX-License-Identifier: CC0-1.0

# Copyright (C) 2022-2023 Wojtek Kosior <koszko@koszko.org>
#
# Available under the terms of Creative Commons Zero v1.0 Universal.

### BEGIN INIT INFO
# Provides:          guix-container
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start Wojtek's Guix container with various services
### END INIT INFO

set -e

if [ -r /lib/lsb/init-functions ]; then
    . /lib/lsb/init-functions
else
    log_anything() {
        while [ 0 -lt $# ]; do
            printf "%s" "$1"
            shift
            if [ 0 -lt $# ]; then
                printf " "
            else
                printf "\n"
            fi
        done
    }
    log_action_msg() {
        log_anything "$@"
    }
    log_failure_msg() {
        printf "Failure\n"
    }
    log_success_msg() {
        printf "Success\n"
    }
    log_daemon_msg() {
        log_anything "$@"
    }
    log_warning_msg() {
        log_anything "$@"
    }
    status_of_proc() {
        if is_running; then
            printf "Guix container running\n"
        else
            printf "Guix container not running\n"
        fi
    }
fi

if [ 0 != $(id -u) ]; then
    log_action_msg "Script '$0' must be run as root"
    exit 1
fi

PIDFILE=/run/guix-container.pid
EXECUTABLE=/usr/local/bin/guix-container
HOST_SYSTEM_ROOT=
MAX_CONTAINER_SPINUP_WAIT=60

ACTION="$1"
shift

OPTIND=1
while getopts p:e:r:s: OPTION_LETTER ; do
    case "$OPTION_LETTER" in
        p)  PIDFILE="$OPTARG"    ;;
        e)  EXECUTABLE="$OPTARG" ;;
        r)  HOST_SYSTEM_ROOT="$OPTARG" ;;
        s)  MAX_CONTAINER_SPINUP_WAIT="$OPTARG" ;;
    esac
done

GUILE_PID=
SUCCESS=
QUIET_EXIT=
FORWARDED_PORTLISTS="tcp:25,12525,465,587 tcp:993 udp:53 tcp:53 tcp:80,443"

colon_sep_field() {
    printf '%s\n' "$1" | awk -F : "{print \$$2}"
}

is_running() {
    test -e "$PIDFILE" && test -n "$(ps -o pid= --pid $(cat "$PIDFILE"))"
    return $?
}

resolve_ipv4_domain() {
    guix shell glibc -- getent ahosts "$1" |
        grep -E '^([0-9]+\.){3}[0-9]+[[:space:]]+STREAM' |
        head -1 |
        awk '{print $1}'
}

network_setup() {
    SHEPHERD_PID="$1"

    ip link add veth-guix-out type veth peer name veth-guix-in
    ip link set veth-guix-in netns "$SHEPHERD_PID"

    ip link set veth-guix-out up
    ip addr add 10.207.87.1/24 dev veth-guix-out

    nsenter --target "$SHEPHERD_PID" --net ip link set lo up
    nsenter --target "$SHEPHERD_PID" --net ip link set veth-guix-in up
    nsenter --target "$SHEPHERD_PID" --net ip addr add \
            10.207.87.2/24 dev veth-guix-in
    nsenter --target "$SHEPHERD_PID" --net ip route add \
            default via 10.207.87.1 dev veth-guix-in

    if [ -n "$HOST_SYSTEM_ROOT" ]; then
        # Don't connect to the real net when running in a test environment.
        return
    fi

    for LINKNAME in $(ip route | grep default | awk '{print $5}'); do
        iptables -t nat -A POSTROUTING \
                 -s 10.207.87.1/24 -o "$LINKNAME" -j MASQUERADE
        for PORTLIST in $FORWARDED_PORTLISTS; do
            iptables -t nat -A PREROUTING                                     \
                     -i "$LINKNAME" -p "$(colon_sep_field "$PORTLIST" 1)"     \
                     -m multiport --dports "$(colon_sep_field "$PORTLIST" 2)" \
                     -j DNAT --to-destination 10.207.87.2
        done
    done

    for PORTLIST in $FORWARDED_PORTLISTS; do
        iptables -t nat -A OUTPUT                                         \
                 -d "$(resolve_ipv4_domain koszko.org)"                   \
                 -p "$(colon_sep_field "$PORTLIST" 1)"                    \
                 -m multiport --dports "$(colon_sep_field "$PORTLIST" 2)" \
                 -j DNAT --to-destination 10.207.87.2
    done

    cat /etc/resolv.conf |
        nsenter --target "$SHEPHERD_PID" --all \
                /run/current-system/profile/bin/tee /etc/resolv.conf > /dev/null

    echo 1 > /proc/sys/net/ipv4/ip_forward
}

iptables_rip_rule() {
    while iptables "$@" 2>/dev/null; do
        true
    done
}

network_rip() {
    ip link delete veth-guix-out 2>/dev/null || true

    if [ -n "$HOST_SYSTEM_ROOT" ]; then
        # There's no connection to the real net when running in a test
        # environment.
        return
    fi

    echo 0 > /proc/sys/net/ipv4/ip_forward

    for LINKNAME in $(ip route | grep default | awk '{print $5}'); do
        for PORTLIST in $FORWARDED_PORTLISTS; do
            iptables_rip_rule -t nat -D PREROUTING                        \
                              -i "$LINKNAME"                              \
                              -p "$(colon_sep_field "$PORTLIST" 1)"       \
                              -m multiport                                \
                              --dports "$(colon_sep_field "$PORTLIST" 2)" \
                              -j DNAT --to-destination 10.207.87.2
        done
        iptables_rip_rule -t nat -D POSTROUTING            \
                          -s 10.207.87.1/24 -o "$LINKNAME" \
                          -j MASQUERADE
    done

    for PORTLIST in $FORWARDED_PORTLISTS; do
        iptables_rip_rule -t nat -D OUTPUT                            \
                          -d "$(resolve_ipv4_domain koszko.org)"      \
                          -p "$(colon_sep_field "$PORTLIST" 1)"       \
                          -m multiport                                \
                          --dports "$(colon_sep_field "$PORTLIST" 2)" \
                          -j DNAT --to-destination 10.207.87.2
    done
}

stop() {
    network_rip

    if ! is_running; then
        return
    fi

    if [ -x /sbin/start-stop-daemon ]; then
        /sbin/start-stop-daemon                                                \
            --stop --signal TERM --pidfile "$PIDFILE" --remove-pidfile --quiet \
            --retry 60  2>/dev/null || true
    else
        DAEMON_PID="$(cat "$PIDFILE")"

        while is_running; do
            kill -TERM "$DAEMON_PID" || true
            printf "Sent TERM, waiting for process to finish\n"
            for I in $(seq 30); do
                if is_running; then
                    sleep 2
                fi
            done
        done

        rm -rf "$PIDFILE"
    fi
}

onexit() {
    if [ -z "$SUCCESS" ]; then
        if [ "x$ACTION" = "xstart" -a -n "$GUILE_PID" ]; then
            stop
            kill $GUILE_PID >/dev/null || true
        fi
        if [ -z "$QUIET_EXIT" ]; then
            log_failure_msg
        fi
    else
        if [ -z "$QUIET_EXIT" ]; then
            log_success_msg
        fi
    fi
}

start() {
    LOG_DIR="$HOST_SYSTEM_ROOT"/var/log/guix-container

    KOSZKO_SIDELOAD_REAL="$HOST_SYSTEM_ROOT"/var/www/koszko.org/html
    HYDRILLA_HTTP_REAL="$HOST_SYSTEM_ROOT"/var/www/hydrilla.koszko.org/html
    HYDRILLAREPOS_HTTP_REAL="$HOST_SYSTEM_ROOT"/var/www/hydrillarepos.koszko.org/html
    HYDRILLABUGS_HTTP_REAL="$HOST_SYSTEM_ROOT"/var/www/hydrillabugs.koszko.org/html
    LOG_REAL="$LOG_DIR"/container
    ETC_LETSENCRYPT_REAL="$HOST_SYSTEM_ROOT"/etc/letsencrypt
    ETC_EXIM_REAL="$HOST_SYSTEM_ROOT"/etc/exim
    ETC_DOVECOT_REAL="$HOST_SYSTEM_ROOT"/etc/dovecot
    ETC_REAL="$HOST_SYSTEM_ROOT"/etc/guix-container
    VAR_SPOOL_EXIM_REAL="$HOST_SYSTEM_ROOT"/var/spool/exim
    VAR_HYDRILLA_REAL="$HOST_SYSTEM_ROOT"/var/lib/hydrilla
    VAR_GITOLITE_REAL="$HOST_SYSTEM_ROOT"/var/lib/gitolite3
    HOME_REAL="$HOST_SYSTEM_ROOT"/home

    KOSZKO_SIDELOAD_DIR_SHARE_OPT=--share="$KOSZKO_SIDELOAD_REAL"=/srv/http/koszko.org
    HYDRILLA_HTTP_DIR_SHARE_OPT=--share="$HYDRILLA_HTTP_REAL"=/srv/http/hydrilla.koszko.org
    HYDRILLAREPOS_HTTP_DIR_SHARE_OPT=--share="$HYDRILLAREPOS_HTTP_REAL"=/srv/http/hydrillarepos.koszko.org
    HYDRILLABUGS_HTTP_DIR_SHARE_OPT=--share="$HYDRILLABUGS_HTTP_REAL"=/srv/http/hydrillabugs.koszko.org
    LOG_DIR_SHARE_OPT=--share="$LOG_REAL"=/var/log
    ETC_LETSENCRYPT_DIR_SHARE_OPT=--share="$ETC_LETSENCRYPT_REAL"=/etc/letsencrypt
    ETC_EXIM_DIR_SHARE_OPT=--share="$ETC_EXIM_REAL"=/etc/exim
    ETC_DOVECOT_DIR_SHARE_OPT=--share="$ETC_DOVECOT_REAL"=/etc/dovecot
    ETC_DIR_SHARE_OPT=--share="$ETC_REAL"=/etc
    VAR_SPOOL_EXIM_DIR_SHARE_OPT=--share="$VAR_SPOOL_EXIM_REAL"=/var/spool/exim
    VAR_HYDRILLA_DIR_SHARE_OPT=--share="$VAR_HYDRILLA_REAL"=/var/lib/hydrilla
    VAR_GITOLITE_DIR_SHARE_OPT=--share="$VAR_GITOLITE_REAL"=/var/lib/gitolite3
    HOME_DIR_SHARE_OPT=--share="$HOME_REAL"=/home

    mkdir --mode=700 -p "$LOG_DIR"
    mkdir --mode=700 -p "$LOG_DIR"/container

    "$EXECUTABLE" "$KOSZKO_SIDELOAD_DIR_SHARE_OPT"                     \
                  "$HYDRILLA_HTTP_DIR_SHARE_OPT"                       \
                  "$HYDRILLAREPOS_HTTP_DIR_SHARE_OPT"                  \
                  "$HYDRILLABUGS_HTTP_DIR_SHARE_OPT"                   \
                  "$LOG_DIR_SHARE_OPT"                                 \
                  "$ETC_LETSENCRYPT_DIR_SHARE_OPT"                     \
                  "$ETC_EXIM_DIR_SHARE_OPT"                            \
                  "$ETC_DOVECOT_DIR_SHARE_OPT"                         \
                  "$ETC_DIR_SHARE_OPT"                                 \
                  "$VAR_SPOOL_EXIM_DIR_SHARE_OPT"                      \
                  "$VAR_HYDRILLA_DIR_SHARE_OPT"                        \
                  "$VAR_GITOLITE_DIR_SHARE_OPT"                        \
                  "$HOME_DIR_SHARE_OPT"                                \
                  >> "$LOG_DIR"/stdout.log 2>> "$LOG_DIR"/stderr.log &

    GUILE_PID=$!
    WAIT_TIME=0
    SHEPHERD_PID=

    while [ $WAIT_TIME -lt "$MAX_CONTAINER_SPINUP_WAIT" ]; do
        sleep 1
        WAIT_TIME=$((WAIT_TIME + 1))
        SHEPHERD_PID=$(ps -o pid= --ppid $GUILE_PID || true)
        if [ -n "$SHEPHERD_PID" ]; then
            mkdir -p "$(dirname "$PIDFILE")"
            printf '%s' $SHEPHERD_PID > "$PIDFILE"
            break
        fi
    done

    if [ -z "$SHEPHERD_PID" ]; then
        exit 1
    fi

    network_rip

    network_setup "$SHEPHERD_PID"
}

trap onexit EXIT

case "$ACTION" in
    start)
        if is_running; then
            log_daemon_msg "Guix container" "already running"
            log_warning_msg
            QUIET_EXIT=1
        else
            log_daemon_msg "Guix container" "starting"
            start
        fi
        ;;
    stop)
        log_daemon_msg "Guix container" "stopping"
        stop
        ;;
    restart)
        QUIET_EXIT=1
        "$0" stop "$@"
        "$0" start "$@"
        ;;
    reload|force-reload)
        QUIET_EXIT=1
        "$0" stop "$@"
        "$0" start "$@"
        ;;
    status)
        status_of_proc -p "$PIDFILE" "$EXECUTABLE" "Guix container"
        QUIET_EXIT=1
        ;;
    *)
        log_action_msg "Usage: $0 {start|stop|status|restart|reload|force-reload}"
        QUIET_EXIT=1
        exit 2
        ;;
esac

SUCCESS=1

exit 0