Compare commits

...

178 Commits

Author SHA1 Message Date
5bffff1d9b nas-samba: configurable admin user (uses SMB_PASSWORD) 2026-03-16 23:34:28 +00:00
61b1dea366 nas-samba: clean up duplicate files, single server.js in app/ 2026-03-16 23:31:40 +00:00
c94cbd7f36 nas-samba: use SMB_PASSWORD for admin login, configurable username 2026-03-16 23:29:10 +00:00
618e023996 nas-samba: rebuilt as single Ubuntu image with Samba + CloudNAS (no MongoDB) 2026-03-16 23:22:42 +00:00
0104827234 feat: add cloudNAS web UI service 2026-03-09 18:53:18 +00:00
92a97e140f revert: remove cockpit, keep nas-samba only 2026-03-09 18:40:46 +00:00
b9d573aeac fix: use internalPort 9090 for cockpit, let Traefik handle routing, use interface field for SMB IP binding 2026-03-09 18:38:20 +00:00
3c438ca093 fix: remove internalPort and port binding for host network cockpit 2026-03-09 18:36:52 +00:00
d6bc7128df fix: add networkMode host to cockpit service 2026-03-09 18:34:29 +00:00
4b0659bd47 fix: use quay.io/cockpit/ws correct image registry 2026-03-09 18:23:31 +00:00
2351ad84bc feat: add Cockpit web UI service to nas-samba app 2026-03-09 18:21:09 +00:00
a16b5a232e fix: use variable BIND_IP in hostPort for SMB ports 2026-03-09 18:19:14 +00:00
0ffb0f7256 fix: use hostPort string format for IP binding, remove hostIp field 2026-03-09 18:18:15 +00:00
37ae3a06de Add nas-samba app 2026-03-09 18:09:37 +00:00
d0ff3536d2 fix: set executable bit on shared scripts for rego-tunnel and cistech-tunnel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:07:09 +00:00
0104b45331 rego-tunnel: fix pipefail crash in host-routing.sh remove_all()
The nft|grep|grep|head pipeline fails when no masquerade rule exists,
causing the script to exit under set -euo pipefail. Add || true to
match the cistech-tunnel version.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 21:11:25 +00:00
efc3ad00af rego-tunnel: move all scripts to dynamic mounts
- Move entrypoint.sh from build/scripts/ to shared/
- Create startup-vnc.sh in shared/ (was base64-encoded in Dockerfile)
- Remove baked-in scripts and CMD from Dockerfile (keep vnc.service unit only)
- Entrypoint now: chmod +x all shared scripts, symlinks startup-vnc.sh
  to /opt/scripts/ so systemd vnc.service still finds it
- Fix host watcher: use /bin/bash in ExecStart for permission resilience
- Bump tipi_version to 7

All scripts are now dynamically controlled via volume mounts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 21:05:14 +00:00
7ac32e9199 cistech-tunnel: use /bin/bash in ExecStart for permission resilience
Invoke host-routing.sh via /bin/bash so the watcher service works
even if the execute bit gets cleared by permission resets.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:51:02 +00:00
cb54689e7c cistech-tunnel: auto-fix script permissions at container startup
Add chmod +x in entrypoint.sh to ensure all shared scripts are
executable even if permissions get reverted by git pull or appstore
update operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:44:33 +00:00
992db16848 cistech-tunnel: remove entrypoint from docker-compose.json
Runtipi's compose generator doesn't translate the entrypoint field.
The entrypoint is instead set via user-config override.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:42:34 +00:00
16b7a66c01 cistech-tunnel: move all scripts to dynamic mounts
- Move entrypoint.sh from build/scripts/ to shared/ (no longer baked into image)
- Add entrypoint directive to docker-compose.json pointing to /shared/entrypoint.sh
- Update entrypoint.sh to reference /shared/startup-vnc.sh instead of /opt/scripts/
- Bump tipi_version to 7

All scripts are now dynamically controlled via volume mounts from the shared/
directory. The Docker image is a clean base with only packages installed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 20:39:20 +00:00
1def782149 Update apps/cistech-tunnel/build/Dockerfile 2026-02-04 20:29:37 +00:00
55c11cce90 Update apps/cistech-tunnel/build/Dockerfile 2026-02-04 20:29:03 +00:00
ed21a14f68 Update apps/cistech-tunnel/shared/entrypoint.sh 2026-02-04 20:16:54 +00:00
004c58b445 Update apps/cistech-tunnel/shared/entrypoint.sh 2026-02-04 20:14:41 +00:00
8c9ebea489 fix: Install noVNC from GitHub instead of apt package
The apt novnc package (v1.0.0) has module export issues causing
JavaScript errors. Switch to noVNC v1.4.0 from GitHub which has
proper ES6 module exports.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 01:04:45 +00:00
19cb09f05e . 2026-01-18 00:57:34 +00:00
ae86df8732 . 2026-01-17 18:01:02 +00:00
a2f0b40fa8 . 2026-01-17 17:58:31 +00:00
bf60412640 Use test_connection function for keepalive check
Replaces inline ping with existing test_connection function
2026-01-17 17:57:46 +00:00
5f057c50ed Add TARGET_SUBNET to openconnect-vpn script
Derive TARGET_SUBNET from TARGET_IP (first 3 octets + .0/24)
for iptables FORWARD rules to allow full subnet routing.
2026-01-17 17:56:45 +00:00
b2e38b3cb4 Derive TARGET_SUBNET dynamically from TARGET_IP
Extract first 3 octets from TARGET_IP and append .0/24
2026-01-17 17:53:34 +00:00
47e1790a8b Add TARGET_SUBNET for iptables rules with /24 CIDR
Keep TARGET_IP as single host, add hardcoded TARGET_SUBNET=10.3.1.0/24
for iptables rules and routes to allow full subnet routing.
2026-01-17 17:52:44 +00:00
b67b8f18a4 Fix TARGET_IP to include /24 CIDR for iptables rules
The iptables rules were using 10.3.1.0 (single IP) instead of
10.3.1.0/24 (subnet), causing routing from other machines to fail.
2026-01-17 17:51:49 +00:00
c6749fe856 refactor(cistech-tunnel): add IBMI_HOST and test_connection function
- Add hardcoded IBMI_HOST=10.3.1.201 for testing
- Create test_connection() function for reuse
- Use IBMI_HOST for connection tests and keepalive pings
- TARGET_IP still used for routing rules
2026-01-17 16:53:40 +00:00
4c7ff9d6a0 fix(cistech-tunnel): reset DNS and clean tun interface before connecting 2026-01-17 16:49:32 +00:00
e93edb73af fix(cistech-tunnel): remove sudo from openconnect command - already running as root 2026-01-17 16:45:01 +00:00
9a6e2f67e6 feat(cistech-tunnel): add auto-connect, menu flag, watchdog, fix host routing
- Auto-connect on startup (skip with -m/--menu flag)
- Add VPN watchdog for auto-reconnect
- Add live TOTP display
- Fix host-routing.sh pipefail issue with grep
- Better forwarding rules similar to rego-tunnel
2026-01-17 16:40:55 +00:00
84b1eb3f5d . 2026-01-17 16:33:22 +00:00
1bd5a21a94 fix(cistech-tunnel): add sudo and system dbus for openconnect-sso 2026-01-17 16:21:26 +00:00
5c3147536c refactor(cistech-tunnel): move runtime scripts to shared folder
- Add entrypoint.sh and startup-vnc.sh to shared folder
- Override command in docker-compose.json to use /shared/entrypoint.sh
- Scripts can now be modified without rebuilding image
2026-01-17 16:10:22 +00:00
8656441976 fix(cistech-tunnel): add software rendering support for Qt WebEngine
- Add QT_QUICK_BACKEND=software, LIBGL_ALWAYS_SOFTWARE=1
- Add mesa-utils, libgl1-mesa-dri for llvmpipe software renderer
- Add missing xcb libraries (libxcb-render0, libxcb-shm0, etc.)
- Use --use-gl=swiftshader in chromium flags
2026-01-17 16:08:51 +00:00
0d52d54eed fix(cistech-tunnel): add Qt no-sandbox flags to xstartup 2026-01-17 15:59:31 +00:00
1b59e304b0 fix(cistech-tunnel): add --no-sandbox for chromium running as root 2026-01-17 15:57:53 +00:00
fb915487dc fix(cistech-tunnel): add all xcb libraries for Qt6 2026-01-17 15:53:24 +00:00
a3b02b694e fix(cistech-tunnel): add libxcb-cursor0 for Qt xcb plugin 2026-01-17 15:42:58 +00:00
9b2a42bdc9 fix(cistech-tunnel): add libegl1 libgl1 libopengl0 for PyQt6 WebEngine 2026-01-17 15:36:00 +00:00
98f3cc95eb . 2026-01-17 15:27:29 +00:00
12f626b088 chore: remove .github workflows 2026-01-17 15:14:03 +00:00
b9b3f89910 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 14:35:13 +00:00
24594915a9 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 14:29:47 +00:00
6f6538fa73 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 14:25:26 +00:00
239179931c .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 14:24:28 +00:00
f1793baa57 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 14:23:50 +00:00
418390fe8d .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 11:43:28 +00:00
4fd8688685 revert(cistech-tunnel): restore to original working state at a7691b1
Some checks failed
Test / test (push) Has been cancelled
- Removed shared/ folder (host routing scripts)
- Restored original config.json, docker-compose.json
- Restored original Dockerfile and entrypoint.sh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:30:34 +00:00
f410510a7f revert(cistech-tunnel): restore to working state at 5d54ed6
Some checks failed
Test / test (push) Has been cancelled
- Removed build/ folder
- Restored source/ folder with original Dockerfile and entrypoint.sh
- Reverted config files to original working state
- Cleaned up shared/ to only contain host routing scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:28:10 +00:00
274125e862 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 11:21:35 +00:00
837dffddd5 refactor(cistech-tunnel): remove all systemd dependencies
Some checks failed
Test / test (push) Has been cancelled
- Dockerfile: Removed systemd, systemd-sysv, network-manager packages
- Dockerfile: Removed systemd service cleanup, vnc.service, cgroup volume
- docker-compose.json/yml: Removed /sys/fs/cgroup volume mount
- Bumped tipi_version to 4

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:16:32 +00:00
1ef9d21ba4 fix(cistech-tunnel): remove systemd dependency, use port 6092
Some checks failed
Test / test (push) Has been cancelled
- entrypoint.sh: Start VNC directly instead of systemd /sbin/init
- Changed NOVNC_PORT from 6080 to 6092 everywhere
- Dockerfile: Updated EXPOSE and default NOVNC_PORT
- Bumped tipi_version to 3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:13:54 +00:00
9307cab1bb fix(cistech-tunnel): correct routing config and sync compose files
Some checks failed
Test / test (push) Has been cancelled
- host-routing.sh: Updated to use cistech values (172.30.0.10, br-vpn-static)
- config.json: Added TARGET_IP form field, bumped tipi_version to 2
- docker-compose.json: Added TARGET_IP environment variable
- docker-compose.yml: Synced with docker-compose.json (correct image, port 6080, all env vars)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:10:59 +00:00
e462edd99b .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 10:53:29 +00:00
48d0407c79 Add build.sh script for cistech-tunnel
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:37:50 +00:00
3c427af6fe Restructure cistech-tunnel to match rego-tunnel pattern
Some checks failed
Test / test (push) Has been cancelled
- build/: Dockerfile + entrypoint.sh (base image with VNC/noVNC)
- shared/: Runtime scripts mounted into container
  - xstartup: VNC startup, launches openconnect-vpn in xterm
  - openconnect-vpn: Main VPN script with menu, auto-connect, watchdog
- Removed source/ folder (replaced by build/)
- Updated docker-compose.json with proper volume mounts
- Changed port to 6080 (noVNC default)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:36:41 +00:00
5d54ed6f80 cistech-tunnel: Remove redundant entrypoint mount
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:25:40 +00:00
685488c7d4 cistech-tunnel: Mount entrypoint.sh from shared folder
Some checks failed
Test / test (push) Has been cancelled
No more image rebuild needed for script changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:22:01 +00:00
ec40aa2ec1 Fix cistech-tunnel: restore echo pipe in elif branch
Some checks failed
Test / test (push) Has been cancelled
The elif branch was missing 'echo "" |' which caused openconnect-sso
to hang waiting for stdin input when OC_PASSWORD is not set.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:20:24 +00:00
498926ae5d cistech-tunnel: Auto-fetch server cert, add VPN password field
Some checks failed
Test / test (push) Has been cancelled
- entrypoint.sh: Auto-fetch pin-sha256 from VPN URL if not provided
- config.json: Remove OC_SERVERCERT (auto-fetched), add OC_PASSWORD
- docker-compose.json: Add OC_PASSWORD env var

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:12:26 +00:00
046552d09a Update cistech-tunnel: proper image tag, clean Dockerfile, add TOTP field
Some checks failed
Test / test (push) Has been cancelled
- docker-compose.json: Use git.alexzaw.dev/alexz/cistech-vpn:latest
- config.json: Add OC_TOTP_SECRET field, keep server cert as default
- Dockerfile: Remove hardcoded credentials (come from env at runtime)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:07:29 +00:00
27c46542e8 Add host routing watcher for cistech-tunnel (same pattern as rego-tunnel)
Some checks failed
Test / test (push) Has been cancelled
- Add shared/host-routing.sh with nft for NAT masquerade
- Add shared/install-host-services.sh to set up systemd watcher
- Add shared/uninstall-host-services.sh for cleanup
- Add /runtime volume mount for trigger file
- Update entrypoint.sh to trigger host routing when VPN connects

