1679 lines
53 KiB
Bash
1679 lines
53 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
############################################################
|
|
# Filename : gns3-remote-install.sh #
|
|
# Author : Shane Sexton #
|
|
# Created : April 18 2026 #
|
|
# Last edit : April 22 2026 #
|
|
# Purpose : Enhancement to remote-install.sh #
|
|
# Reference : GNS3:gns3-server - remote-install.sh #
|
|
# Depends : Ubuntu Server 22.04, 24.04, 26.04 LTS #
|
|
# To do : Consistency, readability, & structure need #
|
|
# improvements at all magnification levels #
|
|
############################################################
|
|
|
|
############################################################
|
|
# PLEASE SEE README FOR FEATURE COMPARISON #
|
|
############################################################
|
|
|
|
########################################################################
|
|
# Usage: sudo ./gns3-remote-install-redux.sh [OPTIONS] #
|
|
# #
|
|
# --with-openvpn Install & configure OpenVPN #
|
|
# --with-wireguard Install & configure WireGuard #
|
|
# --with-welcome Install GNS3-VM welcome.py console UI #
|
|
# --without-kvm Disable KVM (hurts Qemu performance) #
|
|
# --without-docker Skip Docker installation #
|
|
# --without-firewall Skip automatic UFW rule configuration #
|
|
# --without-system-upgrade Skip apt upgrade #
|
|
# --unsafe-configs Serve VPN configs unencrypted (less safe) #
|
|
# --unstable Use GNS3 unstable PPA #
|
|
# --custom-repository REPO Use custom GNS3 PPA name #
|
|
# -h, --help Show this help #
|
|
########################################################################
|
|
|
|
########################################################################
|
|
# Conventions #
|
|
# #
|
|
# Variables: #
|
|
# Globals: ROARING_SNEK_CASE #
|
|
# Local and iters: _snake_case (prefix) #
|
|
# Var expansion: "${VAR}" (not "$VAR") #
|
|
# #
|
|
# Functions: #
|
|
# Use lowercase_snake_case names #
|
|
# Start with `local` declaration block for all locals #
|
|
# Return 0 - success #
|
|
# 1 - error; carrying on #
|
|
# log_fatal - panic #
|
|
# #
|
|
# Logic: #
|
|
# Avoid shorthand [[ condition ]] &&... use if/else/fi #
|
|
# #
|
|
# File writes: #
|
|
# ensure_config() for anything that needs specific perms #
|
|
# ensure_directory() before writing to a new path #
|
|
# require_file() after any action that should produce a file #
|
|
# #
|
|
# Logging: #
|
|
# log_info(): what we're about to do (or did, non-critical) #
|
|
# log_ok(): confirmation that something worked #
|
|
# log_warn(): non-fatal issue, not actionable #
|
|
# log_warn_sticky(): non-fatal but user informed at summary time #
|
|
# log_error(): something failed but we're continuing #
|
|
# log_fatal(): cannot proceed; exits #
|
|
# #
|
|
# Services: #
|
|
# enable_and_start() — not systemctl enable/start/restart #
|
|
# #
|
|
# External resources: #
|
|
# fetch_resource() with ordered fallbacks (local→canonical→mirror) #
|
|
# #
|
|
# Idempotency: #
|
|
# Every function should be safe to re-run #
|
|
# Key writes: [[ -f key ]] || generate (never overwrite keys) #
|
|
# Config writes: always overwrite existing configs #
|
|
# Groups/users: ensure_group / id checks #
|
|
########################################################################
|
|
|
|
### Constants ################################################################
|
|
# This repo
|
|
readonly REPO_BASE_URL="https://raw.githubusercontent.com/lark-cx/gns3-server-deploy/refs/heads/main/"
|
|
readonly REPO_LANDING_HTML="template.html"
|
|
readonly REPO_DOCKER_GPG="docker-gpg"
|
|
# Docker
|
|
readonly DOCKER_BASE_URL="https://download.docker.com/linux/ubuntu"
|
|
readonly DOCKER_KEYRING="/etc/apt/keyrings/docker.asc"
|
|
# GNS3 configurations
|
|
readonly GNS3_USER="gns3"
|
|
readonly GNS3_HOME="/opt/gns3"
|
|
readonly GNS3_CONF_DIR="/etc/gns3"
|
|
readonly GNS3_PORT=3080
|
|
readonly GNS3_SERVICE_FILE="/lib/systemd/system/gns3.service"
|
|
readonly GNS3_VENV="/usr/share/gns3/gns3-server"
|
|
# Config deployment server
|
|
readonly CONFIG_SERVE_PORT=8003
|
|
readonly CONFIG_SERVE_DIR="/var/lib/gns3-config-serve"
|
|
readonly CONFIG_SERVE_HOURS=2
|
|
readonly DEPLOY_MARKER="# deployed by gns3-remote-install-redux"
|
|
readonly DEPLOY_DIR="/root/.gns3-deploy"
|
|
|
|
### VPN Constants #############################################################
|
|
readonly OVPN_CONF_DIR="/etc/openvpn"
|
|
readonly OVPN_PORT=1194
|
|
readonly WG_CONF_DIR="/etc/wireguard"
|
|
readonly WG_PORT=51820
|
|
|
|
### Colors ####################################################################
|
|
readonly RED='\033[0;31m'
|
|
readonly GREEN='\033[0;32m'
|
|
readonly YELLOW='\033[1;33m'
|
|
readonly CYAN='\033[0;36m'
|
|
readonly BOLD='\033[1m'
|
|
readonly NC='\033[0m'
|
|
|
|
### Logging ##################################################################
|
|
log_info() { printf "${CYAN}[INFO]${NC} %s\n" "$1" >&2; }
|
|
log_ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$1" >&2; }
|
|
log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1" >&2; }
|
|
log_error() { printf "${RED}[FAIL]${NC} %s\n" "$1" >&2; }
|
|
log_fatal() {
|
|
printf "${RED}[FATAL]${NC} %s\n" "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
|
|
on_error() {
|
|
local _line_no="$1"
|
|
local _cmd="$2"
|
|
local _exit_code="$3"
|
|
|
|
log_error "Failed at line ${_line_no}: ${_cmd} (exit ${_exit_code})"
|
|
|
|
# Only try to revive GNS3 if systemd knows about it
|
|
if systemctl list-unit-files gns3.service >/dev/null 2>&1; then
|
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
|
sleep 2
|
|
systemctl restart gns3 >/dev/null 2>&1 || systemctl start gns3 >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
exit "${_exit_code}"
|
|
}
|
|
|
|
# Testability wrapper — EUID is readonly in bash, so tests override this function
|
|
is_root() { [[ ${EUID} -eq 0 ]]; }
|
|
|
|
# Sticky warnings — collected throughout execution, displayed before summary
|
|
WARNINGS=()
|
|
log_warn_sticky() {
|
|
log_warn "$1"
|
|
WARNINGS+=("$1")
|
|
}
|
|
|
|
### Mutable arrays (built up by option flags) ##############################─
|
|
REQUIRED_CMDS=(apt apt-add-repository dpkg chown chmod useradd usermod lsmod systemctl ss openssl)
|
|
REQUIRED_PORTS=("${GNS3_PORT}")
|
|
REQUIRED_GROUPS=(kvm ubridge)
|
|
REQUIRED_MODS=(kvm)
|
|
|
|
REQUIRED_PKGS=(
|
|
software-properties-common
|
|
ca-certificates
|
|
curl
|
|
gns3-server
|
|
dynamips
|
|
vpcs
|
|
python3
|
|
python3-pip
|
|
python3-setuptools
|
|
qemu-system-x86
|
|
qemu-utils
|
|
)
|
|
|
|
readonly PKGS_OPENVPN=(openvpn dnsutils)
|
|
readonly PKGS_WIREGUARD=(wireguard-tools)
|
|
readonly PKGS_DOCKER=(docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin)
|
|
readonly PKGS_WELCOME=(net-tools dialog python3-dialog)
|
|
|
|
### Option defaults ##########################################################
|
|
|
|
WITH_OPENVPN=0
|
|
WITH_WIREGUARD=0
|
|
WITH_DOCKER=1
|
|
WITH_WELCOME=0
|
|
DISABLE_KVM=0
|
|
DISABLE_FIREWALL=0
|
|
NO_SYSTEM_UPGRADE=0
|
|
UNSAFE_CONFIGS=0
|
|
USE_LEGACY_RSA=0
|
|
REPOSITORY="ppa"
|
|
|
|
### Help ####################################################################─
|
|
|
|
show_help() {
|
|
cat >&2 <<_EOF_HELP
|
|
${BOLD}gns3-remote-install-redux.sh${NC} — GNS3 remote server installer
|
|
|
|
${BOLD}Usage:${NC} sudo $0 [OPTIONS]
|
|
|
|
${BOLD}Options:${NC}
|
|
--with-openvpn Install and configure OpenVPN
|
|
--with-wireguard Install and configure WireGuard
|
|
--with-welcome Install GNS3-VM welcome.py console UI
|
|
--without-kvm Disable KVM hardware acceleration
|
|
--without-docker Skip Docker CE installation
|
|
--without-firewall Skip automatic UFW rule configuration
|
|
--without-system-upgrade Skip apt upgrade step
|
|
--unsafe-configs Serve VPN configs unencrypted
|
|
--unstable Use the GNS3 unstable PPA
|
|
--custom-repository REPO Use a custom GNS3 PPA name
|
|
-h, --help Show this help
|
|
_EOF_HELP
|
|
}
|
|
|
|
### Argument parsing ##########################################################
|
|
|
|
TEMP=$(getopt -o h --long with-openvpn,with-wireguard,with-welcome,without-kvm,without-docker,without-firewall,without-system-upgrade,unsafe-configs,legacy-rsa,unstable,custom-repository:,help -n "$0" -- "$@") || {
|
|
show_help
|
|
exit 1
|
|
}
|
|
eval set -- "${TEMP}"
|
|
|
|
while true; do
|
|
case "$1" in
|
|
--with-openvpn)
|
|
WITH_OPENVPN=1
|
|
shift
|
|
;;
|
|
--with-wireguard)
|
|
WITH_WIREGUARD=1
|
|
shift
|
|
;;
|
|
--with-welcome)
|
|
WITH_WELCOME=1
|
|
shift
|
|
;;
|
|
--without-kvm)
|
|
DISABLE_KVM=1
|
|
shift
|
|
;;
|
|
--without-docker)
|
|
WITH_DOCKER=0
|
|
shift
|
|
;;
|
|
--without-firewall)
|
|
DISABLE_FIREWALL=1
|
|
shift
|
|
;;
|
|
--without-system-upgrade)
|
|
NO_SYSTEM_UPGRADE=1
|
|
shift
|
|
;;
|
|
--unsafe-configs)
|
|
UNSAFE_CONFIGS=1
|
|
shift
|
|
;;
|
|
--legacy-rsa)
|
|
USE_LEGACY_RSA=1
|
|
shift
|
|
;;
|
|
--unstable)
|
|
REPOSITORY="unstable"
|
|
shift
|
|
;;
|
|
--custom-repository)
|
|
REPOSITORY="$2"
|
|
shift 2
|
|
;;
|
|
-h | --help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
*) log_fatal "Unknown option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
### Roll up arrays based on flags ##########################################─
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 ]]; then
|
|
REQUIRED_PORTS+=("${OVPN_PORT}")
|
|
REQUIRED_PKGS+=("${PKGS_OPENVPN[@]}")
|
|
fi
|
|
|
|
if [[ ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
REQUIRED_PORTS+=("${WG_PORT}")
|
|
REQUIRED_MODS+=(wireguard)
|
|
REQUIRED_PKGS+=("${PKGS_WIREGUARD[@]}")
|
|
fi
|
|
|
|
# Config server port always needed (landing page is default-on)
|
|
REQUIRED_PORTS+=("${CONFIG_SERVE_PORT}")
|
|
|
|
if [[ ${WITH_DOCKER} -eq 1 ]]; then
|
|
REQUIRED_PKGS+=("${PKGS_DOCKER[@]}")
|
|
REQUIRED_GROUPS+=(docker)
|
|
fi
|
|
|
|
if [[ ${WITH_WELCOME} -eq 1 ]]; then
|
|
REQUIRED_PKGS+=("${PKGS_WELCOME[@]}")
|
|
fi
|
|
|
|
### Preflight checks ########################################################
|
|
|
|
preflight_checks() {
|
|
local _has_errors=0
|
|
|
|
if [[ ${OSTYPE} != linux-gnu* ]]; then
|
|
log_fatal "This script requires Linux (detected: ${OSTYPE})."
|
|
fi
|
|
|
|
if [[ -f /etc/os-release ]]; then
|
|
source /etc/os-release
|
|
else
|
|
log_fatal "/etc/os-release not found. Is this Ubuntu?"
|
|
fi
|
|
|
|
if [[ ${ID:-} != "ubuntu" ]]; then
|
|
log_fatal "This script requires Ubuntu (detected: ${ID:-unknown})."
|
|
fi
|
|
|
|
local -a _missing_cmds=()
|
|
for _cmd in "${REQUIRED_CMDS[@]}"; do
|
|
command -v "${_cmd}" &>/dev/null || _missing_cmds+=("${_cmd}")
|
|
done
|
|
if [[ ${#_missing_cmds[@]} -gt 0 ]]; then
|
|
log_error "Missing commands: ${_missing_cmds[*]}"
|
|
_has_errors=1
|
|
fi
|
|
|
|
# Detect reconfigure: our config marker + gns3 service running
|
|
if [[ -f "${GNS3_CONF_DIR}/gns3_server.conf" ]] &&
|
|
grep -q "${DEPLOY_MARKER}" "${GNS3_CONF_DIR}/gns3_server.conf" 2>/dev/null &&
|
|
systemctl is-active --quiet gns3 2>/dev/null; then
|
|
log_info "Existing installation detected — running in reconfigure mode"
|
|
systemctl stop gns3
|
|
systemctl stop gns3-config-serve.service 2>/dev/null || true
|
|
fi
|
|
|
|
local -a _busy_ports=()
|
|
for _port in "${REQUIRED_PORTS[@]}"; do
|
|
if ss -tlnH | grep -q ":${_port} "; then
|
|
_busy_ports+=("${_port}")
|
|
fi
|
|
done
|
|
if [[ ${#_busy_ports[@]} -gt 0 ]]; then
|
|
log_error "Port(s) already in use: ${_busy_ports[*]}"
|
|
_has_errors=1
|
|
fi
|
|
|
|
# Ensure time synchronization is active for VPN certificate validity
|
|
if command -v timedatectl &>/dev/null; then
|
|
timedatectl set-ntp true || log_warn_sticky "Couldn't start NTP. Ensure accurate time for certs."
|
|
fi
|
|
|
|
# Load missing kernel modules automatically unless --without-kvm
|
|
local -a _missing_mods=()
|
|
for _mod in "${REQUIRED_MODS[@]}"; do
|
|
if ! lsmod | grep -wq "${_mod}" 2>/dev/null; then
|
|
if [[ ${_mod} == "kvm" && ${DISABLE_KVM} -eq 1 ]]; then
|
|
continue
|
|
fi
|
|
log_info "Loading kernel module: ${_mod}"
|
|
if modprobe "${_mod}" 2>/dev/null; then
|
|
log_ok "Loaded ${_mod}"
|
|
if ! grep -qx "${_mod}" /etc/modules-load.d/gns3.conf 2>/dev/null; then
|
|
echo "${_mod}" >>/etc/modules-load.d/gns3.conf
|
|
fi
|
|
else
|
|
_missing_mods+=("${_mod}")
|
|
fi
|
|
fi
|
|
done
|
|
if [[ ${#_missing_mods[@]} -gt 0 ]]; then
|
|
log_warn_sticky "Kernel module(s) could not be loaded: ${_missing_mods[*]}"
|
|
log_warn_sticky " Manual fix: modprobe ${_missing_mods[*]}"
|
|
fi
|
|
|
|
if [[ ${DISABLE_KVM} -eq 0 ]] && [[ $(grep -Ec '(vmx|svm)' /proc/cpuinfo) -eq 0 ]]; then
|
|
log_warn_sticky "CPU virtualization extensions not detected. KVM will not function."
|
|
log_warn_sticky " If running in a VM without nested virt, pass --without-kvm"
|
|
fi
|
|
|
|
if [[ ${REPOSITORY} == "ppa-v3" ]]; then
|
|
if ! python3 -c 'import sys; assert sys.version_info >= (3,9)' &>/dev/null; then
|
|
log_error "GNS3 v3+ requires Python >= 3.9"
|
|
_has_errors=1
|
|
fi
|
|
fi
|
|
|
|
if [[ ${_has_errors} -eq 1 ]]; then
|
|
log_fatal "Preflight failed. Fix the above and re-run."
|
|
fi
|
|
|
|
log_ok "Preflight checks passed"
|
|
}
|
|
|
|
### Helpers ##################################################################
|
|
|
|
# Retry apt operation 3 times, quietly.
|
|
# Return final lines from apt on failure.
|
|
apt_retry() {
|
|
local _attempts=3 _i _apt_log
|
|
_apt_log=$(mktemp)
|
|
for ((_i = 1; _i <= _attempts; _i++)); do
|
|
if apt-get "$@" -qq >"${_apt_log}" 2>&1; then
|
|
rm -f "${_apt_log}"
|
|
return 0
|
|
fi
|
|
log_warn "apt failed (attempt ${_i}/${_attempts}), retrying in 5s..."
|
|
tail -5 "${_apt_log}" >&2
|
|
sleep 5
|
|
done
|
|
log_error "apt output:"
|
|
tail -10 "${_apt_log}" >&2
|
|
rm -f "${_apt_log}"
|
|
log_fatal "apt failed after ${_attempts} attempts: apt-get $*"
|
|
}
|
|
|
|
# Groups exists or is created
|
|
ensure_group() {
|
|
getent group "$1" &>/dev/null || groupadd --system "$1"
|
|
}
|
|
|
|
# Who invoked the script with sudo?
|
|
detect_invoking_user() {
|
|
if [[ -n ${SUDO_USER:-} && ${SUDO_USER} != "root" ]]; then
|
|
echo "${SUDO_USER}"
|
|
fi
|
|
}
|
|
|
|
# Get public IP with fallback chain (dig → curl → UNKNOWN)
|
|
get_public_ip() {
|
|
local _ip
|
|
_ip=$(dig @ns1.google.com -t txt o-o.myaddr.l.google.com +short -4 2>/dev/null | sed 's/"//g')
|
|
if [[ -n ${_ip} ]]; then
|
|
echo "${_ip}"
|
|
return
|
|
fi
|
|
_ip=$(curl -sf --max-time 5 https://icanhazip.com 2>/dev/null)
|
|
if [[ -n ${_ip} ]]; then
|
|
echo "${_ip}"
|
|
return
|
|
fi
|
|
_ip=$(curl -sf --max-time 5 https://ifconfig.me 2>/dev/null)
|
|
if [[ -n ${_ip} ]]; then
|
|
echo "${_ip}"
|
|
return
|
|
fi
|
|
echo "UNKNOWN"
|
|
}
|
|
|
|
# Get LAN IP — primary interface address
|
|
get_lan_ip() {
|
|
hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1"
|
|
}
|
|
|
|
# Generate a readable one-time passphrase: XXXX-XXXX-####
|
|
generate_passphrase() {
|
|
local _hex
|
|
_hex=$(openssl rand -hex 4)
|
|
printf "%s-%s" "${_hex:0:4}" "${_hex:4:4}" | tr 'a-f' 'A-F'
|
|
}
|
|
|
|
# Encrypt a file with a passphrase, output .enc alongside original
|
|
encrypted_copy() {
|
|
local _src="$1" _dst="$2" _pass="$3"
|
|
openssl aes-256-cbc -pbkdf2 -iter 10000 -pass "pass:${_pass}" -a -in "${_src}" -out "${_dst}"
|
|
}
|
|
|
|
# Enable IPv4 forwarding, if needed
|
|
enable_ip_forwarding() {
|
|
log_info "Enabling IPv4 forwarding..."
|
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
|
if ! grep -q '^net.ipv4.ip_forward=1' /etc/sysctl.conf 2>/dev/null; then
|
|
echo "net.ipv4.ip_forward=1" >>/etc/sysctl.conf
|
|
fi
|
|
log_ok "IPv4 forwarding enabled (persistent)"
|
|
}
|
|
|
|
# Apply baseline sysctl hardening — safe, standard CIS recommendations
|
|
apply_sysctl_hardening() {
|
|
log_info "Applying sysctl hardening..."
|
|
|
|
local -A _sysctls=(
|
|
["net.ipv4.conf.all.rp_filter"]="2"
|
|
["net.ipv4.conf.default.rp_filter"]="2"
|
|
["net.ipv4.tcp_syncookies"]="1"
|
|
["net.ipv4.conf.all.accept_redirects"]="0"
|
|
["net.ipv4.conf.default.accept_redirects"]="0"
|
|
["net.ipv4.conf.all.send_redirects"]="0"
|
|
["net.ipv4.conf.default.send_redirects"]="0"
|
|
["net.ipv4.conf.all.accept_source_route"]="0"
|
|
["net.ipv4.conf.default.accept_source_route"]="0"
|
|
["net.ipv6.conf.all.accept_redirects"]="0"
|
|
["net.ipv6.conf.default.accept_redirects"]="0"
|
|
)
|
|
|
|
local _sysctl_file="/etc/sysctl.d/90-gns3-hardening.conf"
|
|
: >"${_sysctl_file}"
|
|
|
|
for _key in "${!_sysctls[@]}"; do
|
|
local _val="${_sysctls[${_key}]}"
|
|
echo "${_key}=${_val}" >>"${_sysctl_file}"
|
|
sysctl -w "${_key}=${_val}" >/dev/null 2>&1
|
|
done
|
|
|
|
log_ok "Sysctl hardening applied (${_sysctl_file})"
|
|
}
|
|
|
|
# Safe and quiet service (re)start - with mulligan
|
|
enable_and_start() {
|
|
local _svc="$1"
|
|
local _attempt _max=3
|
|
|
|
log_info "Enabling and starting service: ${_svc}"
|
|
systemctl daemon-reload
|
|
systemctl enable "${_svc}" >/dev/null 2>&1 || true
|
|
|
|
for ((_attempt=1; _attempt<=_max; _attempt++)); do
|
|
if systemctl restart "${_svc}" >/dev/null 2>&1 || systemctl start "${_svc}" >/dev/null 2>&1; then
|
|
log_ok "Service running: ${_svc}"
|
|
return 0
|
|
fi
|
|
|
|
if [[ ${_attempt} -lt ${_max} ]]; then
|
|
log_warn "Service start failed for ${_svc} (attempt ${_attempt}/${_max}); retrying in 1s..."
|
|
sleep 1
|
|
systemctl daemon-reload
|
|
fi
|
|
done
|
|
|
|
log_warn_sticky "Service failed after ${_max} attempts: ${_svc}"
|
|
log_warn_sticky " Check: journalctl -u ${_svc} --no-pager -n 30"
|
|
return 1
|
|
}
|
|
|
|
# Verify a file exists and is non-empty.
|
|
# Returns 1 and logs sticky warning on failure.
|
|
require_file() {
|
|
local _path="$1" _desc="${2:-file}"
|
|
if [[ ! -s ${_path} ]]; then
|
|
log_warn_sticky "${_desc} missing or empty: ${_path}"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Idempotent directory creation with ownership and mode. Creates if missing,
|
|
# applies chown/chmod either way (in case perms drifted).
|
|
ensure_directory() {
|
|
local _path="$1" _owner="${2:-root}" _mode="${3:-}"
|
|
if [[ -d ${_path} ]]; then
|
|
log_info "Directory exists: ${_path}"
|
|
else
|
|
mkdir -p "${_path}"
|
|
log_info "Created directory: ${_path}"
|
|
fi
|
|
chown "${_owner}:${_owner}" "${_path}"
|
|
[[ -n ${_mode} ]] && chmod "${_mode}" "${_path}"
|
|
}
|
|
|
|
# Atomically write a config file with the right perms from start.
|
|
# Content from stdin -> tempfile in target dir -> perms -> rename
|
|
#
|
|
# Usage:
|
|
# ensure_config /etc/wireguard/wg0.conf root 600 <<EOF
|
|
# [Interface]
|
|
# ...
|
|
# EOF
|
|
#
|
|
# openssl ecparam -name secp384r1 -genkey -noout | \
|
|
# ensure_config /etc/openvpn/key.pem root 600
|
|
ensure_config() {
|
|
local _path="$1" _owner="${2:-root}" _mode="${3:-644}"
|
|
local _dir _tmp
|
|
_dir="$(dirname "${_path}")"
|
|
|
|
# Make sure destination directory exists (don't override caller's perms on it)
|
|
[[ -d ${_dir} ]] || mkdir -p "${_dir}"
|
|
|
|
# Temp file in same directory so mv is atomic (same filesystem)
|
|
_tmp=$(mktemp "${_path}.XXXXXX")
|
|
|
|
# Restrictive umask during write — file never exists with open perms
|
|
(umask 077 && cat >"${_tmp}")
|
|
|
|
chown "${_owner}:${_owner}" "${_tmp}"
|
|
chmod "${_mode}" "${_tmp}"
|
|
mv "${_tmp}" "${_path}"
|
|
|
|
# Self-verify: confirm the file actually made it and is non-empty
|
|
require_file "${_path}" "config" || return 1
|
|
log_info "Wrote config: ${_path} (mode ${_mode}, owner ${_owner})"
|
|
}
|
|
|
|
# Try sequential sources until one succeeds. First arg is
|
|
# destination, remaining args are sources, tried in order.
|
|
# Local paths detected by absence of http(s):// prefix.
|
|
# Returns 1 if all sources fail.
|
|
#
|
|
# Usage:
|
|
# fetch_resource /etc/apt/keyrings/docker.asc \
|
|
# "${_script_dir}/docker-gpg" \
|
|
# "https://download.docker.com/linux/ubuntu/gpg" \
|
|
# "${REPO_BASE_URL}docker-gpg"
|
|
fetch_resource() {
|
|
local _dst="$1"
|
|
shift
|
|
local _src
|
|
for _src in "$@"; do
|
|
if [[ ${_src} =~ ^https?:// ]]; then
|
|
if curl -fsSL --max-time 10 "${_src}" -o "${_dst}" 2>/dev/null; then
|
|
log_info "Fetched from remote: ${_src}"
|
|
return 0
|
|
fi
|
|
log_warn "Remote fetch failed: ${_src}"
|
|
elif [[ -f ${_src} ]]; then
|
|
if cp "${_src}" "${_dst}" 2>/dev/null; then
|
|
log_info "Copied from local: ${_src}"
|
|
return 0
|
|
fi
|
|
log_warn "Local copy failed: ${_src}"
|
|
fi
|
|
done
|
|
log_warn_sticky "All sources failed for: ${_dst}"
|
|
return 1
|
|
}
|
|
|
|
### Ephemeral config file server ##############################################
|
|
|
|
setup_config_server() {
|
|
log_info "Config server starting (port ${CONFIG_SERVE_PORT}, ${CONFIG_SERVE_HOURS}h TTL)..."
|
|
|
|
# Stop existing server if running from a previous install
|
|
if systemctl is-active --quiet gns3-config-serve.service 2>/dev/null; then
|
|
systemctl stop gns3-config-serve.service 2>/dev/null || true
|
|
systemctl stop gns3-config-serve-stop.timer 2>/dev/null || true
|
|
log_info "Stopped previous config server"
|
|
fi
|
|
|
|
# Clean previous serve directory (public-facing only)
|
|
[[ -d ${CONFIG_SERVE_DIR} ]] && rm -rf "${CONFIG_SERVE_DIR}"
|
|
|
|
# Ensure deploy directory exists for secrets
|
|
mkdir -p "${DEPLOY_DIR}"
|
|
chmod 700 "${DEPLOY_DIR}"
|
|
|
|
# Persist slug across re-runs so URL stays stable
|
|
local _serve_slug
|
|
if [[ -f "${DEPLOY_DIR}/serve_slug" ]]; then
|
|
_serve_slug=$(cat "${DEPLOY_DIR}/serve_slug")
|
|
log_info "Reusing existing serve slug: ${_serve_slug}"
|
|
else
|
|
_serve_slug=$(openssl rand -hex 3)
|
|
echo "${_serve_slug}" >"${DEPLOY_DIR}/serve_slug"
|
|
chmod 600 "${DEPLOY_DIR}/serve_slug"
|
|
fi
|
|
|
|
local _serve_path="${CONFIG_SERVE_DIR}/${_serve_slug}"
|
|
mkdir -p "${_serve_path}"
|
|
log_info "Config serve path: ${_serve_path}"
|
|
|
|
local _lan_ip _public_ip _gns3_bin _gns3_ver
|
|
_lan_ip=$(get_lan_ip)
|
|
_public_ip=$(get_public_ip)
|
|
_gns3_bin="/usr/bin/gns3server"
|
|
[[ -x "${GNS3_VENV}/bin/gns3server" ]] && _gns3_bin="${GNS3_VENV}/bin/gns3server"
|
|
|
|
# Extract exact semantic version, fallback to "unknown"
|
|
_gns3_ver=$("${_gns3_bin}" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true
|
|
[[ -z ${_gns3_ver} ]] && _gns3_ver="unknown"
|
|
|
|
# VPN config files (encrypted by default)
|
|
local _conf_passphrase=""
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 || ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
# Fatal if we can't determine public IP for VPN configs
|
|
if [[ ${_public_ip} == "UNKNOWN" ]]; then
|
|
log_fatal "Couldn't find public IP. VPN configs need a reachable IP."
|
|
fi
|
|
|
|
if [[ ${UNSAFE_CONFIGS} -eq 0 ]]; then
|
|
# Persist passphrase across re-runs
|
|
if [[ -f "${DEPLOY_DIR}/passphrase" ]]; then
|
|
_conf_passphrase=$(cat "${DEPLOY_DIR}/passphrase")
|
|
log_info "Reusing existing config passphrase"
|
|
else
|
|
_conf_passphrase=$(generate_passphrase)
|
|
echo "${_conf_passphrase}" >"${DEPLOY_DIR}/passphrase"
|
|
chmod 600 "${DEPLOY_DIR}/passphrase"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 && -f /root/client.ovpn ]]; then
|
|
if [[ ${UNSAFE_CONFIGS} -eq 1 ]]; then
|
|
cp /root/client.ovpn "${_serve_path}/o"
|
|
else
|
|
encrypted_copy /root/client.ovpn "${_serve_path}/o" "${_conf_passphrase}"
|
|
fi
|
|
fi
|
|
|
|
if [[ ${WITH_WIREGUARD} -eq 1 && -f "${WG_CONF_DIR}/client1.conf" ]]; then
|
|
if [[ ${UNSAFE_CONFIGS} -eq 1 ]]; then
|
|
cp "${WG_CONF_DIR}/client1.conf" "${_serve_path}/w"
|
|
else
|
|
encrypted_copy "${WG_CONF_DIR}/client1.conf" "${_serve_path}/w" "${_conf_passphrase}"
|
|
fi
|
|
fi
|
|
|
|
# Also stash client configs in deploy dir for safekeeping
|
|
[[ -f /root/client.ovpn ]] && cp /root/client.ovpn "${DEPLOY_DIR}/client.ovpn"
|
|
[[ -f "${WG_CONF_DIR}/client1.conf" ]] && cp "${WG_CONF_DIR}/client1.conf" "${DEPLOY_DIR}/wg-client1.conf"
|
|
|
|
# ## Landing page ##########################################################
|
|
|
|
log_info "Fetching UI template..."
|
|
|
|
local _script_dir _template_src _template_dst _template_ok
|
|
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
|
_template_src="${_script_dir}/${REPO_LANDING_HTML}"
|
|
_template_dst="${CONFIG_SERVE_DIR}/index.html"
|
|
_template_ok=0
|
|
|
|
if fetch_resource "${_template_dst}" \
|
|
"${_template_src}" \
|
|
"${REPO_BASE_URL}${REPO_LANDING_HTML}"; then
|
|
_template_ok=1
|
|
else
|
|
log_error "Failed to obtain template. Using basic fallback UI."
|
|
echo "<html><head><title>Error</title></head>" >"${_template_dst}"
|
|
echo "<body><h1>GNS3 Config Server</h1><p>" >>"${_template_dst}"
|
|
echo "Template missing. Check console for details.</p></body></html>" >>"${_template_dst}"
|
|
fi
|
|
|
|
# Only run sed injections if we got the template (from either local or curl)
|
|
if [[ ${_template_ok} -eq 1 ]]; then
|
|
log_info "Injecting server data into UI template..."
|
|
|
|
# Build warnings JSON array for injection
|
|
local _warnings_json="[]"
|
|
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
|
|
_warnings_json="["
|
|
for _w in "${WARNINGS[@]}"; do
|
|
_warnings_json+="\"$(echo "${_w}" | sed 's/"/\\"/g')\","
|
|
done
|
|
_warnings_json="${_warnings_json%,}]"
|
|
fi
|
|
|
|
# Use | as the sed delimiter to avoid conflicts with slashes in URLs/paths
|
|
sed -i "s|{{LAN_IP}}|${_lan_ip}|g" "${_template_dst}"
|
|
sed -i "s|{{PUBLIC_IP}}|${_public_ip}|g" "${_template_dst}"
|
|
sed -i "s|{{HOSTNAME}}|$(hostname)|g" "${_template_dst}"
|
|
sed -i "s|{{GNS3_VERSION}}|${_gns3_ver}|g" "${_template_dst}"
|
|
sed -i "s|{{GNS3_PORT}}|${GNS3_PORT}|g" "${_template_dst}"
|
|
sed -i "s|{{WITH_OPENVPN}}|${WITH_OPENVPN}|g" "${_template_dst}"
|
|
sed -i "s|{{WITH_WIREGUARD}}|${WITH_WIREGUARD}|g" "${_template_dst}"
|
|
sed -i "s|{{WITH_DOCKER}}|${WITH_DOCKER}|g" "${_template_dst}"
|
|
sed -i "s|{{DISABLE_KVM}}|${DISABLE_KVM}|g" "${_template_dst}"
|
|
sed -i "s|{{UNSAFE_CONFIGS}}|${UNSAFE_CONFIGS}|g" "${_template_dst}"
|
|
sed -i "s|{{SERVE_SLUG}}|${_serve_slug}|g" "${_template_dst}"
|
|
sed -i "s|{{SERVE_HOURS}}|${CONFIG_SERVE_HOURS}|g" "${_template_dst}"
|
|
sed -i "s|{{SERVE_PORT}}|${CONFIG_SERVE_PORT}|g" "${_template_dst}"
|
|
sed -i "s|{{WARNINGS_JSON}}|${_warnings_json}|g" "${_template_dst}"
|
|
sed -i "s|{{OVPN_PORT}}|${OVPN_PORT}|g" "${_template_dst}"
|
|
sed -i "s|{{WG_PORT}}|${WG_PORT}|g" "${_template_dst}"
|
|
|
|
log_ok "Landing page generated"
|
|
fi
|
|
|
|
# ## Systemd units ##########################################################
|
|
|
|
ensure_config /lib/systemd/system/gns3-config-serve.service root 644 <<_EOF_HTTP_SVC
|
|
[Unit]
|
|
Description=GNS3 ephemeral config server
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
WorkingDirectory=${CONFIG_SERVE_DIR}
|
|
ExecStart=/usr/bin/python3 -m http.server ${CONFIG_SERVE_PORT} --bind 0.0.0.0
|
|
Restart=no
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
_EOF_HTTP_SVC
|
|
|
|
ensure_config /lib/systemd/system/gns3-config-serve-stop.timer root 644 <<_EOF_CONF_TMR
|
|
[Unit]
|
|
Description=Stop GNS3 config server after ${CONFIG_SERVE_HOURS} hours
|
|
|
|
[Timer]
|
|
OnActiveSec=${CONFIG_SERVE_HOURS}h
|
|
AccuracySec=1min
|
|
Unit=gns3-config-serve-stop.service
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
_EOF_CONF_TMR
|
|
|
|
ensure_config /lib/systemd/system/gns3-config-serve-stop.service root 644 <<_EOF_CONF_SVC
|
|
[Unit]
|
|
Description=Stop and clean up GNS3 config server
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/bin/systemctl stop gns3-config-serve.service
|
|
ExecStart=/bin/systemctl disable gns3-config-serve.service
|
|
ExecStart=/bin/rm -rf ${CONFIG_SERVE_DIR}
|
|
ExecStart=/bin/systemctl disable gns3-config-serve-stop.timer
|
|
ExecStart=/usr/sbin/ufw delete allow ${CONFIG_SERVE_PORT}/tcp
|
|
_EOF_CONF_SVC
|
|
|
|
enable_and_start gns3-config-serve.service
|
|
enable_and_start gns3-config-serve-stop.timer
|
|
|
|
log_ok "Config server live on port ${CONFIG_SERVE_PORT} (auto-stops in ${CONFIG_SERVE_HOURS}h)"
|
|
|
|
# MOTD
|
|
ensure_config /etc/update-motd.d/70-gns3-vpn root 755 <<_EOF_MOTD
|
|
#!/bin/sh
|
|
if systemctl is-active --quiet gns3-config-serve.service 2>/dev/null; then
|
|
echo ""
|
|
echo " GNS3 config: http://${_lan_ip}:${CONFIG_SERVE_PORT}/"
|
|
echo " (auto-expires — download configs now)"
|
|
fi
|
|
_EOF_MOTD
|
|
}
|
|
|
|
### Firewall (ufw) ##########################################################
|
|
|
|
configure_firewall() {
|
|
if [[ ${DISABLE_FIREWALL} -eq 1 ]]; then
|
|
log_info "Skipping firewall configuration (--without-firewall)"
|
|
return
|
|
fi
|
|
|
|
if ! command -v ufw &>/dev/null; then
|
|
log_warn "No ufw detected. Ensure these ports are open in your firewall:"
|
|
log_warn " ${REQUIRED_PORTS[*]}"
|
|
return
|
|
fi
|
|
|
|
if ! ufw status | grep -q "Status: active"; then
|
|
log_warn "ufw is installed but inactive. Skipping rule creation."
|
|
log_warn " If you enable ufw later, allow ports: ${REQUIRED_PORTS[*]}"
|
|
return
|
|
fi
|
|
|
|
log_info "Configuring ufw rules..."
|
|
|
|
# Detect SSH source IP and local subnets for scoped rules
|
|
local _remote_ssh_ip=""
|
|
_remote_ssh_ip=$(echo "${SSH_CONNECTION:-}" | awk '{print $1}')
|
|
|
|
local -a _local_subnets=()
|
|
readarray -t _local_subnets < <(ip -o -f inet addr show |
|
|
grep -E '(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' |
|
|
awk '{print $4}')
|
|
|
|
if [[ -n ${_remote_ssh_ip} ]]; then
|
|
log_info "SSH connection from ${_remote_ssh_ip}"
|
|
else
|
|
log_warn_sticky "No SSH session detected. Allow your IP in ufw or use VPN for remote access."
|
|
fi
|
|
|
|
if [[ ${#_local_subnets[@]} -gt 0 ]]; then
|
|
log_info "RFC1918 subnets detected: ${_local_subnets[*]}"
|
|
fi
|
|
|
|
for _port in "${REQUIRED_PORTS[@]}"; do
|
|
local _status="PERM"
|
|
|
|
case "${_port}" in
|
|
"${OVPN_PORT}" | "${WG_PORT}")
|
|
# VPN ports: open from anywhere, both TCP and UDP
|
|
ufw allow "${_port}"/tcp comment "GNS3 - ${_status} (VPN)" >/dev/null 2>&1
|
|
ufw allow "${_port}"/udp comment "GNS3 - ${_status} (VPN)" >/dev/null 2>&1
|
|
;;
|
|
"${CONFIG_SERVE_PORT}")
|
|
_status="CLEAR"
|
|
ufw allow "${_port}"/tcp comment "GNS3 - ${_status} (ephem. webserver)" >/dev/null 2>&1
|
|
;;
|
|
*)
|
|
# Non-VPN ports: scope to SSH source and local subnets
|
|
if [[ -n "${_remote_ssh_ip}" ]]; then
|
|
ufw allow from "${_remote_ssh_ip}" to any port "${_port}" proto tcp \
|
|
comment "GNS3 - ${_status}" >/dev/null 2>&1
|
|
fi
|
|
for _subnet in "${_local_subnets[@]}"; do
|
|
ufw allow from "${_subnet}" to any port "${_port}" proto tcp \
|
|
comment "GNS3 - ${_status}" >/dev/null 2>&1
|
|
done
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Enable forwarding in ufw if any VPN is configured
|
|
if [[ ${WITH_OPENVPN} -eq 1 || ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
local _ufw_default="/etc/default/ufw"
|
|
if [[ -f ${_ufw_default} ]]; then
|
|
if grep -q '^DEFAULT_FORWARD_POLICY="DROP"' "${_ufw_default}"; then
|
|
sed -i 's/^DEFAULT_FORWARD_POLICY="DROP"/DEFAULT_FORWARD_POLICY="ACCEPT"/' "${_ufw_default}"
|
|
log_info "Set UFW DEFAULT_FORWARD_POLICY=ACCEPT"
|
|
fi
|
|
fi
|
|
ufw reload >/dev/null 2>&1
|
|
fi
|
|
|
|
log_ok "ufw rules applied for ports: ${REQUIRED_PORTS[*]}"
|
|
}
|
|
|
|
### Core setup functions #################################################
|
|
|
|
setup_groups() {
|
|
log_info "Creating required system groups..."
|
|
for _grp in "${REQUIRED_GROUPS[@]}"; do
|
|
ensure_group "${_grp}"
|
|
done
|
|
log_ok "Groups: ${REQUIRED_GROUPS[*]}"
|
|
}
|
|
|
|
setup_gns3_user() {
|
|
log_info "Setting up GNS3 service user..."
|
|
mkdir -p "${GNS3_HOME}"/{images,projects,appliances,configs}
|
|
|
|
if ! id "${GNS3_USER}" &>/dev/null; then
|
|
local _groups_csv
|
|
printf -v _groups_csv '%s,' "${REQUIRED_GROUPS[@]}"
|
|
_groups_csv="${_groups_csv%,}"
|
|
|
|
useradd --system \
|
|
--home-dir "${GNS3_HOME}" \
|
|
--no-create-home \
|
|
--comment "GNS3 server" \
|
|
--groups "${_groups_csv}" \
|
|
--shell /usr/sbin/nologin \
|
|
"${GNS3_USER}"
|
|
log_ok "Created user ${GNS3_USER}"
|
|
else
|
|
for _grp in "${REQUIRED_GROUPS[@]}"; do
|
|
usermod -aG "${_grp}" "${GNS3_USER}"
|
|
done
|
|
log_ok "User ${GNS3_USER} already exists — updated groups"
|
|
fi
|
|
|
|
chown -R "${GNS3_USER}:${GNS3_USER}" "${GNS3_HOME}"
|
|
}
|
|
|
|
propagate_groups_to_invoker() {
|
|
local _invoker
|
|
_invoker=$(detect_invoking_user)
|
|
if [[ -n ${_invoker} ]]; then
|
|
log_info "Adding ${_invoker} to groups: ${REQUIRED_GROUPS[*]}"
|
|
for _grp in "${REQUIRED_GROUPS[@]}"; do
|
|
usermod -aG "${_grp}" "${_invoker}"
|
|
done
|
|
log_ok "Group membership updated for ${_invoker} (log out/in to take effect)"
|
|
fi
|
|
}
|
|
|
|
# Is repo present?
|
|
gns3_repo_present() {
|
|
grep -rEq "^.*ppa.launchpadcontent.net/gns3/ppa/ubuntu/" \
|
|
/etc/apt/sources.list.d/ 2>/dev/null
|
|
}
|
|
|
|
# Add gns3 repo (if not already present)
|
|
add_gns3_repository() {
|
|
if gns3_repo_present; then
|
|
log_info "GNS3 PPA already configured"
|
|
return
|
|
fi
|
|
log_info "Adding GNS3 PPA: ppa:gns3/${REPOSITORY}"
|
|
apt-add-repository -y "ppa:gns3/${REPOSITORY}" >/dev/null
|
|
log_ok "GNS3 repository added"
|
|
}
|
|
|
|
# Add Docker repo
|
|
add_docker_repository() {
|
|
if [[ -f ${DOCKER_KEYRING} ]]; then
|
|
log_info "Docker GPG key already present — skipping download"
|
|
else
|
|
log_info "Adding Docker CE repository..."
|
|
install -m 0755 -d /etc/apt/keyrings
|
|
|
|
local _script_dir
|
|
_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
|
|
|
# Try local GPG file, their URL, my URL, then fail
|
|
if ! fetch_resource "${DOCKER_KEYRING}" \
|
|
"${_script_dir}/${REPO_DOCKER_GPG}" \
|
|
"${DOCKER_BASE_URL}/gpg" \
|
|
"${REPO_BASE_URL}${REPO_DOCKER_GPG}"; then
|
|
log_fatal "Could not obtain Docker GPG key from any source."
|
|
fi
|
|
|
|
# Sanity check — make sure we got an actual key, not an error page
|
|
if ! file "${DOCKER_KEYRING}" 2>/dev/null | grep -qiE 'pgp|openpgp|gpg'; then
|
|
log_warn_sticky "Docker keyring file may not be a valid GPG key. Check: ${DOCKER_KEYRING}"
|
|
fi
|
|
|
|
chmod a+r "${DOCKER_KEYRING}"
|
|
fi
|
|
|
|
cat >/etc/apt/sources.list.d/docker.list <<EOF
|
|
deb [arch=$(dpkg --print-architecture) signed-by=${DOCKER_KEYRING}] ${DOCKER_BASE_URL} ${OS_CODENAME} stable
|
|
EOF
|
|
log_ok "Docker repository configured"
|
|
}
|
|
|
|
# Do we need to run install?
|
|
needs_install() {
|
|
local _pkg
|
|
for _pkg in "${REQUIRED_PKGS[@]}"; do
|
|
# dpkg-query is faster than apt-cache for this
|
|
if ! dpkg-query -W -f='${Status}' "${_pkg}" 2>/dev/null | grep -q "install ok installed"; then
|
|
return 0 # at least one package needs install
|
|
fi
|
|
done
|
|
return 1 # everything installed
|
|
}
|
|
|
|
# Updates, ugprades, and installs - quietly
|
|
install_packages() {
|
|
apt_retry update -qq
|
|
|
|
if [[ "${NO_SYSTEM_UPGRADE}" -eq 0 ]]; then
|
|
log_info "Upgrading system packages..."
|
|
apt_retry upgrade -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold"
|
|
log_ok "System upgraded"
|
|
else
|
|
log_info "Skipping system upgrade"
|
|
fi
|
|
|
|
if needs_install; then
|
|
log_info "Installing required packages..."
|
|
NEEDRESTART_MODE=a apt_retry install -y "${REQUIRED_PKGS[@]}"
|
|
log_ok "All packages installed"
|
|
else
|
|
log_ok "All required packages already installed"
|
|
fi
|
|
}
|
|
|
|
### GNS3 server configuration #####################################
|
|
|
|
configure_gns3() {
|
|
log_info "Writing GNS3 server configuration..."
|
|
|
|
local _listen_host="0.0.0.0"
|
|
|
|
local _hw_accel="True"
|
|
if [[ ${DISABLE_KVM} -eq 1 ]]; then
|
|
_hw_accel="False"
|
|
log_warn "KVM disabled — Qemu performance will be degraded"
|
|
fi
|
|
|
|
ensure_directory "${GNS3_CONF_DIR}" "${GNS3_USER}" 700
|
|
|
|
ensure_config "${GNS3_CONF_DIR}/gns3_server.conf" "${GNS3_USER}" 600 <<_EOF_GNS3_CONF
|
|
${DEPLOY_MARKER}
|
|
[Server]
|
|
host = ${_listen_host}
|
|
port = ${GNS3_PORT}
|
|
images_path = ${GNS3_HOME}/images
|
|
projects_path = ${GNS3_HOME}/projects
|
|
appliances_path = ${GNS3_HOME}/appliances
|
|
configs_path = ${GNS3_HOME}/configs
|
|
report_errors = True
|
|
|
|
[Qemu]
|
|
enable_hardware_acceleration = ${_hw_accel}
|
|
require_hardware_acceleration = ${_hw_accel}
|
|
_EOF_GNS3_CONF
|
|
|
|
log_ok "GNS3 configuration written"
|
|
}
|
|
|
|
### Systemd service #############################################
|
|
|
|
install_gns3_service() {
|
|
log_info "Installing GNS3 systemd service..."
|
|
|
|
local _gns3_bin="/usr/bin/gns3server"
|
|
if [[ -x "${GNS3_VENV}/bin/gns3server" ]]; then
|
|
_gns3_bin="${GNS3_VENV}/bin/gns3server"
|
|
log_info "Using venv binary: ${_gns3_bin}"
|
|
fi
|
|
|
|
ensure_config "${GNS3_SERVICE_FILE}" root 644 <<_EOF_GNS3_SVC
|
|
[Unit]
|
|
Description=GNS3 server
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
Conflicts=shutdown.target
|
|
|
|
[Service]
|
|
User=gns3
|
|
Group=gns3
|
|
PermissionsStartOnly=true
|
|
EnvironmentFile=/etc/environment
|
|
ExecStartPre=/bin/mkdir -p /var/log/gns3 /var/run/gns3
|
|
ExecStartPre=/bin/chown -R gns3:gns3 /var/log/gns3 /var/run/gns3
|
|
ExecStart=${_gns3_bin} --log /var/log/gns3/gns3.log
|
|
ExecReload=/bin/kill -s HUP \$MAINPID
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
LimitNOFILE=16384
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
_EOF_GNS3_SVC
|
|
|
|
enable_and_start gns3
|
|
log_ok "GNS3 service installed and enabled"
|
|
}
|
|
|
|
### OpenVPN setup ################################################
|
|
|
|
configure_openvpn() {
|
|
log_info "Configuring OpenVPN..."
|
|
|
|
local _public_ip
|
|
_public_ip=$(get_public_ip)
|
|
if [[ ${_public_ip} == "UNKNOWN" ]]; then
|
|
log_fatal "Could not determine public IP for OpenVPN configuration."
|
|
fi
|
|
log_info "Public IP detected: ${_public_ip}"
|
|
|
|
log_info "Generating OpenVPN keys..."
|
|
ensure_directory "${OVPN_CONF_DIR}" root 755
|
|
|
|
# If using *RSA*
|
|
if [[ ${USE_LEGACY_RSA} -eq 1 ]]; then
|
|
log_info "Using legacy RSA crypto (DH params may take a minute)..."
|
|
if [[ -f "${OVPN_CONF_DIR}/dh.pem" ]]; then
|
|
log_info "Already have DH params. Skipping."
|
|
else
|
|
openssl dhparam -quiet 2048 | ensure_config "${OVPN_CONF_DIR}/dh.pem" root 600
|
|
fi
|
|
require_file "${OVPN_CONF_DIR}/dh.pem" "OpenVPN DH params" || log_fatal "DH param generation failed"
|
|
|
|
if [[ -f "${OVPN_CONF_DIR}/key.pem" ]]; then
|
|
log_info "Already have OpenVPN key. Skipping."
|
|
else
|
|
openssl genrsa 2048 | ensure_config "${OVPN_CONF_DIR}/key.pem" root 600
|
|
fi
|
|
|
|
# If using *ECC*
|
|
else
|
|
log_info "Using elliptic curve crypto (P-384)..."
|
|
if [[ -f "${OVPN_CONF_DIR}/key.pem" ]]; then
|
|
log_info "Already have OpenVPN key. Skipping."
|
|
else
|
|
openssl ecparam -name secp384r1 -genkey -noout | ensure_config "${OVPN_CONF_DIR}/key.pem" root 600
|
|
fi
|
|
|
|
fi
|
|
require_file "${OVPN_CONF_DIR}/key.pem" "OpenVPN private key" || log_fatal "Key generation failed"
|
|
|
|
if [[ -f "${OVPN_CONF_DIR}/csr.pem" ]]; then
|
|
log_info "csr.pem already present."
|
|
else
|
|
openssl req -new -key "${OVPN_CONF_DIR}/key.pem" -out "${OVPN_CONF_DIR}/csr.pem" \
|
|
-subj /CN=OpenVPN/ 2>/dev/null
|
|
fi
|
|
require_file "${OVPN_CONF_DIR}/csr.pem" "OpenVPN CSR" || log_fatal "CSR generation failed"
|
|
|
|
if [[ -f "${OVPN_CONF_DIR}/cert.pem" ]]; then
|
|
log_info "cert.pem already present."
|
|
else
|
|
openssl x509 -req -in "${OVPN_CONF_DIR}/csr.pem" -out "${OVPN_CONF_DIR}/cert.pem" \
|
|
-signkey "${OVPN_CONF_DIR}/key.pem" -days 3650 &>/dev/null
|
|
fi
|
|
require_file "${OVPN_CONF_DIR}/cert.pem" "OpenVPN certificate" || log_fatal "Certificate signing failed"
|
|
|
|
local _dh_client_block=""
|
|
local _dh_server_line="dh none"
|
|
if [[ ${USE_LEGACY_RSA} -eq 1 ]]; then
|
|
_dh_client_block="<dh>$(cat "${OVPN_CONF_DIR}/dh.pem")</dh>"
|
|
_dh_server_line="dh dh.pem"
|
|
fi
|
|
|
|
ensure_config /root/client.ovpn root 600 <<_EOF_OVPN_CLI
|
|
client
|
|
nobind
|
|
dev tun
|
|
<key>
|
|
$(cat "${OVPN_CONF_DIR}/key.pem")
|
|
</key>
|
|
<cert>
|
|
$(cat "${OVPN_CONF_DIR}/cert.pem")
|
|
</cert>
|
|
<ca>
|
|
$(cat "${OVPN_CONF_DIR}/cert.pem")
|
|
</ca>
|
|
${_dh_client_block}
|
|
<connection>
|
|
remote ${_public_ip} ${OVPN_PORT} udp
|
|
</connection>
|
|
_EOF_OVPN_CLI
|
|
|
|
ensure_config "${OVPN_CONF_DIR}/udp${OVPN_PORT}.conf" root 644 <<_EOF_OVPN
|
|
server 172.16.253.0 255.255.255.0
|
|
verb 3
|
|
duplicate-cn
|
|
key key.pem
|
|
ca cert.pem
|
|
cert cert.pem
|
|
${_dh_server_line}
|
|
keepalive 10 60
|
|
persist-key
|
|
persist-tun
|
|
proto udp
|
|
port ${OVPN_PORT}
|
|
dev tun${OVPN_PORT}
|
|
status openvpn-status-${OVPN_PORT}.log
|
|
log-append /var/log/openvpn-udp${OVPN_PORT}.log
|
|
_EOF_OVPN
|
|
|
|
enable_and_start openvpn
|
|
log_ok "OpenVPN configured"
|
|
}
|
|
|
|
### WireGuard setup #############################################─
|
|
|
|
configure_wireguard() {
|
|
log_info "Configuring WireGuard..."
|
|
|
|
local _public_ip
|
|
_public_ip=$(get_public_ip)
|
|
if [[ ${_public_ip} == "UNKNOWN" ]]; then
|
|
log_fatal "Could not determine public IP for WireGuard configuration."
|
|
fi
|
|
|
|
ensure_directory "${WG_CONF_DIR}" root 700
|
|
|
|
if [[ ! -f "${WG_CONF_DIR}/server.key" ]]; then
|
|
(
|
|
umask 077
|
|
wg genkey | tee "${WG_CONF_DIR}/server.key" | wg pubkey >"${WG_CONF_DIR}/server.pub"
|
|
)
|
|
fi
|
|
require_file "${WG_CONF_DIR}/server.key" "WireGuard server private key" || log_fatal "WG server keygen failed"
|
|
require_file "${WG_CONF_DIR}/server.pub" "WireGuard server public key" || log_fatal "WG server keygen failed"
|
|
|
|
local _server_privkey _server_pubkey
|
|
_server_privkey=$(cat "${WG_CONF_DIR}/server.key")
|
|
_server_pubkey=$(cat "${WG_CONF_DIR}/server.pub")
|
|
|
|
if [[ ! -f "${WG_CONF_DIR}/client1.key" ]]; then
|
|
(
|
|
umask 077
|
|
wg genkey | tee "${WG_CONF_DIR}/client1.key" | wg pubkey >"${WG_CONF_DIR}/client1.pub"
|
|
)
|
|
fi
|
|
require_file "${WG_CONF_DIR}/client1.key" "WireGuard client private key" || log_fatal "WG client keygen failed"
|
|
|
|
local _client_privkey _client_pubkey
|
|
_client_privkey=$(cat "${WG_CONF_DIR}/client1.key")
|
|
_client_pubkey=$(cat "${WG_CONF_DIR}/client1.pub")
|
|
|
|
ensure_config "${WG_CONF_DIR}/wg0.conf" root 600 <<_EOF_WG_SRV
|
|
[Interface]
|
|
Address = 172.16.254.1/24
|
|
ListenPort = ${WG_PORT}
|
|
PrivateKey = ${_server_privkey}
|
|
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
|
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
|
|
|
[Peer]
|
|
# client1
|
|
PublicKey = ${_client_pubkey}
|
|
AllowedIPs = 172.16.254.2/32
|
|
_EOF_WG_SRV
|
|
|
|
ensure_config "${WG_CONF_DIR}/client1.conf" root 600 <<_EOF_WG_CLI
|
|
[Interface]
|
|
Address = 172.16.254.2/24
|
|
PrivateKey = ${_client_privkey}
|
|
DNS = 1.1.1.1
|
|
|
|
[Peer]
|
|
PublicKey = ${_server_pubkey}
|
|
Endpoint = ${_public_ip}:${WG_PORT}
|
|
AllowedIPs = 172.16.253.0/24, 172.16.254.0/24
|
|
PersistentKeepalive = 25
|
|
_EOF_WG_CLI
|
|
|
|
enable_and_start wg-quick@wg0
|
|
log_ok "WireGuard configured"
|
|
}
|
|
|
|
### Welcome setup ##############################################################─
|
|
|
|
configure_welcome() {
|
|
log_info "Setting up GNS3-VM welcome console..."
|
|
|
|
ensure_config "/etc/sudoers.d/gns3" root 440 <<_EOF
|
|
gns3 ALL = (ALL) NOPASSWD: /usr/bin/apt-key
|
|
gns3 ALL = (ALL) NOPASSWD: /usr/bin/apt-get
|
|
gns3 ALL = (ALL) NOPASSWD: /usr/sbin/reboot
|
|
_EOF
|
|
|
|
if ! visudo -cf /etc/sudoers.d/gns3 &>/dev/null; then
|
|
log_warn_sticky "sudoers fragment failed syntax check — removing"
|
|
rm -f /etc/sudoers.d/gns3
|
|
fi
|
|
|
|
curl -fsSL https://raw.githubusercontent.com/GNS3/gns3-server/master/scripts/welcome.py \
|
|
-o /usr/local/bin/welcome.py
|
|
chmod 755 /usr/local/bin/welcome.py
|
|
chown "${GNS3_USER}:${GNS3_USER}" /usr/local/bin/welcome.py
|
|
|
|
mkdir -p /etc/systemd/system/getty@tty1.service.d
|
|
ensure_config "/etc/systemd/system/getty@tty1.service.d/override.conf" root 644 <<'_EOF'
|
|
[Service]
|
|
ExecStart=
|
|
ExecStart=-/sbin/agetty -a gns3 --noclear %I $TERM
|
|
_EOF
|
|
|
|
grep -q 'welcome.py' "${GNS3_HOME}/.bashrc" 2>/dev/null ||
|
|
echo "python3 /usr/local/bin/welcome.py" >>"${GNS3_HOME}/.bashrc"
|
|
|
|
echo "gns3:gns3" | chpasswd
|
|
usermod --shell /bin/bash "${GNS3_USER}"
|
|
usermod -aG sudo "${GNS3_USER}"
|
|
|
|
log_ok "Welcome console configured"
|
|
}
|
|
|
|
### Start services ##########################################################
|
|
|
|
start_services() {
|
|
log_info "Starting GNS3 service..."
|
|
systemctl restart gns3
|
|
sleep 2
|
|
|
|
if systemctl is-active --quiet gns3; then
|
|
log_ok "GNS3 service running"
|
|
else
|
|
log_warn_sticky "GNS3 service failed to start"
|
|
log_warn_sticky " Check logs: journalctl -u gns3 --no-pager -n 20"
|
|
fi
|
|
|
|
if [[ ${WITH_DOCKER} -eq 1 ]]; then
|
|
enable_and_start docker
|
|
if systemctl is-active --quiet docker; then
|
|
log_ok "Docker service running"
|
|
else
|
|
log_warn_sticky "Docker service failed to start"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
### Post-install validation ##################################################
|
|
|
|
# Helper: curl a URL and report success/failure. Returns 0/1 for chaining.
|
|
probe_http() {
|
|
local _url="$1" _desc="$2"
|
|
if curl -sf --max-time 5 "${_url}" &>/dev/null; then
|
|
log_ok "${_desc} responding: ${_url}"
|
|
return 0
|
|
fi
|
|
log_warn_sticky "${_desc} not responding: ${_url}"
|
|
return 1
|
|
}
|
|
|
|
# Helper: check if a TCP (default) or UDP port is listening
|
|
probe_port_listening() {
|
|
local _port="$1" _desc="$2" _proto="${3:-tcp}"
|
|
local _flag="-tlnH"
|
|
[[ "${_proto}" == "udp" ]] && _flag="-ulnH"
|
|
if ss ${_flag} 2>/dev/null | grep -q ":${_port} "; then
|
|
log_ok "${_desc} listening on ${_proto} port ${_port}"
|
|
return 0
|
|
fi
|
|
log_warn_sticky "${_desc} not listening on ${_proto} port ${_port}"
|
|
return 1
|
|
}
|
|
|
|
validate() {
|
|
log_info "Running post-install validation..."
|
|
local _lan_ip _serve_slug
|
|
_lan_ip=$(get_lan_ip)
|
|
|
|
sleep 3
|
|
|
|
# GNS3 API responding
|
|
if ! probe_http "http://${_lan_ip}:${GNS3_PORT}/v2/version" "GNS3 API"; then
|
|
probe_http "http://localhost:${GNS3_PORT}/v2/version" "GNS3 API (localhost fallback)" ||
|
|
log_warn_sticky " Check: journalctl -u gns3 --no-pager -n 20"
|
|
fi
|
|
|
|
# Port listening checks
|
|
probe_port_listening "${GNS3_PORT}" "GNS3 server" || true
|
|
probe_port_listening "${CONFIG_SERVE_PORT}" "Config server" || true
|
|
|
|
# Config server landing page
|
|
probe_http "http://${_lan_ip}:${CONFIG_SERVE_PORT}/" "Landing page" || true
|
|
|
|
# VPN config downloadable through the slug path
|
|
if [[ -f "${DEPLOY_DIR}/serve_slug" ]]; then
|
|
_serve_slug=$(cat "${DEPLOY_DIR}/serve_slug")
|
|
if [[ ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
probe_http "http://${_lan_ip}:${CONFIG_SERVE_PORT}/${_serve_slug}/w" "WireGuard config download" || true
|
|
fi
|
|
if [[ ${WITH_OPENVPN} -eq 1 ]]; then
|
|
probe_http "http://${_lan_ip}:${CONFIG_SERVE_PORT}/${_serve_slug}/o" "OpenVPN config download" || true
|
|
fi
|
|
fi
|
|
|
|
# VPN tunnel interfaces
|
|
if [[ ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
probe_port_listening "${WG_PORT}" "WireGuard" udp || true
|
|
if ip link show wg0 &>/dev/null; then
|
|
log_ok "WireGuard interface wg0 is up"
|
|
else
|
|
log_warn_sticky "WireGuard interface wg0 not found"
|
|
fi
|
|
fi
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 ]]; then
|
|
if ip link show "tun${OVPN_PORT}" &>/dev/null; then
|
|
log_ok "OpenVPN interface tun${OVPN_PORT} is up"
|
|
else
|
|
log_warn_sticky "OpenVPN interface tun${OVPN_PORT} not found"
|
|
fi
|
|
fi
|
|
|
|
# Config server auto-stop timer armed
|
|
if systemctl list-timers --all 2>/dev/null | grep -q "gns3-config-serve-stop"; then
|
|
log_ok "Config server auto-stop timer armed"
|
|
else
|
|
log_warn_sticky "Config server auto-stop timer NOT armed — server will not expire on schedule"
|
|
fi
|
|
|
|
# Sysctl hardening applied
|
|
if [[ -f /etc/sysctl.d/90-gns3-hardening.conf ]]; then
|
|
log_ok "Sysctl hardening file present"
|
|
else
|
|
log_warn_sticky "Sysctl hardening file missing"
|
|
fi
|
|
|
|
# IP forwarding enabled when VPN configured
|
|
if [[ ${WITH_OPENVPN} -eq 1 || ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
if [[ "$(sysctl -n net.ipv4.ip_forward 2>/dev/null)" == "1" ]]; then
|
|
log_ok "IPv4 forwarding enabled"
|
|
else
|
|
log_warn_sticky "IPv4 forwarding NOT enabled — VPN routing will fail"
|
|
fi
|
|
fi
|
|
|
|
# Deploy dir perms — secrets must not be world-readable
|
|
if [[ -d ${DEPLOY_DIR} ]]; then
|
|
local _mode
|
|
_mode=$(stat -c %a "${DEPLOY_DIR}" 2>/dev/null)
|
|
if [[ ${_mode} == "700" ]]; then
|
|
log_ok "Deploy directory permissions: 700"
|
|
else
|
|
log_warn_sticky "Deploy directory permissions are ${_mode}, expected 700"
|
|
fi
|
|
fi
|
|
|
|
# Docker
|
|
if [[ ${WITH_DOCKER} -eq 1 ]]; then
|
|
if docker info &>/dev/null; then
|
|
log_ok "Docker engine responding"
|
|
else
|
|
log_warn_sticky "Docker installed but not responding"
|
|
fi
|
|
fi
|
|
|
|
# GNS3 binary sanity
|
|
local _gns3_bin="/usr/bin/gns3server"
|
|
[[ -x "${GNS3_VENV}/bin/gns3server" ]] && _gns3_bin="${GNS3_VENV}/bin/gns3server"
|
|
if "${_gns3_bin}" --version &>/dev/null; then
|
|
log_ok "gns3server binary: $("${_gns3_bin}" --version 2>&1 | head -1)"
|
|
else
|
|
log_warn_sticky "gns3server binary cannot execute — possible Python venv issue"
|
|
log_warn_sticky " Binary: ${_gns3_bin}"
|
|
log_warn_sticky " Check: ${_gns3_bin} --version"
|
|
fi
|
|
|
|
# UFW active when firewall configuration was attempted
|
|
if [[ ${DISABLE_FIREWALL} -eq 0 ]] && command -v ufw &>/dev/null; then
|
|
if ufw status 2>/dev/null | grep -q "Status: active"; then
|
|
log_ok "UFW active"
|
|
else
|
|
log_warn_sticky "UFW is installed but not active — no firewall rules enforced"
|
|
fi
|
|
fi
|
|
|
|
if [[ ${#WARNINGS[@]} -eq 0 ]]; then
|
|
log_ok "All post-install checks passed"
|
|
fi
|
|
}
|
|
|
|
### Summary banner ##########################################################
|
|
|
|
print_summary() {
|
|
local _lan_ip _public_ip
|
|
_lan_ip=$(get_lan_ip)
|
|
_public_ip=$(get_public_ip)
|
|
|
|
echo ""
|
|
printf "${GREEN}${BOLD} GNS3 Server Install Complete${NC}\n"
|
|
printf " ##################################\n\n"
|
|
printf " Server: http://%s:%s\n" "${_lan_ip}" "${GNS3_PORT}"
|
|
printf " Config: %s/gns3_server.conf\n" "${GNS3_CONF_DIR}"
|
|
printf " Data: %s/\n" "${GNS3_HOME}"
|
|
echo " Logs: /var/log/gns3/gns3.log"
|
|
echo " Status: systemctl status gns3"
|
|
echo ""
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 || ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
echo " VPN"
|
|
echo " ##################################"
|
|
|
|
if [[ ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
printf " WireGuard: %s:%s\n" "${_public_ip}" "${WG_PORT}"
|
|
printf " Tunnel: 172.16.254.1:%s\n" "${GNS3_PORT}"
|
|
fi
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 ]]; then
|
|
printf " OpenVPN: %s:%s/udp\n" "${_public_ip}" "${OVPN_PORT}"
|
|
printf " Tunnel: 172.16.253.1:%s\n" "${GNS3_PORT}"
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# Config server info
|
|
if [[ -f "${DEPLOY_DIR}/serve_slug" ]]; then
|
|
local _serve_slug
|
|
_serve_slug=$(cat "${DEPLOY_DIR}/serve_slug")
|
|
printf " Landing page:\n"
|
|
printf " http://%s:%s/\n" "${_lan_ip}" "${CONFIG_SERVE_PORT}"
|
|
printf " ${YELLOW}Expires in %sh${NC}\n\n" "${CONFIG_SERVE_HOURS}"
|
|
fi
|
|
|
|
# Encrypted config download commands
|
|
if [[ -f "${DEPLOY_DIR}/passphrase" ]]; then
|
|
local _pass _serve_slug
|
|
_pass=$(cat "${DEPLOY_DIR}/passphrase")
|
|
_serve_slug=$(cat "${DEPLOY_DIR}/serve_slug")
|
|
|
|
echo " Secure config download"
|
|
echo " ##################################"
|
|
|
|
if [[ ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
printf " ${CYAN}WireGuard:${NC}\n"
|
|
echo " curl -s http://${_lan_ip}:${CONFIG_SERVE_PORT}/${_serve_slug}/w | openssl aes-256-cbc -d -pbkdf2 -pass pass:${_pass} -a > wg.conf"
|
|
echo ""
|
|
fi
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 ]]; then
|
|
printf " ${CYAN}OpenVPN:${NC}\n"
|
|
echo " curl -s http://${_lan_ip}:${CONFIG_SERVE_PORT}/${_serve_slug}/o | openssl aes-256-cbc -d -pbkdf2 -pass pass:${_pass} -a > conf.ovpn"
|
|
echo ""
|
|
fi
|
|
fi
|
|
|
|
if [[ ${DISABLE_KVM} -eq 1 ]]; then
|
|
printf " ${YELLOW}KVM: DISABLED${NC}\n"
|
|
echo ""
|
|
fi
|
|
|
|
local _invoker
|
|
_invoker=$(detect_invoking_user)
|
|
if [[ -n ${_invoker} ]]; then
|
|
printf " ${CYAN}Log out/in as ${_invoker}${NC}"
|
|
echo " for group changes"
|
|
echo ""
|
|
fi
|
|
|
|
if [[ ${#WARNINGS[@]} -gt 0 ]]; then
|
|
printf " ${YELLOW}${BOLD}Action Required${NC}\n"
|
|
echo " ##################################"
|
|
for _warning in "${WARNINGS[@]}"; do
|
|
printf " ${YELLOW}!${NC} %s\n" "${_warning}"
|
|
done
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
##############################################################################
|
|
# MAIN
|
|
##############################################################################
|
|
|
|
main() {
|
|
export DEBIAN_FRONTEND="noninteractive"
|
|
|
|
if ! is_root; then
|
|
log_fatal "Must run as root. Try: sudo $0"
|
|
fi
|
|
|
|
# Ensure exclusive execution
|
|
exec 9>/var/lock/gns3-install.lock
|
|
if ! flock -n 9; then
|
|
log_fatal "Another instance of this script is already running."
|
|
fi
|
|
|
|
log_info "Bootstrapping essential packages..."
|
|
apt-get update -qq >/dev/null 2>&1 && log_info "Update done"
|
|
apt-get install -y -qq curl software-properties-common >/dev/null 2>&1 && log_ok "Bootstrap complete"
|
|
|
|
set -Eeuo pipefail
|
|
|
|
trap 'on_error "${LINENO}" "${BASH_COMMAND}" "$?"' ERR
|
|
trap 'log_warn "Interrupted by user."; exit 130' INT TERM
|
|
|
|
# Initialize deploy directory for secrets and state
|
|
mkdir -p "${DEPLOY_DIR}"
|
|
chmod 700 "${DEPLOY_DIR}"
|
|
|
|
# Log everything to file for post-mortem
|
|
exec > >(tee -a "${DEPLOY_DIR}/install.log") 2>&1
|
|
|
|
preflight_checks
|
|
|
|
readonly OS_CODENAME="${UBUNTU_CODENAME:-${VERSION_CODENAME}}"
|
|
|
|
# # Phase 1: Repositories ##################################################
|
|
|
|
add_gns3_repository
|
|
if [[ "${WITH_DOCKER}" -eq 1 ]]; then add_docker_repository; fi
|
|
|
|
# # Phase 2: Packages (single apt pass) ####################################
|
|
|
|
install_packages
|
|
|
|
# # Phase 3: Users and groups ##############################################
|
|
|
|
setup_groups
|
|
setup_gns3_user
|
|
propagate_groups_to_invoker
|
|
|
|
# # Phase 4: Configuration ################################################─
|
|
|
|
configure_gns3
|
|
install_gns3_service
|
|
|
|
if [[ ${WITH_OPENVPN} -eq 1 ]]; then configure_openvpn; fi
|
|
if [[ ${WITH_WIREGUARD} -eq 1 ]]; then configure_wireguard; fi
|
|
if [[ ${WITH_WELCOME} -eq 1 ]]; then configure_welcome; fi
|
|
if [[ ${WITH_OPENVPN} -eq 1 || ${WITH_WIREGUARD} -eq 1 ]]; then
|
|
enable_ip_forwarding
|
|
fi
|
|
|
|
setup_config_server
|
|
configure_firewall
|
|
apply_sysctl_hardening
|
|
|
|
# ## Phase 5: Start ##########################################################
|
|
|
|
start_services
|
|
|
|
# ## Phase 6: Validate ######################################################
|
|
|
|
validate
|
|
|
|
# ## Phase 7: Welcome post-install repair ####################################
|
|
|
|
if [[ ${WITH_WELCOME} -eq 1 ]]; then
|
|
python3 -c 'import sys; sys.path.append("/usr/local/bin/"); import welcome; ws = welcome.Welcome_dialog(); ws.repair_remote_install()' || true
|
|
fi
|
|
|
|
# ## Done ####################################################################
|
|
|
|
print_summary
|
|
}
|
|
|
|
if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then
|
|
main "$@"
|
|
fi
|