#!/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}" # 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 log() { local level="$1" local msg="$2" local timestamp=$(date '+%H:%M:%S') case $level in INFO) echo -e "${GRAY}[$timestamp]${NC} ${GREEN}[INFO]${NC} $msg" ;; WARN) echo -e "${GRAY}[$timestamp]${NC} ${YELLOW}[WARN]${NC} $msg" ;; ERROR) echo -e "${GRAY}[$timestamp]${NC} ${RED}[ERROR]${NC} $msg" ;; DEBUG) echo -e "${GRAY}[$timestamp]${NC} ${CYAN}[DEBUG]${NC} $msg" ;; CMD) echo -e "${GRAY}[$timestamp]${NC} ${GRAY}[CMD]${NC} $msg" ;; *) echo -e "${GRAY}[$timestamp]${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 sudo "$editor" /etc/hosts else log ERROR "No editor found. Set EDITOR environment variable." log INFO "Try: sudo 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 VM's IP on the bridge network (for container routing) get_vm_bridge_ip() { # Get IP from ens3 (main adapter with 100.100.0.x) ip addr show ens3 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 } # Function to get container gateway IP get_container_gateway() { # The container bridge is at 100.100.0.1 echo "100.100.0.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)" sudo 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)" sudo 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)" sudo 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)" sudo 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 } # 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" sudo /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 vm_bridge_ip=$(get_vm_bridge_ip) local container_gw=$(get_container_gateway) log DEBUG "VPN interface: $vpn_iface" log DEBUG "VPN IP: $vpn_ip" log DEBUG "VM bridge IP: $vm_bridge_ip" log DEBUG "Container gateway: $container_gw" # Enable IP forwarding run_cmd "Enabling IP forwarding" sudo sysctl -w net.ipv4.ip_forward=1 # NAT masquerade for traffic from container network (100.100.0.0/24) going through VPN # This is the ONLY masquerade rule needed - source-based, not destination-based if ! sudo iptables -t nat -C POSTROUTING -s 100.100.0.0/24 -o "$vpn_iface" -j MASQUERADE 2>/dev/null; then run_cmd "Adding NAT masquerade for container network -> VPN" sudo iptables -t nat -A POSTROUTING -s 100.100.0.0/24 -o "$vpn_iface" -j MASQUERADE else log DEBUG "NAT masquerade for container network already exists" fi # Forward rules if ! sudo iptables -C FORWARD -d "$TARGET_IP" -j ACCEPT 2>/dev/null; then run_cmd "Adding forward rule (to target)" sudo iptables -A FORWARD -d "$TARGET_IP" -j ACCEPT else log DEBUG "Forward rule (to target) already exists" fi if ! sudo iptables -C FORWARD -s "$TARGET_IP" -j ACCEPT 2>/dev/null; then run_cmd "Adding forward rule (from target)" sudo iptables -A FORWARD -s "$TARGET_IP" -j ACCEPT else log DEBUG "Forward rule (from target) already exists" fi # Accept forwarding from container network if ! sudo iptables -C FORWARD -s 100.100.0.0/24 -j ACCEPT 2>/dev/null; then run_cmd "Adding forward rule (from container network)" sudo iptables -A FORWARD -s 100.100.0.0/24 -j ACCEPT else log DEBUG "Forward rule (from container network) already exists" fi if ! sudo iptables -C FORWARD -d 100.100.0.0/24 -j ACCEPT 2>/dev/null; then run_cmd "Adding forward rule (to container network)" sudo iptables -A FORWARD -d 100.100.0.0/24 -j ACCEPT else log DEBUG "Forward rule (to container network) already exists" fi # Cisco VPN chain bypass (insert at top if chain exists) if sudo iptables -L ciscovpn -n &>/dev/null; then if ! sudo iptables -C ciscovpn -o "$vpn_iface" -d "$TARGET_IP" -j ACCEPT 2>/dev/null; then run_cmd "Adding ciscovpn bypass (outbound)" sudo iptables -I ciscovpn 1 -o "$vpn_iface" -d "$TARGET_IP" -j ACCEPT else log DEBUG "Ciscovpn bypass (outbound) already exists" fi if ! sudo iptables -C ciscovpn -i "$vpn_iface" -s "$TARGET_IP" -j ACCEPT 2>/dev/null; then run_cmd "Adding ciscovpn bypass (inbound)" sudo 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 ! sudo iptables -C ciscovpn -s 100.100.0.0/24 -j ACCEPT 2>/dev/null; then run_cmd "Adding ciscovpn bypass (container network)" sudo iptables -I ciscovpn 3 -s 100.100.0.0/24 -j ACCEPT fi else log DEBUG "ciscovpn chain does not exist (yet)" fi log INFO "Forwarding rules configured" echo "" log INFO "Container should now be able to reach $TARGET_IP through this VM" 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:" # VM IPs echo "" log DEBUG "VM 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 # Bridge IP (for container routing) local vm_bridge_ip=$(get_vm_bridge_ip) if [ -n "$vm_bridge_ip" ]; then log DEBUG "VM IP on bridge: $vm_bridge_ip" fi # Container gateway local container_gw=$(get_container_gateway) log DEBUG "Container gateway: $container_gw" # 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" } # 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..." sudo /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=$! 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 # Restart host routing service log INFO "Restarting host routing service..." if ssh -o ConnectTimeout=5 -o BatchMode=yes root@ssh.alexzaw.dev systemctl restart rego-routing.service; then log INFO "Host routing service restarted" else log WARN "Failed to restart host routing service" fi # 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 # Start background monitor for auto-reconnect ( while true; do sleep 30 if [ -z "$(get_vpn_interface)" ]; then log WARN "VPN disconnected! Will reconnect on next menu action..." fi done ) & log INFO "VPN setup complete" 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 "$@" print_banner if [ "$DO_DISCONNECT" = "true" ]; then disconnect_vpn exit $? fi if [ "$DO_CONNECT" = "true" ]; then start_anyconnect "$( [ "$SKIP_AUTO_LOGIN" = "true" ] && echo "false" || echo "true" )" exit $? fi log INFO "Script started" echo "" # Check current status if check_vpn_status; then echo "" log INFO "VPN already connected. Setting up forwarding..." setup_forwarding elif [ "$SKIP_AUTO_LOGIN" = "true" ]; then echo "" log INFO "Menu mode - skipping auto-login" 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 "" case $choice in 1) start_anyconnect "$( [ "$SKIP_AUTO_LOGIN" = "true" ] && echo "false" || echo "true" )" ;; 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 ;; 8) show_routes ;; 9) show_hosts ;; e|E) edit_hosts ;; q|Q) log INFO "Goodbye!"; exit 0 ;; *) log ERROR "Invalid choice" ;; esac done