Created
October 24, 2025 14:51
-
-
Save tiagobnobrega/1a32e47af7c998fa907bf04cabc6a014 to your computer and use it in GitHub Desktop.
Connect to Cisco AnyConnect VPN on Linux
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Use this script to connect to Cisco AnyConnect VPN on arch linux | |
| # or any usupported linux platform. It spins up a mitm proxy and launches | |
| # a chromium instance to perform the authentication. | |
| # Once the webvpn cookie is identified on the proxy, it uses openconnect | |
| # to establish the vpn connection using the cookie. | |
| # | |
| # In order to use this script you need to properly setup mitm proxy. | |
| # That includes adding the CA certificate | |
| # | |
| set -euo pipefail | |
| # anyconnect.sh | |
| # ---------- defaults ---------- | |
| URL_PARAM="" # no default URL; user must provide --url or --cookie | |
| AUTO_RUN=1 # default to auto-run (invert behavior); use --no-auto-run to disable | |
| TIMEOUT_SECS=300 | |
| MITM_PORT=8080 | |
| BROWSER_BIN="" # optional explicit browser binary | |
| STATUS_INTERVAL=5 # seconds between status prints while waiting | |
| MITM_LOG="" # choose later with timestamp if empty | |
| COOKIE_OVERRIDE="" # if provided, skip mitmdump/browser and run openconnect directly | |
| BROWSER_PID="" # will hold the browser PID if launched by script | |
| BROWSER_PROFILE="" # if empty, a temporary profile dir will be created with mktemp | |
| # ---------- arg parsing ---------- | |
| while (( "$#" )); do | |
| case "$1" in | |
| --url) URL_PARAM="$2"; shift 2;; | |
| --no-auto-run|--no-autoconnect) AUTO_RUN=0; shift ;; | |
| --auto-run) AUTO_RUN=1; shift ;; | |
| --timeout-secs) TIMEOUT_SECS="$2"; shift 2;; | |
| --mitm-port) MITM_PORT="$2"; shift 2;; | |
| --browser-bin) BROWSER_BIN="$2"; shift 2;; | |
| --browser-profile) BROWSER_PROFILE="$2"; shift 2;; | |
| --mitm-log) MITM_LOG="$2"; shift 2;; | |
| --cookie) COOKIE_OVERRIDE="$2"; shift 2;; | |
| -h|--help) | |
| cat <<USAGE | |
| Usage: $0 [options] | |
| You must provide either: | |
| --url <host/path> VPN host/path (protocol optional) | |
| OR | |
| --cookie <cookie-or-value> Provide a webvpn cookie | |
| Options: | |
| --no-auto-run, --no-autoconnect Don't automatically run openconnect after capturing cookie | |
| --timeout-secs N How long to wait for cookie (default: ${TIMEOUT_SECS}s) | |
| --mitm-port P mitmproxy listen port (default: ${MITM_PORT}) | |
| --browser-bin /path/to/bin Browser binary to use (defaults to chromium/chrome) | |
| --browser-profile /path/to/dir Persistent browser profile directory (if omitted, a temporary profile will be created) | |
| --mitm-log /path/to/log Where to save mitmdump stdout/stderr | |
| -h, --help Show this help | |
| USAGE | |
| exit 0 | |
| ;; | |
| *) echo "Unknown arg: $1"; exit 2;; | |
| esac | |
| done | |
| # ---------- verify input ---------- | |
| if [[ -z "$URL_PARAM" && -z "$COOKIE_OVERRIDE" ]]; then | |
| echo "ERROR: You must provide either --url or --cookie." | |
| echo | |
| # print usage | |
| "$0" --help | |
| exit 2 | |
| fi | |
| # ---------- sanitize URL_PARAM ---------- | |
| if [[ -n "$URL_PARAM" ]]; then | |
| while [[ "$URL_PARAM" == *"://"* ]]; do | |
| URL_PARAM="${URL_PARAM#*://}" | |
| done | |
| while [[ "$URL_PARAM" == //* ]]; do | |
| URL_PARAM="${URL_PARAM#//}" | |
| done | |
| URL_PARAM="${URL_PARAM%/}" | |
| VPN_URL="https://${URL_PARAM}" | |
| fi | |
| # ---------- derived values ---------- | |
| if [[ -z "${MITM_LOG:-}" ]]; then | |
| MITM_LOG="/tmp/mitmdump.$(date +%s).log" | |
| fi | |
| err() { echo "ERROR: $*" >&2; } | |
| info() { echo "INFO: $*"; } | |
| # ---------- helper: run openconnect ---------- | |
| run_openconnect() { | |
| local cookie_pair="$1" | |
| if [[ -z "$cookie_pair" ]]; then | |
| err "run_openconnect called with empty cookie_pair" | |
| return 2 | |
| fi | |
| if ! command -v openconnect >/dev/null 2>&1; then | |
| err "openconnect not found on PATH." | |
| return 1 | |
| fi | |
| info "Refreshing sudo credentials..." | |
| sudo -v || { err "sudo failed"; return 4; } | |
| info "Executing: sudo openconnect --protocol=anyconnect ${URL_PARAM} -C \"${cookie_pair}\"" | |
| sudo openconnect --protocol=anyconnect "${URL_PARAM}" -C "${cookie_pair}" & | |
| OC_PID=$! | |
| info "openconnect started (PID ${OC_PID}). Closing browser (if launched)." | |
| if [[ -n "${BROWSER_PID:-}" ]]; then | |
| info "Killing browser PID ${BROWSER_PID}..." | |
| kill "${BROWSER_PID}" >/dev/null 2>&1 || true | |
| sleep 0.5 | |
| if kill -0 "${BROWSER_PID}" >/dev/null 2>&1; then | |
| info "Browser did not exit; sending SIGKILL to ${BROWSER_PID}..." | |
| kill -9 "${BROWSER_PID}" >/dev/null 2>&1 || true | |
| fi | |
| fi | |
| wait "${OC_PID}" | |
| rc=$? | |
| info "openconnect exited with code ${rc}." | |
| return $rc | |
| } | |
| # ---------- cookie-only mode ---------- | |
| if [[ -n "${COOKIE_OVERRIDE:-}" ]]; then | |
| if [[ "${COOKIE_OVERRIDE}" == webvpn=* ]]; then | |
| COOKIE_PAIR="${COOKIE_OVERRIDE}" | |
| else | |
| COOKIE_PAIR="webvpn=${COOKIE_OVERRIDE}" | |
| fi | |
| cookie_val="${COOKIE_PAIR#webvpn=}" | |
| if [[ -z "$cookie_val" ]]; then | |
| err "Provided cookie is empty." | |
| exit 2 | |
| fi | |
| info "Cookie-only mode: will run openconnect with provided cookie." | |
| if (( AUTO_RUN )); then | |
| run_openconnect "$COOKIE_PAIR" | |
| exit $? | |
| else | |
| echo "Would run: sudo openconnect --protocol=anyconnect ${URL_PARAM} -C \"${COOKIE_PAIR}\"" | |
| exit 0 | |
| fi | |
| fi | |
| # ---------- normal (capture) mode preflight ---------- | |
| if ! command -v mitmdump >/dev/null 2>&1; then | |
| err "mitmdump not found on PATH." | |
| exit 1 | |
| fi | |
| if (( AUTO_RUN )); then | |
| if ! command -v openconnect >/dev/null 2>&1; then | |
| err "openconnect not found (required for auto-run)." | |
| exit 1 | |
| fi | |
| fi | |
| # ---------- browser detection helpers ---------- | |
| find_chrome_bin() { | |
| if [[ -n "$BROWSER_BIN" && -x "$BROWSER_BIN" ]]; then | |
| printf '%s' "$BROWSER_BIN" | |
| return 0 | |
| fi | |
| for cmd in chromium chromium-browser google-chrome-stable google-chrome; do | |
| if command -v "$cmd" >/dev/null 2>&1; then | |
| command -v "$cmd" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| # ---------- prepare mitm script & cookie file ---------- | |
| MITM_SCRIPT="$(mktemp --suffix=.py)" | |
| COOKIE_FILE="$(mktemp)" | |
| chmod 600 "$COOKIE_FILE" | |
| : > "$COOKIE_FILE" | |
| cat > "$MITM_SCRIPT" <<'PY' | |
| from mitmproxy import http, ctx | |
| import os | |
| OUT = os.environ.get("OUTFILE", "/tmp/webvpn_cookie.txt") | |
| def _extract_set_cookie_value(header: str) -> str: | |
| pair = header.split(";", 1)[0] | |
| if "=" in pair: | |
| name, value = pair.split("=", 1) | |
| return value | |
| return "" | |
| def response(flow: http.HTTPFlow) -> None: | |
| try: | |
| scs = flow.response.headers.get_all("Set-Cookie") | |
| except Exception: | |
| scs = [] | |
| for h in scs: | |
| if not h: | |
| continue | |
| lower = h.lower() | |
| if lower.startswith("webvpn="): | |
| pair = h.split(";", 1)[0].strip() | |
| value = _extract_set_cookie_value(h) | |
| if value and value.strip(): | |
| try: | |
| with open(OUT, "w") as f: | |
| f.write(pair) | |
| ctx.log.info("Captured webvpn cookie: %s" % pair) | |
| except Exception as e: | |
| ctx.log.error("Failed to write cookie: %s" % str(e)) | |
| else: | |
| ctx.log.info("Ignored empty webvpn cookie (no value).") | |
| return | |
| PY | |
| cleanup() { | |
| rc=$? | |
| info "Cleaning up..." | |
| [[ -n "${MITM_PID:-}" ]] && kill "${MITM_PID}" >/dev/null 2>&1 || true | |
| [[ -n "${BROWSER_PID:-}" ]] && kill "${BROWSER_PID}" >/dev/null 2>&1 || true | |
| [[ -f "$MITM_SCRIPT" ]] && rm -f "$MITM_SCRIPT" | |
| info "Mitmdump log: $MITM_LOG" | |
| info "Cookie file: $COOKIE_FILE" | |
| exit $rc | |
| } | |
| trap cleanup INT TERM EXIT | |
| # ---------- start mitmdump ---------- | |
| info "Starting mitmdump on 127.0.0.1:${MITM_PORT}..." | |
| export OUTFILE="$COOKIE_FILE" | |
| nohup mitmdump -p "$MITM_PORT" -s "$MITM_SCRIPT" >"$MITM_LOG" 2>&1 & | |
| MITM_PID=$! | |
| sleep 0.5 | |
| if ! kill -0 "$MITM_PID" >/dev/null 2>&1; then | |
| err "mitmdump failed to start. See $MITM_LOG" | |
| exit 1 | |
| fi | |
| info "mitmdump started (PID $MITM_PID)." | |
| # ---------- open browser ---------- | |
| open_with_proxy() { | |
| local url="$1" | |
| CHROME_BIN="$(find_chrome_bin || true || :)" | |
| # If no BROWSER_PROFILE set, create a temporary one (will be cleaned up on exit via trap) | |
| if [[ -z "${BROWSER_PROFILE:-}" ]]; then | |
| TMP_BROWSER_PROFILE="$(mktemp -d -t mitm-chrome-XXXX)" | |
| BROWSER_PROFILE="$TMP_BROWSER_PROFILE" | |
| info "No --browser-profile provided; using temporary profile: $BROWSER_PROFILE" | |
| else | |
| mkdir -p "$BROWSER_PROFILE" | |
| fi | |
| if [[ -n "$CHROME_BIN" && -x "$CHROME_BIN" ]]; then | |
| info "Launching Chromium/Chrome with profile '$BROWSER_PROFILE' and proxy -> 127.0.0.1:${MITM_PORT}" | |
| "$CHROME_BIN" --user-data-dir="$BROWSER_PROFILE" --proxy-server="127.0.0.1:${MITM_PORT}" --no-first-run --app="$url" >/dev/null 2>&1 & | |
| BROWSER_PID=$! | |
| return 0 | |
| fi | |
| if command -v firefox >/dev/null 2>&1; then | |
| TMP_FF_PROFILE="$(mktemp -d -t mitm-ff-XXXX)" | |
| cat > "$TMP_FF_PROFILE/user.js" <<JS | |
| user_pref("network.proxy.type", 1); | |
| user_pref("network.proxy.http", "127.0.0.1"); | |
| user_pref("network.proxy.http_port", ${MITM_PORT}); | |
| user_pref("network.proxy.ssl", "127.0.0.1"); | |
| user_pref("network.proxy.ssl_port", ${MITM_PORT}); | |
| JS | |
| info "Launching Firefox with temporary profile and proxy -> 127.0.0.1:${MITM_PORT}" | |
| firefox -profile "$TMP_FF_PROFILE" "$url" >/dev/null 2>&1 & | |
| BROWSER_PID=$! | |
| return 0 | |
| fi | |
| if command -v xdg-open >/dev/null 2>&1; then | |
| info "No Chromium/Chrome/Firefox detected to auto-configure proxy. Falling back to xdg-open." | |
| xdg-open "$url" >/dev/null 2>&1 & | |
| BROWSER_PID=$! | |
| return 0 | |
| fi | |
| err "No method to open a browser found. Please open $url in a browser configured to use 127.0.0.1:${MITM_PORT}" | |
| return 1 | |
| } | |
| info "Opening browser to: $VPN_URL" | |
| open_with_proxy "$VPN_URL" || { err "Failed to open browser"; exit 1; } | |
| # ---------- wait for cookie ---------- | |
| info "Waiting up to ${TIMEOUT_SECS}s for a new non-empty webvpn cookie..." | |
| start_ts=$(date +%s) | |
| last_status_ts=$start_ts | |
| while true; do | |
| if [[ -s "$COOKIE_FILE" ]]; then | |
| COOKIE_LINE="$(tr -d '\r\n' <"$COOKIE_FILE")" | |
| if [[ "$COOKIE_LINE" == webvpn=* ]]; then | |
| cookie_val="${COOKIE_LINE#webvpn=}" | |
| if [[ -n "$cookie_val" ]]; then | |
| info "Captured cookie: $COOKIE_LINE" | |
| break | |
| else | |
| info "Found webvpn= with empty value; waiting..." | |
| fi | |
| else | |
| info "Cookie file contents did not start with 'webvpn='; '$COOKIE_LINE'" | |
| fi | |
| fi | |
| if ! kill -0 "$MITM_PID" >/dev/null 2>&1; then | |
| err "mitmdump exited unexpectedly. See $MITM_LOG" | |
| exit 3 | |
| fi | |
| now_ts=$(date +%s) | |
| if (( now_ts - last_status_ts >= STATUS_INTERVAL )); then | |
| info "Still waiting for cookie... elapsed $((now_ts - start_ts))s" | |
| last_status_ts=$now_ts | |
| fi | |
| if (( now_ts - start_ts >= TIMEOUT_SECS )); then | |
| err "Timed out waiting for cookie after ${TIMEOUT_SECS}s." | |
| exit 2 | |
| fi | |
| sleep 0.5 | |
| done | |
| # ---------- run openconnect ---------- | |
| if (( AUTO_RUN )); then | |
| COOKIE_PAIR="$COOKIE_LINE" | |
| info "Auto-run mode: running openconnect with captured cookie." | |
| run_openconnect "$COOKIE_PAIR" | |
| exit $? | |
| else | |
| echo | |
| echo "==== webvpn cookie (use with openconnect -C): ====" | |
| echo "$COOKIE_LINE" | |
| echo "===============================================" | |
| info "Auto-run disabled; re-run without --no-auto-run to run openconnect automatically." | |
| fi | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment