From f410510a7ffd9d0b769a78685fd2133c798cf8e3 Mon Sep 17 00:00:00 2001 From: alexz Date: Sat, 17 Jan 2026 11:28:10 +0000 Subject: [PATCH] revert(cistech-tunnel): restore to working state at 5d54ed6 - Removed build/ folder - Restored source/ folder with original Dockerfile and entrypoint.sh - Reverted config files to original working state - Cleaned up shared/ to only contain host routing scripts Co-Authored-By: Claude Opus 4.5 --- apps/cistech-tunnel/build/.gitignore | 2 - apps/cistech-tunnel/build/Dockerfile | 80 ------- apps/cistech-tunnel/build/README.md | 51 ---- apps/cistech-tunnel/build/build.sh | 22 -- .../build/scripts/entrypoint.sh | 45 ---- apps/cistech-tunnel/config.json | 11 +- apps/cistech-tunnel/docker-compose.json | 25 +- apps/cistech-tunnel/docker-compose.yml | 26 +- apps/cistech-tunnel/shared/host-routing.sh | 52 ++-- .../shared/install-host-services.sh | 61 +++-- apps/cistech-tunnel/shared/openconnect-vpn | 209 ---------------- .../shared/uninstall-host-services.sh | 14 +- apps/cistech-tunnel/shared/xstartup | 39 --- apps/cistech-tunnel/source/Dockerfile | 43 ++++ apps/cistech-tunnel/source/entrypoint.sh | 224 ++++++++++++++++++ 15 files changed, 356 insertions(+), 548 deletions(-) delete mode 100644 apps/cistech-tunnel/build/.gitignore delete mode 100644 apps/cistech-tunnel/build/Dockerfile delete mode 100644 apps/cistech-tunnel/build/README.md delete mode 100644 apps/cistech-tunnel/build/build.sh delete mode 100644 apps/cistech-tunnel/build/scripts/entrypoint.sh delete mode 100755 apps/cistech-tunnel/shared/openconnect-vpn delete mode 100644 apps/cistech-tunnel/shared/xstartup create mode 100755 apps/cistech-tunnel/source/Dockerfile create mode 100755 apps/cistech-tunnel/source/entrypoint.sh diff --git a/apps/cistech-tunnel/build/.gitignore b/apps/cistech-tunnel/build/.gitignore deleted file mode 100644 index 0746077..0000000 --- a/apps/cistech-tunnel/build/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Large binary files - track tar.gz but not 7z -*.7z diff --git a/apps/cistech-tunnel/build/Dockerfile b/apps/cistech-tunnel/build/Dockerfile deleted file mode 100644 index a953579..0000000 --- a/apps/cistech-tunnel/build/Dockerfile +++ /dev/null @@ -1,80 +0,0 @@ -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 - -# VNC/noVNC settings -ENV DISPLAY=:1 -ENV VNC_PORT=5901 -ENV NOVNC_PORT=6092 - -# Python/Playwright settings -ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright -ENV VIRTUAL_ENV=/opt/venv -ENV PATH=/opt/venv/bin:$PATH - -# Install dependencies (no systemd) -RUN apt-get update && apt-get install -y \ - dbus \ - dbus-x11 \ - libgtk-3-0 \ - libglib2.0-0 \ - libstdc++6 \ - iptables \ - libxml2 \ - zlib1g \ - policykit-1 \ - xdg-utils \ - libwebkit2gtk-4.0-37 \ - tigervnc-standalone-server \ - tigervnc-common \ - novnc \ - websockify \ - openbox \ - xterm \ - procps \ - net-tools \ - curl \ - iproute2 \ - iputils-ping \ - nano \ - x11vnc \ - xvfb \ - fluxbox \ - xdotool \ - oathtool \ - openconnect \ - python3 \ - python3-pip \ - python3-venv \ - vpnc-scripts \ - libasound2 \ - libnss3 \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libxcomposite1 \ - libxrandr2 \ - libgbm1 \ - libxdamage1 \ - libpango-1.0-0 \ - fonts-liberation \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install openconnect-sso with playwright -RUN python3 -m venv "$VIRTUAL_ENV" && \ - pip install --no-cache-dir openconnect-sso[full] playwright keyring keyrings.alt && \ - python -m playwright install --with-deps chromium - -RUN mkdir -p /opt/scripts /shared - -# Copy entrypoint script -COPY scripts/entrypoint.sh /opt/scripts/ -RUN chmod +x /opt/scripts/entrypoint.sh - -EXPOSE 5901 6092 - -CMD ["/opt/scripts/entrypoint.sh"] diff --git a/apps/cistech-tunnel/build/README.md b/apps/cistech-tunnel/build/README.md deleted file mode 100644 index 79aefec..0000000 --- a/apps/cistech-tunnel/build/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Rego Tunnel - Build Files - -This directory contains the Dockerfile and scripts to build the Cisco VPN Docker image. - -## Files - -- `Dockerfile` - Docker image definition (Ubuntu 22.04 + Cisco Secure Client + noVNC) -- `cisco-secure-client-full.tar.gz` - Pre-extracted Cisco Secure Client 5.1.14.145 -- `build.sh` - Build and push script -- `scripts/entrypoint.sh` - Container entrypoint (starts systemd) - -## Building - -```bash -cd /etc/runtipi/repos/runtipi/apps/rego-tunnel/build -./build.sh -``` - -This builds and pushes to `git.alexzaw.dev/alexz/cisco-vpn:latest` - -To build without pushing: -```bash -docker build -t git.alexzaw.dev/alexz/cisco-vpn:latest . -``` - -## What's in the image - -The Dockerfile creates an image with: -- Ubuntu 22.04 with systemd -- Cisco Secure Client 5.1.14.145 (VPN, DART, Posture modules) -- TigerVNC server + noVNC (web-based VNC) -- Tools: xdotool, oathtool (for TOTP), xclip, openbox - -### Systemd services (baked in) -- `vpnagentd.service` - Cisco VPN agent -- `vnc.service` - VNC server + noVNC websockify - -### Scripts (baked in via base64 in Dockerfile) -- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC -- `/opt/scripts/entrypoint.sh` - Container entrypoint - -## Runtime mounts (from shared/) - -When running as rego-tunnel app, these are mounted from `shared/`: -- `/shared/cisco-vpn` - Main VPN automation script -- `/shared/xstartup` → `/root/.vnc/xstartup` - VNC session startup - -## Ports - -- `5901` - VNC server -- `6080` - noVNC web interface diff --git a/apps/cistech-tunnel/build/build.sh b/apps/cistech-tunnel/build/build.sh deleted file mode 100644 index 380e0ed..0000000 --- a/apps/cistech-tunnel/build/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# Build and push the Cistech VPN Docker image -# Run this from the build directory - -set -euo pipefail - -IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/cistech-vpn}" -IMAGE_TAG="${IMAGE_TAG:-latest}" - -echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..." - -docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" . -docker push "${IMAGE_NAME}:${IMAGE_TAG}" - -echo "" -echo "Build complete!" -echo "" -echo "To test locally:" -echo " docker run -d --privileged --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6092:6092 ${IMAGE_NAME}:${IMAGE_TAG}" -echo "" -echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6092/vnc.html" -echo "" \ No newline at end of file diff --git a/apps/cistech-tunnel/build/scripts/entrypoint.sh b/apps/cistech-tunnel/build/scripts/entrypoint.sh deleted file mode 100644 index b5b5e92..0000000 --- a/apps/cistech-tunnel/build/scripts/entrypoint.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# Entrypoint: VNC password setup + DNS fix + direct VNC start (no systemd) - -set -euo pipefail - -export HOME='/root' -export USER='root' -export DISPLAY=:1 - -# Setup TigerVNC password file from env var (passed by runtipi) -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 -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 VNC locks -rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true - -# Start TigerVNC server -echo "[entrypoint] Starting TigerVNC server on display :1..." -vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no - -sleep 2 - -# Start noVNC via websockify -echo "[entrypoint] Starting noVNC on port ${NOVNC_PORT:-6092}..." -websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6092} localhost:5901 & - -echo "[entrypoint] VNC ready. Use noVNC at http://localhost:${NOVNC_PORT:-6092}/vnc.html" - -# Keep container running and tail logs -tail -f /root/.vnc/*.log 2>/dev/null || tail -f /dev/null diff --git a/apps/cistech-tunnel/config.json b/apps/cistech-tunnel/config.json index b21d4a2..d95ff64 100755 --- a/apps/cistech-tunnel/config.json +++ b/apps/cistech-tunnel/config.json @@ -4,13 +4,13 @@ "available": true, "short_desc": "Cistech VPN client container with noVNC.", "author": "alexz", - "port": 6092, + "port": 6902, "categories": [ "utilities", "network" ], "description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.", - "tipi_version": 4, + "tipi_version": 1, "version": "latest", "source": "local", "exposable": true, @@ -48,13 +48,6 @@ "type": "password", "env_variable": "VNC_PASSWORD", "required": true - }, - { - "label": "Target IP", - "type": "text", - "env_variable": "TARGET_IP", - "required": false, - "hint": "IP address to route through VPN (for host routing)" } ], "supported_architectures": [ diff --git a/apps/cistech-tunnel/docker-compose.json b/apps/cistech-tunnel/docker-compose.json index 49a6545..d69aa3b 100755 --- a/apps/cistech-tunnel/docker-compose.json +++ b/apps/cistech-tunnel/docker-compose.json @@ -4,29 +4,24 @@ { "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": "6092" }, - { "key": "TZ", "value": "${TZ}" }, - { "key": "TARGET_IP", "value": "${TARGET_IP}" } + { "key": "NOVNC_PORT", "value": "6902" } ], - "internalPort": 6092, "volumes": [ - { "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 } + { "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }, + { "hostPath": "${APP_DATA_DIR}", "containerPath": "/runtime" }, + { "hostPath": "/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared", "containerPath": "/shared" } + ] } ] } diff --git a/apps/cistech-tunnel/docker-compose.yml b/apps/cistech-tunnel/docker-compose.yml index 6c3d5ef..e4778df 100755 --- a/apps/cistech-tunnel/docker-compose.yml +++ b/apps/cistech-tunnel/docker-compose.yml @@ -1,6 +1,6 @@ services: cistech-tunnel: - image: git.alexzaw.dev/alexz/cistech-vpn:latest + image: cistech-vpn:latest restart: unless-stopped networks: cistech-tunnel_runtipi_network: @@ -9,32 +9,20 @@ services: gw_priority: 1 environment: OC_URL: ${OC_URL} + OC_SERVERCERT: ${OC_SERVERCERT} OC_USER: ${OC_USER} - OC_PASSWORD: ${OC_PASSWORD} - OC_TOTP_SECRET: ${OC_TOTP_SECRET} VNC_PASSWORD: ${VNC_PASSWORD} - NOVNC_PORT: "6092" - TZ: ${TZ} - TARGET_IP: ${TARGET_IP} + NOVNC_PORT: "6902" ports: - - ${APP_PORT}:6092 + - ${APP_PORT}:6902 volumes: - - ${APP_DATA_DIR}/config:/config - - ${APP_DATA_DIR}:/runtime - - /etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared:/shared - - /etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/xstartup:/root/.vnc/xstartup:ro - devices: - - /dev/net/tun - privileged: true - cap_add: - - NET_ADMIN - stop_grace_period: 30s + - ${APP_DATA_DIR}/data:/root labels: generated: true traefik.enable: true traefik.docker.network: runtipi_tipi_main_network traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https - traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6092" + traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6902" traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`) traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi @@ -43,4 +31,4 @@ services: traefik.http.routers.cistech-tunnel-runtipi.entrypoints: websecure traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver - runtipi.managed: true + runtipi.managed: true \ No newline at end of file diff --git a/apps/cistech-tunnel/shared/host-routing.sh b/apps/cistech-tunnel/shared/host-routing.sh index 537ea41..c796301 100644 --- a/apps/cistech-tunnel/shared/host-routing.sh +++ b/apps/cistech-tunnel/shared/host-routing.sh @@ -1,16 +1,16 @@ #!/usr/bin/env bash # # Host routing script for cistech-tunnel -# Routes TARGET_IP through the VPN container +# Routes target subnets through the VPN container # set -euo pipefail ACTION="${1:-start}" -# Fixed configuration (we assigned these) +# Fixed configuration CONTAINER_IP="172.30.0.10" BRIDGE_NAME="br-vpn-static" -TARGET_IP="${TARGET_IP:-}" +TARGET_SUBNETS="10.3.1.0/24 10.255.255.0/24" LAN_SUBNET="192.168.0.0/23" LAN_INTERFACES="eth0 eth1 wlan0" LOG_FILE="/var/log/cistech-routing.log" @@ -25,12 +25,10 @@ get_lan_interface() { } remove_routes() { - log "Removing stale routes for $TARGET_IP..." - - # Remove any existing route to TARGET_IP - ip route del "$TARGET_IP" 2>/dev/null || true - ip route del "$TARGET_IP/32" 2>/dev/null || true - + log "Removing stale routes..." + for subnet in $TARGET_SUBNETS; do + ip route del "$subnet" 2>/dev/null || true + done log "Stale routes removed" } @@ -41,35 +39,35 @@ apply_routes() { log "Applying host routing rules..." log " Container IP: $CONTAINER_IP" log " Bridge: $BRIDGE_NAME" - log " Target IP: $TARGET_IP" + log " Target subnets: $TARGET_SUBNETS" log " LAN interface: ${lan_if:-unknown}" # Enable IP forwarding echo 1 > /proc/sys/net/ipv4/ip_forward log "IP forwarding enabled" - # Add route to TARGET_IP via container - ip route replace "$TARGET_IP/32" via "$CONTAINER_IP" dev "$BRIDGE_NAME" - log "Route added: $TARGET_IP via $CONTAINER_IP dev $BRIDGE_NAME" + # Add routes to target subnets via container + for subnet in $TARGET_SUBNETS; do + ip route replace "$subnet" via "$CONTAINER_IP" dev "$BRIDGE_NAME" + log "Route added: $subnet via $CONTAINER_IP dev $BRIDGE_NAME" + done # Allow forwarding in DOCKER-USER chain for all LAN interfaces for lan_if in $LAN_INTERFACES; do - # Check if interface exists if ip link show "$lan_if" &>/dev/null; then - # Allow traffic from LAN to container for TARGET_IP - iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_IP" -j ACCEPT 2>/dev/null || \ - iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_IP" -j ACCEPT + # Allow traffic from LAN to container bridge + iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -j ACCEPT 2>/dev/null || \ + iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -j ACCEPT # Allow return traffic - iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_IP" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ - iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_IP" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -m state --state RELATED,ESTABLISHED -j ACCEPT log "DOCKER-USER iptables rules added for $lan_if <-> $BRIDGE_NAME" fi done - # Masquerade traffic from LAN subnet to VPN bridge (so return traffic routes correctly) - # Use nft since iptables-nft backend doesn't support iptables -t nat commands + # Masquerade traffic from LAN subnet to VPN bridge (using nft) if ! nft list chain ip nat POSTROUTING 2>/dev/null | grep -q "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade"; then nft add rule ip nat POSTROUTING ip saddr "$LAN_SUBNET" oifname "$BRIDGE_NAME" counter masquerade log "NAT masquerade rule added for $LAN_SUBNET -> $BRIDGE_NAME" @@ -77,19 +75,21 @@ apply_routes() { log "NAT masquerade rule already exists for $LAN_SUBNET -> $BRIDGE_NAME" fi - log "OK: Host routing applied - $TARGET_IP via $CONTAINER_IP ($BRIDGE_NAME)" + log "OK: Host routing applied" } remove_all() { log "Removing all routing rules..." - # Remove route - ip route del "$TARGET_IP/32" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true + # Remove routes + for subnet in $TARGET_SUBNETS; do + ip route del "$subnet" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true + done # Remove iptables rules for all LAN interfaces for lan_if in $LAN_INTERFACES; do - iptables -D DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_IP" -j ACCEPT 2>/dev/null || true - iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_IP" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true + iptables -D DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -j ACCEPT 2>/dev/null || true + iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true done # Remove masquerade rule (using nft) diff --git a/apps/cistech-tunnel/shared/install-host-services.sh b/apps/cistech-tunnel/shared/install-host-services.sh index 56e09ac..8ad7fc7 100644 --- a/apps/cistech-tunnel/shared/install-host-services.sh +++ b/apps/cistech-tunnel/shared/install-host-services.sh @@ -1,55 +1,68 @@ #!/usr/bin/env bash # -# Install host-side systemd services for rego-tunnel -# Run this ONCE on the host after installing the app in Runtipi +# Install host-side systemd services for cistech-tunnel +# Run this ONCE on the host after app install # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -APP_DATA_DIR="/etc/runtipi/app-data/runtipi/rego-tunnel" +APP_DATA_DIR="/etc/runtipi/app-data/runtipi/cistech-tunnel" -echo "Installing rego-tunnel host services..." +echo "Installing cistech-tunnel host services..." -# Create the path watcher unit -cat << 'EOF' | sudo tee /etc/systemd/system/rego-routing-watcher.path +# Create app-data directory for trigger file +sudo mkdir -p "$APP_DATA_DIR" + +# Create the path unit (watches for trigger file) +sudo tee /etc/systemd/system/cistech-routing-watcher.path > /dev/null << EOF [Unit] -Description=Watch for rego-tunnel routing trigger +Description=Watch for cistech-tunnel routing trigger [Path] -PathExists=/etc/runtipi/app-data/runtipi/rego-tunnel/restart-routing -Unit=rego-routing-watcher.service +PathExists=$APP_DATA_DIR/restart-routing +Unit=cistech-routing-watcher.service [Install] WantedBy=multi-user.target EOF -# Create the service unit -cat << EOF | sudo tee /etc/systemd/system/rego-routing-watcher.service +# Create the service unit (applies routes when triggered) +sudo tee /etc/systemd/system/cistech-routing-watcher.service > /dev/null << EOF [Unit] -Description=Apply rego-tunnel routing rules +Description=Apply cistech-tunnel routing rules After=docker.service [Service] Type=oneshot -ExecStart=${SCRIPT_DIR}/host-routing.sh restart -ExecStartPost=/bin/rm -f ${APP_DATA_DIR}/restart-routing -ExecStartPost=/bin/bash -c 'echo "trigger cleared at \$(date)" >> ${APP_DATA_DIR}/watcher.log' +ExecStart=$SCRIPT_DIR/host-routing.sh restart +ExecStartPost=/bin/rm -f $APP_DATA_DIR/restart-routing +ExecStartPost=/bin/bash -c 'echo "trigger cleared at \$(date)" >> $APP_DATA_DIR/watcher.log' EOF # Make host-routing.sh executable -sudo chmod +x "${SCRIPT_DIR}/host-routing.sh" +chmod +x "$SCRIPT_DIR/host-routing.sh" # Reload systemd and enable the watcher sudo systemctl daemon-reload -sudo systemctl enable --now rego-routing-watcher.path +sudo systemctl enable cistech-routing-watcher.path +sudo systemctl start cistech-routing-watcher.path + +# Disable the old boot-only service if it exists +if systemctl is-enabled cistech-routing.service &>/dev/null; then + echo "Disabling old cistech-routing.service (replaced by watcher)..." + sudo systemctl stop cistech-routing.service 2>/dev/null || true + sudo systemctl disable cistech-routing.service 2>/dev/null || true +fi + +# Apply routes now +echo "Applying initial routes..." +sudo "$SCRIPT_DIR/host-routing.sh" start echo "" -echo "Done! Services installed:" -echo " - rego-routing-watcher.path (watches for trigger file)" -echo " - rego-routing-watcher.service (applies routing rules)" +echo "Done! Watcher installed and routes applied." echo "" -echo "To check status:" -echo " systemctl status rego-routing-watcher.path" +echo "To trigger route refresh from container:" +echo " touch /runtime/restart-routing" echo "" -echo "To manually trigger routing:" -echo " touch ${APP_DATA_DIR}/restart-routing" +echo "To check watcher status:" +echo " systemctl status cistech-routing-watcher.path" diff --git a/apps/cistech-tunnel/shared/openconnect-vpn b/apps/cistech-tunnel/shared/openconnect-vpn deleted file mode 100755 index 7abddb4..0000000 --- a/apps/cistech-tunnel/shared/openconnect-vpn +++ /dev/null @@ -1,209 +0,0 @@ -#!/bin/bash - -# Cistech VPN Connection Script with OpenConnect SSO -# Uses config.toml for configuration, openconnect-sso handles auto-connect - -# Credentials from environment variables -OC_URL="${OC_URL:-}" -OC_USER="${OC_USER:-}" -OC_PASSWORD="${OC_PASSWORD:-}" -OC_TOTP_SECRET="${OC_TOTP_SECRET:-}" -OC_INTERFACE="${OC_INTERFACE:-tun0}" - -CONFIG_DIR="$HOME/.config/openconnect-sso" -CONFIG_FILE="$CONFIG_DIR/config.toml" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -GRAY='\033[0;90m' -NC='\033[0m' - -print_banner() { - echo -e "${CYAN}========================================${NC}" - echo -e "${CYAN} Cistech VPN Connection Script ${NC}" - echo -e "${CYAN}========================================${NC}" - echo "" -} - -log() { - local level="$1" - local msg="$2" - local ts=$(date '+%H:%M:%S') - case $level in - INFO) echo -e "${GRAY}[$ts]${NC} ${GREEN}[INFO]${NC} $msg" ;; - WARN) echo -e "${GRAY}[$ts]${NC} ${YELLOW}[WARN]${NC} $msg" ;; - ERROR) echo -e "${GRAY}[$ts]${NC} ${RED}[ERROR]${NC} $msg" ;; - DEBUG) echo -e "${GRAY}[$ts]${NC} ${CYAN}[DEBUG]${NC} $msg" ;; - *) echo -e "${GRAY}[$ts]${NC} $msg" ;; - esac -} - -get_totp() { - [ -n "$OC_TOTP_SECRET" ] && oathtool --totp -b "$OC_TOTP_SECRET" -} - -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) - [ -n "$iface" ] && ip addr show "$iface" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 -} - -check_vpn_status() { - local vpn_iface=$(get_vpn_interface) - if [ -n "$vpn_iface" ]; then - log INFO "VPN: ${GREEN}CONNECTED${NC} ($(get_vpn_ip))" - return 0 - else - log WARN "VPN: ${RED}NOT CONNECTED${NC}" - return 1 - fi -} - -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()) -" 2>/dev/null && log INFO "TOTP stored in keyring" - fi -} - -setup_config() { - mkdir -p "$CONFIG_DIR" - - # Determine browser display mode - local browser_mode="hidden" - [ -z "$OC_USER" ] && browser_mode="shown" - - cat > "$CONFIG_FILE" << EOF -[openconnect] -server = "$OC_URL" -interface = "$OC_INTERFACE" - -[sso] -browser_display_mode = "$browser_mode" -EOF - - [ -n "$OC_USER" ] && echo "user = \"$OC_USER\"" >> "$CONFIG_FILE" - - log INFO "Config written to $CONFIG_FILE" -} - -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!" - return 1 - fi - sysctl -w net.ipv4.ip_forward=1 >/dev/null - iptables -t nat -C POSTROUTING -o "$vpn_iface" -j MASQUERADE 2>/dev/null || \ - iptables -t nat -A POSTROUTING -o "$vpn_iface" -j MASQUERADE - touch /runtime/restart-routing 2>/dev/null - log INFO "Forwarding configured" -} - -kill_vpn() { - log INFO "Stopping VPN..." - pkill -f "openconnect" 2>/dev/null - sleep 1 -} - -start_vpn() { - log INFO "Starting OpenConnect SSO..." - - setup_keyring - setup_config - - log INFO "Credentials:" - echo -e " ${CYAN}URL: $OC_URL${NC}" - echo -e " ${CYAN}User: $OC_USER${NC}" - [ -n "$OC_TOTP_SECRET" ] && echo -e " ${CYAN}TOTP: $(get_totp)${NC}" - echo "" - - # Run openconnect-sso (it handles reconnection) - if [[ -n "$OC_PASSWORD" ]]; then - echo "$OC_PASSWORD" | openconnect-sso - else - echo "" | openconnect-sso - fi -} - -show_totp() { - [ -z "$OC_TOTP_SECRET" ] && { log ERROR "No TOTP secret"; return 1; } - log INFO "Live TOTP (Ctrl+C to stop)" - while true; do - echo -ne "\r TOTP: ${GREEN}$(get_totp)${NC} (${YELLOW}$((30 - $(date +%s) % 30))s${NC}) " - sleep 1 - done -} - -main_menu() { - echo -e "${GREEN}Options:${NC}" - echo -e " ${CYAN}1${NC} - Start VPN" - echo -e " ${CYAN}2${NC} - Stop VPN" - echo -e " ${CYAN}3${NC} - Show TOTP" - echo -e " ${CYAN}4${NC} - Setup forwarding" - echo -e " ${CYAN}5${NC} - Check status" - echo -e " ${CYAN}6${NC} - Show routes" - echo -e " ${CYAN}q${NC} - Quit" - echo "" -} - -# Parse args -for arg in "$@"; do - case $arg in - -m|--menu) SKIP_AUTO=true ;; - -s|--status) check_vpn_status; exit 0 ;; - --help) echo "Usage: $0 [-m|--menu] [-s|--status]"; exit 0 ;; - esac -done - -# Main -print_banner -log INFO "openconnect-vpn started" - -if [ "${SKIP_AUTO:-false}" = "true" ]; then - log INFO "Menu mode - skipping auto-connect" -elif check_vpn_status; then - setup_forwarding -else - log INFO "Auto-starting VPN..." - start_vpn & - VPN_PID=$! - - # Wait for connection - for i in {1..60}; do - sleep 2 - if [ -n "$(get_vpn_interface)" ]; then - log INFO "VPN connected!" - setup_forwarding - break - fi - done -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 ;; - 2) kill_vpn ;; - 3) show_totp ;; - 4) setup_forwarding ;; - 5) check_vpn_status ;; - 6) ip -4 route show ;; - q|Q) log INFO "Goodbye!"; exit 0 ;; - esac -done diff --git a/apps/cistech-tunnel/shared/uninstall-host-services.sh b/apps/cistech-tunnel/shared/uninstall-host-services.sh index b76a920..743b62b 100644 --- a/apps/cistech-tunnel/shared/uninstall-host-services.sh +++ b/apps/cistech-tunnel/shared/uninstall-host-services.sh @@ -1,21 +1,21 @@ #!/usr/bin/env bash # -# Uninstall host-side systemd services for rego-tunnel +# Uninstall host-side systemd services for cistech-tunnel # set -euo pipefail -echo "Removing rego-tunnel host services..." +echo "Removing cistech-tunnel host services..." # Stop and disable the watcher -sudo systemctl stop rego-routing-watcher.path 2>/dev/null || true -sudo systemctl disable rego-routing-watcher.path 2>/dev/null || true +sudo systemctl stop cistech-routing-watcher.path 2>/dev/null || true +sudo systemctl disable cistech-routing-watcher.path 2>/dev/null || true # Remove routing rules -/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/host-routing.sh stop 2>/dev/null || true +/etc/runtipi/repos/runtipi/apps/cistech-tunnel/shared/host-routing.sh stop 2>/dev/null || true # Remove systemd units -sudo rm -f /etc/systemd/system/rego-routing-watcher.path -sudo rm -f /etc/systemd/system/rego-routing-watcher.service +sudo rm -f /etc/systemd/system/cistech-routing-watcher.path +sudo rm -f /etc/systemd/system/cistech-routing-watcher.service # Reload systemd sudo systemctl daemon-reload diff --git a/apps/cistech-tunnel/shared/xstartup b/apps/cistech-tunnel/shared/xstartup deleted file mode 100644 index 5542a79..0000000 --- a/apps/cistech-tunnel/shared/xstartup +++ /dev/null @@ -1,39 +0,0 @@ -#!/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) -# Systemd services don'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 for browser -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 new file mode 100755 index 0000000..36d6324 --- /dev/null +++ b/apps/cistech-tunnel/source/Dockerfile @@ -0,0 +1,43 @@ +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 new file mode 100755 index 0000000..6eac090 --- /dev/null +++ b/apps/cistech-tunnel/source/entrypoint.sh @@ -0,0 +1,224 @@ +#!/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