@@ -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
|
|
||||||
@@ -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://<host>: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) |
|
|
||||||
2
apps/cistech-tunnel/build/.gitignore
vendored
Normal file
2
apps/cistech-tunnel/build/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Large binary files - track tar.gz but not 7z
|
||||||
|
*.7z
|
||||||
99
apps/cistech-tunnel/build/Dockerfile
Normal file
99
apps/cistech-tunnel/build/Dockerfile
Normal file
@@ -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"]
|
||||||
67
apps/cistech-tunnel/build/README.md
Normal file
67
apps/cistech-tunnel/build/README.md
Normal file
@@ -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
|
||||||
22
apps/cistech-tunnel/build/build.sh
Normal file
22
apps/cistech-tunnel/build/build.sh
Normal file
@@ -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 ""
|
||||||
70
apps/cistech-tunnel/build/scripts/entrypoint.sh
Normal file
70
apps/cistech-tunnel/build/scripts/entrypoint.sh
Normal file
@@ -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
|
||||||
BIN
apps/cistech-tunnel/cistech-tunnel.7z
Normal file
BIN
apps/cistech-tunnel/cistech-tunnel.7z
Normal file
Binary file not shown.
@@ -1,53 +1,76 @@
|
|||||||
{
|
{
|
||||||
"name": "Cistech Tunnel",
|
"name": "Rego Tunnel",
|
||||||
"id": "cistech-tunnel",
|
|
||||||
"available": true,
|
"available": true,
|
||||||
"short_desc": "Cistech VPN client container with noVNC.",
|
"port": 6080,
|
||||||
"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,
|
"exposable": true,
|
||||||
"dynamic_config": true,
|
"dynamic_config": true,
|
||||||
"no_gui": false,
|
"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": [
|
"form_fields": [
|
||||||
{
|
{
|
||||||
"label": "VPN URL",
|
"type": "email",
|
||||||
"type": "text",
|
"label": "VPN Email",
|
||||||
"env_variable": "OC_URL",
|
"hint": "Email address for VPN SSO login (configured in /shared/cisco-vpn script)",
|
||||||
"required": true,
|
"placeholder": "your-email@company.com",
|
||||||
"default": "https://vpn.cistech.net/Employees"
|
"required": false,
|
||||||
|
"env_variable": "VPN_EMAIL",
|
||||||
|
"default": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "VNC Password",
|
|
||||||
"type": "password",
|
"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",
|
"env_variable": "VNC_PASSWORD",
|
||||||
"required": true,
|
"default": ""
|
||||||
"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": [
|
"supported_architectures": [
|
||||||
"arm64",
|
|
||||||
"amd64"
|
"amd64"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,74 @@
|
|||||||
{
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
"name": "cistech-tunnel",
|
"name": "cistech-tunnel",
|
||||||
"image": "git.alexzaw.dev/alexz/cistech-vpn:latest",
|
"image": "git.alexzaw.dev/alexz/cisco-vpn:latest",
|
||||||
"isMain": true,
|
"environment": [
|
||||||
"internalPort": 6902,
|
{
|
||||||
"privileged": true,
|
"key": "VPN_EMAIL",
|
||||||
"capAdd": ["NET_ADMIN"],
|
"value": "${VPN_EMAIL}"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
"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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
cistech-tunnel:
|
cistech-tunnel:
|
||||||
image: git.alexzaw.dev/alexz/cistech-vpn:latest
|
image: git.alexzaw.dev/alexz/openconnect-vpn:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
cistech-tunnel_runtipi_network:
|
cistech-tunnel_runtipi_network:
|
||||||
@@ -8,21 +8,26 @@ services:
|
|||||||
tipi_main_network:
|
tipi_main_network:
|
||||||
gw_priority: 1
|
gw_priority: 1
|
||||||
environment:
|
environment:
|
||||||
OC_URL: ${OC_URL}
|
VPN_EMAIL: ${VPN_EMAIL}
|
||||||
OC_SERVERCERT: ${OC_SERVERCERT}
|
VPN_PASSWORD: ${VPN_PASSWORD}
|
||||||
OC_USER: ${OC_USER}
|
VPN_TOTP_SECRET: ${VPN_TOTP_SECRET}
|
||||||
|
VPN_HOST: ${VPN_HOST}
|
||||||
VNC_PASSWORD: ${VNC_PASSWORD}
|
VNC_PASSWORD: ${VNC_PASSWORD}
|
||||||
NOVNC_PORT: "6902"
|
TZ: ${TZ}
|
||||||
|
TARGET_IP: ${TARGET_IP}
|
||||||
ports:
|
ports:
|
||||||
- ${APP_PORT}:6902
|
- ${APP_PORT}:6092
|
||||||
volumes:
|
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:
|
labels:
|
||||||
generated: true
|
generated: true
|
||||||
traefik.enable: true
|
traefik.enable: true
|
||||||
traefik.docker.network: runtipi_tipi_main_network
|
traefik.docker.network: runtipi_tipi_main_network
|
||||||
traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
|
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.rule: Host(`${APP_DOMAIN}`)
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
|
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
|
||||||
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
|
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
|
||||||
@@ -32,3 +37,10 @@ services:
|
|||||||
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
|
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
|
||||||
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
|
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
|
||||||
runtipi.managed: true
|
runtipi.managed: true
|
||||||
|
runtipi.appurn: cistech-tunnel:runtipi
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun
|
||||||
|
privileged: true
|
||||||
|
stop_grace_period: 30s
|
||||||
|
|||||||
1217
apps/cistech-tunnel/metadata/compose-schema.json
Normal file
1217
apps/cistech-tunnel/metadata/compose-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,144 @@
|
|||||||
# Dockerized OpenConnect-SSO with noVNC and Cloudflared
|
# Rego Tunnel - Cisco Secure Client VPN
|
||||||
|
|
||||||
## Setup
|
Native Docker container running Cisco Secure Client (AnyConnect) with full GUI support via noVNC. Provides transparent VPN access to protected resources from your LAN.
|
||||||
1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens).
|
|
||||||
|
|
||||||
2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`.
|
## Features
|
||||||
|
|
||||||
3) Build and start:
|
- **Cisco Secure Client 5.1.14.145** - Full GUI with VPN, DART, and Posture modules
|
||||||
docker compose build
|
- **Web-based access** via noVNC (port 6080)
|
||||||
docker compose up -d vpn_a
|
- **Auto-login with TOTP** - Fully automated VPN connection
|
||||||
# Open http://localhost:6901, complete SSO.
|
- **LAN routing** - Other machines on your network can reach VPN targets
|
||||||
# After success, attach app containers or start cloudflared_a.
|
- **Native Docker** - No QEMU/VM overhead
|
||||||
|
|
||||||
4) Optional: switch to headless after first login:
|
## Architecture
|
||||||
Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service.
|
|
||||||
|
|
||||||
## Notes
|
```
|
||||||
- Each VPN runs in its own net namespace; routes from one cannot affect the other or the host.
|
LAN Devices ──► Linux Host ──► Container (172.31.0.10) ──► VPN Tunnel ──► Target (10.35.33.230)
|
||||||
- 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).
|
│ └── 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://<your-server>: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)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 609 KiB After Width: | Height: | Size: 6.0 KiB |
123
apps/cistech-tunnel/shared/host-routing.sh
Normal file
123
apps/cistech-tunnel/shared/host-routing.sh
Normal file
@@ -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
|
||||||
55
apps/cistech-tunnel/shared/install-host-services.sh
Normal file
55
apps/cistech-tunnel/shared/install-host-services.sh
Normal file
@@ -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"
|
||||||
528
apps/cistech-tunnel/shared/openconnect-vpn
Normal file
528
apps/cistech-tunnel/shared/openconnect-vpn
Normal file
@@ -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
|
||||||
24
apps/cistech-tunnel/shared/uninstall-host-services.sh
Normal file
24
apps/cistech-tunnel/shared/uninstall-host-services.sh
Normal file
@@ -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."
|
||||||
39
apps/cistech-tunnel/shared/xstartup
Normal file
39
apps/cistech-tunnel/shared/xstartup
Normal file
@@ -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
|
||||||
@@ -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"]
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user