diff --git a/apps/cistech-tunnel/.env.example b/apps/cistech-tunnel/.env.example deleted file mode 100755 index c583019..0000000 --- a/apps/cistech-tunnel/.env.example +++ /dev/null @@ -1,11 +0,0 @@ -# Required -OC_URL=https://vpn.cistech.net/Employees -OC_SERVERCERT=pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0= - -# Optional -OC_AUTHGROUP= -OC_SSO_ARGS=--browser-display-mode shown -VNC_PASSWORD=vpnSSO12 -NOVNC_PORT=6901 -PUBLISH_ADDR=0.0.0.0 -SSH_KEY_PATH=/home/alexz/.ssh/id_ed25519-lenovo diff --git a/apps/cistech-tunnel/README.md b/apps/cistech-tunnel/README.md deleted file mode 100755 index 46cddcd..0000000 --- a/apps/cistech-tunnel/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Cistech Tunnel - -OpenConnect-SSO VPN client running in a container with noVNC for browser-based access. - -## Features - -- **OpenConnect-SSO**: Cisco AnyConnect VPN with SSO/SAML authentication -- **TOTP Support**: Automatic 2FA via keyring integration -- **Auto-reconnect**: Automatically reconnects on disconnection -- **noVNC**: Browser-based VNC access on port 6902 -- **NAT/Masquerade**: Routes traffic through VPN tunnel -- **Cloudflared**: Optional Cloudflare tunnel support -- **SSH Tunnels**: Optional SSH port forwarding - -## Runtipi Installation - -1. Install from the app store or custom repo -2. Configure the required environment variables -3. Start the app via Runtipi dashboard - -## First-time SSO Login - -1. Open noVNC at `http://:6902` -2. Enter VNC password -3. Complete SSO login in the browser window -4. VPN will connect and auto-reconnect on disconnect - -## Source Files - -- `source/Dockerfile`: Container build file -- `source/entrypoint.sh`: Container entrypoint with auto-reconnect - -## Environment Variables - -| Variable | Required | Description | -|----------|----------|-------------| -| OC_URL | Yes | VPN server URL | -| OC_SERVERCERT | Yes | Server certificate pin | -| OC_USER | No | Username (enables hidden browser mode) | -| VNC_PASSWORD | Yes | noVNC access password | -| OC_TOTP_SECRET | No | TOTP secret for auto 2FA | -| NOVNC_PORT | No | noVNC port (default: 6901) | diff --git a/apps/cistech-tunnel/build/.gitignore b/apps/cistech-tunnel/build/.gitignore new file mode 100644 index 0000000..0746077 --- /dev/null +++ b/apps/cistech-tunnel/build/.gitignore @@ -0,0 +1,2 @@ +# Large binary files - track tar.gz but not 7z +*.7z diff --git a/apps/cistech-tunnel/build/Dockerfile b/apps/cistech-tunnel/build/Dockerfile new file mode 100644 index 0000000..a96a65d --- /dev/null +++ b/apps/cistech-tunnel/build/Dockerfile @@ -0,0 +1,99 @@ +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=6092 + +# Python/Playwright settings +ENV VIRTUAL_ENV=/opt/venv +ENV PATH=/opt/venv/bin:$PATH +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + # Core tools + openconnect \ + vpnc-scripts \ + iptables \ + iproute2 \ + iputils-ping \ + net-tools \ + procps \ + curl \ + nano \ + ca-certificates \ + # Python + python3 \ + python3-pip \ + python3-venv \ + # VNC/noVNC + tigervnc-standalone-server \ + tigervnc-common \ + novnc \ + websockify \ + x11vnc \ + xvfb \ + # Window manager & terminal + openbox \ + fluxbox \ + xterm \ + # Automation tools + xdotool \ + xclip \ + oathtool \ + # X11/GUI dependencies for browser + dbus \ + dbus-x11 \ + libgtk-3-0 \ + libglib2.0-0 \ + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcomposite1 \ + libxrandr2 \ + libgbm1 \ + libxdamage1 \ + libpango-1.0-0 \ + libxkbcommon0 \ + libxkbcommon-x11-0 \ + fonts-liberation \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Create Python venv and install openconnect-sso with all dependencies +RUN python3 -m venv "$VIRTUAL_ENV" && \ + pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir \ + 'openconnect-sso[full]' \ + playwright \ + keyring \ + keyrings.alt + +# Install Playwright browser (Chromium) +RUN python -m playwright install --with-deps chromium + +# Create directories +RUN mkdir -p /opt/scripts + +# VNC startup script (base64 encoded) +RUN echo 'IyEvYmluL2Jhc2gKc2V0IC1lCmV4cG9ydCBIT01FPScvcm9vdCcKZXhwb3J0IFVTRVI9J3Jvb3QnCnJtIC1mIC90bXAvLlAxLWxvY2sgL3RtcC8uWDExLXVuaXgvWDEgMj4vZGV2L251bGwgfHwgdHJ1ZQpybSAtcmYgL3RtcC8uWCotbG9jayAvdG1wLy5YMTQtdW5peC8qIDI+L2Rldi9udWxsIHx8IHRydWUKZWNobyAiU3RhcnRpbmcgVGlnZXJWTkMgc2VydmVyIG9uIGRpc3BsYXkgOjEuLi4iCnZuY3NlcnZlciA6MSAtZ2VvbWV0cnkgMTI4MHg4MDAgLWRlcHRoIDI0IC1TZWN1cml0eVR5cGVzIFZuY0F1dGggLWxvY2FsaG9zdCBubwpzbGVlcCAyCmVjaG8gIlN0YXJ0aW5nIG5vVk5DIG9uIHBvcnQgJHtOT1ZOQ19QT1JUOi02MDkyfS4uLiIKd2Vic29ja2lmeSAtLXdlYj0vdXNyL3NoYXJlL25vdm5jLyAke05PVk5DX1BPUlQ6LTYwOTJ9IGxvY2FsaG9zdDoke1ZOQ19QT1JUOi01OTAxfSAmCnRhaWwgLWYgL3Jvb3QvLnZuYy8qLmxvZwo=' \ + | 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 "$VNC_PORT" "$NOVNC_PORT" + +CMD ["/opt/scripts/entrypoint.sh"] diff --git a/apps/cistech-tunnel/build/README.md b/apps/cistech-tunnel/build/README.md new file mode 100644 index 0000000..74eb97d --- /dev/null +++ b/apps/cistech-tunnel/build/README.md @@ -0,0 +1,67 @@ +# Cistech Tunnel - Build Files + +This directory contains the Dockerfile and scripts to build the OpenConnect-SSO VPN Docker image. + +## Files + +- `Dockerfile` - Docker image definition (Ubuntu 22.04 + openconnect-sso + noVNC) +- `build.sh` - Build and push script +- `scripts/entrypoint.sh` - Container entrypoint + +## Building + +```bash +cd /etc/runtipi/repos/runtipi/apps/cistech-tunnel/build +./build.sh +``` + +This builds and pushes to `git.alexzaw.dev/alexz/openconnect-vpn:latest` + +To build without pushing: +```bash +docker build -t git.alexzaw.dev/alexz/openconnect-vpn:latest . +``` + +## What's in the image + +The Dockerfile creates an image with: +- Ubuntu 22.04 +- openconnect + openconnect-sso[full] (Python) +- Playwright Chromium browser (for SSO authentication) +- TigerVNC server + noVNC (web-based VNC) +- Tools: oathtool (TOTP), openbox, xterm + +### Scripts (baked in) +- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC +- `/opt/scripts/entrypoint.sh` - Container entrypoint (DNS fix, IP forwarding, config generation) + +## Runtime mounts (from shared/) + +When running as cistech-tunnel app, these are mounted from `shared/`: +- `/shared/openconnect-vpn` - Main VPN connection script +- `/shared/xstartup` -> `/root/.vnc/xstartup` - VNC session startup + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `VPN_EMAIL` | Email/username for SSO login | +| `VPN_PASSWORD` | Password for SSO login | +| `VPN_TOTP_SECRET` | TOTP secret for 2FA (base32) | +| `VPN_HOST` | VPN server URL (e.g., `https://vpn.example.com/Group`) | +| `TARGET_IP` | Target IP for connectivity testing | +| `VNC_PASSWORD` | VNC access password | + +## Ports + +- `5901` - VNC server +- `6092` - noVNC web interface + +## How it works + +1. Container starts, generates openconnect-sso config from env vars +2. VNC server starts with noVNC web interface +3. xterm launches with the `openconnect-vpn` script +4. Script sets up keyring with credentials (password + TOTP) +5. openconnect-sso handles SSO authentication via hidden browser +6. VPN connects and IP forwarding/NAT is configured diff --git a/apps/cistech-tunnel/build/build.sh b/apps/cistech-tunnel/build/build.sh new file mode 100644 index 0000000..4235bdb --- /dev/null +++ b/apps/cistech-tunnel/build/build.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Build and push the OpenConnect-SSO VPN Docker image +# Run this from the build directory + +set -euo pipefail + +IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/openconnect-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 --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6092:6092 -e VNC_PASSWORD=changeme -e VPN_HOST=https://vpn.example.com -e VPN_EMAIL=user@example.com ${IMAGE_NAME}:${IMAGE_TAG}" +echo "" +echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6092/vnc.html" +echo "" diff --git a/apps/cistech-tunnel/build/scripts/entrypoint.sh b/apps/cistech-tunnel/build/scripts/entrypoint.sh new file mode 100644 index 0000000..555deca --- /dev/null +++ b/apps/cistech-tunnel/build/scripts/entrypoint.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Entrypoint: VNC password setup + DNS fix + start VNC + +set -euo pipefail + +# 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 - unmount Docker's read-only mounts +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" + +# Generate openconnect-sso config from environment variables +mkdir -p /root/.config/openconnect-sso +cat > /root/.config/openconnect-sso/config.toml << EOF +on_disconnect = "" + +[default_profile] +address = "${VPN_HOST:-}" +user_group = "" +name = "" + +[credentials] +username = "${VPN_EMAIL:-}" + +[auto_fill_rules] +[[auto_fill_rules."https://*"]] +selector = "div[id=passwordError]" +action = "stop" + +[[auto_fill_rules."https://*"]] +selector = "input[type=email]" +fill = "username" + +[[auto_fill_rules."https://*"]] +selector = "input[name=passwd]" +fill = "password" + +[[auto_fill_rules."https://*"]] +selector = "input[data-report-event=Signin_Submit]" +action = "click" + +[[auto_fill_rules."https://*"]] +selector = "div[data-value=PhoneAppOTP]" +action = "click" + +[[auto_fill_rules."https://*"]] +selector = "a[id=signInAnotherWay]" +action = "click" + +[[auto_fill_rules."https://*"]] +selector = "input[id=idTxtBx_SAOTCC_OTC]" +fill = "totp" +EOF +echo "[entrypoint] openconnect-sso config generated" + +# Start VNC server +exec /opt/scripts/startup-vnc.sh diff --git a/apps/cistech-tunnel/cistech-tunnel.7z b/apps/cistech-tunnel/cistech-tunnel.7z new file mode 100644 index 0000000..dab50ad Binary files /dev/null and b/apps/cistech-tunnel/cistech-tunnel.7z differ diff --git a/apps/cistech-tunnel/config.json b/apps/cistech-tunnel/config.json index 04efcb9..fb64998 100755 --- a/apps/cistech-tunnel/config.json +++ b/apps/cistech-tunnel/config.json @@ -1,53 +1,76 @@ { - "name": "Cistech Tunnel", - "id": "cistech-tunnel", - "available": true, - "short_desc": "Cistech VPN client container with noVNC.", - "author": "alexz", - "port": 6902, - "categories": [ - "utilities", - "network" - ], - "description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.", - "tipi_version": 1, - "version": "latest", - "source": "local", - "exposable": true, - "dynamic_config": true, - "no_gui": false, - "form_fields": [ - { - "label": "VPN URL", - "type": "text", - "env_variable": "OC_URL", - "required": true, - "default": "https://vpn.cistech.net/Employees" - }, - { - "label": "VNC Password", - "type": "password", - "env_variable": "VNC_PASSWORD", - "required": true, - "default": "Az@83278327$$@@" - }, - { - "label": "Server Certificate", - "type": "text", - "env_variable": "OC_SERVERCERT", - "required": true, - "default": "pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" - }, - { - "label": "Username", - "type": "text", - "env_variable": "OC_USER", - "required": true, - "default": "alex.zaw@cistech.net" - } - ], - "supported_architectures": [ - "arm64", - "amd64" - ] -} \ No newline at end of file + "name": "Rego Tunnel", + "available": true, + "port": 6080, + "exposable": true, + "dynamic_config": true, + "id": "rego-tunnel", + "description": "Cisco Secure Client VPN in Docker with noVNC web UI for accessing Rego environments. Native Docker - no VM overhead.", + "tipi_version": 6, + "version": "5.1.14.145", + "categories": [ + "utilities" + ], + "short_desc": "Cisco VPN tunnel to Rego environments (native Docker)", + "author": "alexz", + "source": "https://git.alexzaw.dev/alexz/runtipi", + "form_fields": [ + { + "type": "email", + "label": "VPN Email", + "hint": "Email address for VPN SSO login (configured in /shared/cisco-vpn script)", + "placeholder": "your-email@company.com", + "required": false, + "env_variable": "VPN_EMAIL", + "default": "" + }, + { + "type": "password", + "label": "VPN Password", + "hint": "Password for VPN SSO login (configured in /shared/cisco-vpn script)", + "placeholder": "", + "required": false, + "env_variable": "VPN_PASSWORD", + "default": "" + }, + { + "type": "text", + "label": "TOTP Secret", + "hint": "Base32 TOTP secret for 2FA (configured in /shared/cisco-vpn script)", + "placeholder": "", + "required": false, + "env_variable": "VPN_TOTP_SECRET", + "default": "" + }, + { + "type": "text", + "label": "VPN Host", + "hint": "VPN server hostname", + "placeholder": "vpn.company.com", + "required": false, + "env_variable": "VPN_HOST", + "default": "vpn.cistech.net/Employees" + }, + { + "type": "text", + "label": "Target IP", + "hint": "IP address to route through VPN (e.g., IBM i server)", + "placeholder": "10.3.1.0", + "required": false, + "env_variable": "TARGET_IP", + "default": "10.3.1.0" + }, + { + "type": "password", + "label": "VNC Password", + "hint": "Password for noVNC web interface", + "placeholder": "cisco123", + "required": false, + "env_variable": "VNC_PASSWORD", + "default": "" + } + ], + "supported_architectures": [ + "amd64" + ] +} diff --git a/apps/cistech-tunnel/docker-compose.json b/apps/cistech-tunnel/docker-compose.json index e1e0765..97b749c 100755 --- a/apps/cistech-tunnel/docker-compose.json +++ b/apps/cistech-tunnel/docker-compose.json @@ -1,23 +1,74 @@ { + "schemaVersion": 2, "services": [ { "name": "cistech-tunnel", - "image": "git.alexzaw.dev/alexz/cistech-vpn:latest", - "isMain": true, - "internalPort": 6902, - "privileged": true, - "capAdd": ["NET_ADMIN"], - "devices": ["/dev/net/tun:/dev/net/tun"], - "environment": { - "OC_URL": "${OC_URL}", - "OC_SERVERCERT": "${OC_SERVERCERT}", - "OC_USER": "${OC_USER}", - "VNC_PASSWORD": "${VNC_PASSWORD}", - "NOVNC_PORT": "6902" - }, + "image": "git.alexzaw.dev/alexz/cisco-vpn:latest", + "environment": [ + { + "key": "VPN_EMAIL", + "value": "${VPN_EMAIL}" + }, + { + "key": "VPN_PASSWORD", + "value": "${VPN_PASSWORD}" + }, + { + "key": "VPN_TOTP_SECRET", + "value": "${VPN_TOTP_SECRET}" + }, + { + "key": "VPN_HOST", + "value": "${VPN_HOST}" + }, + { + "key": "VNC_PASSWORD", + "value": "${VNC_PASSWORD}" + }, + { + "key": "TZ", + "value": "${TZ}" + }, + { + "key": "TARGET_IP", + "value": "${TARGET_IP}" + } + ], + "internalPort": 6092, "volumes": [ - { "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" } - ] + { + "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/docker-compose.yml b/apps/cistech-tunnel/docker-compose.yml index e0eef99..42570c9 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: git.alexzaw.dev/alexz/openconnect-vpn:latest restart: unless-stopped networks: cistech-tunnel_runtipi_network: @@ -8,21 +8,26 @@ services: tipi_main_network: gw_priority: 1 environment: - OC_URL: ${OC_URL} - OC_SERVERCERT: ${OC_SERVERCERT} - OC_USER: ${OC_USER} + VPN_EMAIL: ${VPN_EMAIL} + VPN_PASSWORD: ${VPN_PASSWORD} + VPN_TOTP_SECRET: ${VPN_TOTP_SECRET} + VPN_HOST: ${VPN_HOST} VNC_PASSWORD: ${VNC_PASSWORD} - NOVNC_PORT: "6902" + TZ: ${TZ} + TARGET_IP: ${TARGET_IP} ports: - - ${APP_PORT}:6902 + - ${APP_PORT}:6092 volumes: - - ${APP_DATA_DIR}/data:/root + - ${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 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: "6902" + traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6092" 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 @@ -31,4 +36,11 @@ 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 \ No newline at end of file + runtipi.managed: true + runtipi.appurn: cistech-tunnel:runtipi + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun + privileged: true + stop_grace_period: 30s diff --git a/apps/cistech-tunnel/metadata/compose-schema.json b/apps/cistech-tunnel/metadata/compose-schema.json new file mode 100644 index 0000000..e45055b --- /dev/null +++ b/apps/cistech-tunnel/metadata/compose-schema.json @@ -0,0 +1,1217 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "schemaVersion": { + "const": 2 + }, + "services": { + "type": "array", + "message": "CUSTOM_APP_ERROR_SERVICES_MIN_LENGTH", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "image": { + "type": "string", + "message": "CUSTOM_APP_ERROR_IMAGE_REQUIRED" + }, + "name": { + "type": "string", + "message": "CUSTOM_APP_ERROR_NAME_REQUIRED" + }, + "addPorts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "containerPort": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_CONTAINER_PORT_INVALID" + }, + { + "type": "integer", + "message": "CUSTOM_APP_ERROR_CONTAINER_PORT_INVALID", + "exclusiveMaximum": 65536, + "exclusiveMinimum": 0 + } + ], + "message": "CUSTOM_APP_ERROR_CONTAINER_PORT_INVALID" + }, + "hostPort": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_HOST_PORT_INVALID" + }, + { + "type": "integer", + "message": "CUSTOM_APP_ERROR_HOST_PORT_INVALID", + "exclusiveMaximum": 65536, + "exclusiveMinimum": 0 + } + ], + "message": "CUSTOM_APP_ERROR_HOST_PORT_INVALID" + }, + "interface": { + "type": "string" + }, + "tcp": { + "type": "boolean" + }, + "udp": { + "type": "boolean" + } + }, + "required": [ + "containerPort", + "hostPort" + ] + } + }, + "addToMainNetwork": { + "type": "boolean" + }, + "capAdd": { + "type": "array", + "message": "CUSTOM_APP_ERROR_CAP_ADD_INVALID", + "items": { + "type": "string" + } + }, + "capDrop": { + "type": "array", + "message": "CUSTOM_APP_ERROR_CAP_DROP_INVALID", + "items": { + "type": "string" + } + }, + "command": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_COMMAND_INVALID" + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_COMMAND_INVALID", + "items": { + "type": "string" + } + } + ], + "message": "CUSTOM_APP_ERROR_COMMAND_INVALID" + }, + "dependsOn": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "condition": { + "anyOf": [ + { + "const": "service_completed_successfully", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + }, + { + "const": "service_healthy", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + }, + { + "const": "service_started", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + } + ], + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + } + }, + "required": [ + "condition" + ] + } + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_SERVICE_INVALID", + "items": { + "type": "string" + } + } + ] + }, + "deploy": { + "type": "object", + "properties": { + "resources": { + "type": "object", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpus": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "pids": { + "type": "number", + "message": "CUSTOM_APP_ERROR_DEPLOY_PIDS_INVALID" + } + } + }, + "reservations": { + "type": "object", + "properties": { + "devices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "capabilities": { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEVICE_CAPABILITY_INVALID", + "items": { + "type": "string" + } + }, + "count": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_DEVICE_COUNT_INVALID" + }, + { + "const": "all", + "message": "CUSTOM_APP_ERROR_DEVICE_COUNT_INVALID" + } + ], + "message": "CUSTOM_APP_ERROR_DEVICE_COUNT_INVALID" + }, + "deviceIds": { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEVICE_ID_INVALID", + "items": { + "type": "string" + } + }, + "driver": { + "type": "string" + } + }, + "required": [ + "capabilities" + ] + } + }, + "cpus": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "required": [ + "devices" + ] + } + } + } + }, + "required": [ + "resources" + ] + }, + "devices": { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEVICE_INVALID", + "items": { + "type": "string" + } + }, + "dns": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_DNS_INVALID" + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_DNS_INVALID", + "items": { + "type": "string" + } + } + ], + "message": "CUSTOM_APP_ERROR_DNS_INVALID" + }, + "entrypoint": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_ENTRYPOINT_INVALID" + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_ENTRYPOINT_INVALID", + "items": { + "type": "string" + } + } + ], + "message": "CUSTOM_APP_ERROR_ENTRYPOINT_INVALID" + }, + "environment": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "message": "CUSTOM_APP_ERROR_ENV_KEY_REQUIRED", + "minLength": 1 + }, + "value": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ENV_VALUE_REQUIRED" + }, + { + "type": "string", + "message": "CUSTOM_APP_ERROR_ENV_VALUE_REQUIRED", + "minLength": 1 + }, + { + "type": "boolean" + } + ], + "message": "CUSTOM_APP_ERROR_ENV_VALUE_REQUIRED" + } + }, + "required": [ + "key", + "value" + ] + } + }, + "extraHosts": { + "type": "array", + "message": "CUSTOM_APP_ERROR_EXTRA_HOST_INVALID", + "items": { + "type": "string" + } + }, + "extraLabels": { + "type": "object", + "message": "CUSTOM_APP_ERROR_LABEL_KEY_INVALID", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_LABEL_VALUE_INVALID" + }, + { + "type": "boolean" + } + ], + "message": "CUSTOM_APP_ERROR_LABEL_VALUE_INVALID" + } + }, + "healthCheck": { + "type": "object", + "properties": { + "test": { + "type": "string", + "message": "CUSTOM_APP_ERROR_HEALTH_CHECK_TEST_REQUIRED" + }, + "interval": { + "type": "string" + }, + "retries": { + "type": "number", + "message": "CUSTOM_APP_ERROR_HEALTH_CHECK_RETRIES_INVALID" + }, + "startInterval": { + "type": "string" + }, + "startPeriod": { + "type": "string" + }, + "timeout": { + "type": "string" + } + }, + "required": [ + "test" + ] + }, + "hostname": { + "type": "string" + }, + "internalPort": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_INTERNAL_PORT_INVALID" + }, + { + "type": "integer", + "message": "CUSTOM_APP_ERROR_INTERNAL_PORT_INVALID", + "exclusiveMaximum": 65536, + "exclusiveMinimum": 0 + } + ], + "message": "CUSTOM_APP_ERROR_INTERNAL_PORT_INVALID" + }, + "isMain": { + "type": "boolean" + }, + "logging": { + "type": "object", + "properties": { + "driver": { + "type": "string", + "message": "CUSTOM_APP_ERROR_LOGGING_DRIVER_REQUIRED" + }, + "options": { + "type": "object", + "message": "CUSTOM_APP_ERROR_LOGGING_OPTION_KEY_INVALID", + "additionalProperties": { + "type": "string", + "message": "CUSTOM_APP_ERROR_LOGGING_OPTION_VALUE_INVALID" + } + } + }, + "required": [ + "driver" + ] + }, + "networkMode": { + "type": "string" + }, + "pid": { + "type": "string" + }, + "privileged": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "securityOpt": { + "type": "array", + "message": "CUSTOM_APP_ERROR_SECURITY_OPT_INVALID", + "items": { + "type": "string" + } + }, + "shmSize": { + "type": "string" + }, + "stdinOpen": { + "type": "boolean" + }, + "stopGracePeriod": { + "type": "string" + }, + "stopSignal": { + "type": "string" + }, + "sysctls": { + "type": "object", + "message": "CUSTOM_APP_ERROR_SYSCTL_KEY_INVALID", + "additionalProperties": { + "type": "number", + "message": "CUSTOM_APP_ERROR_SYSCTL_VALUE_INVALID" + } + }, + "tty": { + "type": "boolean" + }, + "ulimits": { + "type": "object", + "properties": { + "core": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_CORE_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + }, + "memlock": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_MEMLOCK_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + }, + "nofile": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_NOFILE_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + }, + "nproc": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_NPROC_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + } + } + }, + "user": { + "type": "string" + }, + "volumes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "containerPath": { + "type": "string", + "message": "CUSTOM_APP_ERROR_CONTAINER_PATH_REQUIRED" + }, + "hostPath": { + "type": "string", + "message": "CUSTOM_APP_ERROR_HOST_PATH_REQUIRED" + }, + "bind": { + "type": "object", + "properties": { + "propagation": { + "enum": [ + "private", + "rprivate", + "rshared", + "rslave", + "shared", + "slave" + ] + } + }, + "required": [ + "propagation" + ] + }, + "private": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "shared": { + "type": "boolean" + } + }, + "required": [ + "containerPath", + "hostPath" + ] + } + }, + "workingDir": { + "type": "string" + } + }, + "required": [ + "image", + "name" + ] + } + }, + "overrides": { + "type": "array", + "items": { + "type": "object", + "properties": { + "services": { + "type": "array", + "items": { + "type": "object", + "properties": { + "addPorts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "containerPort": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_CONTAINER_PORT_INVALID" + }, + { + "type": "integer", + "message": "CUSTOM_APP_ERROR_CONTAINER_PORT_INVALID", + "exclusiveMaximum": 65536, + "exclusiveMinimum": 0 + } + ], + "message": "CUSTOM_APP_ERROR_CONTAINER_PORT_INVALID" + }, + "hostPort": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_HOST_PORT_INVALID" + }, + { + "type": "integer", + "message": "CUSTOM_APP_ERROR_HOST_PORT_INVALID", + "exclusiveMaximum": 65536, + "exclusiveMinimum": 0 + } + ], + "message": "CUSTOM_APP_ERROR_HOST_PORT_INVALID" + }, + "interface": { + "type": "string" + }, + "tcp": { + "type": "boolean" + }, + "udp": { + "type": "boolean" + } + }, + "required": [ + "containerPort", + "hostPort" + ] + } + }, + "addToMainNetwork": { + "type": "boolean" + }, + "capAdd": { + "type": "array", + "message": "CUSTOM_APP_ERROR_CAP_ADD_INVALID", + "items": { + "type": "string" + } + }, + "capDrop": { + "type": "array", + "message": "CUSTOM_APP_ERROR_CAP_DROP_INVALID", + "items": { + "type": "string" + } + }, + "command": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_COMMAND_INVALID" + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_COMMAND_INVALID", + "items": { + "type": "string" + } + } + ], + "message": "CUSTOM_APP_ERROR_COMMAND_INVALID" + }, + "dependsOn": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "condition": { + "anyOf": [ + { + "const": "service_completed_successfully", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + }, + { + "const": "service_healthy", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + }, + { + "const": "service_started", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + } + ], + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_CONDITION_INVALID" + } + }, + "required": [ + "condition" + ] + } + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEPENDS_ON_SERVICE_INVALID", + "items": { + "type": "string" + } + } + ] + }, + "deploy": { + "type": "object", + "properties": { + "resources": { + "type": "object", + "properties": { + "limits": { + "type": "object", + "properties": { + "cpus": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "pids": { + "type": "number", + "message": "CUSTOM_APP_ERROR_DEPLOY_PIDS_INVALID" + } + } + }, + "reservations": { + "type": "object", + "properties": { + "devices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "capabilities": { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEVICE_CAPABILITY_INVALID", + "items": { + "type": "string" + } + }, + "count": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_DEVICE_COUNT_INVALID" + }, + { + "const": "all", + "message": "CUSTOM_APP_ERROR_DEVICE_COUNT_INVALID" + } + ], + "message": "CUSTOM_APP_ERROR_DEVICE_COUNT_INVALID" + }, + "deviceIds": { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEVICE_ID_INVALID", + "items": { + "type": "string" + } + }, + "driver": { + "type": "string" + } + }, + "required": [ + "capabilities" + ] + } + }, + "cpus": { + "type": "string" + }, + "memory": { + "type": "string" + } + }, + "required": [ + "devices" + ] + } + } + } + }, + "required": [ + "resources" + ] + }, + "devices": { + "type": "array", + "message": "CUSTOM_APP_ERROR_DEVICE_INVALID", + "items": { + "type": "string" + } + }, + "dns": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_DNS_INVALID" + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_DNS_INVALID", + "items": { + "type": "string" + } + } + ], + "message": "CUSTOM_APP_ERROR_DNS_INVALID" + }, + "entrypoint": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_ENTRYPOINT_INVALID" + }, + { + "type": "array", + "message": "CUSTOM_APP_ERROR_ENTRYPOINT_INVALID", + "items": { + "type": "string" + } + } + ], + "message": "CUSTOM_APP_ERROR_ENTRYPOINT_INVALID" + }, + "environment": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string", + "message": "CUSTOM_APP_ERROR_ENV_KEY_REQUIRED", + "minLength": 1 + }, + "value": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ENV_VALUE_REQUIRED" + }, + { + "type": "string", + "message": "CUSTOM_APP_ERROR_ENV_VALUE_REQUIRED", + "minLength": 1 + }, + { + "type": "boolean" + } + ], + "message": "CUSTOM_APP_ERROR_ENV_VALUE_REQUIRED" + } + }, + "required": [ + "key", + "value" + ] + } + }, + "extraHosts": { + "type": "array", + "message": "CUSTOM_APP_ERROR_EXTRA_HOST_INVALID", + "items": { + "type": "string" + } + }, + "extraLabels": { + "type": "object", + "message": "CUSTOM_APP_ERROR_LABEL_KEY_INVALID", + "additionalProperties": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_LABEL_VALUE_INVALID" + }, + { + "type": "boolean" + } + ], + "message": "CUSTOM_APP_ERROR_LABEL_VALUE_INVALID" + } + }, + "healthCheck": { + "type": "object", + "properties": { + "test": { + "type": "string", + "message": "CUSTOM_APP_ERROR_HEALTH_CHECK_TEST_REQUIRED" + }, + "interval": { + "type": "string" + }, + "retries": { + "type": "number", + "message": "CUSTOM_APP_ERROR_HEALTH_CHECK_RETRIES_INVALID" + }, + "startInterval": { + "type": "string" + }, + "startPeriod": { + "type": "string" + }, + "timeout": { + "type": "string" + } + }, + "required": [ + "test" + ] + }, + "hostname": { + "type": "string" + }, + "image": { + "type": "string", + "message": "CUSTOM_APP_ERROR_IMAGE_REQUIRED" + }, + "internalPort": { + "anyOf": [ + { + "type": "string", + "message": "CUSTOM_APP_ERROR_INTERNAL_PORT_INVALID" + }, + { + "type": "integer", + "message": "CUSTOM_APP_ERROR_INTERNAL_PORT_INVALID", + "exclusiveMaximum": 65536, + "exclusiveMinimum": 0 + } + ], + "message": "CUSTOM_APP_ERROR_INTERNAL_PORT_INVALID" + }, + "isMain": { + "type": "boolean" + }, + "logging": { + "type": "object", + "properties": { + "driver": { + "type": "string", + "message": "CUSTOM_APP_ERROR_LOGGING_DRIVER_REQUIRED" + }, + "options": { + "type": "object", + "message": "CUSTOM_APP_ERROR_LOGGING_OPTION_KEY_INVALID", + "additionalProperties": { + "type": "string", + "message": "CUSTOM_APP_ERROR_LOGGING_OPTION_VALUE_INVALID" + } + } + }, + "required": [ + "driver" + ] + }, + "name": { + "type": "string", + "message": "CUSTOM_APP_ERROR_NAME_REQUIRED" + }, + "networkMode": { + "type": "string" + }, + "pid": { + "type": "string" + }, + "privileged": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "securityOpt": { + "type": "array", + "message": "CUSTOM_APP_ERROR_SECURITY_OPT_INVALID", + "items": { + "type": "string" + } + }, + "shmSize": { + "type": "string" + }, + "stdinOpen": { + "type": "boolean" + }, + "stopGracePeriod": { + "type": "string" + }, + "stopSignal": { + "type": "string" + }, + "sysctls": { + "type": "object", + "message": "CUSTOM_APP_ERROR_SYSCTL_KEY_INVALID", + "additionalProperties": { + "type": "number", + "message": "CUSTOM_APP_ERROR_SYSCTL_VALUE_INVALID" + } + }, + "tty": { + "type": "boolean" + }, + "ulimits": { + "type": "object", + "properties": { + "core": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_CORE_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + }, + "memlock": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_MEMLOCK_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + }, + "nofile": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_NOFILE_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + }, + "nproc": { + "anyOf": [ + { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_NPROC_INVALID" + }, + { + "type": "object", + "properties": { + "hard": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_HARD_INVALID" + }, + "soft": { + "type": "number", + "message": "CUSTOM_APP_ERROR_ULIMIT_SOFT_INVALID" + } + }, + "required": [ + "hard", + "soft" + ] + } + ] + } + } + }, + "user": { + "type": "string" + }, + "volumes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "containerPath": { + "type": "string", + "message": "CUSTOM_APP_ERROR_CONTAINER_PATH_REQUIRED" + }, + "hostPath": { + "type": "string", + "message": "CUSTOM_APP_ERROR_HOST_PATH_REQUIRED" + }, + "bind": { + "type": "object", + "properties": { + "propagation": { + "enum": [ + "private", + "rprivate", + "rshared", + "rslave", + "shared", + "slave" + ] + } + }, + "required": [ + "propagation" + ] + }, + "private": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "shared": { + "type": "boolean" + } + }, + "required": [ + "containerPath", + "hostPath" + ] + } + }, + "workingDir": { + "type": "string" + } + } + } + }, + "architecture": { + "anyOf": [ + { + "const": "amd64", + "message": "CUSTOM_APP_ERROR_ARCHITECTURE_INVALID" + }, + { + "const": "arm64", + "message": "CUSTOM_APP_ERROR_ARCHITECTURE_INVALID" + } + ], + "message": "CUSTOM_APP_ERROR_ARCHITECTURE_INVALID" + } + }, + "required": [ + "services" + ] + } + } + }, + "required": [ + "schemaVersion", + "services" + ] +} \ No newline at end of file diff --git a/apps/cistech-tunnel/metadata/description.md b/apps/cistech-tunnel/metadata/description.md index 9856255..db2f89a 100755 --- a/apps/cistech-tunnel/metadata/description.md +++ b/apps/cistech-tunnel/metadata/description.md @@ -1,20 +1,144 @@ -# Dockerized OpenConnect-SSO with noVNC and Cloudflared +# Rego Tunnel - Cisco Secure Client VPN -## Setup -1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens). +Native Docker container running Cisco Secure Client (AnyConnect) with full GUI support via noVNC. Provides transparent VPN access to protected resources from your LAN. -2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`. +## Features -3) Build and start: - docker compose build - docker compose up -d vpn_a - # Open http://localhost:6901, complete SSO. - # After success, attach app containers or start cloudflared_a. +- **Cisco Secure Client 5.1.14.145** - Full GUI with VPN, DART, and Posture modules +- **Web-based access** via noVNC (port 6080) +- **Auto-login with TOTP** - Fully automated VPN connection +- **LAN routing** - Other machines on your network can reach VPN targets +- **Native Docker** - No QEMU/VM overhead -4) Optional: switch to headless after first login: - Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service. +## Architecture -## Notes -- Each VPN runs in its own net namespace; routes from one cannot affect the other or the host. -- DNS from the VPN applies within its container namespace and attached services only. -- Persisted state lives in the named volumes mounted at `/root` (Playwright cache, configs). +``` +LAN Devices ──► Linux Host ──► Container (172.31.0.10) ──► VPN Tunnel ──► Target (10.35.33.230) + │ │ + │ └── Cisco Secure Client + │ └── noVNC web UI (port 6080) + │ + └── Host routing service + (routes VPN traffic through container) +``` + +## Installation + +### 1. Install the app through Runtipi + +Configure your VPN credentials in app settings: +- VPN Email +- VPN Password +- TOTP Secret (base32) +- VPN Host (default: vpn-ord1.dovercorp.com) +- Target IP (default: 10.35.33.230) + +### 2. Install host routing service (required for LAN access) + +**Run this ONCE on the host after app install:** + +```bash +/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/install-host-services.sh +``` + +This creates systemd services that route VPN traffic through the container. + +### 3. Access the VPN GUI + +Open `http://:6080/vnc.html` + +The VPN will auto-connect using your configured credentials. + +## Usage + +### Access noVNC + +Navigate to port 6080 on your server. The cisco-vpn script runs automatically and provides a menu: + +``` +1 - Start Cisco AnyConnect +2 - Copy credentials to clipboard +3 - Show live TOTP +4 - Setup IP forwarding rules +5 - Test connection to target +6 - Show network status +7 - Kill all Cisco processes +8 - Show routing table +9 - Show /etc/hosts +q - Quit +``` + +### Command line options + +```bash +# Inside container +cisco-vpn -m # Menu only (skip auto-connect) +cisco-vpn -c # Connect and exit +cisco-vpn -d # Disconnect and exit +cisco-vpn -s # Show status +cisco-vpn --help # Show all options +``` + +### View logs + +```bash +# Inside container +cat /var/log/cisco-vpn/$(date +%Y-%m-%d).log + +# On host +cat /var/log/rego-routing.log +``` + +## LAN Access + +After the host routing service is installed, any device on your LAN can reach the VPN target: + +1. **From the host:** Works automatically +2. **From other LAN devices:** Add a static route pointing to your host + +Example (Windows client): +```cmd +route add 10.35.33.230 mask 255.255.255.255 192.168.0.150 -p +``` + +Where `192.168.0.150` is your Linux host IP. + +## Uninstall + +Before removing the app from Runtipi: + +```bash +/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/uninstall-host-services.sh +``` + +## Troubleshooting + +### noVNC not accessible +```bash +docker exec rego-tunnel_runtipi-rego-tunnel-1 systemctl status vnc.service +``` + +### VPN connects but can't reach target +```bash +# Check routes inside container +docker exec rego-tunnel_runtipi-rego-tunnel-1 ip route + +# Check host routing +ip route | grep 10.35.33.230 +``` + +### Host routing not working +```bash +# Check watcher service +systemctl status rego-routing-watcher.path + +# Manually trigger routing +touch /etc/runtipi/app-data/runtipi/rego-tunnel/restart-routing +``` + +## Technical Details + +- **Container IP:** 172.31.0.10 (on br-rego-vpn bridge) +- **Ports:** 6080 (noVNC), 5901 (VNC) +- **Privileges:** `--privileged`, `NET_ADMIN`, `/dev/net/tun` +- **Log retention:** 7 days (auto-cleanup) diff --git a/apps/cistech-tunnel/metadata/logo.jpg b/apps/cistech-tunnel/metadata/logo.jpg index ddc9a56..34a43b7 100755 Binary files a/apps/cistech-tunnel/metadata/logo.jpg and b/apps/cistech-tunnel/metadata/logo.jpg differ diff --git a/apps/cistech-tunnel/shared/host-routing.sh b/apps/cistech-tunnel/shared/host-routing.sh new file mode 100644 index 0000000..5bf2031 --- /dev/null +++ b/apps/cistech-tunnel/shared/host-routing.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash +# +# Host routing script for cistech-tunnel +# Routes TARGET_IP through the VPN container +# +set -euo pipefail + +ACTION="${1:-start}" + +# Fixed configuration (we assigned these) +CONTAINER_IP="172.30.0.10" +BRIDGE_NAME="br-cistech-vpn" +TARGET_IP="${TARGET_IP:-10.3.1.0}" +LAN_SUBNET="192.168.0.0/23" +LAN_INTERFACES="eth0 eth1 wlan0" +LOG_FILE="/var/log/cistech-routing.log" + +log() { + local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [cistech-routing] $*" + echo "$msg" | tee -a "$LOG_FILE" >&2 +} + +get_lan_interface() { + ip route show default | awk '/default/ {for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1 +} + +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/24" 2>/dev/null || true + + log "Stale routes removed" +} + +apply_routes() { + local lan_if + lan_if="$(get_lan_interface)" + + log "Applying host routing rules..." + log " Container IP: $CONTAINER_IP" + log " Bridge: $BRIDGE_NAME" + log " Target IP: $TARGET_IP" + 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/24" via "$CONTAINER_IP" dev "$BRIDGE_NAME" + log "Route added: $TARGET_IP via $CONTAINER_IP dev $BRIDGE_NAME" + + # 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 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 + + 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 + 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" + else + log "NAT masquerade rule already exists for $LAN_SUBNET -> $BRIDGE_NAME" + fi + + log "OK: Host routing applied - $TARGET_IP via $CONTAINER_IP ($BRIDGE_NAME)" +} + +remove_all() { + log "Removing all routing rules..." + + # Remove route + ip route del "$TARGET_IP/24" via "$CONTAINER_IP" dev "$BRIDGE_NAME" 2>/dev/null || true + + # 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 + done + + # Remove masquerade rule (using nft) + local handle + handle=$(nft -a list chain ip nat POSTROUTING 2>/dev/null | grep "saddr $LAN_SUBNET.*oifname.*$BRIDGE_NAME.*masquerade" | grep -oP 'handle \K\d+' | head -1) + if [ -n "$handle" ]; then + nft delete rule ip nat POSTROUTING handle "$handle" 2>/dev/null || true + fi + + log "All routing rules removed" +} + +case "$ACTION" in + start) + remove_routes + apply_routes + ;; + stop) + remove_all + ;; + restart) + remove_all + sleep 1 + remove_routes + apply_routes + ;; + *) + echo "Usage: $0 {start|stop|restart}" >&2 + exit 2 + ;; +esac diff --git a/apps/cistech-tunnel/shared/install-host-services.sh b/apps/cistech-tunnel/shared/install-host-services.sh new file mode 100644 index 0000000..7e26f1f --- /dev/null +++ b/apps/cistech-tunnel/shared/install-host-services.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# Install host-side systemd services for cistech-tunnel +# Run this ONCE on the host after installing the app in Runtipi +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DATA_DIR="/etc/runtipi/app-data/runtipi/cistech-tunnel" + +echo "Installing cistech-tunnel host services..." + +# Create the path watcher unit +cat << 'EOF' | sudo tee /etc/systemd/system/cistech-routing-watcher.path +[Unit] +Description=Watch for cistech-tunnel routing trigger + +[Path] +PathExists=/etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing +Unit=cistech-routing-watcher.service + +[Install] +WantedBy=multi-user.target +EOF + +# Create the service unit +cat << EOF | sudo tee /etc/systemd/system/cistech-routing-watcher.service +[Unit] +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' +EOF + +# Make host-routing.sh executable +sudo chmod +x "${SCRIPT_DIR}/host-routing.sh" + +# Reload systemd and enable the watcher +sudo systemctl daemon-reload +sudo systemctl enable --now cistech-routing-watcher.path + +echo "" +echo "Done! Services installed:" +echo " - cistech-routing-watcher.path (watches for trigger file)" +echo " - cistech-routing-watcher.service (applies routing rules)" +echo "" +echo "To check status:" +echo " systemctl status cistech-routing-watcher.path" +echo "" +echo "To manually trigger routing:" +echo " touch ${APP_DATA_DIR}/restart-routing" diff --git a/apps/cistech-tunnel/shared/openconnect-vpn b/apps/cistech-tunnel/shared/openconnect-vpn new file mode 100644 index 0000000..e1b9c47 --- /dev/null +++ b/apps/cistech-tunnel/shared/openconnect-vpn @@ -0,0 +1,528 @@ +#!/bin/bash + +# OpenConnect-SSO VPN Connection Script +# Usage: ./openconnect-vpn [-c|--connect] [-d|--disconnect] [-s|--status] [--setup-keyring] [--help] + +# Credentials from environment variables (set by runtipi) +VPN_EMAIL="${VPN_EMAIL:-}" +VPN_PASSWORD="${VPN_PASSWORD:-}" +VPN_TOTP_SECRET="${VPN_TOTP_SECRET:-}" +VPN_HOST="${VPN_HOST:-}" +TARGET_IP="${TARGET_IP:-}" +VPN_INTERFACE="${VPN_INTERFACE:-tun0}" + + +# Log directory +LOG_DIR="/var/log/openconnect-vpn" +LOG_RETENTION_DAYS=7 +mkdir -p "$LOG_DIR" 2>/dev/null + +get_log_file() { + echo "$LOG_DIR/$(date '+%Y-%m-%d').log" +} + +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' + +print_banner() { + echo -e "${CYAN}========================================${NC}" + echo -e "${CYAN} OpenConnect-SSO VPN Connection ${NC}" + echo -e "${CYAN}========================================${NC}" + echo "" +} + +# Flags +DO_CONNECT=false +DO_DISCONNECT=false +DO_SETUP_KEYRING=false + +# Logging +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" ;; + *) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;; + esac +} + +run_cmd() { + local desc="$1" + shift + log DEBUG "$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 +} + +# Fetch server certificate fingerprint from VPN host +get_server_cert() { + local host="$1" + # Strip protocol and path to get just hostname:port + local server=$(echo "$host" | sed -E 's|^https?://||; s|/.*$||') + # Add default port if not specified + [[ "$server" != *:* ]] && server="${server}:443" + + log DEBUG "Fetching server certificate from $server..." + + local cert_pin=$(echo | openssl s_client -connect "$server" 2>/dev/null \ + | openssl x509 -pubkey -noout 2>/dev/null \ + | openssl pkey -pubin -outform der 2>/dev/null \ + | openssl dgst -sha256 -binary \ + | openssl enc -base64) + + if [[ -n "$cert_pin" ]]; then + echo "pin-sha256:${cert_pin}" + else + log WARN "Could not fetch server certificate" + echo "" + fi +} + +# Setup keyring with credentials for openconnect-sso +setup_keyring() { + log INFO "Setting up keyring credentials..." + + if [[ -z "$VPN_EMAIL" ]]; then + log ERROR "VPN_EMAIL is not set" + return 1 + fi + + python3 << PYTHON +import keyring +from keyrings.alt.file import PlaintextKeyring + +# Use plaintext keyring (works without desktop environment) +keyring.set_keyring(PlaintextKeyring()) + +email = "$VPN_EMAIL" +password = "$VPN_PASSWORD" +totp_secret = "$VPN_TOTP_SECRET" + +# Store password +if password: + keyring.set_password('openconnect-sso', email, password) + print(f"Password stored in keyring for {email}") + +# Store TOTP secret +if totp_secret: + keyring.set_password('openconnect-sso', f'totp/{email}', totp_secret.upper()) + print(f"TOTP secret stored in keyring for {email}") + +print("Keyring setup complete") +PYTHON + + if [ $? -eq 0 ]; then + log INFO "Keyring credentials configured" + else + log ERROR "Failed to setup keyring" + return 1 + fi +} + +show_help() { + echo -e "${CYAN}OpenConnect-SSO VPN Connection Script${NC}" + echo "" + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " -c, --connect Connect to VPN" + echo " -d, --disconnect Disconnect from VPN" + echo " -s, --status Show VPN status" + echo " -r, --routes Show routing table" + echo " --setup-keyring Setup keyring credentials only" + echo " --help Show this help message" + echo "" + echo "Environment Variables:" + echo " VPN_EMAIL Email/username for SSO" + echo " VPN_PASSWORD Password for SSO" + echo " VPN_TOTP_SECRET TOTP secret for 2FA" + echo " VPN_HOST VPN server hostname" + echo " TARGET_IP Target IP for connectivity test" + echo " VPN_INTERFACE TUN interface name (default: tun0)" +} + +get_vpn_interface() { + local iface=$(ip link show | grep -oP 'tun\d+(?=:.*UP)' | head -1) + if [ -z "$iface" ]; then + iface=$(ip link show | grep -oP 'tun\d+' | head -1) + fi + echo "$iface" +} + +get_container_ip() { + ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 +} + +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 +} + +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 +} + +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}" + elif echo "$line" | grep -qE "10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\."; then + echo -e " ${CYAN}$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() { + log INFO "Current network status:" + echo "" + log DEBUG "Container 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 "" + log DEBUG "Default gateway:" + ip route show default | while IFS= read -r line; do + echo -e " ${GRAY}│${NC} $line" + done + echo "" +} + +kill_vpn_processes() { + log INFO "Killing VPN processes..." + local killed=0 + + for pid in $(pgrep -x "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 2 + fi +} + +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_forwarding() { + log INFO "Setting up IP forwarding rules..." + + local vpn_iface=$(get_vpn_interface) + if [ -z "$vpn_iface" ]; then + log ERROR "No VPN interface found! Is VPN connected?" + return 1 + fi + + local vpn_ip=$(get_vpn_ip) + local container_ip=$(get_container_ip) + + log DEBUG "VPN interface: $vpn_iface" + log DEBUG "VPN IP: $vpn_ip" + log DEBUG "Container IP: $container_ip" + + run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1 + + # NAT masquerade for container network + 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 + + log INFO "Forwarding rules configured" + + # Trigger host routing service restart if available + if [ -d /runtime ]; then + touch /runtime/restart-routing 2>/dev/null || true + fi +} + +connect_vpn() { + log INFO "=== Starting OpenConnect-SSO VPN ===" + echo "" + + # Kill any existing VPN processes + kill_vpn_processes + + # Validate required variables + if [[ -z "$VPN_HOST" ]]; then + log ERROR "VPN_HOST is not set" + return 1 + fi + + # Setup keyring credentials + setup_keyring + + # Build openconnect-sso command + local sso_args=() + sso_args+=("-s" "$VPN_HOST") + + if [[ -n "$VPN_EMAIL" ]]; then + sso_args+=("-u" "$VPN_EMAIL") + fi + + sso_args+=("--browser-display-mode" "hidden") + + # Fetch server certificate + log INFO "Fetching server certificate..." + local servercert=$(get_server_cert "$VPN_HOST") + + # Build openconnect args + local oc_args=() + oc_args+=("--protocol=anyconnect") + oc_args+=("--interface" "$VPN_INTERFACE") + oc_args+=("--script" "/usr/share/vpnc-scripts/vpnc-script") + + if [[ -n "$servercert" ]]; then + oc_args+=("--servercert" "$servercert") + log DEBUG "Server cert: $servercert" + fi + + log INFO "Connecting to: $VPN_HOST" + log DEBUG "Interface: $VPN_INTERFACE" + + # Launch openconnect-sso + log INFO "Launching openconnect-sso..." + openconnect-sso "${sso_args[@]}" -- /usr/sbin/openconnect "${oc_args[@]}" & + OC_PID=$! + disown $OC_PID + log DEBUG "openconnect-sso started with PID $OC_PID" + + # Wait for VPN to connect + log INFO "Waiting for VPN connection..." + local wait_count=0 + local max_wait=300 + + while [ -z "$(get_vpn_interface)" ]; do + sleep 2 + ((wait_count+=2)) + if [ $((wait_count % 10)) -eq 0 ]; then + log DEBUG "Still waiting for VPN... (${wait_count}s)" + fi + if [ $wait_count -ge $max_wait ]; then + log ERROR "Timeout waiting for VPN connection after ${max_wait}s" + return 1 + fi + done + + log INFO "VPN connected!" + local vpn_iface=$(get_vpn_interface) + local vpn_ip=$(get_vpn_ip) + log DEBUG " Interface: $vpn_iface" + log DEBUG " VPN IP: $vpn_ip" + + sleep 3 + setup_forwarding + + # Test connection if TARGET_IP is set + if [[ -n "$TARGET_IP" ]]; then + log INFO "Testing connection to $TARGET_IP..." + if ping -c 2 -W 3 "$TARGET_IP" &>/dev/null; then + log INFO "Connection test: ${GREEN}SUCCESS${NC}" + else + log WARN "Connection test: ${RED}FAILED${NC}" + fi + fi + + log INFO "VPN setup complete" + return 0 +} + +# Main menu +main_menu() { + echo -e "${GREEN}Options:${NC}" + echo -e " ${CYAN}1${NC} - Connect VPN" + echo -e " ${CYAN}2${NC} - Disconnect VPN" + echo -e " ${CYAN}3${NC} - Show VPN status" + echo -e " ${CYAN}4${NC} - Setup IP forwarding" + echo -e " ${CYAN}5${NC} - Test connection" + echo -e " ${CYAN}6${NC} - Show network status" + echo -e " ${CYAN}7${NC} - Show routing table" + echo -e " ${CYAN}8${NC} - Setup keyring" + echo -e " ${CYAN}q${NC} - Quit" + echo "" +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + -c|--connect) + DO_CONNECT=true + shift + ;; + -d|--disconnect) + DO_DISCONNECT=true + shift + ;; + -s|--status) + print_banner + check_vpn_status + echo "" + show_network_status + exit 0 + ;; + -r|--routes) + show_routes + exit 0 + ;; + --setup-keyring) + DO_SETUP_KEYRING=true + shift + ;; + --help) + show_help + exit 0 + ;; + *) + echo "Unknown option: $1" + echo "Use --help for usage information" + exit 1 + ;; + esac + done +} + +# Main +parse_args "$@" +cleanup_old_logs + +echo "" >> "$(get_log_file)" +echo "========================================" >> "$(get_log_file)" +log INFO "openconnect-vpn script started" +log DEBUG "VPN_EMAIL=$VPN_EMAIL" +log DEBUG "VPN_HOST=$VPN_HOST" +log DEBUG "TARGET_IP=$TARGET_IP" +log DEBUG "VPN_TOTP_SECRET is $([ -n "$VPN_TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')" + +print_banner + +if [ "$DO_SETUP_KEYRING" = "true" ]; then + setup_keyring + exit $? +fi + +if [ "$DO_DISCONNECT" = "true" ]; then + disconnect_vpn + exit $? +fi + +if [ "$DO_CONNECT" = "true" ]; then + connect_vpn + exit $? +fi + +# Interactive menu mode +while true; do + echo "" + main_menu + echo -ne "${CYAN}Choice: ${NC}" + read -r choice + echo "" + + [[ -z "${choice// }" ]] && continue + + case $choice in + 1) connect_vpn ;; + 2) disconnect_vpn ;; + 3) check_vpn_status ;; + 4) setup_forwarding ;; + 5) if [[ -n "$TARGET_IP" ]]; then + log INFO "Testing connection to $TARGET_IP..." + ping -c 3 "$TARGET_IP" && log INFO "Connection test: ${GREEN}SUCCESS${NC}" || log ERROR "Connection test: ${RED}FAILED${NC}" + else + log WARN "TARGET_IP not set" + fi ;; + 6) show_network_status ;; + 7) show_routes ;; + 8) setup_keyring ;; + 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 new file mode 100644 index 0000000..743b62b --- /dev/null +++ b/apps/cistech-tunnel/shared/uninstall-host-services.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# Uninstall host-side systemd services for cistech-tunnel +# +set -euo pipefail + +echo "Removing cistech-tunnel host services..." + +# Stop and disable the watcher +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/cistech-tunnel/shared/host-routing.sh stop 2>/dev/null || true + +# Remove systemd units +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 + +echo "" +echo "Done! Host services removed." diff --git a/apps/cistech-tunnel/shared/xstartup b/apps/cistech-tunnel/shared/xstartup new file mode 100644 index 0000000..9cc9ca7 --- /dev/null +++ b/apps/cistech-tunnel/shared/xstartup @@ -0,0 +1,39 @@ +#!/bin/bash +# VNC xstartup - launches terminal with cisco-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 Cisco UI +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 28111e1..0000000 --- a/apps/cistech-tunnel/source/Dockerfile +++ /dev/null @@ -1,46 +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" \ - OC_URL="https://vpn.cistech.net/Employees" \ - OC_SERVERCERT="pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" \ - OC_USER="alex.zaw@cistech.net" \ - OC_TOTP_SECRET="t6ypnjqvyx2yvw2l" \ - VNC_PASSWORD="Az@83278327\$\$@@" - -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 30540fb..0000000 --- a/apps/cistech-tunnel/source/entrypoint.sh +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${OC_URL:?OC_URL required}" -: "${OC_SERVERCERT:?OC_SERVERCERT required}" - -NOVNC_PORT="${NOVNC_PORT:-6901}" -VNC_PASSWORD="${VNC_PASSWORD:-changeme}" -DISPLAY_ADDR="${DISPLAY:-:1}" -OC_INTERFACE="${OC_INTERFACE:-tun0}" -OC_USER="${OC_USER:-}" -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_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..." - -# openconnect-sso reads TOTP from keyring automatically -if [[ -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" - 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