Offline bundle now carries every addon that works on a network without internet: LMS (Lyrion's own .deb, from the addon's LMS_DEB_URL) + Squeezelite, Asterisk Intercom (baresip), VNC, WireGuard and the Emergency Hotspot packages - each list in a variable next to the code that installs it (SQUEEZELITE_APT_PACKAGES, ASTERISK_INTERCOM_APT_PACKAGES, VNC_APT_PACKAGES, WIREGUARD_APT_PACKAGES, EMERGENCY_HOTSPOT_APT_PACKAGES), read by build-bundle.sh. Each install goes through run_with_offline_fallback like CUPS, so they install offline from the normal menus / Web UI. LMS installs as a bundle package when offline (lms_offline_install_wanted); its online repo/direct-download paths are unchanged. Tailscale/Netbird deliberately not bundled (vendor scripts, vendor servers). Bundle: 524 MB (+87 MB). Emergency hotspot: the boot check pinged only 8.8.8.8, so on a LAN with no internet the hotspot started on every boot. Now chosen at setup: "network" (default - router or internet must answer; a router that ignores ping still counts if it answered ARP), "internet" (old check), or "host" (a specific address). Three tries 10s apart. Existing scripts keep the old check until Reconfigure. New Core Settings -> Time Server (NTP) (menus/time_sync.sh): point systemd-timesyncd at LAN time server(s) via a timesyncd.conf.d drop-in, back to Ubuntu's defaults, or set the clock by hand (and the RTC) for a network with no time server. Complete Uninstall resets it. Also: lms_service_name used `systemctl list-unit-files | grep -q`, the same pipefail/SIGPIPE pattern as the earlier cups_is_installed bug; now `systemctl cat`. Tested in a network-less chroot of the 26.04.1 server filesystem: kiosk base, then action_wireguard_install and action_install_lms (real functions) and the Squeezelite/Intercom/VNC/hotspot package sets all installed from the bundle, apt left on normal sources after each. Hotspot script generated for all three modes (plus an invalid address, which falls back to "network") and its check run against simulated networks: LAN-only with router answering ping or only ARP -> no hotspot in "network" mode; nothing reachable -> hotspot. Time Server actions driven with timedatectl stubbed: drop-in contents, input validation, manual set-time sequence, reset removes the drop-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
441 lines
14 KiB
Bash
441 lines
14 KiB
Bash
#!/bin/bash
|
|
################################################################################
|
|
# menus/addon_remote_access.sh - "Remote Access" addon (VNC, WireGuard,
|
|
# Tailscale, Netbird).
|
|
#
|
|
# Third Addon migrated, and the biggest so far in scope (4 sub-areas).
|
|
# All four genuinely mutate real system state at fixed paths this project
|
|
# doesn't own the layout of (apt packages, /etc/wireguard, real VPN
|
|
# client CLIs) - same risk class as CUPS. Only $WIREGUARD_DIR and
|
|
# $SYSTEMD_DIR (lib/config.sh) are parameterized, since those are the
|
|
# only paths this file itself writes to; every command (apt, systemctl,
|
|
# wg, tailscale, netbird, x11vnc) gets full stubbing in every test.
|
|
#
|
|
# Tailscale and Netbird install themselves via `curl -fsSL <vendor
|
|
# url> | sh` - the vendors' own documented install method, preserved as-
|
|
# is rather than redesigned. This is NEVER allowed to run for real in
|
|
# any test: curl itself is stubbed, not just sudo, so there is no path
|
|
# by which a test could reach the network.
|
|
#
|
|
# None of x11vnc/wg/tailscale/netbird are installed in a fresh
|
|
# environment, so their "not installed" detection is real/unstubbed and
|
|
# safe to exercise end-to-end - only the "install" actions need stubs.
|
|
#
|
|
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
|
################################################################################
|
|
|
|
# Also read by iso/offline/build-bundle.sh to bundle them for offline
|
|
# installs. (Tailscale/Netbird aren't bundled: they install from their
|
|
# vendors' scripts and need their vendors' servers to work anyway.)
|
|
VNC_APT_PACKAGES=(x11vnc)
|
|
WIREGUARD_APT_PACKAGES=(wireguard wireguard-tools)
|
|
|
|
remote_access_status() {
|
|
echo "VNC: $(is_service_active x11vnc && echo "running" || echo "not installed")"
|
|
echo "WireGuard: $(wireguard_connected && echo "connected" || (command -v wg &>/dev/null && echo "installed, not connected" || echo "not installed"))"
|
|
echo "Tailscale: $(command -v tailscale &>/dev/null && echo "installed" || echo "not installed")"
|
|
echo "Netbird: $(command -v netbird &>/dev/null && echo "installed" || echo "not installed")"
|
|
}
|
|
|
|
remote_access_menu_builder() {
|
|
MENU_LABELS=("VNC Remote Desktop" "WireGuard VPN" "Tailscale VPN" "Netbird VPN")
|
|
MENU_HANDLERS=(vnc_menu wireguard_menu tailscale_menu netbird_menu)
|
|
}
|
|
|
|
remote_access_menu() {
|
|
run_menu "REMOTE ACCESS" remote_access_menu_builder remote_access_status
|
|
}
|
|
|
|
################################################################################
|
|
# VNC (x11vnc)
|
|
################################################################################
|
|
|
|
vnc_status() {
|
|
if is_service_active x11vnc; then
|
|
echo "VNC: running - connect to $(get_ip_address):5900"
|
|
else
|
|
echo "VNC: not installed"
|
|
fi
|
|
}
|
|
|
|
vnc_menu_builder() {
|
|
if is_service_active x11vnc; then
|
|
MENU_LABELS=("Reconfigure password" "Uninstall")
|
|
MENU_HANDLERS=(action_vnc_change_password action_vnc_uninstall)
|
|
else
|
|
MENU_LABELS=("Install x11vnc")
|
|
MENU_HANDLERS=(action_vnc_install)
|
|
fi
|
|
}
|
|
|
|
vnc_menu() {
|
|
run_menu "VNC REMOTE DESKTOP" vnc_menu_builder vnc_status
|
|
}
|
|
|
|
action_vnc_install() {
|
|
echo
|
|
ask_yes_no "Install x11vnc?" "n" || { echo "Cancelled"; return; }
|
|
|
|
if ! run_with_offline_fallback sudo apt install -y "${VNC_APT_PACKAGES[@]}"; then
|
|
log_error "x11vnc installation failed"
|
|
return 1
|
|
fi
|
|
|
|
local vnc_pass
|
|
read -r -s -p "VNC password: " vnc_pass
|
|
echo
|
|
if [[ -z "$vnc_pass" ]]; then
|
|
log_error "No password provided - cancelled"
|
|
return 1
|
|
fi
|
|
|
|
sudo -u "$KIOSK_USER" mkdir -p "$KIOSK_HOME/.vnc"
|
|
sudo -u "$KIOSK_USER" x11vnc -storepasswd "$vnc_pass" "$KIOSK_HOME/.vnc/passwd"
|
|
|
|
sudo tee "$SYSTEMD_DIR/x11vnc.service" > /dev/null <<EOF
|
|
[Unit]
|
|
Description=x11vnc Remote Desktop
|
|
After=lightdm.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${KIOSK_USER}
|
|
ExecStart=/usr/bin/x11vnc -display :0 -auth guess -rfbauth ${KIOSK_HOME}/.vnc/passwd -forever -loop -noxdamage -repeat -shared
|
|
Restart=always
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
if enable_and_start_units x11vnc; then
|
|
sudo ufw allow 5900/tcp comment 'VNC' 2>/dev/null || true
|
|
log_success "VNC installed - connect to $(get_ip_address):5900"
|
|
else
|
|
log_warning "x11vnc installed but systemctl enable/start failed - check 'systemctl status x11vnc'"
|
|
fi
|
|
}
|
|
|
|
action_vnc_change_password() {
|
|
echo
|
|
local vnc_pass
|
|
read -r -s -p "New VNC password: " vnc_pass
|
|
echo
|
|
if [[ -z "$vnc_pass" ]]; then
|
|
log_error "No password provided - cancelled"
|
|
return 1
|
|
fi
|
|
|
|
sudo -u "$KIOSK_USER" x11vnc -storepasswd "$vnc_pass" "$KIOSK_HOME/.vnc/passwd"
|
|
if sudo systemctl restart x11vnc 2>/dev/null; then
|
|
log_success "VNC password updated"
|
|
else
|
|
log_warning "Password file updated, but restarting x11vnc failed - check 'systemctl status x11vnc'"
|
|
fi
|
|
}
|
|
|
|
action_vnc_uninstall() {
|
|
echo
|
|
ask_yes_no "Remove VNC?" "n" || { echo "Cancelled"; return; }
|
|
vnc_do_uninstall
|
|
}
|
|
|
|
# Shared with Complete Uninstall - same reasoning as cups_do_uninstall.
|
|
vnc_do_uninstall() {
|
|
sudo systemctl stop x11vnc 2>/dev/null || true
|
|
sudo systemctl disable x11vnc 2>/dev/null || true
|
|
sudo rm -f "$SYSTEMD_DIR/x11vnc.service"
|
|
sudo apt remove -y x11vnc 2>/dev/null || true
|
|
log_success "VNC removed"
|
|
}
|
|
|
|
################################################################################
|
|
# WireGuard
|
|
################################################################################
|
|
|
|
wireguard_connected() {
|
|
command -v wg &>/dev/null && sudo wg show 2>/dev/null | grep -q interface
|
|
}
|
|
|
|
wireguard_status() {
|
|
if wireguard_connected; then
|
|
echo "WireGuard: connected"
|
|
sudo wg show 2>/dev/null | grep -E "interface:|endpoint:|allowed ips:" | sed 's/^/ /' || true
|
|
elif command -v wg &>/dev/null; then
|
|
echo "WireGuard: installed, not connected"
|
|
else
|
|
echo "WireGuard: not installed"
|
|
fi
|
|
}
|
|
|
|
wireguard_menu_builder() {
|
|
if wireguard_connected; then
|
|
MENU_LABELS=("Show full config" "Paste new config" "Uninstall")
|
|
MENU_HANDLERS=(action_wireguard_show_config action_wireguard_paste_config action_wireguard_uninstall)
|
|
elif command -v wg &>/dev/null; then
|
|
MENU_LABELS=("Paste config" "Uninstall")
|
|
MENU_HANDLERS=(action_wireguard_paste_config action_wireguard_uninstall)
|
|
else
|
|
MENU_LABELS=("Install WireGuard")
|
|
MENU_HANDLERS=(action_wireguard_install)
|
|
fi
|
|
}
|
|
|
|
wireguard_menu() {
|
|
run_menu "WIREGUARD VPN" wireguard_menu_builder wireguard_status
|
|
}
|
|
|
|
action_wireguard_install() {
|
|
echo
|
|
ask_yes_no "Install WireGuard?" "n" || { echo "Cancelled"; return; }
|
|
|
|
if ! run_with_offline_fallback sudo apt install -y "${WIREGUARD_APT_PACKAGES[@]}"; then
|
|
log_error "WireGuard installation failed"
|
|
return 1
|
|
fi
|
|
log_success "WireGuard installed"
|
|
|
|
echo
|
|
if ask_yes_no "Paste a config now?" "n"; then
|
|
action_wireguard_paste_config
|
|
fi
|
|
}
|
|
|
|
action_wireguard_show_config() {
|
|
echo
|
|
sudo wg show all
|
|
}
|
|
|
|
# Reads a WireGuard config from stdin until EOF (Ctrl+D on a real
|
|
# terminal) - same as the legacy addon. Writes to $WIREGUARD_DIR rather
|
|
# than a hardcoded /etc/wireguard, so tests can point it at scratch space
|
|
# and verify the written content without touching the real directory.
|
|
action_wireguard_paste_config() {
|
|
echo
|
|
echo "Paste your WireGuard config (Ctrl+D when done):"
|
|
local config
|
|
config=$(cat)
|
|
|
|
if [[ -z "$config" ]]; then
|
|
log_error "No config provided"
|
|
return 1
|
|
fi
|
|
|
|
local wg_name
|
|
wg_name=$(ask_text "Config name" "wg0")
|
|
|
|
sudo mkdir -p "$WIREGUARD_DIR"
|
|
echo "$config" | sudo tee "$WIREGUARD_DIR/${wg_name}.conf" > /dev/null
|
|
sudo chmod 600 "$WIREGUARD_DIR/${wg_name}.conf"
|
|
|
|
if enable_and_start_units "wg-quick@${wg_name}"; then
|
|
log_success "WireGuard configured: $wg_name"
|
|
else
|
|
log_warning "Config written, but systemctl enable/start failed - check 'systemctl status wg-quick@${wg_name}'"
|
|
fi
|
|
}
|
|
|
|
action_wireguard_uninstall() {
|
|
echo
|
|
ask_yes_no "Remove WireGuard?" "n" || { echo "Cancelled"; return; }
|
|
wireguard_do_uninstall
|
|
}
|
|
|
|
# Shared with Complete Uninstall - same reasoning as cups_do_uninstall.
|
|
wireguard_do_uninstall() {
|
|
sudo systemctl stop 'wg-quick@*' 2>/dev/null || true
|
|
sudo systemctl disable 'wg-quick@*' 2>/dev/null || true
|
|
sudo apt remove -y wireguard wireguard-tools 2>/dev/null || true
|
|
log_success "WireGuard removed"
|
|
}
|
|
|
|
################################################################################
|
|
# Tailscale
|
|
################################################################################
|
|
|
|
tailscale_backend_state() {
|
|
tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"' 2>/dev/null || echo "unknown"
|
|
}
|
|
|
|
tailscale_status() {
|
|
if ! command -v tailscale &>/dev/null; then
|
|
echo "Tailscale: not installed"
|
|
return
|
|
fi
|
|
|
|
if [[ "$(tailscale_backend_state)" == "Running" ]]; then
|
|
echo "Tailscale: connected"
|
|
echo " Hostname: $(tailscale status --json 2>/dev/null | jq -r '.Self.HostName // "unknown"')"
|
|
echo " IP: $(tailscale ip -4 2>/dev/null)"
|
|
else
|
|
echo "Tailscale: installed, not connected"
|
|
fi
|
|
}
|
|
|
|
tailscale_menu_builder() {
|
|
if command -v tailscale &>/dev/null; then
|
|
MENU_LABELS=("Connect (interactive)" "Connect with auth key" "Show status" "Uninstall")
|
|
MENU_HANDLERS=(action_tailscale_connect_interactive action_tailscale_connect_authkey action_tailscale_show_status action_tailscale_uninstall)
|
|
else
|
|
MENU_LABELS=("Install Tailscale")
|
|
MENU_HANDLERS=(action_tailscale_install)
|
|
fi
|
|
}
|
|
|
|
tailscale_menu() {
|
|
run_menu "TAILSCALE VPN" tailscale_menu_builder tailscale_status
|
|
}
|
|
|
|
action_tailscale_install() {
|
|
echo
|
|
ask_yes_no "Install Tailscale?" "n" || { echo "Cancelled"; return; }
|
|
|
|
if ! curl -fsSL https://tailscale.com/install.sh | sh; then
|
|
log_error "Tailscale installation failed"
|
|
return 1
|
|
fi
|
|
log_success "Tailscale installed"
|
|
|
|
echo
|
|
echo "Options:"
|
|
echo " 1. Connect now (interactive)"
|
|
echo " 2. Connect with auth key"
|
|
echo " 3. Connect later"
|
|
local choice
|
|
choice=$(ask_integer "Choose" "3" 1 3)
|
|
case "$choice" in
|
|
1) action_tailscale_connect_interactive ;;
|
|
2) action_tailscale_connect_authkey ;;
|
|
esac
|
|
}
|
|
|
|
action_tailscale_connect_interactive() {
|
|
echo
|
|
if sudo tailscale up; then
|
|
log_success "Tailscale connected"
|
|
else
|
|
log_error "Tailscale connection failed"
|
|
fi
|
|
}
|
|
|
|
action_tailscale_connect_authkey() {
|
|
echo
|
|
echo "Get an auth key from: https://login.tailscale.com/admin/settings/keys"
|
|
local authkey
|
|
read -r -p "Enter auth key: " authkey
|
|
if [[ -z "$authkey" ]]; then
|
|
echo "Cancelled"
|
|
return
|
|
fi
|
|
|
|
if sudo tailscale up --authkey="$authkey"; then
|
|
log_success "Tailscale connected"
|
|
else
|
|
log_error "Tailscale connection failed"
|
|
fi
|
|
}
|
|
|
|
action_tailscale_show_status() {
|
|
echo
|
|
tailscale status
|
|
}
|
|
|
|
action_tailscale_uninstall() {
|
|
echo
|
|
ask_yes_no "Remove Tailscale?" "n" || { echo "Cancelled"; return; }
|
|
tailscale_do_uninstall
|
|
}
|
|
|
|
# Shared with Complete Uninstall - same reasoning as cups_do_uninstall.
|
|
tailscale_do_uninstall() {
|
|
sudo tailscale down 2>/dev/null || true
|
|
sudo apt remove -y tailscale 2>/dev/null || true
|
|
log_success "Tailscale removed"
|
|
}
|
|
|
|
################################################################################
|
|
# Netbird
|
|
################################################################################
|
|
|
|
netbird_connected() {
|
|
[[ "$(netbird status 2>/dev/null | grep "Status:" | awk '{print $2}')" == "Connected" ]]
|
|
}
|
|
|
|
netbird_status() {
|
|
if ! command -v netbird &>/dev/null; then
|
|
echo "Netbird: not installed"
|
|
return
|
|
fi
|
|
|
|
if netbird_connected; then
|
|
echo "Netbird: connected"
|
|
netbird status 2>/dev/null | grep -E "NetBird IP:|Public key:" | sed 's/^/ /' || true
|
|
else
|
|
echo "Netbird: installed, not connected"
|
|
fi
|
|
}
|
|
|
|
netbird_menu_builder() {
|
|
if command -v netbird &>/dev/null; then
|
|
MENU_LABELS=("Connect with setup key" "Show status" "Uninstall")
|
|
MENU_HANDLERS=(action_netbird_connect action_netbird_show_status action_netbird_uninstall)
|
|
else
|
|
MENU_LABELS=("Install Netbird")
|
|
MENU_HANDLERS=(action_netbird_install)
|
|
fi
|
|
}
|
|
|
|
netbird_menu() {
|
|
run_menu "NETBIRD VPN" netbird_menu_builder netbird_status
|
|
}
|
|
|
|
action_netbird_install() {
|
|
echo
|
|
ask_yes_no "Install Netbird?" "n" || { echo "Cancelled"; return; }
|
|
|
|
if ! curl -fsSL https://pkgs.netbird.io/install.sh | sh; then
|
|
log_error "Netbird installation failed"
|
|
return 1
|
|
fi
|
|
log_success "Netbird installed"
|
|
|
|
echo
|
|
if ask_yes_no "Connect with a setup key now?" "n"; then
|
|
action_netbird_connect
|
|
fi
|
|
}
|
|
|
|
action_netbird_connect() {
|
|
echo
|
|
echo "Get a setup key from the Netbird dashboard"
|
|
local setup_key
|
|
read -r -p "Enter setup key: " setup_key
|
|
if [[ -z "$setup_key" ]]; then
|
|
echo "Cancelled"
|
|
return
|
|
fi
|
|
|
|
if sudo netbird up --setup-key "$setup_key"; then
|
|
log_success "Netbird connected"
|
|
else
|
|
log_error "Netbird connection failed"
|
|
fi
|
|
}
|
|
|
|
action_netbird_show_status() {
|
|
echo
|
|
netbird status
|
|
}
|
|
|
|
action_netbird_uninstall() {
|
|
echo
|
|
ask_yes_no "Remove Netbird?" "n" || { echo "Cancelled"; return; }
|
|
netbird_do_uninstall
|
|
}
|
|
|
|
# Shared with Complete Uninstall - same reasoning as cups_do_uninstall.
|
|
netbird_do_uninstall() {
|
|
sudo netbird down 2>/dev/null || true
|
|
sudo apt remove -y netbird 2>/dev/null || true
|
|
log_success "Netbird removed"
|
|
}
|