Run install-host-services.sh on host after app install.
Requires image rebuild for entrypoint changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:58:28 +00:00
0c952a2623 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 09:43:31 +00:00
50cdd3ea1c Suppress noisy job control messages and ignore empty menu input
Some checks failed
Test / test (push) Has been cancelled
- Add disown after vpnui & to suppress "killed" messages
- Ignore empty/whitespace input in menu loop
- Remove "Invalid choice" error (just ignore silently)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:38:49 +00:00
7c76016fcf Fix FORWARD rules: wait for Cisco chains, then delete+reinsert at pos 1
Some checks failed
Test / test (push) Has been cancelled
After VPN reconnects, Cisco agent creates its chains asynchronously,
pushing our ACCEPT rules down where they're ineffective. Fix:
1. Wait up to 30s for ciscovpn chain to exist
2. Delete any existing rules (they may be in wrong position)
3. Insert fresh rules at position 1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:26:18 +00:00
0dca06fbc8 Fix host routing: use nft for NAT, insert FORWARD rules before Cisco chains
Some checks failed
Test / test (push) Has been cancelled
- host-routing.sh: Use nft instead of iptables for NAT masquerade
  (iptables-nft backend doesn't support iptables -t nat commands)
- cisco-vpn: Use -I FORWARD 1 instead of -A FORWARD to insert rules
  BEFORE Cisco VPN chains (which have catch-all DROP rules)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:21:09 +00:00
4c067c14d8 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 08:49:44 +00:00
529842a411 Add VPN watchdog with auto-reconnect and disable screen blanking
Some checks failed
Test / test (push) Has been cancelled
- Added start_watchdog() function that:
  - Checks VPN every 60 seconds
  - Sends keepalive ping every 5 minutes to prevent idle timeout
  - Auto-reconnects up to 3 times if VPN drops
- Disabled screen blanking in xstartup and after VPN connects
- Removed useless monitor loop that only logged
2026-01-17 05:26:58 +00:00
99847c3ff0 Update build/README.md for current architecture
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 04:02:07 +00:00
96d4e32672 Update documentation for native Docker architecture
Some checks failed
Test / test (push) Has been cancelled
- Rewrote description.md with current architecture
- Removed README.md (outdated Windows VM docs)
- Added install/uninstall instructions for host services
2026-01-17 04:01:14 +00:00
c3581c7ecc Add install/uninstall scripts for host systemd services
Some checks failed
Test / test (push) Has been cancelled
- install-host-services.sh: Creates watcher path/service units
- uninstall-host-services.sh: Removes units and cleans up
- Run once on host after app install
2026-01-17 03:59:06 +00:00
657081678f cisco-vpn: Remove all VM references, use container IP
Some checks failed
Test / test (push) Has been cancelled
- Removed get_vm_bridge_ip() and get_container_gateway()
- Added get_container_ip() for eth0 (172.31.0.x network)
- Updated setup_forwarding() and show_network_status()
- No more ens3/VM references
2026-01-17 03:03:53 +00:00
89e8f5cffc host-routing.sh: Complete rewrite - simplified, no VM/redsocks
Some checks failed
Test / test (push) Has been cancelled
- Hardcoded container IP (172.31.0.10) and bridge (br-rego-vpn)
- Simple start/stop/restart actions
- Removes stale routes before applying new ones
- Logs to /var/log/rego-routing.log
- Removed: redsocks, nft, VM subnet, container_apply
2026-01-17 02:59:34 +00:00
5e0004c0d8 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 02:42:09 +00:00
aa071a1fdb cisco-vpn: -m flag goes straight to menu without any checks
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 02:38:38 +00:00
2b9688ce44 cisco-vpn: Fix missing ;; in case statement for menu option 1
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 02:37:39 +00:00
fa398e8c86 cisco-vpn: Strip ANSI color codes from log file
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 02:36:32 +00:00
c7cf401b0a cisco-vpn: Daily log rotation with 7-day retention
Some checks failed
Test / test (push) Has been cancelled
- Logs now saved to /var/log/cisco-vpn/YYYY-MM-DD.log
- Automatic cleanup of logs older than 7 days
- Each day gets its own log file
2026-01-17 02:34:22 +00:00
38530ea0df cisco-vpn: Remove sudo (running as root) and add file logging
Some checks failed
Test / test (push) Has been cancelled
- Removed all sudo commands since container runs as root
- Added LOG_FILE at /var/log/cisco-vpn.log
- Modified log() to write to both console and file
- Added startup logging with env var status
2026-01-17 02:33:07 +00:00
c933d6e6da Fix: Import Docker env vars into VNC session
Some checks failed
Test / test (push) Has been cancelled
Systemd services don't inherit container environment variables.
Added sourcing from /proc/1/environ in xstartup to fix this.
2026-01-17 02:19:13 +00:00
e4b648e447 fix script
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 02:12:39 +00:00
e5d4b4d2e5 Fix noVNC startup script: correct bash variable syntax
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
Changed ${NOVNC_PORT-:-6080} to ${NOVNC_PORT:-6080}
The extra dash was causing websockify to not start properly.
2026-01-17 01:55:18 +00:00
35e0d67446 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 01:08:29 +00:00
747f71e27c .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 00:44:10 +00:00
6e5656a00b .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 00:42:44 +00:00
ab004b78eb .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 00:42:15 +00:00
b42bac35bb .
Some checks failed
Test / test (push) Has been cancelled
2026-01-17 00:37:40 +00:00
7b874169cb Update cisco-vpn to use 172.31.0.0/24 container network
Some checks failed
Test / test (push) Has been cancelled
- Replace 100.100.0.0/24 with 172.31.0.0/24
- Update gateway to 172.31.0.1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:26:26 +00:00
2f7a51d2b7 Move restart-routing trigger into setup_forwarding
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:16:29 +00:00
6097bcbe8f Remove unnecessary runtime mkdir from entrypoint
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:13:52 +00:00
f1fd3572c5 Use APP_DATA_DIR directly for runtime mount
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:12:55 +00:00
3d715664db Create runtime directory in entrypoint
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:11:53 +00:00
31cb8f6db5 Add trigger-based host routing restart (no SSH needed)
Some checks failed
Test / test (push) Has been cancelled
- Add runtime volume mount for trigger files
- cisco-vpn now creates /runtime/restart-routing trigger file
- Host systemd path watcher handles restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:11:11 +00:00
abe7a7ab08 Remove deleted init-rego.sh mount from docker-compose files
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:03:03 +00:00
46fe4f5557 Add xstartup to launch cisco-vpn in terminal
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:01:39 +00:00
35e4f1f6b7 Remove unused shared scripts and vpn_scripts-not-used folder
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 23:00:00 +00:00
767526054e Add IP forwarding to entrypoint
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:58:43 +00:00
69062bd828 Remove unused build scripts, fix cisco-vpn monitor loop
Some checks failed
Test / test (push) Has been cancelled
- Delete init-vpn.sh, vpn-connect.sh, xstartup from build/scripts
- Change cisco-vpn monitor to background process so menu shows after connect

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:56:34 +00:00
b3259d2981 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-16 22:48:04 +00:00
dc463f4cf0 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-16 22:44:58 +00:00
74f4ab982e .
Some checks failed
Test / test (push) Has been cancelled
2026-01-16 22:10:57 +00:00
070e358463 change build step
Some checks failed
Test / test (push) Has been cancelled
2026-01-16 22:08:37 +00:00
99fc5a5600 update dockerfile
Some checks failed
Test / test (push) Has been cancelled
2026-01-16 21:45:38 +00:00
ee6cb6c90d refactor(rego-tunnel): Inline startup-vnc.sh and vnc.service in Dockerfile
Some checks failed
Test / test (push) Has been cancelled
These two files cannot be overridden at runtime, so they're now
baked directly into the Dockerfile using heredocs.

Remaining scripts (can be overridden at runtime):
- init-vpn.sh
- xstartup
- vpn-connect.sh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 21:19:55 +00:00
b52ba03be4 fix(rego-tunnel): Make app work out of the box from repo
Some checks failed
Test / test (push) Has been cancelled
- Add init-rego.sh and xstartup to repo's shared folder
- Update docker-compose.json with all volume mounts
- Update docker-compose.yml with cgroup: host
- Mount scripts directly from repo (not user-config)

Now works on fresh install without any user-config overrides.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:49:39 +00:00
38c4eea2f0 feat(rego-tunnel): Add cisco-secure-client tarball to repo
Some checks failed
Test / test (push) Has been cancelled
Includes the pre-extracted Cisco Secure Client 5.1.14.145 installation
for building the Docker image.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:47:42 +00:00
838b33d6c5 feat(rego-tunnel): Add Dockerfile and build scripts for cisco-vpn image
Some checks failed
Test / test (push) Has been cancelled
Includes:
- Dockerfile for native Cisco Secure Client in Docker
- Build scripts (init-vpn.sh, startup-vnc.sh, vpn-connect.sh)
- VNC configuration (xstartup, vnc.service)
- build.sh for manual image builds
- README documenting the architecture

Note: cisco-secure-client-full.tar.gz is gitignored (large binary)
Copy it from ~/projects/cisco-vpn/build/ before building.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:47:20 +00:00
470517a00f refactor(rego-tunnel): Complete migration to native Docker VPN
Some checks failed
Test / test (push) Has been cancelled
- Add custom init-rego.sh that unmounts /etc/resolv.conf and /etc/hosts for VPN
- Add custom xstartup that launches terminal with cisco-vpn script
- Add TARGET_IP environment variable
- Remove QEMU/VM dependencies (TAPs, bridges, dnsmasq not needed)
- The cisco-vpn script handles: vpnagentd, auto-login with TOTP, IP forwarding

Architecture:
1. init-rego.sh: DNS fix + IP forwarding + start systemd
2. systemd: manages vpnagentd and vnc services
3. xstartup: opens xterm with cisco-vpn script
4. cisco-vpn: auto-connects VPN, sets up routing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:45:16 +00:00
d44a3c1a3b feat(rego-tunnel): Mount custom xstartup to launch terminal with cisco-vpn script
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:34:14 +00:00
865a96c2ec fix(rego-tunnel): Remove Traefik basic auth
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:55:48 +00:00
21bbeef579 fix(rego-tunnel): Add cgroup volume for systemd support
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:53:21 +00:00
8523c79999 refactor(rego-tunnel): Replace QEMU VM with native Docker Cisco VPN
Some checks failed
Test / test (push) Has been cancelled
- Switch from linux-vm QEMU image to cisco-vpn native Docker image
- Change port from 8006 to 6080 (noVNC)
- Remove VM-specific config (RAM, CPU, bridges, taps, QEMU)
- Add VPN credential fields (email, password, TOTP, VPN host)
- Add auto-connect and VNC password options
- Update description.md with new documentation
- Simplify Docker requirements (no /dev/kvm needed)

Benefits:
- No QEMU/VM overhead - runs natively in Docker
- Full Cisco Secure Client 5.1.14.145 with GUI
- Auto-login with TOTP support
- Auto-reconnect on disconnect

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:47:11 +00:00
96153fa557 Fix cfddns to use latest tag
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:07:43 +00:00
4d1bc9dbd0 Fix cfddns environment format to array
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:05:12 +00:00
ca826a6229 Add cfddns app - Cloudflare DDNS using favonia image
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:00:41 +00:00
a7691b16f0 assign ip to npm
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
2026-01-13 14:35:30 +00:00
d87429f98d Fix npm environment format to array
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:43:06 +00:00
ccd1fbc52f Remove misaligned docker-compose.yml for npm app
Some checks failed
Test / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:42:00 +00:00
b0ba737d0d .
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 11:36:46 +00:00
33aa6d361e Fix npm app: add schemaVersion, fix internalPort
Some checks failed
Test / test (push) Has been cancelled
- Added schemaVersion: 2 to docker-compose.json
- Changed internalPort from env var to number (81)
- Fixes 'invalid hostPort: NaN' error

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:29:28 +00:00
1eee23953c change npm
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:53:40 +00:00
1c548281b0 Update apps/nginx-proxy-manager/docker-compose.json
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:46:07 +00:00
38ebb88ac6 Update apps/nginx-proxy-manager/config.json
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:44:04 +00:00
e4fa0ba9cd Update apps/nginx-proxy-manager/config.json
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:39:10 +00:00
cb50b25081 Update apps/nginx-proxy-manager/config.json
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:35:42 +00:00
6d8015bdc9 Update apps/nginx-proxy-manager/config.json
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:28:02 +00:00
982a4bbff9 Update apps/nginx-proxy-manager/docker-compose.json
Some checks failed
Test / test (push) Has been cancelled
2026-01-13 09:19:27 +00:00
fa571c9ccd .
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
2026-01-09 11:33:00 +00:00
ef0058c93f .
Some checks failed
Test / test (push) Has been cancelled
2026-01-09 11:17:18 +00:00
e2e7c44bf6 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-09 11:03:49 +00:00
3aadd164f0 .
Some checks failed
Test / test (push) Has been cancelled
2026-01-09 10:42:41 +00:00
d6cafc67b2 add scalar as an app
Some checks failed
Test / test (push) Has been cancelled
2026-01-09 10:33:58 +00:00
8b9b6e798a .
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
2026-01-04 12:22:16 +00:00
b55708721c Add host routing service restart after VPN connects
Some checks failed
Test / test (push) Has been cancelled
SSH to host and restart rego-routing.service after VPN connection
is established in the VM.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 10:28:56 +00:00
3e50f5a465 Add Traefik basicauth to docker-compose.json
Some checks failed
Test / test (push) Has been cancelled
Runtipi uses docker-compose.json, not .yml for labels.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 09:52:52 +00:00
e13cc2b851 Add Traefik basic auth for rego-tunnel noVNC
Some checks failed
Test / test (push) Has been cancelled
- Remove websockify BasicHTTPAuth (doesn't trigger browser prompts)
- Add Traefik basicauth middleware instead (proper browser auth dialog)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 09:49:08 +00:00
6e1d7efa6d Remove unnecessary chmod from Dockerfile
Some checks failed
Test / test (push) Has been cancelled
Files in /shared/ are already executable from host mount,
no need to chmod at build time (which fails anyway since
/shared/ doesn't exist during build).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 09:36:30 +00:00
ed48c37706 new hostshare dir for rego-tunnel app
Some checks failed
Test / test (push) Has been cancelled
2026-01-04 09:06:33 +00:00
0d773fba51 new hostshare dir for rego-tunnel app
Some checks failed
Test / test (push) Has been cancelled
2026-01-04 09:03:51 +00:00
f1ba1f050d new image structure for cisco-vpn and related scripts
Some checks failed
Test / test (push) Has been cancelled
2026-01-04 09:01:52 +00:00
62ca42bb18 Update apps/rego-tunnel/build/setup-network.sh
Some checks failed
Test / test (push) Has been cancelled
2026-01-04 08:25:43 +00:00
bc34fad485 Update apps/rego-tunnel/build/supervisord.conf
Some checks failed
Test / test (push) Has been cancelled
2026-01-04 04:39:06 +00:00
500b5f4045 auto-generated commit message
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
2025-12-29 15:08:45 +00:00
24d28c649c forms update
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 07:23:04 +00:00
6fd57b0ce2 feat(rego-tunnel): optional shared network via NIC2
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 06:59:52 +00:00
2dae9f667e feat(rego-tunnel): optional second VM NIC + robust QCOW2 patch
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 06:49:19 +00:00
cb7e309915 fix(rego-tunnel): align compose with RunTipi
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 06:12:04 +00:00
55ca6fe620 rego-tunnel: relax qcow2 root detection
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 05:46:19 +00:00
5478623d19 rego-tunnel: add configurable hostshare dir
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 05:38:06 +00:00
302c52c784 rego-tunnel: add configurable hostshare dir
Some checks failed
Test / test (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
2025-12-29 01:41:37 +00:00
0020c539ea rego-tunnel: share APP_DATA_DIR via /hostshare + fix compose.json env
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 01:27:12 +00:00
6c790f84aa rego-tunnel: default TSCLIENT to APP_DATA_DIR + auto-mount 9p
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 00:57:47 +00:00
0ab6bb934d rego-tunnel: wire TSCLIENT + fix CIDR defaults
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 00:53:44 +00:00
a5871d399b fix ip
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 00:49:16 +00:00
9c2c67fbe1 a
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 00:42:23 +00:00
b6a6623406 z
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 00:40:32 +00:00
beedccdc29 z
Some checks failed
Test / test (push) Has been cancelled
2025-12-29 00:36:23 +00:00
11aaf00d8d add hostshare
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 23:51:22 +00:00
21c1fa5d9a a
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 23:31:45 +00:00
509814f3a8 rego-tunnel: export app-data as 9p TSCLIENT tag
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 23:15:19 +00:00
203e04101b update json
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 22:57:59 +00:00
2d98ca843f rego-tunnel: parameterize net + add DHCP static lease
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 22:56:51 +00:00
919f5904f8 ?
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 14:52:08 +00:00
ea2a1ba94d feat(rego-tunnel): share /shared into VM via 9p
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 14:41:13 +00:00
776666a77a fix zip
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 14:11:58 +00:00
9bde91f047 fix(rego-tunnel): exclude swap files from ssh.zip
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 14:08:32 +00:00
03c1181186 change nic
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 13:52:20 +00:00
022c455334 fix(rego-tunnel): detect outbound iface for NAT
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 13:14:29 +00:00
0461ffec7c .
Some checks failed
Test / test (push) Has been cancelled
2025-12-28 13:10:05 +00:00
f9c17c644a . 2025-12-28 13:09:48 +00:00
117 changed files with 32466 additions and 1988 deletions

View File

@@ -1,45 +0,0 @@
name: Renovate
on:
workflow_dispatch:
inputs:
log_level:
type: choice
description: Log level
default: INFO
options:
- DEBUG
- INFO
- WARN
- ERROR
- FATAL
schedule:
- cron: 0 2 * * *
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install bun
uses: oven-sh/setup-bun@v2
- name: Cache Bun global packages
uses: actions/cache@v4
with:
path: ~/.bun/install/global
key: ${{ runner.os }}-bun-global-renovate-40
restore-keys: |
${{ runner.os }}-bun-global-
- name: Install Renovate
run: bun install -g renovate@40
- name: Run renovate
run: LOG_LEVEL=${{ github.event.inputs.log_level || 'INFO' }} renovate --token ${{ secrets.GITHUB_TOKEN }} ${{ github.repository }}

View File

@@ -1,23 +0,0 @@
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test

0
CLAUDE.md Normal file → Executable file
View File

65
apps/cfddns/config.json Normal file
View File

@@ -0,0 +1,65 @@
{
"$schema": "../app-info-schema.json",
"name": "Cloudflare DDNS",
"id": "cfddns",
"available": true,
"exposable": false,
"dynamic_config": true,
"no_gui": true,
"tipi_version": 7,
"version": "latest",
"categories": [
"network",
"utilities"
],
"description": "Automatically update Cloudflare DNS records with your current public IP. Supports multiple domains, IPv4/IPv6, and proxy toggle.",
"short_desc": "Dynamic DNS updater for Cloudflare",
"author": "favonia",
"source": "https://github.com/favonia/cloudflare-ddns",
"form_fields": [
{
"type": "password",
"label": "Cloudflare API Token",
"env_variable": "CLOUDFLARE_API_TOKEN",
"required": true,
"hint": "API token with DNS edit permissions"
},
{
"type": "text",
"label": "Domains (comma-separated)",
"env_variable": "DOMAINS",
"required": true,
"hint": "e.g. home.example.com,vpn.example.com"
},
{
"type": "boolean",
"label": "Proxied (Orange Cloud)",
"env_variable": "PROXIED",
"required": false,
"default": false
},
{
"type": "text",
"label": "Update Schedule",
"env_variable": "UPDATE_CRON",
"required": false,
"default": "@every 5m",
"hint": "Cron expression or @every Xm"
},
{
"type": "text",
"label": "Timezone",
"env_variable": "TZ",
"required": false,
"default": "UTC"
}
],
"supported_architectures": [
"arm64",
"amd64"
],
"created_at": 1736974800000,
"updated_at": 1736974800000,
"deprecated": false,
"min_tipi_version": "4.5.0"
}

View File

@@ -0,0 +1,33 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "cfddns",
"image": "favonia/cloudflare-ddns:latest",
"isMain": true,
"environment": [
{
"key": "CLOUDFLARE_API_TOKEN",
"value": "${CLOUDFLARE_API_TOKEN}"
},
{
"key": "DOMAINS",
"value": "${DOMAINS}"
},
{
"key": "PROXIED",
"value": "${PROXIED:-false}"
},
{
"key": "UPDATE_CRON",
"value": "${UPDATE_CRON:-@every 5m}"
},
{
"key": "TZ",
"value": "${TZ:-UTC}"
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

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

View File

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

@@ -0,0 +1,2 @@
# Large binary files - track tar.gz but not 7z
*.7z

View File

@@ -0,0 +1,110 @@
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 (novnc installed from GitHub below)
tigervnc-standalone-server \
tigervnc-common \
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 \
# EGL/GL for PyQt6 WebEngine + software rendering
libegl1 \
libgl1 \
libopengl0 \
libdbus-1-3 \
mesa-utils \
libgl1-mesa-dri \
# XCB libraries for Qt6 (complete set)
libxcb1 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-render0 \
libxcb-render-util0 \
libxcb-shm0 \
libxcb-xfixes0 \
libxcb-xinerama0 \
libxcb-randr0 \
libxcb-glx0 \
libxcb-shape0 \
# sudo needed for openconnect-sso
sudo \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install noVNC from GitHub (v1.4.0 - stable release with ES6 modules)
RUN curl -fsSL https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz | tar -xz -C /usr/share/ \
&& mv /usr/share/noVNC-1.4.0 /usr/share/novnc \
&& ln -sf /usr/share/novnc/vnc.html /usr/share/novnc/index.html
# 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

View 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

View 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 ""

Binary file not shown.

View File

@@ -1,53 +1,76 @@
{ {
"name": "Cistech Tunnel", "name": "cistech Tunnel",
"id": "cistech-tunnel", "available": true,
"available": true, "port": 6092,
"short_desc": "Cistech VPN client container with noVNC.", "exposable": true,
"author": "alexz", "dynamic_config": true,
"port": 6902, "id": "cistech-tunnel",
"categories": [ "description": "openconnect-sso in Docker with noVNC web UI for accessing cistech environments. Native Docker - no VM overhead.",
"utilities", "tipi_version": 7,
"network" "version": "5.1.14.145",
], "categories": [
"description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.", "utilities"
"tipi_version": 1, ],
"version": "latest", "short_desc": "openconnect-sso VPN tunnel to cistech environments (native Docker)",
"source": "local", "author": "alexz",
"exposable": true, "source": "https://git.alexzaw.dev/alexz/runtipi",
"dynamic_config": true, "form_fields": [
"no_gui": false, {
"form_fields": [ "type": "email",
{ "label": "VPN Email",
"label": "VPN URL", "hint": "Email address for VPN SSO login (configured in /shared/openconnect-vpn script)",
"type": "text", "placeholder": "your-email@company.com",
"env_variable": "OC_URL", "required": false,
"required": true, "env_variable": "VPN_EMAIL",
"default": "https://vpn.cistech.net/Employees" "default": ""
}, },
{ {
"label": "VNC Password", "type": "password",
"type": "password", "label": "VPN Password",
"env_variable": "VNC_PASSWORD", "hint": "Password for VPN SSO login (configured in /shared/openconnect-vpn script)",
"required": true, "placeholder": "",
"default": "Az@83278327$$@@" "required": false,
}, "env_variable": "VPN_PASSWORD",
{ "default": ""
"label": "Server Certificate", },
"type": "text", {
"env_variable": "OC_SERVERCERT", "type": "text",
"required": true, "label": "TOTP Secret",
"default": "pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" "hint": "Base32 TOTP secret for 2FA (configured in /shared/openconnect-vpn script)",
}, "placeholder": "",
{ "required": false,
"label": "Username", "env_variable": "VPN_TOTP_SECRET",
"type": "text", "default": ""
"env_variable": "OC_USER", },
"required": true, {
"default": "alex.zaw@cistech.net" "type": "text",
} "label": "VPN Host",
], "hint": "VPN server hostname",
"supported_architectures": [ "placeholder": "vpn.company.com",
"arm64", "required": false,
"amd64" "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.201",
"required": false,
"env_variable": "TARGET_IP",
"default": "10.3.1.201"
},
{
"type": "password",
"label": "VNC Password",
"hint": "Password for noVNC web interface",
"placeholder": "cisco123",
"required": false,
"env_variable": "VNC_PASSWORD",
"default": ""
}
],
"supported_architectures": [
"amd64"
]
} }

View File

@@ -1,23 +1,74 @@
{ {
"schemaVersion": 2,
"services": [ "services": [
{ {
"name": "cistech-tunnel", "name": "cistech-tunnel",
"image": "cistech-vpn:latest", "image": "git.alexzaw.dev/alexz/openconnect-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}", "key": "VPN_PASSWORD",
"OC_SERVERCERT": "${OC_SERVERCERT}", "value": "${VPN_PASSWORD}"
"OC_USER": "${OC_USER}", },
"VNC_PASSWORD": "${VNC_PASSWORD}", {
"NOVNC_PORT": "6902" "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
}
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
services: services:
cistech-tunnel: cistech-tunnel:
image: 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,147 @@
# Dockerized OpenConnect-SSO with noVNC and Cloudflared # Cistech Tunnel - OpenConnect-SSO VPN
## Setup Docker container running OpenConnect-SSO for Cisco AnyConnect VPN with SSO/SAML authentication 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: - **OpenConnect-SSO** - Handles SAML/SSO authentication automatically
docker compose build - **Playwright browser** - Headless Chromium for SSO login
docker compose up -d vpn_a - **Web-based access** via noVNC (port 6092)
# Open http://localhost:6901, complete SSO. - **Auto-login with TOTP** - Credentials stored in keyring
# After success, attach app containers or start cloudflared_a. - **LAN routing** - Other machines on your network can reach VPN targets
- **Lightweight** - No systemd, no Cisco bloat
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.30.0.10) ──► VPN Tunnel ──► Target
- 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). │ └── openconnect-sso + openconnect
│ └── noVNC web UI (port 6092)
└── 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 (e.g., `https://vpn.cistech.net/Employees`)
- Target IP (for connectivity testing)
### 2. Install host routing service (required for LAN access)
**Run this ONCE on the host after app install:**
```bash
/etc/runtipi/repos/runtipi/apps/cistech-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>:6092/vnc.html`
The VPN will auto-connect using your configured credentials.
## Usage
### Access noVNC
Navigate to port 6092 on your server. The openconnect-vpn script runs automatically and provides a menu:
```
1 - Connect VPN
2 - Disconnect VPN
3 - Show VPN status
4 - Setup IP forwarding
5 - Test connection
6 - Show network status
7 - Show routing table
8 - Setup keyring
q - Quit
```
### Command line options
```bash
# Inside container
openconnect-vpn -c # Connect and exit
openconnect-vpn -d # Disconnect and exit
openconnect-vpn -s # Show status
openconnect-vpn --help # Show all options
```
### View logs
```bash
# Inside container
cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
# On host
cat /var/log/cistech-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.3.1.0 mask 255.255.255.0 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/cistech-tunnel/shared/uninstall-host-services.sh
```
## Troubleshooting
### VPN not connecting
```bash
# Check logs
docker exec cistech-tunnel cat /var/log/openconnect-vpn/$(date +%Y-%m-%d).log
# Try manual connect
docker exec -it cistech-tunnel /shared/openconnect-vpn -c
```
### VPN connects but can't reach target
```bash
# Check routes inside container
docker exec cistech-tunnel ip route
# Check host routing
ip route | grep <target-ip>
```
### Host routing not working
```bash
# Check watcher service
systemctl status cistech-routing-watcher.path
# Manually trigger routing
touch /etc/runtipi/app-data/runtipi/cistech-tunnel/restart-routing
```
## Technical Details
- **Container IP:** 172.30.0.10 (on br-cistech-vpn bridge)
- **Ports:** 6092 (noVNC), 5901 (VNC)
- **Capabilities:** `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

View File

@@ -0,0 +1,83 @@
#!/bin/bash
# Entrypoint: VNC password setup + DNS fix + start VNC
set -euo pipefail
# Force software rendering (no GPU/OpenGL)
export QT_QUICK_BACKEND=software
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
export MESA_GL_VERSION_OVERRIDE=3.3
# Qt/Chromium flags for running as root
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu --use-gl=swiftshader"
export QTWEBENGINE_DISABLE_SANDBOX=1
# Ensure all shared scripts are executable (permissions may reset after git pull/appstore update)
chmod +x /shared/*.sh /shared/openconnect-vpn /root/.vnc/xstartup 2>/dev/null || true
# 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 /shared/startup-vnc.sh

View 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}"
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
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_SUBNET..."
# Remove any existing route to TARGET_SUBNET
ip route del "$TARGET_SUBNET" 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 Subnet: $TARGET_SUBNET"
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_SUBNET via container
ip route replace "$TARGET_SUBNET" via "$CONTAINER_IP" dev "$BRIDGE_NAME"
log "Route added: $TARGET_SUBNET 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_SUBNET
iptables -C DOCKER-USER -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || \
iptables -I DOCKER-USER 1 -i "$lan_if" -o "$BRIDGE_NAME" -d "$TARGET_SUBNET" -j ACCEPT
# Allow return traffic
iptables -C DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -I DOCKER-USER 1 -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -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_SUBNET via $CONTAINER_IP ($BRIDGE_NAME)"
}
remove_all() {
log "Removing all routing rules..."
# Remove route
ip route del "$TARGET_SUBNET" 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_SUBNET" -j ACCEPT 2>/dev/null || true
iptables -D DOCKER-USER -i "$BRIDGE_NAME" -o "$lan_if" -s "$TARGET_SUBNET" -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 || true)
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

View 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=/bin/bash ${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"

View File

@@ -0,0 +1,662 @@
#!/bin/bash
# Cistech VPN Connection Script with OpenConnect-SSO
# Usage: ./openconnect-vpn [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-s|--status] [--help]
#
# Options:
# -c, --connect Connect to VPN and exit
# -d, --disconnect Disconnect VPN and exit
# -m, --menu Skip auto-connect, show menu directly
# -r, --routes Show routing table and exit
# -s, --status Show VPN status and exit
# --help Show this help message
# 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:-10.3.1.201}"
TARGET_SUBNET="$(echo "$TARGET_IP" | cut -d. -f1-3).0/24"
VPN_INTERFACE="${VPN_INTERFACE:-tun0}"
CONTAINER_NETWORK="172.30.0.0/24"
# 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} Cistech VPN Connection Script ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
}
# Flags
SKIP_AUTO_CONNECT=false
DO_CONNECT=false
DO_DISCONNECT=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" ;;
CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;;
*) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;;
esac
}
run_cmd() {
local desc="$1"
shift
log CMD "$desc: $*"
output=$("$@" 2>&1)
local rc=$?
if [ -n "$output" ]; then
echo "$output" | while IFS= read -r line; do
echo -e " ${GRAY}│${NC} $line"
done
fi
return $rc
}
show_help() {
echo -e "${CYAN}Cistech VPN Connection Script${NC}"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -c, --connect Connect to VPN and exit"
echo " -d, --disconnect Disconnect VPN and exit"
echo " -m, --menu Skip auto-connect, show menu directly"
echo " -r, --routes Show routing table and exit"
echo " -s, --status Show VPN status and exit"
echo " --help Show this help message"
echo ""
echo "Menu Options:"
echo " 1 - Connect VPN"
echo " 2 - Disconnect VPN"
echo " 3 - Show VPN status"
echo " 4 - Setup IP forwarding"
echo " 5 - Test connection"
echo " 6 - Show network status"
echo " 7 - Show routing table"
echo " 8 - Show live TOTP"
echo " q - Quit"
}
# Fetch server certificate fingerprint
get_server_cert() {
local host="$1"
local server=$(echo "$host" | sed -E 's|^https?://||; s|/.*$||')
[[ "$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
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
keyring.set_keyring(PlaintextKeyring())
email = "$VPN_EMAIL"
password = "$VPN_PASSWORD"
totp_secret = "$VPN_TOTP_SECRET"
if password:
keyring.set_password('openconnect-sso', email, password)
print(f"Password stored in keyring for {email}")
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
}
get_totp() {
oathtool --totp -b "$VPN_TOTP_SECRET"
}
# Test connection to TARGET_IP
test_connection() {
if [[ -z "$TARGET_IP" ]]; then
log WARN "TARGET_IP not set"
return 1
fi
log INFO "Testing connection to $TARGET_IP..."
if ping -c 3 -W 3 "$TARGET_IP" &>/dev/null; then
log INFO "Connection test: ${GREEN}SUCCESS${NC}"
return 0
else
log WARN "Connection test: ${RED}FAILED${NC} (may need manual route on host)"
return 1
fi
}
show_totp() {
log INFO "Starting live TOTP display (Ctrl+C to stop)"
echo ""
while true; do
TOTP=$(get_totp)
SECONDS_LEFT=$((30 - ($(date +%s) % 30)))
echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) "
sleep 1
done
}
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 for $TARGET_SUBNET..."
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"
# Enable IP forwarding
run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1
# NAT masquerade for container network going through VPN
if ! iptables -t nat -C POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then
run_cmd "Adding NAT masquerade for container network -> VPN" iptables -t nat -A POSTROUTING -s "$CONTAINER_NETWORK" -o "$vpn_iface" -j MASQUERADE
else
log DEBUG "NAT masquerade for container network already exists"
fi
# Forward rules at position 1
iptables -D FORWARD -d "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -s "$TARGET_SUBNET" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -s "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -d "$CONTAINER_NETWORK" -j ACCEPT 2>/dev/null || true
run_cmd "Inserting forward rule (to container network)" iptables -I FORWARD 1 -d "$CONTAINER_NETWORK" -j ACCEPT
run_cmd "Inserting forward rule (from container network)" iptables -I FORWARD 1 -s "$CONTAINER_NETWORK" -j ACCEPT
run_cmd "Inserting forward rule (from target)" iptables -I FORWARD 1 -s "$TARGET_SUBNET" -j ACCEPT
run_cmd "Inserting forward rule (to target)" iptables -I FORWARD 1 -d "$TARGET_SUBNET" -j ACCEPT
log INFO "Forwarding rules configured"
echo ""
# Trigger host routing service restart
log INFO "Triggering host routing service restart..."
touch /runtime/restart-routing 2>/dev/null || true
sleep 2
if [ ! -f /runtime/restart-routing ]; then
log INFO "Host routing service restarted"
else
log WARN "Host watcher may not be running (trigger file still exists)"
fi
log INFO "Routing configured for $TARGET_SUBNET through VPN tunnel"
echo ""
}
connect_vpn() {
log INFO "=== Starting OpenConnect-SSO VPN ==="
echo ""
# Kill any existing VPN processes
kill_vpn_processes
# Reset DNS to public servers (VPN script may have overwritten it)
log DEBUG "Resetting DNS to public servers..."
echo "nameserver 8.8.8.8" > /etc/resolv.conf
echo "nameserver 1.1.1.1" >> /etc/resolv.conf
# Clean up stale tun interface
ip link delete tun0 2>/dev/null || true
# Validate required variables
if [[ -z "$VPN_HOST" ]]; then
log ERROR "VPN_HOST is not set"
return 1
fi
# Setup keyring credentials
setup_keyring
# Fetch server certificate
log INFO "Fetching server certificate..."
local servercert=$(get_server_cert "$VPN_HOST")
log INFO "Connecting to: $VPN_HOST"
log DEBUG "Interface: $VPN_INTERFACE"
# Build openconnect-sso command
local sso_args=()
sso_args+=("-s" "$VPN_HOST")
if [[ -n "$VPN_EMAIL" ]]; then
sso_args+=("-u" "$VPN_EMAIL")
fi
# Use hidden browser for automated login
sso_args+=("--browser-display-mode" "hidden")
# 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
# 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"
# Wait for routes to stabilize
log DEBUG "Waiting for routes to stabilize..."
sleep 3
# Setup forwarding
setup_forwarding
# Test connection to IBMI host
test_connection
# Disable screen blanking
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
log DEBUG "Screen blanking disabled"
log INFO "VPN setup complete"
# Start watchdog in background
start_watchdog &
WATCHDOG_PID=$!
log DEBUG "Watchdog started with PID $WATCHDOG_PID"
return 0
}
# Watchdog - monitors VPN and reconnects if dropped
start_watchdog() {
log INFO "Starting VPN watchdog (check every 60s, keepalive ping every 5min)..."
local check_interval=60
local keepalive_interval=300
local last_keepalive=0
local reconnect_attempts=0
local max_reconnect_attempts=3
while true; do
sleep $check_interval
local now=$(date +%s)
local vpn_iface=$(get_vpn_interface)
if [ -n "$vpn_iface" ]; then
# VPN is up
reconnect_attempts=0
# Keepalive ping every 5 minutes
if [ $((now - last_keepalive)) -ge $keepalive_interval ]; then
test_connection || log WARN "VPN may be degraded"
last_keepalive=$now
fi
else
# VPN is down
((reconnect_attempts++))
log WARN "VPN disconnected! Reconnect attempt $reconnect_attempts/$max_reconnect_attempts..."
if [ $reconnect_attempts -le $max_reconnect_attempts ]; then
connect_vpn
else
log ERROR "Max reconnect attempts reached. Manual intervention required."
sleep 300
reconnect_attempts=0
fi
fi
done
}
# 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 only"
echo -e " ${CYAN}5${NC} - Test connection to $TARGET_IP"
echo -e " ${CYAN}6${NC} - Show network status"
echo -e " ${CYAN}7${NC} - Show routing table"
echo -e " ${CYAN}8${NC} - Show live TOTP"
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
;;
-m|--menu)
SKIP_AUTO_CONNECT=true
shift
;;
-s|--status)
print_banner
check_vpn_status
echo ""
show_network_status
exit 0
;;
-r|--routes)
show_routes
exit 0
;;
--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
if [ "$DO_CONNECT" = "true" ] && [ "$DO_DISCONNECT" = "true" ]; then
echo "Error: --connect and --disconnect are mutually exclusive"
exit 1
fi
}
# Main
parse_args "$@"
cleanup_old_logs
echo "" >> "$(get_log_file)"
echo "========================================" >> "$(get_log_file)"
log INFO "=== Starting OpenConnect-SSO VPN ==="
echo ""
log DEBUG "VPN_EMAIL=$VPN_EMAIL"
log DEBUG "VPN_HOST=$VPN_HOST"
log DEBUG "TARGET_IP=$TARGET_IP"
log DEBUG "TARGET_SUBNET=$TARGET_SUBNET"
log DEBUG "VPN_TOTP_SECRET is $([ -n "$VPN_TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')"
print_banner
if [ "$DO_DISCONNECT" = "true" ]; then
disconnect_vpn
exit $?
fi
if [ "$DO_CONNECT" = "true" ]; then
connect_vpn
exit $?
fi
# Auto-connect logic (unless -m flag)
if [ "$SKIP_AUTO_CONNECT" = "true" ]; then
log INFO "Menu mode - skipping auto-connect"
elif check_vpn_status; then
echo ""
log INFO "VPN already connected. Setting up forwarding..."
setup_forwarding
else
echo ""
log INFO "Auto-starting VPN connection..."
echo ""
connect_vpn
fi
# Interactive menu loop
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) test_connection ;;
6) show_network_status ;;
7) show_routes ;;
8) show_totp ;;
q|Q) log INFO "Goodbye!"; exit 0 ;;
*) ;;
esac
done

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e
export HOME='/root'
export USER='root'
rm -f /tmp/.P1-lock /tmp/.X11-unix/X1 2>/dev/null || true
rm -rf /tmp/.X*-lock /tmp/.X14-unix/* 2>/dev/null || true
echo "Starting TigerVNC server on display :1..."
vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no
sleep 2
echo "Starting noVNC on port ${NOVNC_PORT:-6092}..."
websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6092} localhost:${VNC_PORT:-5901} &
tail -f /root/.vnc/*.log

View 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."

View File

@@ -0,0 +1,53 @@
#!/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
# Force software rendering (no GPU/OpenGL)
export QT_QUICK_BACKEND=software
export LIBGL_ALWAYS_SOFTWARE=1
export GALLIUM_DRIVER=llvmpipe
export MESA_GL_VERSION_OVERRIDE=3.3
# GPU/WebKit workarounds for Cisco UI
export GDK_BACKEND=x11
export WEBKIT_DISABLE_DMABUF_RENDERER=1
# Qt/Chromium flags for running as root (no sandbox)
export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
export QTWEBENGINE_DISABLE_SANDBOX=1
# Start system dbus daemon (needed for Chromium)
mkdir -p /run/dbus
dbus-daemon --system --fork 2>/dev/null || true
# 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 -fn fixed -bg black -fg white -geometry 130x45+10+10 \
-title "Cistech VPN Terminal" \
-e "bash -c '/shared/openconnect-vpn; exec bash'" &
wait

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
{
"$schema": "../app-info-schema.json",
"name": "NAS Samba + CloudNAS",
"id": "nas-samba",
"available": true,
"port": 8080,
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"tipi_version": 2,
"version": "1.0.0",
"categories": ["network", "utilities"],
"description": "All-in-one NAS container with Samba SMB file sharing and CloudNAS web file manager. Browse files via web UI or connect via SMB at \\\\192.168.0.15\\data with username alexz. No database required — uses filesystem + SQLite.",
"short_desc": "NAS with SMB + web file manager",
"author": "alexz",
"source": "https://gits.alexzaw.dev/alexz/runtipi",
"form_fields": [
{
"type": "password",
"label": "SMB Password",
"env_variable": "SMB_PASSWORD",
"required": true,
"hint": "Password for SMB user alexz"
},
{
"type": "random",
"label": "JWT Secret",
"env_variable": "JWT_SECRET",
"min": 32,
"encoding": "hex"
},
{
"type": "text",
"label": "NAS Host Path",
"env_variable": "NAS_PATH",
"required": true,
"default": "/nas",
"hint": "Host path to share via SMB and web UI"
},
{
"type": "text",
"label": "Bind IP",
"env_variable": "BIND_IP",
"required": true,
"default": "192.168.0.15",
"hint": "IP address to bind SMB ports to"
}
],
"supported_architectures": ["arm64", "amd64"],
"created_at": 1741536000000,
"updated_at": 1742169600000,
"deprecated": false,
"min_tipi_version": "4.5.0"
}

View File

@@ -0,0 +1,25 @@
{
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/dynamic-compose.json",
"services": [
{
"name": "nas-samba",
"image": "git.alexzaw.dev/alexz/nas-samba:1.0.0",
"isMain": true,
"internalPort": 8080,
"addPorts": [
{ "hostPort": 445, "containerPort": 445, "interface": "${BIND_IP}" },
{ "hostPort": 139, "containerPort": 139, "interface": "${BIND_IP}" }
],
"volumes": [
{ "hostPath": "${NAS_PATH:-/nas}", "containerPath": "/nas" },
{ "hostPath": "${APP_DATA_DIR}/data/config", "containerPath": "/config" }
],
"environment": [
{ "key": "SMB_PASSWORD", "value": "${SMB_PASSWORD}" },
{ "key": "JWT_SECRET", "value": "${JWT_SECRET}" },
{ "key": "TZ", "value": "${TZ}" }
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,51 @@
FROM ubuntu:24.04
LABEL maintainer="alexz"
LABEL description="NAS Samba + CloudNAS file manager"
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime: Samba, supervisor, tini, Node.js 20.x, build tools for native modules
RUN apt-get update && \
apt-get install -y --no-install-recommends \
samba \
samba-common-bin \
supervisor \
curl \
ca-certificates \
tini \
python3 \
make \
g++ \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Copy app source
COPY app/ /app/
WORKDIR /app
# Install production dependencies (compiles better-sqlite3 native module)
RUN npm ci --omit=dev && npm cache clean --force
# Clean up
RUN rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man
# Create directories
RUN mkdir -p /nas /config /var/log/supervisor /var/log/samba /run/samba
# Samba configuration
COPY smb.conf /etc/samba/smb.conf
# Supervisord configuration
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Entrypoint
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 8080 139 445
ENTRYPOINT ["tini", "--"]
CMD ["/entrypoint.sh"]

View File

@@ -0,0 +1,13 @@
{
"files": {
"main.css": "/static/css/main.da1289f2.css",
"main.js": "/static/js/main.fcd0e5c7.js",
"index.html": "/index.html",
"main.da1289f2.css.map": "/static/css/main.da1289f2.css.map",
"main.fcd0e5c7.js.map": "/static/js/main.fcd0e5c7.js.map"
},
"entrypoints": [
"static/css/main.da1289f2.css",
"static/js/main.fcd0e5c7.js"
]
}

View File

@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#0F172A"/><meta name="description" content="CloudSync - Enterprise File Manager"/><title>CloudSync - Enterprise File Manager</title><script defer="defer" src="/static/js/main.fcd0e5c7.js"></script><link href="/static/css/main.da1289f2.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license lucide-react v0.312.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "cloudsync-frontend",
"version": "2.0.0",
"private": true,
"dependencies": {
"axios": "^1.6.5",
"clsx": "^2.1.1",
"date-fns": "^3.3.0",
"lucide-react": "^0.312.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#0F172A" />
<meta name="description" content="CloudSync - Enterprise File Manager" />
<title>CloudSync - Enterprise File Manager</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1 @@
/* Additional app-specific styles */

View File

@@ -0,0 +1,51 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Toaster, toast } from 'sonner';
import { AuthProvider, useAuth } from './context/AuthContext';
import { ThemeProvider, useTheme } from './context/ThemeContext';
import LoginPage from './components/LoginPage';
import Dashboard from './components/Dashboard';
import SharePage from './components/SharePage';
import LoadingScreen from './components/ui/LoadingScreen';
function AppContent() {
const { user, loading } = useAuth();
// Handle /share/:token routes without React Router
const path = window.location.pathname;
const shareMatch = path.match(/^\/share\/([a-f0-9]+)$/);
if (shareMatch) {
return <SharePage token={shareMatch[1]} />;
}
if (loading) {
return <LoadingScreen />;
}
if (!user) {
return <LoginPage />;
}
return <Dashboard />;
}
function App() {
return (
<ThemeProvider>
<AuthProvider>
<Toaster
position="top-right"
richColors
closeButton
toastOptions={{
style: {
fontFamily: 'Inter, system-ui, sans-serif',
},
}}
/>
<AppContent />
</AuthProvider>
</ThemeProvider>
);
}
export default App;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,145 @@
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import { toast } from 'sonner';
import { Cloud, Eye, EyeOff, Loader2 } from 'lucide-react';
function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
if (!username.trim() || !password.trim()) {
toast.error('Please enter username and password');
return;
}
setLoading(true);
try {
const user = await login(username, password);
toast.success(`Welcome back, ${user.name}!`);
} catch (error) {
const message = error.response?.data?.detail || 'Invalid credentials';
toast.error(message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex">
{/* Left side - Background image */}
<div
className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative bg-cover bg-center"
style={{
backgroundImage: 'url(https://images.pexels.com/photos/28428586/pexels-photo-28428586.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940)'
}}
>
<div className="absolute inset-0 bg-black/40"></div>
<div className="relative z-10 flex flex-col justify-end p-12 text-white">
<h1 className="font-secondary text-4xl font-bold mb-4">CloudSync</h1>
<p className="text-lg text-white/80 max-w-md">
Enterprise-grade file management with secure sharing, storage quotas, and comprehensive activity tracking.
</p>
</div>
</div>
{/* Right side - Login form */}
<div className="flex-1 flex items-center justify-center p-8 bg-background">
<div className="w-full max-w-md">
{/* Logo for mobile */}
<div className="lg:hidden flex items-center gap-3 mb-8">
<div className="w-10 h-10 bg-primary rounded-lg flex items-center justify-center">
<Cloud className="w-5 h-5 text-primary-foreground" />
</div>
<span className="font-secondary text-2xl font-bold text-foreground">CloudSync</span>
</div>
<div className="mb-8">
<h2 className="font-secondary text-3xl font-bold text-foreground mb-2">Sign in</h2>
<p className="text-muted-foreground">Enter your credentials to access your files</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="username" className="block text-sm font-medium text-foreground mb-2">
Username
</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Enter your username"
disabled={loading}
data-testid="login-username-input"
className="w-full h-11 px-4 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
autoComplete="username"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground mb-2">
Password
</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter your password"
disabled={loading}
data-testid="login-password-input"
className="w-full h-11 px-4 pr-12 rounded-lg border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-all disabled:opacity-50"
autoComplete="current-password"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
data-testid="toggle-password-visibility"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
data-testid="login-submit-btn"
className="w-full h-11 bg-primary text-primary-foreground font-medium rounded-lg hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 transition-all disabled:opacity-50 flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Signing in...
</>
) : (
'Sign in'
)}
</button>
</form>
<div className="mt-8 p-4 bg-muted rounded-lg">
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Demo credentials:</span>
<br />
Username: <code className="font-mono bg-background px-1 rounded">admin</code>
<br />
Password: <code className="font-mono bg-background px-1 rounded">admin123</code>
</p>
</div>
</div>
</div>
</div>
);
}
export default LoginPage;

View File

@@ -0,0 +1,182 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Cloud, CheckCircle, Lock, FileText, AlertCircle } from 'lucide-react';
import { formatBytes } from '../lib/utils';
import * as api from '../lib/api';
export default function SharePage({ token }) {
const [fileInfo, setFileInfo] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const [password, setPassword] = useState('');
const [downloaded, setDownloaded] = useState(false);
const [downloading, setDownloading] = useState(false);
const doDownload = useCallback(async (pwd) => {
if (!fileInfo || downloading) return;
setDownloading(true);
setError(null);
try {
const url = api.downloadSharedFile(token, pwd || null);
const res = await fetch(url, { method: 'POST' });
if (!res.ok) {
const data = await res.json();
throw new Error(data.detail || 'Download failed');
}
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileInfo.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
setDownloaded(true);
} catch (err) {
setError(err.message);
setDownloading(false);
}
}, [fileInfo, downloading, token]);
useEffect(() => {
async function load() {
try {
const res = await api.getSharedFileInfo(token);
setFileInfo(res.data);
} catch (err) {
const status = err.response?.status;
if (status === 410) {
setError('This share link has expired.');
} else if (status === 404) {
setError('Share link not found.');
} else {
setError('Failed to load shared file.');
}
} finally {
setLoading(false);
}
}
load();
}, [token]);
// Auto-download if no password required
useEffect(() => {
if (fileInfo && !fileInfo.requires_password && !downloaded && !downloading) {
doDownload(null);
}
}, [fileInfo, downloaded, downloading, doDownload]);
if (loading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full"></div>
</div>
);
}
if (error && !fileInfo) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<AlertCircle className="w-12 h-12 text-destructive mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Unavailable</h2>
<p className="text-muted-foreground">{error}</p>
</div>
</div>
);
}
// Downloaded state
if (downloaded) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<div className="flex items-center justify-center gap-2 mb-6">
<Cloud className="w-6 h-6 text-primary" />
<span className="text-lg font-semibold">CloudSync</span>
</div>
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">File Downloaded</h2>
<div className="flex items-center gap-3 justify-center p-3 bg-accent/50 rounded-lg mt-4">
<FileText className="w-6 h-6 text-muted-foreground flex-shrink-0" />
<div className="text-left min-w-0">
<p className="font-medium text-sm truncate">{fileInfo.name}</p>
<p className="text-xs text-muted-foreground">{formatBytes(fileInfo.size)}</p>
</div>
</div>
<p className="text-sm text-muted-foreground mt-4">This link has been used and is no longer active.</p>
</div>
</div>
);
}
// Password required state
if (fileInfo.requires_password && !downloaded) {
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full">
<div className="flex items-center gap-2 mb-6">
<Cloud className="w-6 h-6 text-primary" />
<span className="text-lg font-semibold">CloudSync</span>
</div>
<div className="flex items-center gap-4 mb-6 p-4 bg-accent/50 rounded-lg">
<FileText className="w-10 h-10 text-muted-foreground flex-shrink-0" />
<div className="min-w-0">
<p className="font-medium truncate">{fileInfo.name}</p>
<p className="text-sm text-muted-foreground">{formatBytes(fileInfo.size)}</p>
</div>
</div>
{error && (
<div className="mb-4 p-3 bg-destructive/10 text-destructive text-sm rounded-lg">
{error}
</div>
)}
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
<Lock className="w-4 h-4 inline mr-1" />
This file is password protected
</label>
<input
type="password"
value={password}
onChange={(e) => { setPassword(e.target.value); setError(null); }}
onKeyDown={(e) => { if (e.key === 'Enter' && password) doDownload(password); }}
placeholder="Enter password"
className="w-full h-10 px-4 bg-background border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-ring"
autoFocus
/>
</div>
<button
onClick={() => doDownload(password)}
disabled={downloading || !password}
className="w-full h-10 bg-primary text-primary-foreground rounded-lg hover:opacity-90 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{downloading ? (
<div className="animate-spin w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full"></div>
) : (
'Download'
)}
</button>
</div>
</div>
);
}
// Downloading state (auto-download in progress, no password)
return (
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="bg-card border border-border rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<div className="flex items-center justify-center gap-2 mb-6">
<Cloud className="w-6 h-6 text-primary" />
<span className="text-lg font-semibold">CloudSync</span>
</div>
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-muted-foreground">Downloading {fileInfo.name}...</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { cn } from '../../lib/utils';
export function LoadingScreen() {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative w-12 h-12">
<div className="absolute inset-0 rounded-full border-2 border-muted"></div>
<div className="absolute inset-0 rounded-full border-2 border-primary border-t-transparent animate-spin"></div>
</div>
<p className="text-muted-foreground font-medium">Loading CloudSync...</p>
</div>
</div>
);
}
export default LoadingScreen;

View File

@@ -0,0 +1,69 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { login as apiLogin, getMe } from '../lib/api';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [token, setToken] = useState(localStorage.getItem('token'));
const checkAuth = useCallback(async () => {
const storedToken = localStorage.getItem('token');
if (!storedToken) {
setLoading(false);
return;
}
try {
const response = await getMe();
setUser(response.data);
setToken(storedToken);
} catch (error) {
localStorage.removeItem('token');
setUser(null);
setToken(null);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
checkAuth();
}, [checkAuth]);
const login = async (username, password) => {
const response = await apiLogin(username, password);
const { token: newToken, user: userData } = response.data;
localStorage.setItem('token', newToken);
setToken(newToken);
setUser(userData);
return userData;
};
const logout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
};
const updateUser = useCallback((userData) => {
setUser(prev => ({ ...prev, ...userData }));
}, []);
return (
<AuthContext.Provider value={{ user, token, loading, login, logout, updateUser, checkAuth }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,36 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState(() => {
const stored = localStorage.getItem('theme');
if (stored) return stored;
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -0,0 +1,116 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #FFFFFF;
--foreground: #0A0A0A;
--card: #FFFFFF;
--card-foreground: #0A0A0A;
--popover: #FFFFFF;
--popover-foreground: #0A0A0A;
--primary: #0F172A;
--primary-foreground: #F8FAFC;
--secondary: #F1F5F9;
--secondary-foreground: #0F172A;
--muted: #F1F5F9;
--muted-foreground: #64748B;
--accent: #F1F5F9;
--accent-foreground: #0F172A;
--destructive: #EF4444;
--destructive-foreground: #F8FAFC;
--border: #E2E8F0;
--input: #E2E8F0;
--ring: #0F172A;
--radius: 0.5rem;
}
.dark {
--background: #020617;
--foreground: #F8FAFC;
--card: #0F172A;
--card-foreground: #F8FAFC;
--popover: #0F172A;
--popover-foreground: #F8FAFC;
--primary: #F8FAFC;
--primary-foreground: #0F172A;
--secondary: #1E293B;
--secondary-foreground: #F8FAFC;
--muted: #1E293B;
--muted-foreground: #94A3B8;
--accent: #1E293B;
--accent-foreground: #F8FAFC;
--destructive: #7F1D1D;
--destructive-foreground: #F8FAFC;
--border: #1E293B;
--input: #1E293B;
--ring: #D1D5DB;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
height: 100%;
}
body {
font-family: 'Inter', system-ui, sans-serif;
background-color: var(--background);
color: var(--foreground);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--muted);
}
::-webkit-scrollbar-thumb {
background: var(--muted-foreground);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--foreground);
}
/* Skeleton loader animation */
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.skeleton {
animation: skeleton-pulse 1.5s ease-in-out infinite;
background: linear-gradient(90deg, var(--muted) 25%, var(--border) 50%, var(--muted) 75%);
background-size: 200% 100%;
}
/* File drop zone styles */
.drop-zone-active {
border-color: var(--ring) !important;
background-color: var(--accent) !important;
}
/* Focus visible styles */
.focus-visible:focus {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* Transition utilities */
.transition-default {
transition: all 200ms ease-in-out;
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,136 @@
import axios from 'axios';
const API_URL = process.env.REACT_APP_BACKEND_URL || '';
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor to add auth token
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor to handle auth errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.reload();
}
return Promise.reject(error);
}
);
// Auth
export const login = (username, password) =>
api.post('/api/auth/login', { username, password });
export const getMe = () =>
api.get('/api/auth/me');
// Users
export const getUsers = () =>
api.get('/api/users');
export const createUser = (userData) =>
api.post('/api/users', userData);
export const updateUser = (userId, userData) =>
api.put(`/api/users/${userId}`, userData);
export const deleteUser = (userId) =>
api.delete(`/api/users/${userId}`);
// Files
export const getFiles = (parentId = null, search = null) => {
const params = new URLSearchParams();
if (parentId) params.append('parent_id', parentId);
if (search) params.append('search', search);
return api.get(`/api/files?${params.toString()}`);
};
export const getFile = (fileId) =>
api.get(`/api/files/${fileId}`);
export const createFolder = (name, parentId = null) =>
api.post('/api/files/folder', { name, parent_id: parentId, is_folder: true });
export const uploadFile = (file, parentId, onProgress) => {
const formData = new FormData();
formData.append('file', file);
if (parentId) {
formData.append('parent_id', parentId);
}
return api.post('/api/files/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (onProgress) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress(progress);
}
},
});
};
export const renameFile = (fileId, newName) =>
api.put(`/api/files/${fileId}/rename`, { new_name: newName });
export const moveFile = (fileId, destinationId) =>
api.put(`/api/files/${fileId}/move`, { destination_id: destinationId });
export const copyFile = (fileId, destinationId) =>
api.post(`/api/files/${fileId}/copy`, { destination_id: destinationId });
export const deleteFile = (fileId) =>
api.delete(`/api/files/${fileId}`);
export const bulkDeleteFiles = (fileIds) =>
api.post('/api/files/bulk-delete', fileIds);
export const getDownloadUrl = (fileId) =>
`${API_URL}/api/files/${fileId}/download`;
export const getPreviewUrl = (fileId) =>
`${API_URL}/api/files/${fileId}/preview`;
export const getBreadcrumb = (fileId) =>
api.get(`/api/files/${fileId}/breadcrumb`);
// Shares
export const getShares = () =>
api.get('/api/shares');
export const createShare = (fileId, expiresInHours = 24, password = null) =>
api.post('/api/shares', { file_id: fileId, expires_in_hours: expiresInHours, password });
export const deleteShare = (shareId) =>
api.delete(`/api/shares/${shareId}`);
export const getSharedFileInfo = (token) =>
api.get(`/api/share/${token}`);
export const downloadSharedFile = (token, password = null) => {
const params = password ? `?password=${encodeURIComponent(password)}` : '';
return `${API_URL}/api/share/${token}/download${params}`;
};
// Activity
export const getActivity = (limit = 50) =>
api.get(`/api/activity?limit=${limit}`);
// Stats
export const getStats = () =>
api.get('/api/stats');
export default api;

View File

@@ -0,0 +1,33 @@
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
export function formatBytes(bytes, decimals = 1) {
if (bytes === 0) return '0 B';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export function formatDate(dateString) {
if (!dateString) return '-';
const date = new Date(dateString);
const now = new Date();
const diff = Math.floor((now - date) / 1000);
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
export function getFileExtension(filename) {
return filename.includes('.') ? filename.split('.').pop().toLowerCase() : '';
}

View File

@@ -0,0 +1,60 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
darkMode: 'class',
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
card: "var(--card)",
"card-foreground": "var(--card-foreground)",
popover: "var(--popover)",
"popover-foreground": "var(--popover-foreground)",
primary: "var(--primary)",
"primary-foreground": "var(--primary-foreground)",
secondary: "var(--secondary)",
"secondary-foreground": "var(--secondary-foreground)",
muted: "var(--muted)",
"muted-foreground": "var(--muted-foreground)",
accent: "var(--accent)",
"accent-foreground": "var(--accent-foreground)",
destructive: "var(--destructive)",
"destructive-foreground": "var(--destructive-foreground)",
border: "var(--border)",
input: "var(--input)",
ring: "var(--ring)",
},
fontFamily: {
primary: ['Inter', 'system-ui', 'sans-serif'],
secondary: ['Manrope', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
animation: {
'fade-in': 'fadeIn 0.3s ease-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
slideDown: {
'0%': { opacity: '0', transform: 'translateY(-10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
},
},
plugins: [],
};

2558
apps/nas-samba/source/app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
{
"name": "cloudsync",
"version": "1.0.0",
"description": "CloudSync File Manager - NAS Edition",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"archiver": "^7.0.1",
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.7.0",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"mime-types": "^2.1.35",
"multer": "^1.4.5-lts.1"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -e
# Create users group if it doesn't exist
getent group users >/dev/null 2>&1 || groupadd -g 100 users
# Create alexz user with UID 1000 if not exists
if ! id -u alexz >/dev/null 2>&1; then
# Remove any existing user with UID 1000 (e.g. ubuntu)
existing_user=$(getent passwd 1000 | cut -d: -f1)
if [ -n "$existing_user" ] && [ "$existing_user" != "alexz" ]; then
userdel "$existing_user" 2>/dev/null || true
fi
useradd -u 1000 -g users -M -s /usr/sbin/nologin alexz
echo "Created user alexz (UID 1000)"
fi
# Set Samba password from environment variable
if [ -n "$SMB_PASSWORD" ]; then
echo -e "${SMB_PASSWORD}\n${SMB_PASSWORD}" | smbpasswd -a -s alexz
echo "Samba password set for alexz"
else
echo "WARNING: SMB_PASSWORD not set. Samba authentication will fail."
fi
# Ensure directories exist
mkdir -p /config /var/log/samba /run/samba /nas
# Ensure correct ownership
chown alexz:users /nas 2>/dev/null || true
echo "Starting supervisord..."
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

View File

@@ -0,0 +1,18 @@
[global]
workgroup = WORKGROUP
server string = NAS Samba
server role = standalone server
log file = /var/log/samba/log.%m
max log size = 50
logging = file
map to guest = bad user
server min protocol = SMB2
server max protocol = SMB3
[data]
path = /nas
valid users = alexz
browsable = yes
writable = yes
create mask = 0664
directory mask = 0775

View File

@@ -0,0 +1,34 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
childlogdir=/var/log/supervisor
[program:smbd]
command=/usr/sbin/smbd -F --no-process-group
autostart=true
autorestart=true
priority=10
stdout_logfile=/var/log/supervisor/smbd-stdout.log
stderr_logfile=/var/log/supervisor/smbd-stderr.log
[program:nmbd]
command=/usr/sbin/nmbd -F --no-process-group
autostart=true
autorestart=true
priority=10
stdout_logfile=/var/log/supervisor/nmbd-stdout.log
stderr_logfile=/var/log/supervisor/nmbd-stderr.log
[program:cloudnas]
command=node /app/server.js
directory=/app
autostart=true
autorestart=true
priority=20
environment=PORT="8080",UPLOAD_DIR="/nas",DB_PATH="/config/cloudsync.db",JWT_SECRET="%(ENV_JWT_SECRET)s",SMB_PASSWORD="%(ENV_SMB_PASSWORD)s"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

View File

@@ -1,46 +0,0 @@
services:
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:2.13.5
restart: unless-stopped
networks:
nginx-proxy-manager_runtipi_network:
gw_priority: 0
tipi_main_network:
gw_priority: 1
cistech-macvlan:
environment:
DISABLE_IPV6: "true"
ports:
- ${NPM_HTTP_PORT}:80
- ${NPM_HTTPS_PORT}:443
- ${APP_PORT}:81
volumes:
- ${APP_DATA_DIR}/data:/data
- ${APP_DATA_DIR}/letsencrypt:/etc/letsencrypt
labels:
generated: true
traefik.enable: true
traefik.docker.network: runtipi_tipi_main_network
traefik.http.middlewares.nginx-proxy-manager-runtipi-web-redirect.redirectscheme.scheme: https
traefik.http.services.nginx-proxy-manager-runtipi.loadbalancer.server.port: "81"
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.entrypoints: web
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.service: nginx-proxy-manager-runtipi
traefik.http.routers.nginx-proxy-manager-runtipi-insecure.middlewares: nginx-proxy-manager-runtipi-web-redirect
traefik.http.routers.nginx-proxy-manager-runtipi.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.nginx-proxy-manager-runtipi.entrypoints: websecure
traefik.http.routers.nginx-proxy-manager-runtipi.service: nginx-proxy-manager-runtipi
traefik.http.routers.nginx-proxy-manager-runtipi.tls.certresolver: myresolver
runtipi.managed: true
runtipi.appurn: nginx-proxy-manager:runtipi
networks:
tipi_main_network:
name: runtipi_tipi_main_network
external: true
nginx-proxy-manager_runtipi_network:
name: nginx-proxy-manager_runtipi_network
external: false
cistech-macvlan:
name: cistech-macvlan
external: true

View File

@@ -1,10 +1,10 @@
{ {
"name": "Nginx Proxy Manager", "name": "Nginx Proxy Manager",
"id": "nginx-proxy-manager", "id": "npm",
"available": true, "available": true,
"short_desc": "Docker container for managing Nginx proxy hosts with a simple, powerful web interface", "short_desc": "Docker container for managing Nginx proxy hosts with a simple, powerful web interface",
"author": "jc21", "author": "jc21",
"port": 81, "hostname": "nginx.alexzaw.dev",
"categories": [ "categories": [
"utilities", "utilities",
"network" "network"
@@ -22,7 +22,7 @@
"type": "number", "type": "number",
"label": "HTTP Port", "label": "HTTP Port",
"env_variable": "NPM_HTTP_PORT", "env_variable": "NPM_HTTP_PORT",
"default": "1080", "default": "80",
"required": true, "required": true,
"hint": "Port for HTTP traffic (mapped to container port 80)" "hint": "Port for HTTP traffic (mapped to container port 80)"
}, },
@@ -30,7 +30,15 @@
"type": "number", "type": "number",
"label": "HTTPS Port", "label": "HTTPS Port",
"env_variable": "NPM_HTTPS_PORT", "env_variable": "NPM_HTTPS_PORT",
"default": "10443", "default": "443",
"required": true,
"hint": "Port for HTTPS traffic (mapped to container port 443)"
},
{
"type": "number",
"label": "Web UI Port",
"env_variable": "NPM_WEBUI_PORT",
"default": "81",
"required": true, "required": true,
"hint": "Port for HTTPS traffic (mapped to container port 443)" "hint": "Port for HTTPS traffic (mapped to container port 443)"
} }
@@ -38,7 +46,5 @@
"supported_architectures": [ "supported_architectures": [
"arm64", "arm64",
"amd64" "amd64"
], ]
"created_at": 1731607800000,
"updated_at": 1731607800000
} }

View File

@@ -1,23 +1,28 @@
{ {
"schemaVersion": 2,
"services": [ "services": [
{ {
"name": "nginx-proxy-manager", "name": "npm",
"image": "jc21/nginx-proxy-manager:2.13.5", "image": "jc21/nginx-proxy-manager:2.13.5",
"isMain": true, "isMain": true,
"internalPort": 81, "internalPort": 81,
"addPorts": [ "addPorts": [
{ {
"hostPort": "${NPM_HTTP_PORT}", "hostPort": "192.168.1.150:${NPM_HTTP_PORT}",
"containerPort": 80 "containerPort": 80
}, },
{ {
"hostPort": "${NPM_HTTPS_PORT}", "hostPort": "192.168.1.150:${NPM_HTTPS_PORT}",
"containerPort": 443 "containerPort": 443
},
{
"hostPort": "192.168.1.150:${NPM_WEBUI_PORT}",
"containerPort": 81
} }
], ],
"environment": { "environment": [
"DISABLE_IPV6": "true" {"key": "DISABLE_IPV6", "value": "true"}
}, ],
"volumes": [ "volumes": [
{ {
"hostPath": "${APP_DATA_DIR}/data", "hostPath": "${APP_DATA_DIR}/data",

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

2
apps/rego-tunnel/build/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Large binary files - track tar.gz but not 7z
*.7z

83
apps/rego-tunnel/build/Dockerfile Normal file → Executable file
View File

@@ -1,17 +1,80 @@
FROM ubuntu:24.04 FROM ubuntu:22.04
LABEL maintainer="alexz"
LABEL description="Cisco Secure Client VPN in Docker with noVNC"
LABEL version="5.1.14.145"
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV container=docker
RUN apt-get update && apt-get install -y qemu-system-x86 qemu-utils novnc websockify openssh-server supervisor iproute2 bridge-utils iptables nano net-tools p7zip-full && rm -rf /var/lib/apt/lists/* # VNC/noVNC settings
ENV DISPLAY=:1
ENV VNC_PORT=5901
ENV NOVNC_PORT=6080
# Setup SSH # Install systemd and dependencies
RUN mkdir /var/run/sshd && echo 'root:vmpassword' | chpasswd && sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config RUN apt-get update && apt-get install -y \
systemd \
systemd-sysv \
dbus \
dbus-x11 \
libgtk-3-0 \
libglib2.0-0 \
libstdc++6 \
iptables \
libxml2 \
network-manager \
zlib1g \
policykit-1 \
xdg-utils \
libwebkit2gtk-4.0-37 \
tigervnc-standalone-server \
tigervnc-common \
novnc \
websockify \
openbox \
xterm \
procps \
net-tools \
curl \
iproute2 \
iputils-ping \
nano \
x11vnc \
xvfb \
fluxbox \
xdotool \
oathtool \
xclip \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /vm # Remove unnecessary systemd services that cause issues in containers
RUN rm -f /lib/systemd/system/multi-user.target.wants/* \
/etc/systemd/system/*.wants/* \
/lib/systemd/system/local-fs.target.wants/* \
/lib/systemd/system/sockets.target.wants/*udev* \
/lib/systemd/system/sockets.target.wants/*initctl* \
/lib/systemd/system/sysinit.target.wants/systemd-tmpfiles-setup* \
/lib/systemd/system/systemd-update-utmp*
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf # Copy and extract Cisco Secure Client
COPY start-vm.sh /usr/local/bin/start-vm.sh COPY cisco-secure-client-full.tar.gz /tmp/
COPY setup-network.sh /usr/local/bin/setup-network.sh RUN tar -xzf /tmp/cisco-secure-client-full.tar.gz -C / && rm /tmp/cisco-secure-client-full.tar.gz
RUN chmod +x /usr/local/bin/start-vm.sh /usr/local/bin/setup-network.sh
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] # Enable vpnagentd service
RUN systemctl enable vpnagentd.service
# vnc.service - started by systemd, calls /opt/scripts/startup-vnc.sh
# The entrypoint symlinks /shared/startup-vnc.sh -> /opt/scripts/startup-vnc.sh at runtime
RUN mkdir -p /opt/scripts && \
echo 'W1VuaXRdCkRlc2NyaXB0aW9uPVZOQyBhbmQgbm9WTkMgU2VydmVyCkFmdGVyPW5ldHdvcmsudGFyZ2V0IHZwbmFnZW50ZC5zZXJ2aWNlCgpbU2VydmljZV0KVHlwZT1zaW1wbGUKRXhlY1N0YXJ0PS9vcHQvc2NyaXB0cy9zdGFydHVwLXZuYy5zaApSZXN0YXJ0PWFsd2F5cwpSZXN0YXJ0U2VjPTUKRW52aXJvbm1lbnQ9SE9NRT0vcm9vdApFbnZpcm9ubWVudD1VU0VSPXJvb3QKCltJbnN0YWxsXQpXYW50ZWRCeT1tdWx0aS11c2VyLnRhcmdldAo=' \
| base64 -d > /lib/systemd/system/vnc.service && \
chmod 644 /lib/systemd/system/vnc.service && \
systemctl enable vnc.service
VOLUME ["/sys/fs/cgroup"]
EXPOSE 5901 6080
STOPSIGNAL SIGRTMIN+3

View File

@@ -0,0 +1,51 @@
# Rego Tunnel - Build Files
This directory contains the Dockerfile and scripts to build the Cisco VPN Docker image.
## Files
- `Dockerfile` - Docker image definition (Ubuntu 22.04 + Cisco Secure Client + noVNC)
- `cisco-secure-client-full.tar.gz` - Pre-extracted Cisco Secure Client 5.1.14.145
- `build.sh` - Build and push script
- `scripts/entrypoint.sh` - Container entrypoint (starts systemd)
## Building
```bash
cd /etc/runtipi/repos/runtipi/apps/rego-tunnel/build
./build.sh
```
This builds and pushes to `git.alexzaw.dev/alexz/cisco-vpn:latest`
To build without pushing:
```bash
docker build -t git.alexzaw.dev/alexz/cisco-vpn:latest .
```
## What's in the image
The Dockerfile creates an image with:
- Ubuntu 22.04 with systemd
- Cisco Secure Client 5.1.14.145 (VPN, DART, Posture modules)
- TigerVNC server + noVNC (web-based VNC)
- Tools: xdotool, oathtool (for TOTP), xclip, openbox
### Systemd services (baked in)
- `vpnagentd.service` - Cisco VPN agent
- `vnc.service` - VNC server + noVNC websockify
### Scripts (baked in via base64 in Dockerfile)
- `/opt/scripts/startup-vnc.sh` - Starts VNC server and noVNC
- `/opt/scripts/entrypoint.sh` - Container entrypoint
## Runtime mounts (from shared/)
When running as rego-tunnel app, these are mounted from `shared/`:
- `/shared/cisco-vpn` - Main VPN automation script
- `/shared/xstartup``/root/.vnc/xstartup` - VNC session startup
## Ports
- `5901` - VNC server
- `6080` - noVNC web interface

View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Build and push the Cisco VPN Docker image
# Run this from the build directory
set -euo pipefail
IMAGE_NAME="${IMAGE_NAME:-git.alexzaw.dev/alexz/cisco-vpn}"
IMAGE_TAG="${IMAGE_TAG:-latest}"
echo "Building ${IMAGE_NAME}:${IMAGE_TAG}..."
docker build "$@" -t "${IMAGE_NAME}:${IMAGE_TAG}" .
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Build complete!"
echo ""
echo "To test locally:"
echo " docker run -d --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw --cap-add=NET_ADMIN --device=/dev/net/tun -p 5901:5901 -p 6080:6080 ${IMAGE_NAME}:${IMAGE_TAG}"
echo ""
echo "Then connect via VNC to localhost:5901 or open noVNC at http://localhost:6080/vnc.html"
echo ""

Binary file not shown.

View File

@@ -1,41 +0,0 @@
#!/bin/bash
# Setup TAP/Bridge networking for QEMU VM
# Bridge: 100.100.0.1/24
# VM will be: 100.100.0.2/24
set -e
# Create bridge if not exists
if ! ip link show br0 &>/dev/null; then
ip link add br0 type bridge
ip addr add 100.100.0.1/24 dev br0
ip link set br0 up
echo "Bridge br0 created with IP 100.100.0.1/24"
fi
# Create TAP device if not exists
if ! ip link show tap0 &>/dev/null; then
ip tuntap add tap0 mode tap
ip link set tap0 master br0
ip link set tap0 up
echo "TAP device tap0 created and attached to br0"
fi
# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# Setup NAT/masquerade for outbound traffic from VM
iptables -t nat -C POSTROUTING -s 100.100.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 100.100.0.0/24 -o eth0 -j MASQUERADE
# Forward traffic destined for VPN networks to VM (10.35.33.230 = IBM i)
# The VM will route this through its VPN tunnel
iptables -C FORWARD -d 10.35.33.230 -j ACCEPT 2>/dev/null || iptables -A FORWARD -d 10.35.33.230 -j ACCEPT
iptables -C FORWARD -s 10.35.33.230 -j ACCEPT 2>/dev/null || iptables -A FORWARD -s 10.35.33.230 -j ACCEPT
# Route to IBM i through VM
ip route add 10.35.33.230 via 100.100.0.2 2>/dev/null || true
echo "Network setup complete"
echo "Bridge: br0 = 100.100.0.1/24"
echo "TAP: tap0 attached to br0"
echo "Route: 10.35.33.230 via 100.100.0.2 (VM)"

View File

@@ -1,36 +0,0 @@
#!/bin/bash
set -euo pipefail
# If provided, extract ssh.zip to /root/.ssh/zip (not baked into the image)
SSH_ZIP_PATH="/shared/ssh.zip"
SSH_ZIP_DEST="/root/.ssh/zip"
if [ -f "$SSH_ZIP_PATH" ]; then
mkdir -p "$SSH_ZIP_DEST"
chmod 700 /root/.ssh
chmod 700 "$SSH_ZIP_DEST"
echo "[rego-tunnel] Extracting $SSH_ZIP_PATH -> $SSH_ZIP_DEST"
7z x -y -o"$SSH_ZIP_DEST" "$SSH_ZIP_PATH" >/dev/null
find "$SSH_ZIP_DEST" -type d -exec chmod 700 {} \;
find "$SSH_ZIP_DEST" -type f -exec chmod 600 {} \;
else
echo "[rego-tunnel] No $SSH_ZIP_PATH found; skipping SSH zip extraction"
fi
# Wait for network setup
sleep 2
exec qemu-system-x86_64 \
-enable-kvm \
-cpu host \
-m ${VM_RAM:-8G} \
-smp ${VM_CPUS:-4} \
-hda /vm/linux-vm.qcow2 \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-pci,netdev=net0,mac=52:54:00:12:34:56 \
-vnc :0 \
-vga virtio \
-usb \
-device usb-tablet

View File

@@ -1,36 +0,0 @@
[supervisord]
nodaemon=true
logfile=/var/log/supervisord.log
[program:network-setup]
command=/usr/local/bin/setup-network.sh
autostart=true
autorestart=false
startsecs=0
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:sshd]
command=/usr/sbin/sshd -D
autostart=true
autorestart=true
priority=10
[program:qemu]
command=/usr/local/bin/start-vm.sh
autostart=true
autorestart=true
priority=20
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:novnc]
command=/usr/share/novnc/utils/novnc_proxy --vnc localhost:5900 --listen 8006
autostart=true
autorestart=true
priority=30

View File

@@ -1,34 +1,76 @@
{ {
"name": "Rego Tunnel", "name": "Rego Tunnel",
"available": true, "available": true,
"port": 8006, "port": 6080,
"exposable": true, "exposable": true,
"dynamic_config": true, "dynamic_config": true,
"id": "rego-tunnel", "id": "rego-tunnel",
"description": "Linux VM with Cisco AnyConnect VPN for accessing Rego environments securely.", "description": "Cisco Secure Client VPN in Docker with noVNC web UI for accessing Rego environments. Native Docker - no VM overhead.",
"tipi_version": 3, "tipi_version": 7,
"version": "latest", "version": "5.1.14.145",
"categories": ["utilities"], "categories": [
"short_desc": "Linux VM VPN tunnel to Rego environments.", "utilities"
],
"short_desc": "Cisco VPN tunnel to Rego environments (native Docker)",
"author": "alexz", "author": "alexz",
"source": "https://git.alexzaw.dev/alexz/runtipi", "source": "https://git.alexzaw.dev/alexz/runtipi",
"form_fields": [ "form_fields": [
{ {
"type": "number", "type": "email",
"label": "RAM (GB)", "label": "VPN Email",
"hint": "RAM to assign to the VM (in gigabytes)", "hint": "Email address for VPN SSO login (configured in /shared/cisco-vpn script)",
"placeholder": "8", "placeholder": "your-email@company.com",
"required": true, "required": false,
"env_variable": "WINDOWS_RAM_GB" "env_variable": "VPN_EMAIL",
"default": ""
}, },
{ {
"type": "number", "type": "password",
"label": "CPU Cores", "label": "VPN Password",
"hint": "CPU cores to assign to the VM", "hint": "Password for VPN SSO login (configured in /shared/cisco-vpn script)",
"placeholder": "4", "placeholder": "",
"required": true, "required": false,
"env_variable": "WINDOWS_CPU_CORES" "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-ord1.dovercorp.com"
},
{
"type": "text",
"label": "Target IP",
"hint": "IP address to route through VPN (e.g., IBM i server)",
"placeholder": "10.35.33.230",
"required": false,
"env_variable": "TARGET_IP",
"default": "10.35.33.230"
},
{
"type": "password",
"label": "VNC Password",
"hint": "Password for noVNC web interface",
"placeholder": "cisco123",
"required": false,
"env_variable": "VNC_PASSWORD",
"default": ""
} }
], ],
"supported_architectures": ["amd64"] "supported_architectures": [
"amd64"
]
} }

View File

@@ -1,39 +1,79 @@
{ {
"schemaVersion": 2,
"services": [ "services": [
{ {
"name": "rego-tunnel", "name": "rego-tunnel",
"image": "git.alexzaw.dev/alexz/linux-vm:latest", "image": "git.alexzaw.dev/alexz/cisco-vpn:latest",
"isMain": true,
"internalPort": 8006,
"environment": [ "environment": [
{ {
"key": "VM_RAM", "key": "VPN_EMAIL",
"value": "${WINDOWS_RAM_GB}G" "value": "${VPN_EMAIL}"
}, },
{ {
"key": "VM_CPUS", "key": "VPN_PASSWORD",
"value": "${WINDOWS_CPU_CORES}" "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": 6080,
"volumes": [ "volumes": [
{ {
"hostPath": "/etc/runtipi/user-config/runtipi/rego-tunnel/storage/linux-vm.qcow2", "hostPath": "${APP_DATA_DIR}/config",
"containerPath": "/vm/linux-vm.qcow2" "containerPath": "/config",
"readOnly": false
}, },
{ {
"hostPath": "/etc/runtipi/user-config/runtipi/rego-tunnel/shared", "hostPath": "${APP_DATA_DIR}",
"containerPath": "/shared" "containerPath": "/runtime",
"readOnly": false
},
{
"hostPath": "/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared",
"containerPath": "/shared",
"readOnly": false
},
{
"hostPath": "/sys/fs/cgroup",
"containerPath": "/sys/fs/cgroup",
"readOnly": false
},
{
"hostPath": "/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/xstartup",
"containerPath": "/root/.vnc/xstartup",
"readOnly": true
} }
], ],
"sysctls": { "stopGracePeriod": "30s",
"net.ipv4.ip_forward": 1 "devices": [
}, "/dev/net/tun"
"devices": ["/dev/kvm", "/dev/net/tun"], ],
"capAdd": ["NET_ADMIN"],
"privileged": true, "privileged": true,
"stopGracePeriod": "2m" "capAdd": [
"NET_ADMIN"
],
"isMain": true,
"extraLabels": {
"runtipi.managed": true
}
} }
], ]
"schemaVersion": 2,
"$schema": "https://schemas.runtipi.io/v2/dynamic-compose.json"
} }

View File

@@ -1,45 +1,48 @@
services: services:
rego-tunnel: rego-tunnel:
container_name: rego-tunnel image: git.alexzaw.dev/alexz/cisco-vpn:latest
image: git.alexzaw.dev/alexz/linux-vm:latest
restart: unless-stopped restart: unless-stopped
privileged: true networks:
devices: rego-tunnel_runtipi_network:
- /dev/kvm gw_priority: 0
- /dev/net/tun tipi_main_network:
gw_priority: 1
environment:
VPN_EMAIL: ${VPN_EMAIL}
VPN_PASSWORD: ${VPN_PASSWORD}
VPN_TOTP_SECRET: ${VPN_TOTP_SECRET}
VPN_HOST: ${VPN_HOST}
VNC_PASSWORD: ${VNC_PASSWORD}
TZ: ${TZ}
TARGET_IP: ${TARGET_IP}
ports:
- ${APP_PORT}:6080
volumes:
- ${APP_DATA_DIR}/config:/config
- ${APP_DATA_DIR}:/runtime
- /etc/runtipi/repos/runtipi/apps/rego-tunnel/shared:/shared
- /sys/fs/cgroup:/sys/fs/cgroup:rw
- /etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/xstartup:/root/.vnc/xstartup:ro
labels:
generated: true
traefik.enable: true
traefik.docker.network: runtipi_tipi_main_network
traefik.http.middlewares.rego-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
traefik.http.services.rego-tunnel-runtipi.loadbalancer.server.port: "6080"
traefik.http.routers.rego-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.rego-tunnel-runtipi-insecure.entrypoints: web
traefik.http.routers.rego-tunnel-runtipi-insecure.service: rego-tunnel-runtipi
traefik.http.routers.rego-tunnel-runtipi-insecure.middlewares: rego-tunnel-runtipi-web-redirect
traefik.http.routers.rego-tunnel-runtipi.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.rego-tunnel-runtipi.entrypoints: websecure
traefik.http.routers.rego-tunnel-runtipi.service: rego-tunnel-runtipi
traefik.http.routers.rego-tunnel-runtipi.tls.certresolver: myresolver
runtipi.managed: true
runtipi.appurn: rego-tunnel:runtipi
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
stop_grace_period: 2m devices:
ports: - /dev/net/tun
- ${APP_PORT}:8006 privileged: true
environment: cgroup: host
- VM_RAM=${WINDOWS_RAM_GB}G stop_grace_period: 30s
- VM_CPUS=${WINDOWS_CPU_CORES}
volumes:
- /etc/runtipi/user-config/runtipi/rego-tunnel/storage/linux-vm.qcow2:/vm/linux-vm.qcow2
- /etc/runtipi/user-config/runtipi/rego-tunnel/shared:/shared
networks:
- tipi_main_network
sysctls:
- net.ipv4.ip_forward=1
labels:
traefik.enable: true
traefik.http.middlewares.rego-tunnel-web-redirect.redirectscheme.scheme: https
traefik.http.services.rego-tunnel.loadbalancer.server.port: 8006
traefik.http.routers.rego-tunnel-insecure.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.rego-tunnel-insecure.entrypoints: web
traefik.http.routers.rego-tunnel-insecure.service: rego-tunnel
traefik.http.routers.rego-tunnel-insecure.middlewares: rego-tunnel-web-redirect
traefik.http.routers.rego-tunnel.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.rego-tunnel.entrypoints: websecure
traefik.http.routers.rego-tunnel.service: rego-tunnel
traefik.http.routers.rego-tunnel.tls.certresolver: myresolver
traefik.http.routers.rego-tunnel-local-insecure.rule: Host(`rego-tunnel.${LOCAL_DOMAIN}`)
traefik.http.routers.rego-tunnel-local-insecure.entrypoints: web
traefik.http.routers.rego-tunnel-local-insecure.service: rego-tunnel
traefik.http.routers.rego-tunnel-local-insecure.middlewares: rego-tunnel-web-redirect
traefik.http.routers.rego-tunnel-local.rule: Host(`rego-tunnel.${LOCAL_DOMAIN}`)
traefik.http.routers.rego-tunnel-local.entrypoints: websecure
traefik.http.routers.rego-tunnel-local.service: rego-tunnel
traefik.http.routers.rego-tunnel-local.tls: true
runtipi.managed: true

View File

@@ -1,258 +0,0 @@
# Rego-Tunnel VPN Bridge
This app runs a Windows VM inside a Docker container with Cisco AnyConnect VPN, providing transparent access to VPN-protected resources (IBM i at 10.35.33.230) from the local network.
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────┐
│ Laptop (192.168.0.230) │
│ Route: 172.31.0.0/24 via 192.168.0.150 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Linux Host (192.168.0.150 / 192.168.1.150) │
│ │
│ rego-routing.service: │
│ - Routes 172.32.0.0/24 and 10.35.33.0/24 via 172.31.0.10 │
│ - Removes Docker nft isolation rules for 172.31.0.10 │
│ - DOCKER-USER iptables rules for forwarding │
│ │
│ Bridge: br-vpn-rego (172.31.0.1/24) │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Container: rego-tunnel (172.31.0.10) │
│ │
│ start.sh: │
│ - socat: port 2222 → VM:2222 (SSH to VM) │
│ - DNAT: ports 22,23,446,448,449,8470-8476,2000-2020,3000-3020, │
│ 10000-10020,36000-36010 → VM │
│ - MASQUERADE for docker bridge │
│ │
│ Internal docker bridge: 172.32.0.1/24 │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Windows VM (172.32.0.20) │
│ │
│ SSH Server: port 2222 │
│ Cisco AnyConnect VPN: connected to corporate network │
│ VPN IP: 10.215.x.x │
│ │
│ Portproxy rules (persistent): │
│ - 0.0.0.0:22 → 10.35.33.230:22 │
│ - 0.0.0.0:23 → 10.35.33.230:23 │
│ - 0.0.0.0:446,448,449 → 10.35.33.230:* │
│ - 0.0.0.0:8470-8476 → 10.35.33.230:* │
│ - 0.0.0.0:2000-2020 → 10.35.33.230:* │
│ - 0.0.0.0:3000-3020 → 10.35.33.230:* │
│ - 0.0.0.0:10000-10020 → 10.35.33.230:* │
│ - 0.0.0.0:36000-36010 → 10.35.33.230:* │
│ │
│ vpn-login.js: │
│ - Auto-login to Cisco AnyConnect via WebView DevTools │
│ - TOTP authentication │
│ - Watchdog: monitors VPN and reconnects if dropped │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ IBM i (10.35.33.230) │
│ Via Cisco VPN tunnel │
└─────────────────────────────────────────────────────────────────────────┘
```
## Network Configuration
### IP Addresses
| Component | IP Address |
|-----------|------------|
| Container external (br-vpn-rego) | 172.31.0.10 |
| Container internal bridge | 172.32.0.1 |
| Windows VM | 172.32.0.20 |
| IBM i (via VPN) | 10.35.33.230 |
### Ports
| Port | Destination | Purpose |
|------|-------------|---------|
| 2222 | VM SSH (2222) | SSH access to Windows VM |
| 22 | IBM i (via portproxy) | SSH to IBM i |
| 23 | IBM i (via portproxy) | Telnet to IBM i |
| 446,448,449 | IBM i (via portproxy) | IBM i services |
| 8470-8476 | IBM i (via portproxy) | IBM i data ports |
| 2000-2020 | IBM i (via portproxy) | Additional ports |
| 3000-3020 | IBM i (via portproxy) | Additional ports |
| 10000-10020 | IBM i (via portproxy) | Additional ports |
| 36000-36010 | IBM i (via portproxy) | Additional ports |
| 8006 | Container | Web-based Windows viewer |
## Host Configuration
### Systemd Service: rego-routing.service
Location: `/etc/systemd/system/rego-routing.service`
This service runs after docker.service and:
1. Adds routes for 172.32.0.0/24 and 10.35.33.0/24 via 172.31.0.10
2. Adds DOCKER-USER iptables rules for forwarding
3. Removes Docker's nft isolation rules that block external access to 172.31.0.10
```bash
# Check status
sudo systemctl status rego-routing.service
# Restart if needed
sudo systemctl restart rego-routing.service
```
### Client Route (Windows Laptop)
Add a persistent route to reach the container network:
```cmd
route add 172.31.0.0 mask 255.255.255.0 192.168.0.150 -p
```
Where 192.168.0.150 is the Linux host IP.
## Files
### vpn_scripts/start.sh
Startup script that runs before the Windows VM entry.sh:
- Installs required packages (socat, openssh-client, netcat-openbsd)
- Sets up SSH key for VM access
- Waits for Windows VM to boot
- Configures iptables MASQUERADE and FORWARD rules
- Sets up socat for SSH forwarding (port 2222)
- Configures DNAT rules for all IBM i ports
**Important**: Uses `return 0` (not `exit 0`) at the end because it's sourced.
### vpn_scripts/vpn-login.js
Automated Cisco AnyConnect VPN login:
- Connects via WebView DevTools protocol (port 9222)
- Handles Microsoft/ADFS authentication
- Generates TOTP codes for 2FA
- Watchdog mode: monitors VPN every 2 minutes, reconnects if dropped
### vpn_scripts/id_ed25519-lenovo
SSH private key for accessing the Windows VM from the container.
## Windows VM Configuration
### SSH Server
Windows OpenSSH is configured to listen on port 2222 (not 22) to allow port 22 for IBM i portproxy.
Config: `C:\ProgramData\ssh\sshd_config`
```
Port 2222
```
### Portproxy Rules
Portproxy rules forward IBM i ports through the VPN. These are persistent (stored in registry).
```cmd
# View all portproxy rules
netsh interface portproxy show all
# Add a rule
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=22 connectaddress=10.35.33.230 connectport=22
# Delete all rules
netsh interface portproxy reset
```
Rules are defined in: `/etc/runtipi/user-config/runtipi/rego-tunnel/port-proxy.txt`
### IP Helper Service
The IP Helper service (iphlpsvc) must be running for portproxy to work:
```cmd
net start iphlpsvc
```
## User Config
Location: `/etc/runtipi/user-config/runtipi/rego-tunnel/docker-compose.yml`
```yaml
networks:
vpn_static-rego:
driver: bridge
driver_opts:
com.docker.network.bridge.name: "br-vpn-rego"
ipam:
config:
- subnet: 172.31.0.0/24
services:
rego-tunnel:
entrypoint: ["/bin/bash", "-c", "source /vpn_scripts/start.sh; exec /run/entry.sh"]
sysctls:
- net.ipv4.conf.all.rp_filter=0
- net.ipv4.conf.default.rp_filter=0
cap_add:
- NET_ADMIN
environment:
- VM_NET_IP=172.32.0.20
volumes:
- /etc/runtipi/repos/runtipi/apps/rego-tunnel/vpn_scripts:/vpn_scripts:ro
networks:
vpn_static-rego:
ipv4_address: 172.31.0.10
```
## Troubleshooting
### Container won't start / restarts immediately
Check if start.sh has `exit 0` instead of `return 0` at the end. Since it's sourced, `exit` terminates the parent shell.
### Can't reach container from laptop
1. Check route on laptop: `route print | findstr 172.31`
2. Check rego-routing.service: `sudo systemctl status rego-routing.service`
3. Check if Docker nft rules are blocking: `sudo nft list ruleset | grep 172.31`
### Portproxy not working
1. Restart IP Helper: `net stop iphlpsvc && net start iphlpsvc`
2. Check if SSH is on port 2222: `netstat -an | findstr :22`
3. Verify portproxy rules: `netsh interface portproxy show all`
### VPN not connecting
1. Check vpn-login.js logs in Windows VM
2. Verify time sync (TOTP requires accurate time)
3. Check if VPN credentials in vpn-login.js are correct
### Bridge name too long error
Linux bridge names are limited to 15 characters. "br-vpn-static-rego" (18 chars) won't work; use "br-vpn-rego" (11 chars).
## Maintenance
### Updating vpn_scripts
1. Edit files in `/etc/runtipi/repos/runtipi/apps/rego-tunnel/vpn_scripts/`
2. Commit and push to git
3. Run `sudo ./runtipi-cli appstore update`
4. Restart app: `sudo ./runtipi-cli app stop rego-tunnel:runtipi && sudo ./runtipi-cli app start rego-tunnel:runtipi`
### Updating portproxy rules
1. Edit `/etc/runtipi/user-config/runtipi/rego-tunnel/port-proxy.txt`
2. SSH to VM: `ssh -p 2222 docker@172.31.0.10`
3. Reset and re-apply: `netsh interface portproxy reset` then run the commands from port-proxy.txt

View File

@@ -1,82 +1,144 @@
<h1 align="center">Windows<br /> # Rego Tunnel - Cisco Secure Client VPN
<div align="center">
<a href="https://github.com/dockur/windows"><img src="https://github.com/dockur/windows/raw/master/.github/logo.png" title="Logo" style="max-width:100%;" width="128" /></a>
</div>
<div align="center">
</div></h1> Native Docker container running Cisco Secure Client (AnyConnect) with full GUI support via noVNC. Provides transparent VPN access to protected resources from your LAN.
Windows in a Docker container.
## Features ## Features
- ISO downloader - **Cisco Secure Client 5.1.14.145** - Full GUI with VPN, DART, and Posture modules
- KVM acceleration - **Web-based access** via noVNC (port 6080)
- Web-based viewer - **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
## FAQ ## Architecture
* ### How do I use it? ```
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)
```
Very simple! These are the steps: ## Installation
- Start the container and connect to [port 8006](http://localhost:8006) using your web browser. ### 1. Install the app through Runtipi
- Sit back and relax while the magic happens, the whole installation will be performed fully automatic. 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)
- Once you see the desktop, your Windows installation is ready for use. ### 2. Install host routing service (required for LAN access)
Enjoy your brand new machine, and don't forget to star this repo! **Run this ONCE on the host after app install:**
* ### How do I select the Windows version? ```bash
/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/install-host-services.sh
```
By default, Windows 11 will be installed. But you can change that in settings, in order to specify an alternative Windows version to be downloaded: This creates systemd services that route VPN traffic through the container.
Select from the values below: ### 3. Access the VPN GUI
| **Value** | **Description** | **Source** | **Transfer** | **Size** | Open `http://<your-server>:6080/vnc.html`
|---|---|---|---|---|
| `win11` | Windows 11 Pro | Microsoft | Fast | 6.4 GB |
| `win10` | Windows 10 Pro | Microsoft | Fast | 5.8 GB |
| `ltsc10` | Windows 10 LTSC | Microsoft | Fast | 4.6 GB |
| `win81` | Windows 8.1 Pro | Microsoft | Fast | 4.2 GB |
| `win7` | Windows 7 SP1 | Bob Pony | Medium | 3.0 GB |
| `vista` | Windows Vista SP2 | Bob Pony | Medium | 3.6 GB |
| `winxp` | Windows XP SP3 | Bob Pony | Medium | 0.6 GB |
||||||
| `2022` | Windows Server 2022 | Microsoft | Fast | 4.7 GB |
| `2019` | Windows Server 2019 | Microsoft | Fast | 5.3 GB |
| `2016` | Windows Server 2016 | Microsoft | Fast | 6.5 GB |
| `2012` | Windows Server 2012 R2 | Microsoft | Fast | 4.3 GB |
| `2008` | Windows Server 2008 R2 | Microsoft | Fast | 3.0 GB |
||||||
| `core11` | Tiny 11 Core | Archive.org | Slow | 2.1 GB |
| `tiny11` | Tiny 11 | Archive.org | Slow | 3.8 GB |
| `tiny10` | Tiny 10 | Archive.org | Slow | 3.6 GB |
* ### How do I connect using RDP? The VPN will auto-connect using your configured credentials.
The web-viewer is mainly meant to be used during installation, as its picture quality is low, and it has no audio or clipboard for example. ## Usage
So for a better experience you can connect using any Microsoft Remote Desktop client to the IP of the container, using the username `docker` and by leaving the password empty. ### Access noVNC
There is a good RDP client for [Android](https://play.google.com/store/apps/details?id=com.microsoft.rdc.androidx) available from the Play Store. One for [iOS](https://apps.apple.com/nl/app/microsoft-remote-desktop/id714464092?l=en-GB) is in the Apple Store. For Linux you can use [rdesktop](http://www.rdesktop.org/) and for Windows you don't need to install anything as it is already ships as part of the operating system. Navigate to port 6080 on your server. The cisco-vpn script runs automatically and provides a menu:
* ### How do I verify if my system supports KVM? ```
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
```
To verify if your system supports KVM, run the following commands: ### Command line options
```bash ```bash
sudo apt install cpu-checker # Inside container
sudo kvm-ok 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
```
If you receive an error from `kvm-ok` indicating that KVM acceleration can't be used, check the virtualization settings in the BIOS. ### View logs
* ### Is this project legal? ```bash
# Inside container
cat /var/log/cisco-vpn/$(date +%Y-%m-%d).log
Yes, this project contains only open-source code and does not distribute any copyrighted material. Any product keys found in the code are just generic placeholders provided by Microsoft for trial purposes. So under all applicable laws, this project would be considered legal. # On host
cat /var/log/rego-routing.log
```
## Disclaimer ## LAN Access
The product names, logos, brands, and other trademarks referred to within this project are the property of their respective trademark holders. This project is not affiliated, sponsored, or endorsed by Microsoft Corporation. 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)

888
apps/rego-tunnel/shared/cisco-vpn Executable file
View File

@@ -0,0 +1,888 @@
#!/bin/bash
# Dover VPN Connection Script with Semi-Automation
# Usage: ./cisco-vpn.sh [-c|--connect] [-d|--disconnect] [-m|--menu] [-r|--routes] [-h|--hosts] [--help]
#
# Options:
# -c, --connect Start Cisco AnyConnect (optionally auto-login) and exit
# -d, --disconnect Disconnect (kill Cisco processes) and exit
# -m, --menu Skip auto-login, show menu directly
# -r, --routes Show current routing table and exit
# -h, --hosts Show /etc/hosts and exit
# -s, --status Show VPN and network status and exit
# --help Show this help message
#
# Keyboard shortcuts (global, work anywhere):
# Ctrl+1 - Type email
# Ctrl+2 - Type password
# Ctrl+3 - Type TOTP code
# Ctrl+4 - Type email + Tab + password (combo)
# Ctrl+5 - Full sequence: email + Tab + password + Tab + TOTP + Enter
# Credentials from environment variables (set by runtipi)
EMAIL="${VPN_EMAIL:-}"
PASSWORD="${VPN_PASSWORD:-}"
TOTP_SECRET="${VPN_TOTP_SECRET:-}"
VPN_HOST="${VPN_HOST:-vpn-ord1.dovercorp.com}"
TARGET_IP="${TARGET_IP:-10.35.33.230}"
# Log directory and file (date-based rotation)
LOG_DIR="/var/log/cisco-vpn"
LOG_RETENTION_DAYS=7
mkdir -p "$LOG_DIR" 2>/dev/null
# Function to get current log file (changes daily)
get_log_file() {
echo "$LOG_DIR/$(date '+%Y-%m-%d').log"
}
# Cleanup old log files (older than LOG_RETENTION_DAYS)
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
print_banner() {
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} Dover VPN Connection Script ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
}
# Flags
SKIP_AUTO_LOGIN=false
DO_CONNECT=false
DO_DISCONNECT=false
# Logging function with timestamp - writes to both console and daily log file
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)
# Strip ANSI color codes for log file
local msg_plain=$(echo -e "$msg" | sed 's/\x1b\[[0-9;]*m//g')
# Write to log file (plain text, no colors)
echo "[$timestamp] [$level] $msg_plain" >> "$log_file"
# Write to console (with colors)
case $level in
INFO) echo -e "${GRAY}[$timestamp_short]${NC} ${GREEN}[INFO]${NC} $msg" ;;
WARN) echo -e "${GRAY}[$timestamp_short]${NC} ${YELLOW}[WARN]${NC} $msg" ;;
ERROR) echo -e "${GRAY}[$timestamp_short]${NC} ${RED}[ERROR]${NC} $msg" ;;
DEBUG) echo -e "${GRAY}[$timestamp_short]${NC} ${CYAN}[DEBUG]${NC} $msg" ;;
CMD) echo -e "${GRAY}[$timestamp_short]${NC} ${GRAY}[CMD]${NC} $msg" ;;
*) echo -e "${GRAY}[$timestamp_short]${NC} $msg" ;;
esac
}
# Run command with logging
run_cmd() {
local desc="$1"
shift
log CMD "$desc: $*"
output=$("$@" 2>&1)
local rc=$?
if [ -n "$output" ]; then
echo "$output" | while IFS= read -r line; do
echo -e " ${GRAY}│${NC} $line"
done
fi
return $rc
}
# Show help
show_help() {
echo -e "${CYAN}Dover VPN Connection Script${NC}"
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -c, --connect Start Cisco AnyConnect and exit"
echo " -d, --disconnect Disconnect (kill Cisco processes) and exit"
echo " -m, --menu Skip auto-login, show menu directly"
echo " -r, --routes Show current routing table and exit"
echo " -h, --hosts Show /etc/hosts and exit"
echo " -s, --status Show VPN and network status and exit"
echo " --help Show this help message"
echo ""
echo "Menu Options:"
echo " 1 - Start Cisco AnyConnect (kill existing + launch)"
echo " 2 - Copy credentials to clipboard (one by one)"
echo " 3 - Show live TOTP"
echo " 4 - Setup IP forwarding rules only"
echo " 5 - Test connection to target"
echo " 6 - Show network status"
echo " 7 - Kill all Cisco processes"
echo " 8 - Show routing table"
echo " 9 - Show /etc/hosts"
echo " e - Edit /etc/hosts"
echo " q - Quit"
}
# Show routing table
show_routes() {
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} Current Routing Table ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
echo -e "${GREEN}IPv4 Routes:${NC}"
ip -4 route show | while IFS= read -r line; do
# Highlight VPN-related routes
if echo "$line" | grep -qE "cscotun|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 /etc/hosts
show_hosts() {
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} /etc/hosts ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
cat -n /etc/hosts
}
# Edit /etc/hosts
edit_hosts() {
local editor="${EDITOR:-nano}"
if command -v "$editor" &>/dev/null; then
"$editor" /etc/hosts
else
log ERROR "No editor found. Set EDITOR environment variable."
log INFO "Try: nano /etc/hosts"
fi
}
# Function to get current TOTP
get_totp() {
oathtool --totp -b "$TOTP_SECRET"
}
# Function to detect VPN tunnel interface dynamically
get_vpn_interface() {
# Look for cscotun* or tun* interfaces that are UP
local iface=$(ip link show | grep -oP '(cscotun\d+|tun\d+)(?=:.*UP)' | head -1)
if [ -z "$iface" ]; then
# Fallback: any cscotun interface
iface=$(ip link show | grep -oP 'cscotun\d+' | head -1)
fi
echo "$iface"
}
# Function to get container's IP on the rego-tunnel network
get_container_ip() {
# eth0 is the rego-tunnel network (172.31.0.x)
ip addr show eth0 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1
}
# Function to get VPN tunnel IP
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
}
# Start xbindkeys for keyboard macros
start_xbindkeys() {
log INFO "Starting keyboard macro listener (xbindkeys)..."
# Kill any existing xbindkeys
pkill xbindkeys 2>/dev/null
sleep 0.5
# Start xbindkeys
xbindkeys -f ~/.xbindkeysrc 2>/dev/null &
XBINDKEYS_PID=$!
if pgrep xbindkeys >/dev/null; then
log DEBUG "xbindkeys started (PID: $(pgrep xbindkeys))"
log INFO "Keyboard shortcuts active: Ctrl+1=email, Ctrl+2=pass, Ctrl+3=TOTP, Ctrl+4=combo, Ctrl+5=all"
else
log WARN "Failed to start xbindkeys"
fi
}
# Stop xbindkeys
stop_xbindkeys() {
if pgrep xbindkeys >/dev/null; then
log INFO "Stopping keyboard macro listener..."
pkill xbindkeys 2>/dev/null
log DEBUG "xbindkeys stopped"
fi
}
# Kill all Cisco-related processes
kill_cisco_processes() {
local kill_agentd="${1:-false}"
log INFO "Killing all Cisco-related processes..."
local killed=0
local my_pid=$$
local my_ppid=$(ps -o ppid= -p $$ | tr -d ' ')
# Kill vpnui specifically (not just any process with "vpn" in name)
for pid in $(pgrep -x "vpnui" 2>/dev/null); do
if [ "$pid" != "$my_pid" ] && [ "$pid" != "$my_ppid" ]; then
log DEBUG "Killing vpnui (PID $pid)"
kill -9 "$pid" 2>/dev/null && ((killed++))
fi
done
# Optionally kill vpnagentd (useful to clear stale state)
if [ "$kill_agentd" = "true" ]; then
for pid in $(pgrep -x "vpnagentd" 2>/dev/null); do
if [ "$pid" != "$my_pid" ] && [ "$pid" != "$my_ppid" ]; then
log DEBUG "Killing vpnagentd (PID $pid)"
kill -9 "$pid" 2>/dev/null && ((killed++))
fi
done
fi
# Kill Cisco-specific processes by exact path
for proc in cstub cscan acwebsecagent vpndownloader; do
for pid in $(pgrep -x "$proc" 2>/dev/null); do
log DEBUG "Killing $proc (PID $pid)"
kill -9 "$pid" 2>/dev/null && ((killed++))
done
done
# Kill openconnect (exact match)
for pid in $(pgrep -x "openconnect" 2>/dev/null); do
log DEBUG "Killing openconnect (PID $pid)"
kill -9 "$pid" 2>/dev/null && ((killed++))
done
if [ $killed -eq 0 ]; then
log INFO "No Cisco processes were running"
else
log INFO "Killed $killed process(es)"
sleep 1
fi
sleep 5
}
# Disconnect VPN (best-effort)
disconnect_vpn() {
log INFO "Disconnecting Cisco AnyConnect..."
# If vpncli exists, attempt a clean disconnect first (ignore failures)
if [ -x /opt/cisco/secureclient/bin/vpncli ]; then
run_cmd "Attempting clean disconnect via vpncli" /opt/cisco/secureclient/bin/vpncli -s <<'EOF' || true
disconnect
exit
EOF
fi
# Always kill agent/UI processes afterwards
kill_cisco_processes "true"
# Confirm status
if check_vpn_status; then
log WARN "VPN still appears connected (interface still up)"
return 1
fi
log INFO "VPN disconnected"
return 0
}
# Function to setup iptables rules for forwarding
setup_forwarding() {
log INFO "Setting up IP forwarding rules for $TARGET_IP..."
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"
# Enable IP forwarding
run_cmd "Enabling IP forwarding" sysctl -w net.ipv4.ip_forward=1
# NAT masquerade for traffic from container network (172.31.0.0/24) going through VPN
# This is the ONLY masquerade rule needed - source-based, not destination-based
if ! iptables -t nat -C POSTROUTING -s 172.31.0.0/24 -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then
run_cmd "Adding NAT masquerade for container network -> VPN" iptables -t nat -A POSTROUTING -s 172.31.0.0/24 -o "$vpn_iface" -j MASQUERADE
else
log DEBUG "NAT masquerade for container network already exists"
fi
# Forward rules - MUST be at position 1 to run BEFORE cisco VPN chains
# The cisco VPN chains have catch-all DROP rules that would block our traffic
# Wait for Cisco to create its chains (they appear after VPN connects)
local wait_count=0
while ! iptables -L ciscovpn -n &>/dev/null && [ $wait_count -lt 30 ]; do
log DEBUG "Waiting for Cisco VPN chains to be created..."
sleep 1
((wait_count++))
done
if iptables -L ciscovpn -n &>/dev/null; then
log DEBUG "Cisco VPN chains detected"
else
log WARN "Cisco VPN chains not created after ${wait_count}s - proceeding anyway"
fi
# Remove any existing rules first (ignore errors if they don't exist)
iptables -D FORWARD -d "$TARGET_IP" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -s "$TARGET_IP" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -s 172.31.0.0/24 -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -d 172.31.0.0/24 -j ACCEPT 2>/dev/null || true
# Insert at position 1 (reverse order so they end up in correct order)
run_cmd "Inserting forward rule (to container network)" iptables -I FORWARD 1 -d 172.31.0.0/24 -j ACCEPT
run_cmd "Inserting forward rule (from container network)" iptables -I FORWARD 1 -s 172.31.0.0/24 -j ACCEPT
run_cmd "Inserting forward rule (from target)" iptables -I FORWARD 1 -s "$TARGET_IP" -j ACCEPT
run_cmd "Inserting forward rule (to target)" iptables -I FORWARD 1 -d "$TARGET_IP" -j ACCEPT
# Cisco VPN chain bypass (insert at top if chain exists)
if iptables -L ciscovpn -n &>/dev/null; then
if ! iptables -C ciscovpn -o "$vpn_iface" -d "$TARGET_IP" -j ACCEPT 2>/dev/null; then
run_cmd "Adding ciscovpn bypass (outbound)" iptables -I ciscovpn 1 -o "$vpn_iface" -d "$TARGET_IP" -j ACCEPT
else
log DEBUG "Ciscovpn bypass (outbound) already exists"
fi
if ! iptables -C ciscovpn -i "$vpn_iface" -s "$TARGET_IP" -j ACCEPT 2>/dev/null; then
run_cmd "Adding ciscovpn bypass (inbound)" iptables -I ciscovpn 2 -i "$vpn_iface" -s "$TARGET_IP" -j ACCEPT
else
log DEBUG "Ciscovpn bypass (inbound) already exists"
fi
# Also allow container network through ciscovpn chain
if ! iptables -C ciscovpn -s 172.31.0.0/24 -j ACCEPT 2>/dev/null; then
run_cmd "Adding ciscovpn bypass (container network)" iptables -I ciscovpn 3 -s 172.31.0.0/24 -j ACCEPT
fi
else
log DEBUG "ciscovpn chain does not exist (yet)"
fi
log INFO "Forwarding rules configured"
echo ""
# Trigger host routing service restart
log INFO "Triggering host routing service restart..."
touch /runtime/restart-routing
sleep 2
if [ ! -f /runtime/restart-routing ]; then
log INFO "Host routing service restarted"
else
log WARN "Host watcher may not be running (trigger file still exists)"
fi
log INFO "Routing configured for $TARGET_IP through VPN tunnel"
echo ""
}
# Copy credentials to clipboard as alternative
copy_to_clipboard() {
log INFO "Starting clipboard credential rotation..."
echo ""
log INFO "Copying EMAIL to clipboard"
echo "$EMAIL" | xclip -selection clipboard
echo -e " ${CYAN}Email ready: $EMAIL${NC}"
echo -e " Paste now (Ctrl+V), then press ${GREEN}Enter${NC} here for password..."
read -r
log INFO "Copying PASSWORD to clipboard"
echo "$PASSWORD" | xclip -selection clipboard
echo -e " ${CYAN}Password ready${NC}"
echo -e " Paste now (Ctrl+V), then press ${GREEN}Enter${NC} here for TOTP..."
read -r
TOTP=$(get_totp)
log INFO "Copying TOTP to clipboard"
echo "$TOTP" | xclip -selection clipboard
echo -e " ${CYAN}TOTP ready: $TOTP${NC}"
echo -e " Paste now (Ctrl+V)"
}
# Print current TOTP with countdown
show_totp() {
log INFO "Starting live TOTP display (Ctrl+C to stop)"
echo ""
while true; do
TOTP=$(get_totp)
SECONDS_LEFT=$((30 - ($(date +%s) % 30)))
echo -ne "\r ${CYAN}Current TOTP:${NC} ${GREEN}$TOTP${NC} (expires in ${YELLOW}${SECONDS_LEFT}s${NC}) "
sleep 1
done
}
# Show network status
show_network_status() {
log INFO "Current network status:"
# Container IPs
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
# VPN status
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
# Container IP on rego-tunnel network
local container_ip=$(get_container_ip)
if [ -n "$container_ip" ]; then
log DEBUG "Container IP: $container_ip"
fi
# Default gateway
echo ""
log DEBUG "Default gateway:"
ip route show default | while IFS= read -r line; do
echo -e " ${GRAY}│${NC} $line"
done
echo ""
}
# Main menu
main_menu() {
echo -e "${GREEN}Options:${NC}"
echo -e " ${CYAN}1${NC} - Start Cisco AnyConnect (kill existing + launch)"
echo -e " ${CYAN}2${NC} - Copy credentials to clipboard (one by one)"
echo -e " ${CYAN}3${NC} - Show live TOTP"
echo -e " ${CYAN}4${NC} - Setup IP forwarding rules only"
echo -e " ${CYAN}5${NC} - Test connection to $TARGET_IP"
echo -e " ${CYAN}6${NC} - Show network status"
echo -e " ${CYAN}7${NC} - Kill all Cisco processes"
echo -e " ${CYAN}8${NC} - Show routing table"
echo -e " ${CYAN}9${NC} - Show /etc/hosts"
echo -e " ${CYAN}e${NC} - Edit /etc/hosts"
echo -e " ${CYAN}q${NC} - Quit"
echo ""
}
# Check if VPN is already connected
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
}
# Focus on Cisco AnyConnect window
focus_vpn_window() {
local win_id=$(xdotool search --name "Cisco" 2>/dev/null | head -1)
if [ -n "$win_id" ]; then
xdotool windowactivate --sync "$win_id" 2>/dev/null
sleep 0.3
return 0
fi
return 1
}
# Auto-login sequence using xdotool (no auto-focus, types to active window)
auto_login() {
log INFO "Starting automated login sequence..."
# Wait for UI to fully load
log DEBUG "Waiting 5s for UI to load..."
sleep 5
# Press Enter to initiate connection
log DEBUG "Pressing Enter to start connection..."
xdotool key Return
sleep 5
# Type email
log DEBUG "Typing email..."
xdotool type --delay 50 "$EMAIL"
xdotool key Return
sleep 5
# Type password
log DEBUG "Typing password..."
xdotool type --delay 50 "$PASSWORD"
xdotool key Return
sleep 5
# Type TOTP
log DEBUG "Typing TOTP..."
local totp=$(oathtool --totp -b "$TOTP_SECRET")
log DEBUG "TOTP: $totp"
xdotool type --delay 50 "$totp"
xdotool key Return
sleep 5
# Extra enters for any confirmation dialogs
log DEBUG "Sending confirmation enters..."
xdotool key Return
sleep 2
xdotool key Return
sleep 5
xdotool key Return
log INFO "Auto-login sequence completed"
}
# Watchdog - monitors VPN and reconnects if dropped
start_watchdog() {
log INFO "Starting VPN watchdog (check every 60s, keepalive ping every 5min)..."
local check_interval=60
local keepalive_interval=300
local last_keepalive=0
local reconnect_attempts=0
local max_reconnect_attempts=3
while true; do
sleep $check_interval
local now=$(date +%s)
local vpn_iface=$(get_vpn_interface)
if [ -n "$vpn_iface" ]; then
# VPN is up
reconnect_attempts=0
# Keepalive ping every 5 minutes to prevent idle timeout
if [ $((now - last_keepalive)) -ge $keepalive_interval ]; then
if ping -c 1 -W 5 "$TARGET_IP" &>/dev/null; then
log DEBUG "Keepalive ping to $TARGET_IP successful"
else
log WARN "Keepalive ping to $TARGET_IP failed (VPN may be degraded)"
fi
last_keepalive=$now
fi
else
# VPN is down
((reconnect_attempts++))
log WARN "VPN disconnected! Reconnect attempt $reconnect_attempts/$max_reconnect_attempts..."
if [ $reconnect_attempts -le $max_reconnect_attempts ]; then
# Kill stale processes and restart
kill_cisco_processes "true"
sleep 2
# Start vpnagentd
if ! pgrep -x vpnagentd >/dev/null; then
/opt/cisco/secureclient/bin/vpnagentd &
sleep 5
fi
# Start vpnui
export GDK_BACKEND=x11
export WEBKIT_DISABLE_DMABUF_RENDERER=1
/opt/cisco/secureclient/bin/vpnui &
disown
sleep 3
# Run auto-login
auto_login &
# Wait for connection
local wait_count=0
while [ -z "$(get_vpn_interface)" ] && [ $wait_count -lt 120 ]; do
sleep 2
((wait_count+=2))
done
if [ -n "$(get_vpn_interface)" ]; then
log INFO "VPN reconnected successfully!"
setup_forwarding
reconnect_attempts=0
else
log ERROR "Reconnect attempt $reconnect_attempts failed"
fi
else
log ERROR "Max reconnect attempts reached. Manual intervention required."
log ERROR "Use menu option 1 to restart VPN manually."
# Reset counter after a longer wait
sleep 300
reconnect_attempts=0
fi
fi
done
}
# Start Cisco AnyConnect with logging
start_anyconnect() {
local do_auto_login="$1"
log INFO "=== Starting Cisco AnyConnect VPN ==="
echo ""
# Kill existing processes first
# Always restart vpnagentd for a clean session
kill_cisco_processes "true"
# Start vpnagentd if not running
if ! pgrep -x vpnagentd >/dev/null; then
log INFO "Starting vpnagentd..."
/opt/cisco/secureclient/bin/vpnagentd &
log DEBUG "Waiting for vpnagentd to initialize..."
sleep 5
fi
# Show credentials
log INFO "Credentials for SSO login:"
echo -e " ${CYAN}Email: $EMAIL${NC}"
echo -e " ${CYAN}Password: $PASSWORD${NC}"
TOTP=$(get_totp)
echo -e " ${CYAN}TOTP: $TOTP${NC}"
echo ""
# Start AnyConnect with GPU/WebKit workarounds
log INFO "Launching Cisco AnyConnect UI..."
export GDK_BACKEND=x11
export WEBKIT_DISABLE_DMABUF_RENDERER=1
/opt/cisco/secureclient/bin/vpnui &
VPNUI_PID=$!
disown $VPNUI_PID
log DEBUG "vpnui started with PID $VPNUI_PID"
if [ "$do_auto_login" = "true" ]; then
# Run auto-login in background
auto_login &
AUTO_LOGIN_PID=$!
log DEBUG "Auto-login started with PID $AUTO_LOGIN_PID"
else
log INFO "Manual login mode - use keyboard shortcuts or menu option 2 for credentials"
return 0
fi
# Wait for VPN to connect
log INFO "Waiting for VPN connection..."
local wait_count=0
local max_wait=300 # 5 minutes
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"
stop_xbindkeys
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"
# Wait a bit for routes to stabilize
log DEBUG "Waiting for routes to stabilize..."
sleep 3
# Setup forwarding
setup_forwarding
# Test connection
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} (may need manual route on Windows)"
fi
log INFO "VPN setup complete"
# Disable screen blanking/power saving
xset s off 2>/dev/null || true
xset -dpms 2>/dev/null || true
xset s noblank 2>/dev/null || true
log DEBUG "Screen blanking disabled"
# Start watchdog in background
start_watchdog &
WATCHDOG_PID=$!
log DEBUG "Watchdog started with PID $WATCHDOG_PID"
return 0
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-c|--connect)
DO_CONNECT=true
shift
;;
-d|--disconnect)
DO_DISCONNECT=true
shift
;;
-m|--menu)
SKIP_AUTO_LOGIN=true
shift
;;
-r|--routes)
show_routes
exit 0
;;
-h|--hosts)
show_hosts
exit 0
;;
-s|--status)
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} VPN and Network Status ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
check_vpn_status
echo ""
show_network_status
exit 0
;;
--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
if [ "$DO_CONNECT" = "true" ] && [ "$DO_DISCONNECT" = "true" ]; then
echo "Error: --connect and --disconnect are mutually exclusive"
exit 1
fi
}
# Main
parse_args "$@"
# Cleanup old logs and start fresh
cleanup_old_logs
# Log script start
echo "" >> "$(get_log_file)"
echo "========================================" >> "$(get_log_file)"
log INFO "cisco-vpn script started"
log DEBUG "VPN_EMAIL=$EMAIL"
log DEBUG "VPN_HOST=$VPN_HOST"
log DEBUG "TARGET_IP=$TARGET_IP"
log DEBUG "TOTP_SECRET is $([ -n "$TOTP_SECRET" ] && echo 'set' || echo 'NOT SET')"
print_banner
if [ "$DO_DISCONNECT" = "true" ]; then
disconnect_vpn
exit $?
fi
if [ "$DO_CONNECT" = "true" ]; then
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} Dover VPN Connection Script ${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
if [ "$SKIP_AUTO_LOGIN" = "true" ]; then
start_anyconnect "false"
else
start_anyconnect "true"
fi
exit $?
fi
log INFO "Script started"
echo ""
# If -m/--menu flag, skip everything and go straight to menu
if [ "$SKIP_AUTO_LOGIN" = "true" ]; then
log INFO "Menu mode - skipping auto-login"
# Check current status
elif check_vpn_status; then
echo ""
log INFO "VPN already connected. Setting up forwarding..."
setup_forwarding
else
echo ""
log INFO "Auto-starting VPN connection..."
echo ""
start_anyconnect "true"
fi
while true; do
echo ""
main_menu
echo -ne "${CYAN}Choice: ${NC}"
read -r choice
echo ""
# Ignore empty/whitespace input
[[ -z "${choice// }" ]] && continue
case $choice in
1) if [ "$SKIP_AUTO_LOGIN" = "true" ]; then
start_anyconnect "false"
else
start_anyconnect "true"
fi ;;
2) copy_to_clipboard ;;
3) show_totp ;;
4) setup_forwarding ;;
5) 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}" ;;
6) show_network_status ;;
7) kill_cisco_processes "true";;
8) show_routes ;;
9) show_hosts ;;
e|E) edit_hosts ;;
q|Q) log INFO "Goodbye!"; exit 0 ;;
*) ;; # Ignore invalid input silently
esac
done

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Entrypoint: VNC password setup + DNS fix + systemd
set -euo pipefail
# Ensure all shared scripts are executable (permissions may reset after git pull/appstore update)
chmod +x /shared/*.sh /shared/cisco-vpn /root/.vnc/xstartup 2>/dev/null || true
# Symlink shared scripts into /opt/scripts/ so systemd services (vnc.service) find them
mkdir -p /opt/scripts
ln -sf /shared/startup-vnc.sh /opt/scripts/startup-vnc.sh
# Setup TigerVNC password file from env var (passed by runtipi)
# TigerVNC expects /root/.vnc/passwd when using SecurityTypes=VncAuth.
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
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
echo 1 > /proc/sys/net/ipv4/ip_forward
echo "[entrypoint] IP forwarding enabled"
exec /sbin/init

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env bash
#
# Host routing script for rego-tunnel
# Routes TARGET_IP through the VPN container
#
set -euo pipefail
ACTION="${1:-start}"
# Fixed configuration (we assigned these)
CONTAINER_IP="172.31.0.10"
BRIDGE_NAME="br-rego-vpn"
TARGET_IP="${TARGET_IP:-10.35.33.230}"
LAN_SUBNET="192.168.0.0/23"
LAN_INTERFACES="eth0 eth1 wlan0"
LOG_FILE="/var/log/rego-routing.log"
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [rego-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/32" 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/32" 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/32" 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 || true)
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

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
#
# Install host-side systemd services for rego-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/rego-tunnel"
echo "Installing rego-tunnel host services..."
# Create the path watcher unit
cat << 'EOF' | sudo tee /etc/systemd/system/rego-routing-watcher.path
[Unit]
Description=Watch for rego-tunnel routing trigger
[Path]
PathExists=/etc/runtipi/app-data/runtipi/rego-tunnel/restart-routing
Unit=rego-routing-watcher.service
[Install]
WantedBy=multi-user.target
EOF
# Create the service unit
cat << EOF | sudo tee /etc/systemd/system/rego-routing-watcher.service
[Unit]
Description=Apply rego-tunnel routing rules
After=docker.service
[Service]
Type=oneshot
ExecStart=/bin/bash ${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 rego-routing-watcher.path
echo ""
echo "Done! Services installed:"
echo " - rego-routing-watcher.path (watches for trigger file)"
echo " - rego-routing-watcher.service (applies routing rules)"
echo ""
echo "To check status:"
echo " systemctl status rego-routing-watcher.path"
echo ""
echo "To manually trigger routing:"
echo " touch ${APP_DATA_DIR}/restart-routing"

View File

@@ -0,0 +1,12 @@
#!/bin/bash
set -e
export HOME='/root'
export USER='root'
rm -f /tmp/.P1-lock /tmp/.X11-unix/X1 2>/dev/null || true
rm -rf /tmp/.X*-lock /tmp/.X14-unix/* 2>/dev/null || true
echo "Starting TigerVNC server on display :1..."
vncserver :1 -geometry 1280x800 -depth 24 -SecurityTypes VncAuth -localhost no
sleep 2
echo "Starting noVNC on port ${NOVNC_PORT:-6080}..."
websockify --web=/usr/share/novnc/ ${NOVNC_PORT:-6080} localhost:5901 &
tail -f /root/.vnc/*.log

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
#
# Uninstall host-side systemd services for rego-tunnel
#
set -euo pipefail
echo "Removing rego-tunnel host services..."
# Stop and disable the watcher
sudo systemctl stop rego-routing-watcher.path 2>/dev/null || true
sudo systemctl disable rego-routing-watcher.path 2>/dev/null || true
# Remove routing rules
/etc/runtipi/repos/runtipi/apps/rego-tunnel/shared/host-routing.sh stop 2>/dev/null || true
# Remove systemd units
sudo rm -f /etc/systemd/system/rego-routing-watcher.path
sudo rm -f /etc/systemd/system/rego-routing-watcher.service
# Reload systemd
sudo systemctl daemon-reload
echo ""
echo "Done! Host services removed."

View 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/cisco-vpn 2>/dev/null || true
xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 130x45+10+10 \
-title "Rego VPN Terminal" \
-e "bash -c '/shared/cisco-vpn; exec bash'" &
wait

View File

@@ -1,31 +0,0 @@
# Install Node.js on Windows
# Run as Administrator in PowerShell
$Username = if ($env:REGO_USER) { $env:REGO_USER } else { $env:USERNAME }
$nodeVersion = "22.9.0"
$nodeUrl = "https://nodejs.org/dist/v$nodeVersion/node-v$nodeVersion-x64.msi"
$installerPath = "$env:TEMP\node-installer.msi"
Write-Host "Downloading Node.js v$nodeVersion..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $nodeUrl -OutFile $installerPath
Write-Host "Installing Node.js..." -ForegroundColor Cyan
Start-Process msiexec.exe -Wait -ArgumentList "/i `"$installerPath`" /quiet /norestart"
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
Write-Host "Verifying installation..." -ForegroundColor Cyan
node --version
npm --version
Write-Host "Node.js installed successfully!" -ForegroundColor Green
# Cleanup
Remove-Item $installerPath -Force
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host "1. Copy vpn-login.js, socks5.js, vpn.bat to C:\Users\$Username\vpn_scripts\"
Write-Host "2. Open CMD in C:\Users\$Username\vpn_scripts\ and run: npm install ws otplib"
Write-Host "3. Add vpn.bat shortcut to shell:startup folder"

View File

@@ -1,45 +0,0 @@
# Setup OpenSSH Server and Auto-Login
# Run as Administrator in PowerShell
$Username = if ($env:REGO_USER) { $env:REGO_USER } else { $env:USERNAME }
$Password = if ($env:REGO_PASS) { $env:REGO_PASS } else { "admin" }
Write-Host "=== Setting up OpenSSH Server ===" -ForegroundColor Cyan
Write-Host "Using username: $Username" -ForegroundColor Yellow
# Install OpenSSH Server
$sshCapability = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
if ($sshCapability.State -ne 'Installed') {
Write-Host "Installing OpenSSH Server..." -ForegroundColor Yellow
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
} else {
Write-Host "OpenSSH Server already installed" -ForegroundColor Green
}
# Start and enable SSH service
Write-Host "Starting SSH service..." -ForegroundColor Yellow
Start-Service sshd
Set-Service -Name sshd -StartupType 'Automatic'
# Configure firewall
$fwRule = Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue
if (-not $fwRule) {
Write-Host "Adding firewall rule..." -ForegroundColor Yellow
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
}
Write-Host "OpenSSH Server configured!" -ForegroundColor Green
Write-Host ""
Write-Host "=== Setting up Auto-Login ===" -ForegroundColor Cyan
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon"
Set-ItemProperty -Path $RegPath -Name "AutoAdminLogon" -Value "1"
Set-ItemProperty -Path $RegPath -Name "DefaultUserName" -Value $Username
Set-ItemProperty -Path $RegPath -Name "DefaultPassword" -Value $Password
Remove-ItemProperty -Path $RegPath -Name "DefaultDomainName" -ErrorAction SilentlyContinue
Write-Host "Auto-login configured for '$Username'!" -ForegroundColor Green
Write-Host ""
Write-Host "Setup complete! SSH is now available and auto-login is enabled." -ForegroundColor Green
Write-Host "Reboot to test auto-login." -ForegroundColor Yellow

View File

@@ -1,34 +0,0 @@
# Setup SSH Keys
# Run as Administrator in PowerShell
$Username = if ($env:REGO_USER) { $env:REGO_USER } else { $env:USERNAME }
$PublicKey = if ($env:REGO_SSH_PUBKEY) { $env:REGO_SSH_PUBKEY } else { "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGHUQnw0WfeFRQx76UlImXXhu3xeOH41PmDRid8pWK1D default-key" }
Write-Host "=== Setting up SSH Keys ===" -ForegroundColor Cyan
Write-Host "Using username: $Username" -ForegroundColor Yellow
# User authorized_keys
$userSshDir = "C:\Users\$Username\.ssh"
$userAuthKeys = "$userSshDir\authorized_keys"
Write-Host "Creating user .ssh directory..." -ForegroundColor Yellow
New-Item -ItemType Directory -Path $userSshDir -Force | Out-Null
Write-Host "Adding key to user authorized_keys..." -ForegroundColor Yellow
Add-Content -Path $userAuthKeys -Value $PublicKey -Force
# Fix permissions for user file
icacls $userAuthKeys /inheritance:r /grant "${Username}:F" /grant "SYSTEM:F"
# Administrator authorized_keys (for admin users)
$adminAuthKeys = "C:\ProgramData\ssh\administrators_authorized_keys"
Write-Host "Adding key to administrators_authorized_keys..." -ForegroundColor Yellow
Add-Content -Path $adminAuthKeys -Value $PublicKey -Force
# Fix permissions for admin file (required by OpenSSH)
icacls $adminAuthKeys /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
Write-Host ""
Write-Host "SSH keys configured!" -ForegroundColor Green
Write-Host "You can now SSH in with the configured key." -ForegroundColor Yellow

View File

@@ -1,188 +0,0 @@
# Rego VPN Automation - Technical Setup Guide
## Overview
Cisco Secure Client VPN running in Windows VM (dockurr/windows) inside Docker container, with SOCKS5 proxy for transparent routing to IBM i systems.
## Architecture
```
Clients → Host (iptables/redsocks) → Container (socat) → Windows VM (SOCKS5) → VPN → 10.35.33.x
```
## Components
### 1. Windows VM (inside container)
- **Container**: `rego-tunnel_runtipi-rego-tunnel-1`
- **Windows VM IP**: `172.30.0.16` or `172.30.0.17` (internal to container)
- **VPN**: Cisco Secure Client with SAML auth (email + password + TOTP)
- **Files on Windows** (`C:\Users\alexz\vpn_scripts`):
- `vpn.bat` - Startup batch file
- `vpn-login.js` - Node.js script that automates SAML login via Chrome DevTools Protocol
- `socks5.js` - Simple SOCKS5 proxy server
- `node_modules/` - ws, otplib packages
### 2. Container
- **External IPs**: `10.128.16.2` or similar
- **Internal bridge**: `172.30.0.1/24` (Windows VM at .16 or .17)
- **socat**: Forwards port 1080 from container to Windows VM SOCKS5
- **start.sh**: Mounted at `/run/start.sh` - sets up iptables DNAT rules
### 3. Host
- **redsocks**: Transparent SOCKS5 redirector (optional)
- **iptables**: Redirects traffic to VPN network through container
## VPN Credentials
Located in `vpn-login.js`:
```javascript
const CONFIG = {
email: "c-azaw@regoproducts.com",
password: "Fuckyou4suhail",
totpSecret: "RZQTQSKDWKHZ6ZYR",
devtoolsPort: 9222,
vpnTestIp: "10.35.33.230"
};
```
## Windows Setup Steps
### 1. Install Node.js
Run PowerShell as Administrator:
```powershell
# Option A: Run the install script
.\install-nodejs.ps1
# Option B: Manual download from https://nodejs.org/
```
### 2. Install Cisco Secure Client
- Download from company VPN portal or Cisco
- Install with default options
- Path: `C:\Program Files (x86)\Cisco\Cisco Secure Client\`
### 3. Setup VPN Scripts
```cmd
mkdir C:\Users\alexz\vpn_scripts
copy \\TSCLIENT\shared\vpn-scripts\*.js C:\Users\alexz\vpn_scripts\
copy \\TSCLIENT\shared\vpn-scripts\vpn.bat C:\Users\alexz\vpn_scripts\
cd C:\Users\alexz\vpn_scripts
npm install ws otplib
```
### 4. Add to Windows Startup
```cmd
# Create shortcut to vpn.bat in:
shell:startup
# Or: C:\Users\alexz\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
```
### 5. Enable Remote Debugging for Cisco UI
The vpn-login.js script sets this environment variable before launching Cisco:
```
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --remote-allow-origins=*
```
## Container Configuration
### docker-compose.yml (user-config)
```yaml
services:
rego-tunnel:
environment:
USER: alexz
PASS: Az@83278327$$@@
VERSION: win10
entrypoint: ["/bin/bash", "-c", "source /run/start.sh; exec /usr/bin/tini -s /run/entry.sh"]
```
### start.sh (Container Startup Script)
Located at: `/etc/runtipi/user-config/runtipi/rego-tunnel/scripts/start.sh`
Sets up:
- iptables MASQUERADE for docker bridge
- Route to IBM i network via Windows VM
- DNAT rules for port forwarding (SSH, IBM i ports)
## Key Ports
| Port | Service |
|------|---------|
| 22 | SSH |
| 23 | Telnet (IBM i) |
| 446, 448, 449 | IBM i services |
| 1080 | SOCKS5 proxy |
| 8006 | noVNC web console |
| 8470-8476 | IBM i data ports |
| 9222 | Chrome DevTools (for automation) |
## Manual Commands
### Start VPN from host:
```bash
docker exec rego-tunnel_runtipi-rego-tunnel-1 ssh docker@172.30.0.16 'C:\Users\alexz\vpn_scripts\vpn.bat'
```
### Start socat in container:
```bash
docker exec -d rego-tunnel_runtipi-rego-tunnel-1 socat TCP-LISTEN:1080,fork,reuseaddr TCP:172.30.0.16:1080
```
### Test SOCKS5 connectivity:
```bash
nc -zv 10.128.16.2 1080
```
### Check VPN status in Windows:
```cmd
ipconfig | findstr 10\.
```
## Troubleshooting
### VPN not connecting
1. Check time sync: `w32tm /resync /force`
2. Verify Cisco agent: `net start "Cisco Secure Client Agent"`
3. Check DevTools: `http://172.30.0.16:9222/json`
### SOCKS5 not working
1. Verify VPN connected first (ping 10.35.33.230)
2. Check socks5.js running: `tasklist | findstr node`
3. Test locally: `nc -zv 127.0.0.1 1080`
### Container issues
1. Check logs: `docker logs rego-tunnel_runtipi-rego-tunnel-1`
2. Verify start.sh: `docker exec rego-tunnel_runtipi-rego-tunnel-1 cat /run/start.sh`
3. Check Windows VM IP: `docker exec rego-tunnel_runtipi-rego-tunnel-1 cat /run/qemu.pid`
## File Locations
### Host
- `/etc/runtipi/user-config/runtipi/rego-tunnel/docker-compose.yml` - User overrides
- `/etc/runtipi/user-config/runtipi/rego-tunnel/scripts/start.sh` - Container startup
- `/etc/runtipi/repos/runtipi/apps/rego-tunnel/docker-compose.yml` - Base config
- `/etc/runtipi/app-data/runtipi/rego-tunnel/data/storage/` - Windows disk image
- `/etc/runtipi/app-data/runtipi/rego-tunnel/data/shared/` - Shared folder with Windows
### Windows VM
- `C:\Users\alexz\vpn_scripts\vpn-login.js` - Main automation script
- `C:\Users\alexz\vpn_scripts\socks5.js` - SOCKS5 proxy
- `C:\Users\alexz\vpn_scripts\vpn.bat` - Startup batch file
- `C:\Program Files (x86)\Cisco\Cisco Secure Client\` - Cisco installation
## Watchdog Mode
The vpn-login.js script includes a watchdog that:
- Monitors VPN connectivity every 2 minutes
- Auto-reconnects after 2 consecutive failures
- Restarts SOCKS5 proxy after reconnection
- Logs memory usage every hour
## Notes
- Windows VM takes ~2-3 minutes to boot
- VPN login takes ~30 seconds
- TOTP requires accurate system time (script syncs automatically)
- The container uses VERSION=win10 for dockurr/windows compatibility
- noVNC password: `Az@83278327$@@`

View File

@@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBh1EJ8NFn3hUUMe+lJSJl14bt8Xjh+NT5g0YnfKVitQwAAAJhfESWEXxEl
hAAAAAtzc2gtZWQyNTUxOQAAACBh1EJ8NFn3hUUMe+lJSJl14bt8Xjh+NT5g0YnfKVitQw
AAAEDgYZN7HwPbKY++p612bnhGC10P3GUHQdJlprPEFbODgWHUQnw0WfeFRQx76UlImXXh
u3xeOH41PmDRid8pWK1DAAAAEWFsZXh6QEFaYXdQQy1QMTZTAQIDBA==
-----END OPENSSH PRIVATE KEY-----

View File

@@ -1,35 +0,0 @@
@echo off
echo === Rego VPN Windows Setup ===
echo.
echo [1/7] Installing Node.js...
powershell -ExecutionPolicy Bypass -File "%~dp001_install-nodejs.ps1"
echo [2/7] Setting up OpenSSH and Auto-Login...
powershell -ExecutionPolicy Bypass -File "%~dp002_setup-autologin-sshd.ps1"
echo [3/7] Configuring SSH Keys...
powershell -ExecutionPolicy Bypass -File "%~dp003_setup-ssh-keys.ps1"
echo [4/7] Creating vpn_scripts folder...
mkdir "C:\Users\%USERNAME%\vpn_scripts" 2>nul
echo [5/7] Copying runtime scripts...
copy "%~dp0vpn-login.js" "C:\Users\%USERNAME%\vpn_scripts\"
copy "%~dp0socks5.js" "C:\Users\%USERNAME%\vpn_scripts\"
copy "%~dp0vpn.bat" "C:\Users\%USERNAME%\vpn_scripts\"
echo [6/7] Installing npm dependencies...
cd /d "C:\Users\%USERNAME%\vpn_scripts"
call npm install ws otplib
echo [7/7] Adding to startup (Run as Admin)...
copy "%~dp0vpn-startup.lnk" "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\vpn.lnk"
echo.
echo === Setup Complete ===
echo.
echo Manual step remaining:
echo - Install Anyconnect (run anyconnect-win-*.exe from this folder)
echo.
pause

View File

@@ -1,69 +0,0 @@
const net = require('net');
const PORT = 1080;
const HOST = '0.0.0.0';
const server = net.createServer((client) => {
client.once('data', (data) => {
// SOCKS5 handshake
if (data[0] !== 0x05) {
client.end();
return;
}
// No auth required
client.write(Buffer.from([0x05, 0x00]));
client.once('data', (data) => {
if (data[0] !== 0x05 || data[1] !== 0x01) {
client.write(Buffer.from([0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
client.end();
return;
}
let host, port;
const atyp = data[3];
if (atyp === 0x01) {
// IPv4
host = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
port = data.readUInt16BE(8);
} else if (atyp === 0x03) {
// Domain
const len = data[4];
host = data.slice(5, 5 + len).toString();
port = data.readUInt16BE(5 + len);
} else if (atyp === 0x04) {
// IPv6
host = '';
for (let i = 0; i < 16; i += 2) {
host += data.slice(4 + i, 6 + i).toString('hex');
if (i < 14) host += ':';
}
port = data.readUInt16BE(20);
} else {
client.write(Buffer.from([0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
client.end();
return;
}
const remote = net.createConnection(port, host, () => {
const response = Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
client.write(response);
client.pipe(remote);
remote.pipe(client);
});
remote.on('error', () => {
client.write(Buffer.from([0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]));
client.end();
});
});
});
client.on('error', () => {});
});
server.listen(PORT, HOST, () => {
console.log(`SOCKS5 proxy running on ${HOST}:${PORT}`);
});

View File

@@ -1,117 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Startup hook - runs after container starts
# Dynamically detects Windows VM IP and sets up networking
# Install required packages (not persistent across restarts)
echo "[rego-tunnel] Installing required packages..."
apt-get update -qq && apt-get install -y -qq socat openssh-client netcat-openbsd >/dev/null 2>&1 || true
# Setup SSH key for accessing Windows VM
echo "[rego-tunnel] Setting up SSH key..."
mkdir -p /root/.ssh
cp /vpn_scripts/id_ed25519-lenovo /root/.ssh/ 2>/dev/null || true
chmod 600 /root/.ssh/id_ed25519-lenovo 2>/dev/null || true
get_windows_ip() {
# Use VM_NET_IP env var if set, otherwise detect from DHCP leases
if [[ -n "${VM_NET_IP:-}" ]]; then
echo "$VM_NET_IP"
return
fi
awk '/Windows/ {print $3}' /var/lib/misc/dnsmasq.leases 2>/dev/null | head -1
}
get_container_ip() {
# Get container's external IP (172.31.0.10) - exclude docker bridge gateway (.1)
ip -4 addr 2>/dev/null | grep -oE '172\.31\.0\.[0-9]+' | grep -v '\.1$' | head -1
}
(
# Wait for Windows VM to boot and get IP
echo "[rego-tunnel] Waiting for Windows VM..."
WINDOWS_IP=""
for i in {1..120}; do
WINDOWS_IP=$(get_windows_ip)
if [[ -n "$WINDOWS_IP" ]]; then
echo "[rego-tunnel] Windows VM IP: $WINDOWS_IP"
break
fi
sleep 2
done
if [[ -z "$WINDOWS_IP" ]]; then
echo "[rego-tunnel] ERROR: Could not detect Windows VM IP"
exit 1
fi
# Wait for SSH to be available on Windows
echo "[rego-tunnel] Waiting for SSH on Windows..."
for i in {1..60}; do
if nc -z "$WINDOWS_IP" 22 2>/dev/null; then
echo "[rego-tunnel] SSH is available"
break
fi
sleep 2
done
CONTAINER_IP=$(get_container_ip)
echo "[rego-tunnel] Container IP: $CONTAINER_IP"
# Add MASQUERADE for docker bridge
iptables -t nat -C POSTROUTING -o docker -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -o docker -j MASQUERADE
# Allow forwarding to Windows VM
iptables -C FORWARD -d "$WINDOWS_IP" -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -d "$WINDOWS_IP" -j ACCEPT
# Forward port 2222 to VM's SSH (2222) for VM access
pkill -f "socat.*:2222" 2>/dev/null || true
socat TCP-LISTEN:2222,fork,reuseaddr TCP:"$WINDOWS_IP":2222 &
echo "[rego-tunnel] SSH to VM available on port 2222"
# Add DNAT rules for port forwarding
add_dnat() {
local port=$1
iptables -t nat -C PREROUTING -d "$CONTAINER_IP" -p tcp --dport "$port" -j DNAT --to-destination "$WINDOWS_IP:$port" 2>/dev/null || \
iptables -t nat -A PREROUTING -d "$CONTAINER_IP" -p tcp --dport "$port" -j DNAT --to-destination "$WINDOWS_IP:$port"
}
# IBM i standard ports (via VM portproxy)
add_dnat 22
add_dnat 23
add_dnat 446
add_dnat 448
add_dnat 449
# IBM i data ports
for port in $(seq 8470 8476); do add_dnat $port; done
# Additional port ranges
for port in $(seq 2000 2020); do add_dnat $port; done
for port in $(seq 3000 3020); do add_dnat $port; done
for port in $(seq 10000 10020); do add_dnat $port; done
for port in $(seq 36000 36010); do add_dnat $port; done
echo "[rego-tunnel] iptables DNAT rules configured"
echo "[rego-tunnel] Port forwarding ready via $CONTAINER_IP"
# Set VNC password if VNC_PASSWORD env var is set
if [[ -n "${VNC_PASSWORD:-}" ]]; then
echo "[rego-tunnel] Setting VNC password..."
for i in {1..30}; do
if nc -z localhost 7100 2>/dev/null; then
sleep 2
echo "set_password vnc ${VNC_PASSWORD}" | nc -q1 localhost 7100 >/dev/null 2>&1 && \
echo "[rego-tunnel] VNC password set successfully" || \
echo "[rego-tunnel] Failed to set VNC password"
break
fi
sleep 2
done
fi
) &
return 0

View File

@@ -1,441 +0,0 @@
const WebSocket = require("ws");
const { authenticator } = require("otplib");
const { execSync, spawn } = require("child_process");
const CONFIG = {
email: "c-azaw@regoproducts.com",
password: "Fuckyou4suhail",
totpSecret: "RZQTQSKDWKHZ6ZYR",
devtoolsPort: 9222,
vpnTestIp: "10.35.33.230"
};
let ws;
let msgId = 1;
function log(msg) {
console.log("[" + new Date().toLocaleTimeString() + "] " + msg);
}
function run(cmd) {
try { execSync(cmd, { stdio: "ignore", timeout: 10000 }); } catch (e) {}
}
function runOutput(cmd) {
try {
return execSync(cmd, { encoding: "utf8", timeout: 10000 });
} catch (e) {
return "";
}
}
async function getPages() {
const res = await fetch("http://localhost:" + CONFIG.devtoolsPort + "/json");
return res.json();
}
function send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = msgId++;
const timeout = setTimeout(() => reject(new Error("Timeout: " + method)), 15000);
const handler = (data) => {
const msg = JSON.parse(data);
if (msg.id === id) {
clearTimeout(timeout);
ws.off("message", handler);
resolve(msg.result);
}
};
ws.on("message", handler);
ws.send(JSON.stringify({ id, method, params }));
});
}
async function waitForSelector(selector, timeout = 30000) {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
const result = await send("Runtime.evaluate", {
expression: "document.querySelector('" + selector + "') !== null",
returnByValue: true
});
if (result.result.value === true) return true;
} catch (e) {}
await sleep(500);
}
return false;
}
async function typeText(selector, text) {
await send("Runtime.evaluate", {
expression: "var el = document.querySelector('" + selector + "'); el.focus(); el.value = '';"
});
await sleep(100);
for (const char of text) {
await send("Input.dispatchKeyEvent", { type: "char", text: char });
await sleep(30);
}
}
async function click(selector) {
await send("Runtime.evaluate", {
expression: "document.querySelector('" + selector + "').click()"
});
}
async function clickSubmit() {
const methods = [
"document.querySelector('#submitButton') && document.querySelector('#submitButton').click()",
"typeof Login !== 'undefined' && Login.submitLoginRequest && Login.submitLoginRequest()",
"document.querySelector('input[type=\"submit\"]') && document.querySelector('input[type=\"submit\"]').click()",
"document.querySelector('button[type=\"submit\"]') && document.querySelector('button[type=\"submit\"]').click()",
"document.querySelector('#idSIButton9') && document.querySelector('#idSIButton9').click()"
];
for (const expr of methods) {
try { await send("Runtime.evaluate", { expression: expr }); } catch (e) {}
}
}
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function waitForDevtools(maxWait = 120000) {
const start = Date.now();
while (Date.now() - start < maxWait) {
try {
const pages = await getPages();
const page = pages.find(p => p.type === "page");
if (page) return page;
} catch (e) {}
log("Waiting for WebView...");
await sleep(2000);
}
return null;
}
// VPN adapter check skipped - IP range is unpredictable
// We rely solely on connectivity test to target IP
// Test connectivity via ping (check for actual reply, not TTL expired)
function testVpnConnectivity(ip) {
try {
const output = execSync(`ping -n 1 -w 3000 ${ip}`, { encoding: "utf8", timeout: 5000 });
// Must have "Reply from <ip>" - not "TTL expired" or "Request timed out"
return output.includes(`Reply from ${ip}`);
} catch (e) {
return false;
}
}
// Verify VPN is connected with retries (connectivity test only)
async function verifyVpnConnection(maxRetries = 10, retryDelay = 5000) {
log("--- VPN VERIFICATION ---");
for (let i = 1; i <= maxRetries; i++) {
log(`Attempt ${i}/${maxRetries}: Pinging ${CONFIG.vpnTestIp}...`);
const connected = testVpnConnectivity(CONFIG.vpnTestIp);
if (connected) {
log("VPN connectivity confirmed!");
return true;
}
log("Not reachable yet, waiting...");
if (i < maxRetries) {
await sleep(retryDelay);
}
}
log("VPN verification failed after " + maxRetries + " attempts");
return false;
}
async function main() {
console.log("");
console.log("========================================");
console.log(" CISCO VPN AUTO-LOGIN");
console.log("========================================");
console.log("");
// Sync time first (TOTP requires accurate time)
log("Syncing system time...");
run("sc config w32time start= auto");
run("net start w32time");
run("w32tm /config /manualpeerlist:pool.ntp.org /syncfromflags:manual /update");
run("w32tm /resync /force");
await sleep(2000);
// Kill everything
log("Killing Cisco processes...");
run("taskkill /F /IM csc_ui.exe");
run("taskkill /F /IM vpnui.exe");
run("taskkill /F /IM vpnagent.exe");
run('net stop csc_vpnagent');
await sleep(2000);
// Start agent
log("Starting Cisco agent...");
run('net start csc_vpnagent');
await sleep(3000);
// Start UI
log("Starting Cisco UI...");
process.env.WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS = "--remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --remote-allow-origins=*";
spawn("C:\\Program Files (x86)\\Cisco\\Cisco Secure Client\\UI\\csc_ui.exe", [], {
detached: true,
stdio: "ignore"
}).unref();
await sleep(5000);
// Wait for WebView
const page = await waitForDevtools();
if (!page) {
log("ERROR: WebView not found - rebooting...");
run("shutdown /r /t 1");
process.exit(1);
}
log("WebView: " + page.title);
ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
ws.on("open", resolve);
ws.on("error", reject);
});
await send("DOM.enable");
await send("Runtime.enable");
await send("Input.enable");
log("Connected to DevTools");
const url = page.url || "";
const isADFS = url.includes("adfs");
log("Login type: " + (isADFS ? "ADFS" : "Microsoft"));
if (isADFS) {
log("--- ADFS LOGIN ---");
if (!await waitForSelector("#passwordInput", 15000)) {
log("Password field not found");
process.exit(1);
}
log("Entering password...");
await typeText("#passwordInput", CONFIG.password);
await sleep(500);
log("Clicking Sign In...");
await clickSubmit();
await sleep(3000);
} else {
log("--- EMAIL ---");
if (await waitForSelector('input[type="email"]', 5000)) {
log("Entering email...");
await typeText('input[type="email"]', CONFIG.email);
await sleep(500);
log("Clicking Next...");
await clickSubmit();
await sleep(3000);
}
log("--- PASSWORD ---");
if (!await waitForSelector('input[type="password"]', 15000)) {
log("Password field not found");
process.exit(1);
}
log("Entering password...");
await typeText('input[type="password"]', CONFIG.password);
await sleep(500);
log("Clicking Sign In...");
await clickSubmit();
await sleep(3000);
}
// TOTP
log("--- TOTP ---");
if (await waitForSelector('input[name="otc"]', 15000)) {
await sleep(500);
const totp = authenticator.generate(CONFIG.totpSecret);
log("TOTP: " + totp);
await typeText('input[name="otc"]', totp);
await sleep(500);
log("Submitting...");
await clickSubmit();
await sleep(3000);
} else {
log("No TOTP field");
}
// Stay signed in
log("--- STAY SIGNED IN ---");
if (await waitForSelector("#idBtn_Back", 5000)) {
log("Clicking No...");
await click("#idBtn_Back");
}
await sleep(2000);
ws.close();
// Verify VPN connection
const vpnConnected = await verifyVpnConnection();
if (!vpnConnected) {
log("ERROR: VPN connection could not be verified");
process.exit(1);
}
console.log("");
console.log("========================================");
console.log(" VPN CONNECTED!");
console.log(" Entering watchdog mode...");
console.log("========================================");
console.log("");
// Enter watchdog mode - monitor and reconnect if needed
await watchdogLoop();
}
async function watchdogLoop() {
const checkInterval = 2 * 60 * 1000; // 2 minutes
let consecutiveFailures = 0;
let checkCount = 0;
log("Watchdog: Monitoring every 2 minutes...");
while (true) {
await sleep(checkInterval);
checkCount++;
// Force garbage collection every 10 checks (~20 min)
if (checkCount % 10 === 0 && global.gc) {
global.gc();
}
// Log memory every 30 checks (~1 hour)
if (checkCount % 30 === 0) {
const mem = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
log(`Watchdog: Memory ${mem}MB, checks ${checkCount}`);
}
const connected = testVpnConnectivity(CONFIG.vpnTestIp);
if (connected) {
if (consecutiveFailures > 0) {
log("Watchdog: Connection restored");
}
consecutiveFailures = 0;
} else {
consecutiveFailures++;
log(`Watchdog: Connection FAILED (${consecutiveFailures})`);
if (consecutiveFailures >= 2) {
log("Watchdog: Reconnecting...");
await reconnectVpn();
consecutiveFailures = 0;
}
}
}
}
async function reconnectVpn() {
// Sync time first (TOTP requires accurate time)
log("Syncing system time...");
run("sc config w32time start= auto");
run("net start w32time");
run("w32tm /resync /force");
await sleep(1000);
// Kill and restart VPN
log("Killing Cisco processes...");
run("taskkill /F /IM csc_ui.exe");
run("taskkill /F /IM vpnui.exe");
run("taskkill /F /IM vpnagent.exe");
run('net stop "Cisco Secure Client Agent"');
await sleep(2000);
log("Starting Cisco agent...");
run('net start "Cisco Secure Client Agent"');
await sleep(3000);
log("Starting Cisco UI...");
spawn("C:\\Program Files (x86)\\Cisco\\Cisco Secure Client\\UI\\csc_ui.exe", [], {
detached: true,
stdio: "ignore"
}).unref();
await sleep(5000);
const page = await waitForDevtools();
if (!page) {
log("ERROR: WebView not found - rebooting...");
run("shutdown /r /t 1");
return false;
}
ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
ws.on("open", resolve);
ws.on("error", reject);
});
await send("DOM.enable");
await send("Runtime.enable");
await send("Input.enable");
const url = page.url || "";
const isADFS = url.includes("adfs");
if (isADFS) {
if (await waitForSelector("#passwordInput", 15000)) {
await typeText("#passwordInput", CONFIG.password);
await sleep(500);
await clickSubmit();
await sleep(3000);
}
} else {
if (await waitForSelector('input[type="email"]', 5000)) {
await typeText('input[type="email"]', CONFIG.email);
await sleep(500);
await clickSubmit();
await sleep(3000);
}
if (await waitForSelector('input[type="password"]', 15000)) {
await typeText('input[type="password"]', CONFIG.password);
await sleep(500);
await clickSubmit();
await sleep(3000);
}
}
// TOTP
if (await waitForSelector('input[name="otc"]', 15000)) {
await sleep(500);
const totp = authenticator.generate(CONFIG.totpSecret);
log("TOTP: " + totp);
await typeText('input[name="otc"]', totp);
await sleep(500);
await clickSubmit();
await sleep(3000);
}
// Stay signed in - No
if (await waitForSelector("#idBtn_Back", 5000)) {
await click("#idBtn_Back");
}
await sleep(2000);
ws.close();
// Verify reconnection
const verified = await verifyVpnConnection();
if (verified) {
log("Reconnection successful!");
return true;
}
log("Reconnection failed");
return false;
}
main().catch(err => {
log("ERROR: " + err.message);
process.exit(1);
});

View File

@@ -1,4 +0,0 @@
@echo off
cd /d C:\Users\%USERNAME%\vpn_scripts\
node vpn-login.js
pause

Some files were not shown because too many files have changed in this diff Show More