From 3c427af6feb8b117ab085d59b85035f9794ab4f9 Mon Sep 17 00:00:00 2001 From: alexz Date: Sat, 17 Jan 2026 10:36:41 +0000 Subject: [PATCH] Restructure cistech-tunnel to match rego-tunnel pattern - build/: Dockerfile + entrypoint.sh (base image with VNC/noVNC) - shared/: Runtime scripts mounted into container - xstartup: VNC startup, launches openconnect-vpn in xterm - openconnect-vpn: Main VPN script with menu, auto-connect, watchdog - Removed source/ folder (replaced by build/) - Updated docker-compose.json with proper volume mounts - Changed port to 6080 (noVNC default) Co-Authored-By: Claude Opus 4.5 --- apps/cistech-tunnel/build/Dockerfile | 105 ++++ .../build/scripts/entrypoint.sh | 42 ++ apps/cistech-tunnel/config.json | 2 +- apps/cistech-tunnel/docker-compose.json | 24 +- apps/cistech-tunnel/shared/openconnect-vpn | 563 ++++++++++++++++++ apps/cistech-tunnel/shared/xstartup | 39 ++ apps/cistech-tunnel/source/Dockerfile | 43 -- apps/cistech-tunnel/source/entrypoint.sh | 224 ------- 8 files changed, 764 insertions(+), 278 deletions(-) create mode 100644 apps/cistech-tunnel/build/Dockerfile create mode 100644 apps/cistech-tunnel/build/scripts/entrypoint.sh create mode 100644 apps/cistech-tunnel/shared/openconnect-vpn create mode 100644 apps/cistech-tunnel/shared/xstartup delete mode 100755 apps/cistech-tunnel/source/Dockerfile delete mode 100755 apps/cistech-tunnel/source/entrypoint.sh diff --git a/apps/cistech-tunnel/build/Dockerfile b/apps/cistech-tunnel/build/Dockerfile new file mode 100644 index 0000000..c515fe7 --- /dev/null +++ b/apps/cistech-tunnel/build/Dockerfile @@ -0,0 +1,105 @@ +FROM ubuntu:22.04 + +LABEL maintainer="alexz" +LABEL description="OpenConnect SSO VPN in Docker with noVNC" +LABEL version="1.0.0" + +ENV DEBIAN_FRONTEND=noninteractive +ENV container=docker + +# VNC/noVNC settings +ENV DISPLAY=:1 +ENV VNC_PORT=5901 +ENV NOVNC_PORT=6080 + +# Python/Playwright settings +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright +ENV VIRTUAL_ENV=/opt/venv +ENV PATH=/opt/venv/bin:$PATH +ENV QTWEBENGINE_DISABLE_SANDBOX=1 +ENV QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + openconnect \ + iproute2 \ + iptables \ + ca-certificates \ + python3 \ + python3-pip \ + python3-venv \ + vpnc-scripts \ + curl \ + wget \ + openssh-client \ + tigervnc-standalone-server \ + tigervnc-common \ + novnc \ + websockify \ + openbox \ + xterm \ + procps \ + net-tools \ + nano \ + x11vnc \ + xvfb \ + fluxbox \ + oathtool \ + xauth \ + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcomposite1 \ + libxrandr2 \ + libgbm1 \ + libxdamage1 \ + libpango-1.0-0 \ + fonts-liberation \ + libegl1 \ + libgl1 \ + libopengl0 \ + libdbus-1-3 \ + libglib2.0-0 \ + libxkbcommon0 \ + libxkbcommon-x11-0 \ + libxcb1 \ + libxcb-cursor0 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-render0 \ + libxcb-render-util0 \ + libxcb-shm0 \ + libxcb-xfixes0 \ + libxcb-xinerama0 \ + libxcb-randr0 \ + libxcb-glx0 \ + sudo \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install libasound (different package name on different Ubuntu versions) +RUN apt-get update && (apt-get install -y libasound2t64 || apt-get install -y libasound2) && rm -rf /var/lib/apt/lists/* + +# Python venv + openconnect-sso + playwright +RUN python3 -m venv "$VIRTUAL_ENV" +RUN pip install --no-cache-dir openconnect-sso[full] playwright keyring keyrings.alt && \ + python -m playwright install --with-deps chromium + +# Create directories +RUN mkdir -p /opt/scripts /shared /root/.vnc + +# Create VNC startup script (embedded) +RUN echo 'IyEvYmluL2Jhc2gKc2V0IC1lCmV4cG9ydCBIT01FPScvcm9vdCcKZXhwb3J0IFVTRVI9J3Jvb3QnCnJtIC1mIC90bXAvLlgxLWxvY2sgL3RtcC8uWDExLXVuaXgvWDEgMj4vZGV2L251bGwgfHwgdHJ1ZQpybSAtcmYgL3RtcC8uWCotbG9jayAvdG1wLy5YMTEtdW5peC8qIDI+L2Rldi9udWxsIHx8IHRydWUKZWNobyAiU3RhcnRpbmcgVGlnZXJWTkMgc2VydmVyIG9uIGRpc3BsYXkgOjEuLi4iCnZuY3NlcnZlciA6MSAtZ2VvbWV0cnkgMTI4MHg4MDAgLWRlcHRoIDI0IC1TZWN1cml0eVR5cGVzIFZuY0F1dGggLWxvY2FsaG9zdCBubwpzbGVlcCAyCmVjaG8gIlN0YXJ0aW5nIG5vVk5DIG9uIHBvcnQgJHtOT1ZOQ19QT1JUOi02MDgwfS4uLiIKd2Vic29ja2lmeSAtLXdlYj0vdXNyL3NoYXJlL25vdm5jLyAke05PVk5DX1BPUlQ6LTYwODB9IGxvY2FsaG9zdDo1OTAxICYKdGFpbCAtZiAvcm9vdC8udm5jLyoubG9nCg==' \ + | base64 -d > /opt/scripts/startup-vnc.sh && \ + chmod +x /opt/scripts/startup-vnc.sh + +# Copy entrypoint script +COPY scripts/entrypoint.sh /opt/scripts/ +RUN chmod +x /opt/scripts/entrypoint.sh + +EXPOSE 5901 6080 + +CMD ["/opt/scripts/entrypoint.sh"] diff --git a/apps/cistech-tunnel/build/scripts/entrypoint.sh b/apps/cistech-tunnel/build/scripts/entrypoint.sh new file mode 100644 index 0000000..825d9d4 --- /dev/null +++ b/apps/cistech-tunnel/build/scripts/entrypoint.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Entrypoint: VNC password setup + DNS fix + start VNC + +set -euo pipefail + +# Setup TigerVNC password file from env var +if [ -n "${VNC_PASSWORD:-}" ]; then + mkdir -p /root/.vnc + printf '%s\n%s\n' "$VNC_PASSWORD" "$VNC_PASSWORD" | vncpasswd -f > /root/.vnc/passwd + chmod 600 /root/.vnc/passwd +fi + +# DNS fix for containers +cp /etc/resolv.conf /tmp/resolv.conf.bak 2>/dev/null || true +cp /etc/hosts /tmp/hosts.bak 2>/dev/null || true +umount /etc/resolv.conf 2>/dev/null || true +umount /etc/hosts 2>/dev/null || true +cat /tmp/resolv.conf.bak > /etc/resolv.conf 2>/dev/null || echo "nameserver 8.8.8.8" > /etc/resolv.conf +cat /tmp/hosts.bak > /etc/hosts 2>/dev/null || echo "127.0.0.1 localhost" > /etc/hosts + +# Enable IP forwarding +echo 1 > /proc/sys/net/ipv4/ip_forward +echo "[entrypoint] IP forwarding enabled" + +# Clean up stale X locks +rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true + +# Start VNC server +echo "[entrypoint] Starting TigerVNC server..." +mkdir -p /root/.vnc +vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no + +# Wait for VNC to start +sleep 2 + +# Start noVNC websockify +echo "[entrypoint] Starting noVNC on port ${NOVNC_PORT:-6080}..." +websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6080} localhost:5901 & + +# Keep container running +echo "[entrypoint] VNC ready. Tailing logs..." +tail -f /root/.vnc/*.log diff --git a/apps/cistech-tunnel/config.json b/apps/cistech-tunnel/config.json index d95ff64..ca201a7 100755 --- a/apps/cistech-tunnel/config.json +++ b/apps/cistech-tunnel/config.json @@ -4,7 +4,7 @@ "available": true, "short_desc": "Cistech VPN client container with noVNC.", "author": "alexz", - "port": 6902, + "port": 6080, "categories": [ "utilities", "network" diff --git a/apps/cistech-tunnel/docker-compose.json b/apps/cistech-tunnel/docker-compose.json index d69aa3b..f4e57ae 100755 --- a/apps/cistech-tunnel/docker-compose.json +++ b/apps/cistech-tunnel/docker-compose.json @@ -4,24 +4,28 @@ { "name": "cistech-tunnel", "image": "git.alexzaw.dev/alexz/cistech-vpn:latest", - "isMain": true, - "internalPort": 6902, - "privileged": true, - "capAdd": ["NET_ADMIN"], - "devices": ["/dev/net/tun"], "environment": [ { "key": "OC_URL", "value": "${OC_URL}" }, { "key": "OC_USER", "value": "${OC_USER}" }, { "key": "OC_PASSWORD", "value": "${OC_PASSWORD}" }, { "key": "OC_TOTP_SECRET", "value": "${OC_TOTP_SECRET}" }, { "key": "VNC_PASSWORD", "value": "${VNC_PASSWORD}" }, - { "key": "NOVNC_PORT", "value": "6902" } + { "key": "NOVNC_PORT", "value": "6080" }, + { "key": "TZ", "value": "${TZ}" } ], + "internalPort": 6080, "volumes": [ - { "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }, - { "hostPath": "${APP_DATA_DIR}", "containerPath": "/runtime" }, - { "hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared", "containerPath": "/shared" } - ] + { "hostPath": "${APP_DATA_DIR}/config", "containerPath": "/config", "readOnly": false }, + { "hostPath": "${APP_DATA_DIR}", "containerPath": "/runtime", "readOnly": false }, + { "hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared", "containerPath": "/shared", "readOnly": false }, + { "hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup", "containerPath": "/root/.vnc/xstartup", "readOnly": true } + ], + "stopGracePeriod": "30s", + "devices": ["/dev/net/tun"], + "privileged": true, + "capAdd": ["NET_ADMIN"], + "isMain": true, + "extraLabels": { "runtipi.managed": true } } ] } diff --git a/apps/cistech-tunnel/shared/openconnect-vpn b/apps/cistech-tunnel/shared/openconnect-vpn new file mode 100644 index 0000000..350fe64 --- /dev/null +++ b/apps/cistech-tunnel/shared/openconnect-vpn @@ -0,0 +1,563 @@ +#!/bin/bash + +# Cistech VPN Connection Script with OpenConnect SSO +# Usage: ./openconnect-vpn [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-s|--status] [--help] + +# Credentials from environment variables (set by runtipi) +OC_URL="${OC_URL:-}" +OC_USER="${OC_USER:-}" +OC_PASSWORD="${OC_PASSWORD:-}" +OC_TOTP_SECRET="${OC_TOTP_SECRET:-}" +OC_SERVERCERT="${OC_SERVERCERT:-}" +OC_INTERFACE="${OC_INTERFACE:-tun0}" +OC_AUTHGROUP="${OC_AUTHGROUP:-}" +OC_USERAGENT="${OC_USERAGENT:-}" +OC_EXTRA_ARGS="${OC_EXTRA_ARGS:-}" + +# Log directory +LOG_DIR="/var/log/openconnect-vpn" +LOG_RETENTION_DAYS=7 +mkdir -p "$LOG_DIR" 2>/dev/null + +# Get current log file (changes daily) +get_log_file() { + echo "$LOG_DIR/$(date '+%Y-%m-%d').log" +} + +# Cleanup old logs +cleanup_old_logs() { + find "$LOG_DIR" -name "*.log" -type f -mtime +$LOG_RETENTION_DAYS -delete 2>/dev/null +} + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +GRAY='\033[0;90m' +NC='\033[0m' + +# Flags +SKIP_AUTO_LOGIN=false +DO_CONNECT=false +DO_DISCONNECT=false + +# Logging function +log() { + local level="$1" + local msg="$2" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + local timestamp_short=$(date '+%H:%M:%S') + local log_file=$(get_log_file) + + local msg_plain=$(echo -e "$msg" | sed 's/\x1b\[[0-9;]*m//g') + echo "[$timestamp] [$level] $msg_plain" >> "$log_file" + + case $level in + INFO) echo -e "${GRAY}[$timestamp_short]${NC} ${GREEN}[INFO]${NC} $msg" ;; + WARN) echo -e "${GRAY}[$timestamp_short]${NC} ${YELLOW}[WARN]${NC} $msg" ;; + ERROR) echo -e "${GRAY}[$timestamp_short]${NC} ${RED}[ERROR]${NC} $msg" ;; + DEBUG) echo -e "${GRAY}[$timestamp_short]${NC} ${CYAN}[DEBUG]${NC} $msg" ;; + CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;; + *) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;; + esac +} + +# Run command with logging +run_cmd() { + local desc="$1" + shift + log CMD "$desc: $*" + output=$("$@" 2>&1) + local rc=$? + if [ -n "$output" ]; then + echo "$output" | while IFS= read -r line; do + echo -e " ${GRAY}|${NC} $line" + done + fi + return $rc +} + +# Print banner +print_banner() { + echo -e "${CYAN}========================================${NC}" + echo -e "${CYAN} Cistech VPN Connection Script ${NC}" + echo -e "${CYAN}========================================${NC}" + echo "" +} + +# Show help +show_help() { + echo -e "${CYAN}Cistech VPN Connection Script${NC}" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -c, --connect Start VPN connection and exit" + echo " -d, --disconnect Disconnect VPN and exit" + echo " -m, --menu Skip auto-connect, show menu directly" + echo " -r, --routes Show current routing table and exit" + echo " -s, --status Show VPN and network status and exit" + echo " --help Show this help message" +} + +# Auto-fetch server certificate pin +get_server_cert_pin() { + local url="$1" + local host=$(echo "$url" | sed -E 's|https?://([^/:]+).*|\1|') + local port=443 + + log DEBUG "Fetching certificate pin from $host:$port..." + + local pin=$(echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null | \ + openssl x509 -pubkey -noout 2>/dev/null | \ + openssl pkey -pubin -outform DER 2>/dev/null | \ + openssl dgst -sha256 -binary | \ + base64) + + if [[ -n "$pin" ]]; then + echo "pin-sha256:$pin" + else + log ERROR "Failed to fetch certificate from $host" + return 1 + fi +} + +# Get current TOTP +get_totp() { + if [ -n "$OC_TOTP_SECRET" ]; then + oathtool --totp -b "$OC_TOTP_SECRET" + fi +} + +# Check if VPN is connected +get_vpn_interface() { + ip link show "$OC_INTERFACE" 2>/dev/null | grep -q "UP" && echo "$OC_INTERFACE" +} + +get_vpn_ip() { + local iface=$(get_vpn_interface) + if [ -n "$iface" ]; then + ip addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 + fi +} + +get_container_ip() { + ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 +} + +# Check VPN status +check_vpn_status() { + local vpn_iface=$(get_vpn_interface) + if [ -n "$vpn_iface" ]; then + local vpn_ip=$(get_vpn_ip) + log INFO "VPN is ${GREEN}CONNECTED${NC}" + log DEBUG " Interface: $vpn_iface" + log DEBUG " VPN IP: $vpn_ip" + return 0 + else + log WARN "VPN is ${RED}NOT CONNECTED${NC}" + return 1 + fi +} + +# Setup keyring with TOTP secret +setup_keyring() { + if [[ -n "$OC_TOTP_SECRET" && -n "$OC_USER" ]]; then + python3 -c " +import keyring +keyring.set_password('openconnect-sso', 'totp/$OC_USER', '$OC_TOTP_SECRET'.upper()) +print('TOTP secret stored in keyring for $OC_USER') +" + fi +} + +# Kill openconnect processes +kill_vpn_processes() { + log INFO "Killing VPN processes..." + local killed=0 + + for pid in $(pgrep -f "openconnect" 2>/dev/null); do + log DEBUG "Killing openconnect (PID $pid)" + kill -9 "$pid" 2>/dev/null && ((killed++)) + done + + for pid in $(pgrep -f "openconnect-sso" 2>/dev/null); do + log DEBUG "Killing openconnect-sso (PID $pid)" + kill -9 "$pid" 2>/dev/null && ((killed++)) + done + + if [ $killed -eq 0 ]; then + log INFO "No VPN processes were running" + else + log INFO "Killed $killed process(es)" + sleep 1 + fi +} + +# Disconnect VPN +disconnect_vpn() { + log INFO "Disconnecting VPN..." + kill_vpn_processes + + if check_vpn_status; then + log WARN "VPN still appears connected" + return 1 + fi + + log INFO "VPN disconnected" + return 0 +} + +# Setup IP forwarding +setup_forwarding() { + log INFO "Setting up IP forwarding..." + + local vpn_iface=$(get_vpn_interface) + if [ -z "$vpn_iface" ]; then + log ERROR "No VPN interface found! Is VPN connected?" + return 1 + fi + + run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1 + + # NAT masquerade for traffic going through VPN + if ! iptables -t nat -C POSTROUTING -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then + run_cmd "Adding NAT masquerade" iptables -t nat -A POSTROUTING -o "$vpn_iface" -j MASQUERADE + fi + + # Trigger host routing service + log INFO "Triggering host routing service..." + touch /runtime/restart-routing + sleep 2 + if [ ! -f /runtime/restart-routing ]; then + log INFO "Host routing service restarted" + else + log WARN "Host watcher may not be running" + fi + + log INFO "Forwarding configured" +} + +# Show routing table +show_routes() { + echo -e "${CYAN}========================================${NC}" + echo -e "${CYAN} Current Routing Table ${NC}" + echo -e "${CYAN}========================================${NC}" + echo "" + echo -e "${GREEN}IPv4 Routes:${NC}" + ip -4 route show | while IFS= read -r line; do + if echo "$line" | grep -qE "tun[0-9]"; then + echo -e " ${YELLOW}$line${NC}" + else + echo " $line" + fi + done + echo "" + echo -e "${GREEN}VPN Interface:${NC}" + local vpn_iface=$(get_vpn_interface) + if [ -n "$vpn_iface" ]; then + ip addr show "$vpn_iface" 2>/dev/null | grep -E "inet|link" | while IFS= read -r line; do + echo " $line" + done + else + echo -e " ${RED}No VPN interface found${NC}" + fi +} + +# Show network status +show_network_status() { + log INFO "Current network status:" + + echo "" + log DEBUG "Network Interfaces:" + ip -4 addr show | grep -E "inet |^[0-9]+:" | while IFS= read -r line; do + echo -e " ${GRAY}|${NC} $line" + done + + echo "" + local vpn_iface=$(get_vpn_interface) + if [ -n "$vpn_iface" ]; then + local vpn_ip=$(get_vpn_ip) + log INFO "VPN Status: ${GREEN}CONNECTED${NC}" + log DEBUG " Interface: $vpn_iface" + log DEBUG " VPN IP: $vpn_ip" + else + log WARN "VPN Status: ${RED}NOT CONNECTED${NC}" + fi + + local container_ip=$(get_container_ip) + if [ -n "$container_ip" ]; then + log DEBUG "Container IP: $container_ip" + fi + + echo "" +} + +# Print TOTP +show_totp() { + if [ -z "$OC_TOTP_SECRET" ]; then + log ERROR "TOTP secret not configured" + return 1 + fi + + log INFO "Starting live TOTP display (Ctrl+C to stop)" + echo "" + while true; do + TOTP=$(get_totp) + SECONDS_LEFT=$((30 - ($(date +%s) % 30))) + echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) " + sleep 1 + done +} + +# Start VPN connection +start_vpn() { + local do_auto_login="$1" + log INFO "=== Starting OpenConnect VPN ===" + echo "" + + # Kill existing + kill_vpn_processes + + # Get server cert if not set + if [[ -z "$OC_SERVERCERT" ]]; then + log INFO "Fetching server certificate..." + OC_SERVERCERT=$(get_server_cert_pin "$OC_URL") + log DEBUG "Server cert: $OC_SERVERCERT" + fi + + # Setup keyring for TOTP + setup_keyring + + # Build openconnect command + OPENCONNECT_CMD="/usr/sbin/openconnect --protocol=anyconnect --servercert $OC_SERVERCERT --interface $OC_INTERFACE --script /usr/share/vpnc-scripts/vpnc-script" + [[ -n "$OC_AUTHGROUP" ]] && OPENCONNECT_CMD+=" --authgroup $OC_AUTHGROUP" + [[ -n "$OC_USERAGENT" ]] && OPENCONNECT_CMD+=" --useragent $OC_USERAGENT" + [[ -n "$OC_EXTRA_ARGS" ]] && OPENCONNECT_CMD+=" $OC_EXTRA_ARGS" + + # Determine SSO args + if [[ -n "$OC_USER" ]]; then + OC_SSO_ARGS="--browser-display-mode hidden -u $OC_USER" + else + OC_SSO_ARGS="--browser-display-mode shown" + fi + + # Show credentials + log INFO "Credentials:" + echo -e " ${CYAN}URL: $OC_URL${NC}" + echo -e " ${CYAN}User: $OC_USER${NC}" + if [ -n "$OC_PASSWORD" ]; then + echo -e " ${CYAN}Password: (set)${NC}" + fi + if [ -n "$OC_TOTP_SECRET" ]; then + TOTP=$(get_totp) + echo -e " ${CYAN}TOTP: $TOTP${NC}" + fi + echo "" + + log INFO "Launching openconnect-sso..." + + # Run openconnect-sso + if [[ -n "$OC_USER" && -n "$OC_PASSWORD" ]]; then + echo "$OC_PASSWORD" | openconnect-sso -s "$OC_URL" $OC_SSO_ARGS -- $OPENCONNECT_CMD & + elif [[ -n "$OC_USER" ]]; then + echo "" | openconnect-sso -s "$OC_URL" $OC_SSO_ARGS -- $OPENCONNECT_CMD & + else + openconnect-sso -s "$OC_URL" $OC_SSO_ARGS -- $OPENCONNECT_CMD & + fi + VPN_PID=$! + + # Wait for connection + log INFO "Waiting for VPN connection..." + local wait_count=0 + local max_wait=120 + while [ -z "$(get_vpn_interface)" ]; do + sleep 2 + ((wait_count+=2)) + if [ $((wait_count % 10)) -eq 0 ]; then + log DEBUG "Still waiting... (${wait_count}s)" + fi + if [ $wait_count -ge $max_wait ]; then + log ERROR "Timeout waiting for VPN after ${max_wait}s" + return 1 + fi + # Check if process died + if ! kill -0 $VPN_PID 2>/dev/null; then + log ERROR "openconnect-sso process died" + return 1 + fi + done + + log INFO "VPN connected!" + local vpn_ip=$(get_vpn_ip) + log DEBUG " Interface: $OC_INTERFACE" + log DEBUG " VPN IP: $vpn_ip" + + sleep 3 + setup_forwarding + + log INFO "VPN setup complete" + return 0 +} + +# Main menu +main_menu() { + echo -e "${GREEN}Options:${NC}" + echo -e " ${CYAN}1${NC} - Start VPN connection" + echo -e " ${CYAN}2${NC} - Disconnect VPN" + echo -e " ${CYAN}3${NC} - Show live TOTP" + echo -e " ${CYAN}4${NC} - Setup IP forwarding rules" + echo -e " ${CYAN}5${NC} - Show network status" + echo -e " ${CYAN}6${NC} - Show routing table" + echo -e " ${CYAN}7${NC} - Kill VPN processes" + echo -e " ${CYAN}q${NC} - Quit" + echo "" +} + +# Watchdog +start_watchdog() { + log INFO "Starting VPN watchdog (check every 60s)..." + + local check_interval=60 + local reconnect_attempts=0 + local max_reconnect_attempts=3 + + while true; do + sleep $check_interval + + local vpn_iface=$(get_vpn_interface) + + if [ -n "$vpn_iface" ]; then + reconnect_attempts=0 + else + ((reconnect_attempts++)) + log WARN "VPN disconnected! Attempt $reconnect_attempts/$max_reconnect_attempts..." + + if [ $reconnect_attempts -le $max_reconnect_attempts ]; then + kill_vpn_processes + sleep 2 + start_vpn "true" & + + local wait_count=0 + while [ -z "$(get_vpn_interface)" ] && [ $wait_count -lt 120 ]; do + sleep 2 + ((wait_count+=2)) + done + + if [ -n "$(get_vpn_interface)" ]; then + log INFO "VPN reconnected!" + reconnect_attempts=0 + else + log ERROR "Reconnect failed" + fi + else + log ERROR "Max reconnect attempts reached" + sleep 300 + reconnect_attempts=0 + fi + fi + done +} + +# Parse args +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -c|--connect) + DO_CONNECT=true + shift + ;; + -d|--disconnect) + DO_DISCONNECT=true + shift + ;; + -m|--menu) + SKIP_AUTO_LOGIN=true + shift + ;; + -r|--routes) + show_routes + exit 0 + ;; + -s|--status) + print_banner + check_vpn_status + echo "" + show_network_status + exit 0 + ;; + --help) + show_help + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac + done +} + +# Main +parse_args "$@" +cleanup_old_logs + +echo "" >> "$(get_log_file)" +log INFO "openconnect-vpn script started" +log DEBUG "OC_URL=$OC_URL" +log DEBUG "OC_USER=$OC_USER" + +print_banner + +if [ "$DO_DISCONNECT" = "true" ]; then + disconnect_vpn + exit $? +fi + +if [ "$DO_CONNECT" = "true" ]; then + start_vpn "true" + exit $? +fi + +# Check current status or auto-connect +if [ "$SKIP_AUTO_LOGIN" = "true" ]; then + log INFO "Menu mode - skipping auto-connect" +elif check_vpn_status; then + echo "" + log INFO "VPN already connected. Setting up forwarding..." + setup_forwarding +else + echo "" + log INFO "Auto-starting VPN connection..." + echo "" + if start_vpn "true"; then + # Start watchdog in background + start_watchdog & + WATCHDOG_PID=$! + log DEBUG "Watchdog started (PID $WATCHDOG_PID)" + fi +fi + +# Menu loop +while true; do + echo "" + main_menu + echo -ne "${CYAN}Choice: ${NC}" + read -r choice + echo "" + + [[ -z "${choice// }" ]] && continue + + case $choice in + 1) start_vpn "true" && { start_watchdog & } ;; + 2) disconnect_vpn ;; + 3) show_totp ;; + 4) setup_forwarding ;; + 5) show_network_status ;; + 6) show_routes ;; + 7) kill_vpn_processes ;; + q|Q) log INFO "Goodbye!"; exit 0 ;; + *) ;; + esac +done diff --git a/apps/cistech-tunnel/shared/xstartup b/apps/cistech-tunnel/shared/xstartup new file mode 100644 index 0000000..73985f6 --- /dev/null +++ b/apps/cistech-tunnel/shared/xstartup @@ -0,0 +1,39 @@ +#!/bin/bash +# VNC xstartup - launches terminal with openconnect-vpn script + +unset SESSION_MANAGER +unset DBUS_SESSION_BUS_ADDRESS + +# Import environment variables from container (PID 1) +# VNC doesn't inherit Docker env vars, so we source them here +while IFS= read -r -d '' line; do + export "$line" +done < /proc/1/environ + +export XDG_RUNTIME_DIR=/tmp/runtime-root +mkdir -p $XDG_RUNTIME_DIR +chmod 700 $XDG_RUNTIME_DIR + +# GPU/WebKit workarounds +export GDK_BACKEND=x11 +export WEBKIT_DISABLE_DMABUF_RENDERER=1 + +# Start dbus session +[ -x /usr/bin/dbus-launch ] && eval $(dbus-launch --sh-syntax --exit-with-session) + +# Start window manager +openbox & +sleep 2 + +# Disable screen blanking and power saving +xset s off 2>/dev/null || true +xset -dpms 2>/dev/null || true +xset s noblank 2>/dev/null || true + +# Make script executable and launch in terminal +chmod +x /shared/openconnect-vpn 2>/dev/null || true +xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 130x45+10+10 \ + -title "Cistech VPN Terminal" \ + -e "bash -c '/shared/openconnect-vpn; exec bash'" & + +wait diff --git a/apps/cistech-tunnel/source/Dockerfile b/apps/cistech-tunnel/source/Dockerfile deleted file mode 100755 index 36d6324..0000000 --- a/apps/cistech-tunnel/source/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM ubuntu:24.04 -ENV DEBIAN_FRONTEND=noninteractive \ - PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \ - VIRTUAL_ENV=/opt/venv \ - PATH=/opt/venv/bin:$PATH \ - QTWEBENGINE_DISABLE_SANDBOX=1 \ - QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" -# Credentials come from environment variables at runtime: -# OC_URL, OC_SERVERCERT, OC_USER, OC_TOTP_SECRET, VNC_PASSWORD - -RUN apt-get update && apt-get install -y \ - openconnect iproute2 iptables ca-certificates \ - python3 python3-pip python3-venv \ - vpnc-scripts curl wget openssh-client \ - x11vnc xvfb fluxbox novnc websockify xterm nano oathtool \ - xauth libnss3 libatk1.0-0 libatk-bridge2.0-0 \ - libx11-6 libx11-xcb1 libxcomposite1 libxrandr2 libgbm1 libxdamage1 \ - libpango-1.0-0 fonts-liberation \ - libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 \ - libxkbcommon0 libxkbcommon-x11-0 \ - libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render0 libxcb-render-util0 libxcb-shm0 libxcb-xfixes0 libxcb-xinerama0 libxcb-randr0 libxcb-glx0 \ - sudo && rm -rf /var/lib/apt/lists/* - -RUN apt-get update && (apt-get install -y libasound2t64 || apt-get install -y libasound2) && rm -rf /var/lib/apt/lists/* - -# Python venv + Playwright + openconnect-sso -RUN python3 -m venv "$VIRTUAL_ENV" -RUN pip install --no-cache-dir openconnect-sso playwright keyring keyrings.alt && \ - python -m playwright install --with-deps chromium - -# Cloudflared (amd64) -RUN arch=$(dpkg --print-architecture) && \ - if [ "$arch" = "amd64" ]; then \ - curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cloudflared.deb && \ - apt-get update && apt-get install -y /tmp/cloudflared.deb && rm -f /tmp/cloudflared.deb ; \ - else \ - echo "Install cloudflared manually for arch=$arch" && exit 1 ; \ - fi - -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh -EXPOSE 6901 -ENTRYPOINT ["/entrypoint.sh"] diff --git a/apps/cistech-tunnel/source/entrypoint.sh b/apps/cistech-tunnel/source/entrypoint.sh deleted file mode 100755 index 6eac090..0000000 --- a/apps/cistech-tunnel/source/entrypoint.sh +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${OC_URL:?OC_URL required}" - -# Auto-fetch server certificate pin if not provided -get_server_cert_pin() { - local url="$1" - local host=$(echo "$url" | sed -E 's|https?://([^/:]+).*|\1|') - local port=443 - - echo "Fetching certificate pin from $host:$port..." >&2 - - # Get certificate and compute pin-sha256 - local pin=$(echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null | \ - openssl x509 -pubkey -noout 2>/dev/null | \ - openssl pkey -pubin -outform DER 2>/dev/null | \ - openssl dgst -sha256 -binary | \ - base64) - - if [[ -n "$pin" ]]; then - echo "pin-sha256:$pin" - else - echo "ERROR: Failed to fetch certificate from $host" >&2 - return 1 - fi -} - -# Get or fetch OC_SERVERCERT -if [[ -z "${OC_SERVERCERT:-}" ]]; then - OC_SERVERCERT=$(get_server_cert_pin "$OC_URL") - echo "Auto-detected server cert: $OC_SERVERCERT" -fi - -NOVNC_PORT="${NOVNC_PORT:-6901}" -VNC_PASSWORD="${VNC_PASSWORD:-changeme}" -DISPLAY_ADDR="${DISPLAY:-:1}" -OC_INTERFACE="${OC_INTERFACE:-tun0}" -OC_USER="${OC_USER:-}" -OC_PASSWORD="${OC_PASSWORD:-}" -OC_TOTP_SECRET="${OC_TOTP_SECRET:-}" - -# Default to hidden browser if OC_USER is set -if [[ -n "$OC_USER" ]]; then - OC_SSO_ARGS_DEFAULT="--browser-display-mode hidden -u $OC_USER" -else - OC_SSO_ARGS_DEFAULT="--browser-display-mode shown" -fi - -CLOUDFLARED_MODE="${CLOUDFLARED_MODE:-off}" # off|token|config -CLOUDFLARED_TOKEN="${CLOUDFLARED_TOKEN:-}" -SSH_TUNNEL_ENABLE="${SSH_TUNNEL_ENABLE:-0}" -SSH_DEST="${SSH_DEST:-zawa@10.3.1.201}" -SSH_FORWARDS="${SSH_FORWARDS:-0.0.0.0:8090:localhost:8090}" - -pids=() - -# Setup keyring with TOTP secret if provided -setup_keyring() { - if [[ -n "$OC_TOTP_SECRET" && -n "$OC_USER" ]]; then - python3 -c " -import keyring -keyring.set_password('openconnect-sso', 'totp/$OC_USER', '$OC_TOTP_SECRET'.upper()) -print('TOTP secret stored in keyring for $OC_USER') -" - fi -} - -# Create vpn_connect command in PATH and save environment -create_vpn_command() { - # Save environment variables to a file - cat > /etc/vpn.env << ENVFILE -export OC_URL="$OC_URL" -export OC_SERVERCERT="$OC_SERVERCERT" -export OC_INTERFACE="$OC_INTERFACE" -export OC_USER="$OC_USER" -export OC_PASSWORD="$OC_PASSWORD" -export OC_SSO_ARGS_DEFAULT="$OC_SSO_ARGS_DEFAULT" -export OC_SSO_ARGS="${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT}" -export OC_AUTHGROUP="${OC_AUTHGROUP:-}" -export OC_USERAGENT="${OC_USERAGENT:-}" -export OC_EXTRA_ARGS="${OC_EXTRA_ARGS:-}" -export OC_TOTP_SECRET="$OC_TOTP_SECRET" -export DISPLAY=":1" -ENVFILE - - # Build openconnect command - OPENCONNECT_CMD="/usr/sbin/openconnect --protocol=anyconnect --servercert $OC_SERVERCERT --interface $OC_INTERFACE --script /usr/share/vpnc-scripts/vpnc-script" - [[ -n "${OC_AUTHGROUP:-}" ]] && OPENCONNECT_CMD+=" --authgroup $OC_AUTHGROUP" - [[ -n "${OC_USERAGENT:-}" ]] && OPENCONNECT_CMD+=" --useragent $OC_USERAGENT" - [[ -n "${OC_EXTRA_ARGS:-}" ]] && OPENCONNECT_CMD+=" ${OC_EXTRA_ARGS}" - echo "export OPENCONNECT_CMD=\"$OPENCONNECT_CMD\"" >> /etc/vpn.env - - cat > /usr/local/bin/vpn_connect << 'VPNCMD' -#!/usr/bin/env bash -source /etc/vpn.env -echo "[$(date)] Starting VPN connection..." - -# Set password for openconnect -export OPENCONNECT_PASSWORD="$OC_PASSWORD" - -# openconnect-sso reads TOTP from keyring automatically -# Pass password via stdin for SSO form if needed -if [[ -n "$OC_USER" && -n "$OC_PASSWORD" ]]; then - echo "$OC_PASSWORD" | openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD -elif [[ -n "$OC_USER" ]]; then - echo "" | openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD -else - openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD -fi -VPNCMD - chmod +x /usr/local/bin/vpn_connect -} - -# Create VPN runner script that keeps shell open -create_vpn_script() { - cat > /tmp/vpn-runner.sh << 'VPNSCRIPT' -#!/usr/bin/env bash -cd /root - -echo "============================================" -echo " Cistech VPN Container" -echo "============================================" -echo "" -echo "Commands:" -echo " vpn_connect - Start/restart VPN connection" -echo " Ctrl+C - Stop auto-reconnect and drop to shell" -echo "" -echo "Starting VPN with auto-reconnect..." -echo "" - -while true; do - vpn_connect - echo "" - echo "[$(date)] VPN disconnected. Reconnecting in 10 seconds..." - echo "(Press Ctrl+C to stop auto-reconnect)" - sleep 10 -done -VPNSCRIPT - chmod +x /tmp/vpn-runner.sh -} - -start_gui() { - mkdir -p /root/.vnc - x11vnc -storepasswd "$VNC_PASSWORD" /root/.vnc/pass >/dev/null 2>&1 || true - rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true - Xvfb "$DISPLAY_ADDR" -screen 0 ${XVFB_WxHxD:-1280x800x24} +extension RANDR & - pids+=($!) - sleep 0.5 - export DISPLAY="$DISPLAY_ADDR" - fluxbox >/tmp/fluxbox.log 2>&1 & - pids+=($!) - x11vnc -display "$DISPLAY_ADDR" -rfbauth /root/.vnc/pass -forever -shared -rfbport 5900 -quiet & - pids+=($!) - websockify --web=/usr/share/novnc/ 0.0.0.0:"$NOVNC_PORT" localhost:5900 >/tmp/websockify.log 2>&1 & - pids+=($!) -} - -start_vpn_terminal() { - # Start xterm with VPN script - sleep 1 - xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 120x35+50+50 \ - -T "Cistech VPN" -e /tmp/vpn-runner.sh & - pids+=($!) -} - -start_cloudflared() { - case "$CLOUDFLARED_MODE" in - token) - [ -n "$CLOUDFLARED_TOKEN" ] && cloudflared tunnel run --token "$CLOUDFLARED_TOKEN" >/tmp/cloudflared.log 2>&1 & - pids+=($!) - ;; - config) - cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run >/tmp/cloudflared.log 2>&1 & - pids+=($!) - ;; - off|*) - ;; - esac -} - -start_ssh_tunnel() { - [ "$SSH_TUNNEL_ENABLE" = "1" ] || return 0 - IFS=',' read -ra LINES <<< "$SSH_FORWARDS" - args=(-N -o StrictHostKeyChecking=no -o ServerAliveInterval=60) - for m in "${LINES[@]}"; do args+=(-L "$m"); done - ssh "${args[@]}" "$SSH_DEST" & - pids+=($!) -} - -setup_nat() { - ( - for i in {1..60}; do - if ip link show "$OC_INTERFACE" >/dev/null 2>&1; then - sysctl -w net.ipv4.ip_forward=1 >/dev/null - iptables -t nat -C POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE 2>/dev/null || \ - iptables -t nat -A POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE - echo "NAT enabled on $OC_INTERFACE" - - # Trigger host routing service - if [ -d /runtime ]; then - touch /runtime/restart-routing - echo "Host routing trigger sent" - fi - break - fi - sleep 2 - done - ) & -} - -trap 'kill 0' INT TERM - -# Always start GUI now -setup_keyring -create_vpn_command -create_vpn_script -start_gui -start_vpn_terminal -setup_nat -start_cloudflared -start_ssh_tunnel - -wait