#!/bin/sh # SPDX-License-Identifier: CC0-1.0 # Copyright (C) 2022-2023 Wojtek Kosior # # 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