Skip to content

Instantly share code, notes, and snippets.

@luislobo
Created October 9, 2025 02:39
Show Gist options
  • Save luislobo/a2d1d48982e9290e26a2d47c0f0613a2 to your computer and use it in GitHub Desktop.
Save luislobo/a2d1d48982e9290e26a2d47c0f0613a2 to your computer and use it in GitHub Desktop.
Fix Docker bridge conflicts with Cisco VPN
#!/usr/bin/env bash
# fix-docker-vpn.sh
# Tames NetworkManager + Docker defaults so VPN clients stop restarting
set -euo pipefail
log() { printf '\n[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
warn() { printf '\n[%s] WARNING: %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
require() { command -v "$1" >/dev/null 2>&1 || { warn "'$1' is required"; missing=1; }; }
if [[ $EUID -ne 0 ]]; then
warn "run this script with sudo or as root"
exit 1
fi
missing=0
require nmcli
require systemctl
require python3
require docker
[[ $missing -eq 0 ]] || { warn "install missing commands first"; exit 1; }
NM_CONF_DIR=/etc/NetworkManager/conf.d
NM_CONF_FILE=$NM_CONF_DIR/docker-unmanaged.conf
DOCKER_DAEMON=/etc/docker/daemon.json
DOCKER_TMP=$(mktemp)
NM_TMP=$(mktemp)
cleanup() { rm -f "$DOCKER_TMP" "$NM_TMP"; }
trap cleanup EXIT INT TERM
log "Removing stale NetworkManager profile for docker0 (if present)"
if nmcli --fields NAME,DEVICE connection show | grep -q '^docker0'; then
nmcli connection delete docker0 || warn "failed to delete docker0 profile (continuing)"
else
log "No NetworkManager connection named 'docker0' found"
fi
log "Ensuring NetworkManager leaves Docker bridges unmanaged"
cat <<'EONM' >"$NM_TMP"
[keyfile]
unmanaged-devices=interface-name:docker0;interface-name:veth*;interface-name:br-*
EONM
install -m 0644 -D "$NM_TMP" "$NM_CONF_FILE"
log "Reloading NetworkManager"
systemctl reload NetworkManager || systemctl restart NetworkManager
log "Updating Docker default address pool to avoid VPN ranges"
python3 - "$DOCKER_DAEMON" "$DOCKER_TMP" <<'PY'
import json, sys
from pathlib import Path
daemon_path = Path(sys.argv[1])
tmp_path = Path(sys.argv[2])
desired = [{"base": "10.239.0.0/16", "size": 24}]
data = {}
if daemon_path.exists():
with daemon_path.open() as f:
data = json.load(f)
if data.get("default-address-pools") != desired:
data["default-address-pools"] = desired
tmp_path.write_text(json.dumps(data, indent=4) + "\n")
tmp_path.replace(daemon_path)
sys.exit(10) # signal caller to restart Docker
sys.exit(0)
PY
docker_updated=$?
if [[ $docker_updated -eq 10 ]]; then
log "Restarting Docker to apply address pool change"
systemctl restart docker
else
log "Docker address pool already configured (no restart needed)"
fi
log "Verifying bridge network"
docker network inspect bridge \
--format '{{range .IPAM.Config}}{{println " - Subnet:" .Subnet "Gateway:" .Gateway}}{{end}}'
log "Done. You may need to reconnect VPN once so it picks up the new routing."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment