Restructure cistech-tunnel to match rego-tunnel pattern
Some checks failed
Test / test (push) Has been cancelled

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-01-17 10:36:41 +00:00
parent 5d54ed6f80
commit 3c427af6fe
8 changed files with 764 additions and 278 deletions

View File

@@ -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"]

View File

@@ -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

View File

@@ -4,7 +4,7 @@
"available": true, "available": true,
"short_desc": "Cistech VPN client container with noVNC.", "short_desc": "Cistech VPN client container with noVNC.",
"author": "alexz", "author": "alexz",
"port": 6902, "port": 6080,
"categories": [ "categories": [
"utilities", "utilities",
"network" "network"

View File

@@ -4,24 +4,28 @@
{ {
"name": "cistech-tunnel", "name": "cistech-tunnel",
"image": "git.alexzaw.dev/alexz/cistech-vpn:latest", "image": "git.alexzaw.dev/alexz/cistech-vpn:latest",
"isMain": true,
"internalPort": 6902,
"privileged": true,
"capAdd": ["NET_ADMIN"],
"devices": ["/dev/net/tun"],
"environment": [ "environment": [
{ "key": "OC_URL", "value": "${OC_URL}" }, { "key": "OC_URL", "value": "${OC_URL}" },
{ "key": "OC_USER", "value": "${OC_USER}" }, { "key": "OC_USER", "value": "${OC_USER}" },
{ "key": "OC_PASSWORD", "value": "${OC_PASSWORD}" }, { "key": "OC_PASSWORD", "value": "${OC_PASSWORD}" },
{ "key": "OC_TOTP_SECRET", "value": "${OC_TOTP_SECRET}" }, { "key": "OC_TOTP_SECRET", "value": "${OC_TOTP_SECRET}" },
{ "key": "VNC_PASSWORD", "value": "${VNC_PASSWORD}" }, { "key": "VNC_PASSWORD", "value": "${VNC_PASSWORD}" },
{ "key": "NOVNC_PORT", "value": "6902" } { "key": "NOVNC_PORT", "value": "6080" },
{ "key": "TZ", "value": "${TZ}" }
], ],
"internalPort": 6080,
"volumes": [ "volumes": [
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }, { "hostPath": "${APP_DATA_DIR}/config", "containerPath": "/config", "readOnly": false },
{ "hostPath": "${APP_DATA_DIR}", "containerPath": "/runtime" }, { "hostPath": "${APP_DATA_DIR}", "containerPath": "/runtime", "readOnly": false },
{ "hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared", "containerPath": "/shared" } { "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 }
} }
] ]
} }

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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