Dalhousie VPN Setup on Ubuntu/Debian Linux

Tested on XUbuntu 24.04 · Python 3.12 · openconnect-sso 0.8.1 · openconnect 9.12

This guide replaces the Cisco AnyConnect client with openconnect + openconnect-sso + vpn-slice. The result is a more stable connection with split tunneling (only Dalhousie traffic goes through the VPN), persistent SSO sessions (no re-authentication for ~2 weeks), and fast automatic reconnection after network interruptions.


1. Install packages

sudo apt update
sudo apt install openconnect pipx
pipx install vpn-slice
pipx ensurepath
source ~/.bashrc
sudo ln -s ~/.local/bin/vpn-slice /usr/local/bin/vpn-slice
✓ Verify
openconnect --version      # should print version info
vpn-slice --version        # should print e.g. "vpn-slice 0.16.1"
sudo which vpn-slice       # should print /usr/local/bin/vpn-slice

2. Install openconnect-sso

pipx install openconnect-sso

# Fix Python 3.12 compatibility (pkg_resources is missing from newer setuptools)
~/.local/share/pipx/venvs/openconnect-sso/bin/python -m pip install "setuptools<72"
✓ Verify
openconnect-sso --version   # should print version info without errors
~/.local/share/pipx/venvs/openconnect-sso/bin/python -c "import pkg_resources; print('ok')"

3. Patch openconnect-sso

Two bugs need to be fixed in openconnect-sso 0.8.1. Both patches may be upstreamed eventually; check before applying.

Patch 1: Persistent browser session cookies

Without this patch, you must re-authenticate every time you connect. With it, your SSO session persists for ~2 weeks (matching the Cisco client behavior).

Edit:

~/.local/share/pipx/venvs/openconnect-sso/lib/python3.12/site-packages/openconnect_sso/browser/webengine_process.py

Change 1: In WebBrowser.__init__, replace:

        cookie_store = self.page().profile().cookieStore()

With:

        self._profile = QWebEngineProfile("openconnect-sso", self)
        self.setPage(QWebEnginePage(self._profile, self))
        cookie_store = self.page().profile().cookieStore()

Change 2: In _on_cookie_added, add two lines after the existing content:

        cookie2 = QNetworkCookie()
        self.page().profile().cookieStore().deleteCookie(cookie2)

The full method should look like:

    def _on_cookie_added(self, cookie):
        logger.debug("Cookie set", name=to_str(cookie.name()))
        self._on_update(SetCookie(to_str(cookie.name()), to_str(cookie.value())))
        cookie2 = QNetworkCookie()
        self.page().profile().cookieStore().deleteCookie(cookie2)
✓ Verify
openconnect-sso --server vpn.its.dal.ca --authgroup DAL   # connect, authenticate when prompted
# (wait for "ring size 32" message, then Ctrl-C to disconnect)
openconnect-sso --server vpn.its.dal.ca --authgroup DAL   # reconnect: should skip authentication
# (Ctrl-C to disconnect when done)

Patch 2: Fix argument passthrough to openconnect

Without this patch, arguments intended for openconnect (such as --script) are incorrectly rejected by openconnect-sso's argument parser. This fix allows them to be passed on the command line, which is how the shell alias in Section 6 supplies the --script argument for split tunneling.

Edit:

~/.local/share/pipx/venvs/openconnect-sso/lib/python3.12/site-packages/openconnect_sso/cli.py

In main(), replace:

    args = parser.parse_args()

With:

    args, extra = parser.parse_known_args()
    args.openconnect_args = (args.openconnect_args or []) + extra

Note: This fix has been submitted as PR #213 and may already be included in newer versions of openconnect-sso. Check before applying.

✓ Verify
openconnect-sso --help -- --script "vpn-slice 129.173.0.0/16"
# should print help without "unrecognized arguments" error

4. Allow openconnect to run without sudo password

sudo visudo -f /etc/sudoers.d/openconnect

Add (replace YOUR_USERNAME with your username):

YOUR_USERNAME ALL=(ALL) NOPASSWD: /usr/sbin/openconnect
YOUR_USERNAME ALL=(ALL) NOPASSWD: /usr/bin/pkill openconnect
✓ Verify
sudo openconnect --version   # should not prompt for password
sudo pkill openconnect        # should not prompt for password (exit code 1 if not running is fine)

5. NetworkManager dispatcher script

This script ensures the VPN server route is restored after a network interruption, and signals openconnect to reconnect immediately.

sudo nano /etc/NetworkManager/dispatcher.d/99-vpn-route
#!/bin/bash
IF=$1
ACTION=$2

if [ "$ACTION" = "up" ] && [ "$IF" = "wlp2s0" ]; then
    # Restore direct route to VPN server (lost when wifi reconnects)
    ip route add 129.173.0.12 via 192.168.0.1 dev wlp2s0 2>/dev/null || true
    # Signal openconnect to reconnect immediately (SIGUSR2 = reconnect)
    pkill -USR2 openconnect
fi
sudo chmod +x /etc/NetworkManager/dispatcher.d/99-vpn-route

Replace wlp2s0 with your wifi interface name (check with ip link).

✓ Verify
ls -la /etc/NetworkManager/dispatcher.d/99-vpn-route   # should be executable
sudo /etc/NetworkManager/dispatcher.d/99-vpn-route wlp2s0 up   # should run without errors

6. Shell aliases

Add to ~/.bashrc or ~/.bash_aliases:

alias vpn='pgrep openconnect > /dev/null && echo "VPN already running" || (openconnect-sso --server vpn.its.dal.ca --authgroup DAL -- -b --script "vpn-slice 129.173.0.0/16" --reconnect-timeout 30 --force-dpd 10 >/tmp/vpn.log 2>&1)'
alias novpn='sudo pkill openconnect'

The -b flag tells openconnect to stay in the foreground during the connection handshake, then background itself once connected. This means vpn can be used in scripts that need the VPN to be up before proceeding:

vpn && ssh chase.mathstat.dal.ca

VPN activity is logged to /tmp/vpn.log for troubleshooting. Then:

source ~/.bashrc
✓ Verify
type vpn    # should print the alias definition
type novpn  # should print the alias definition

7. First connection

Run vpn. A browser window will open asking for:

  1. Your Dal NetID (username)
  2. Your password
  3. A 6-digit TOTP token

After successful authentication, the window closes and the VPN connects silently in the background. Subsequent connections within ~2 weeks will not require re-authentication.

To disconnect: novpn

✓ Verify — split tunneling
# Should show your home IP, not a Dalhousie IP
curl ifconfig.me

# Should work (routed through VPN)
ssh chase.mathstat.dal.ca

# Should show default route via wlp2s0 (not tun0), plus 129.173.0.0/16 via tun0
ip route show
✓ Verify — session persistence
novpn
vpn   # should connect without asking for credentials

Reconnection behavior


Caveats and known issues