Skip to content

Instantly share code, notes, and snippets.

@codemedic
Last active June 25, 2024 14:24
Show Gist options
  • Save codemedic/80200e558df9104e0de0c5110af414ee to your computer and use it in GitHub Desktop.
Save codemedic/80200e558df9104e0de0c5110af414ee to your computer and use it in GitHub Desktop.
Docker network through host IPSec / Strongswan VPN
#!/usr/bin/env bash
# Link up docker network via IPSec VPN on docker-host.
#
# NOTE: This script can either be "sourced" into your .bashrc or executed directly. Be
# it sourced or executed, the usage syntax below is the same.
#
# Usage: [dry_run=1] [debug=1] vpn-docker-fix [docker-network-1 [docker-network-2 ...]]
#
# Env Variables:
# dry_run - Set to 1 to have a dry run, just printing out the iptables command
# debug - Set to 1 to see bash substitutions
vpn-docker-fix() {
# subshell to reduce "pollution" of the terminal shell when this script is "sourced"
# into .bashrc
(
: "${dry_run:=0}"
: "${debug:=0}"
_grep() {
/usr/bin/env grep "$@"
}
_awk() {
/usr/bin/env awk "$@"
}
_cut() {
/usr/bin/env cut "$@"
}
_log_stderr() {
echo "$*" >&2
}
_dry_run() {
if [ "$dry_run" = 1 ] || [ "$debug" = 1 ]; then
_log_stderr "$*"
fi
if [ "$dry_run" = 1 ]; then
return
fi
eval "$@"
}
_bash_debug_on() {
if [[ "$debug" != 1 ]]; then
return 0
fi
: "${bash_debug_level:=0}"
((bash_debug_level++))
[ -n "$bash_debug_state" ] || {
bash_debug_state=$(set +o | _grep xtrace)
set -x
}
}
_bash_debug_off() {
if [[ "$debug" != 1 ]]; then
return 0
fi
: "${bash_debug_level:=1}"
((bash_debug_level--))
if [ "$bash_debug_level" -le 0 ]; then
if [ -n "$bash_debug_state" ]; then
eval "$bash_debug_state"
unset bash_debug_state
fi
fi
}
# get docker network (if not specified)
get_docker_networks() {
local nets=()
if [[ $# == 0 ]]; then
nets=("$default_docker_network")
else
nets=("$@")
fi
printf '%s\n' "${nets[@]}"
}
# get docker network's CIDR
get_docker_network_subnet() {
if [ $# -lt 1 ] || [[ "$1" == "" ]]; then
_log_stderr "No docker network specified"
return 1
fi
local docker_network="$default_docker_network"
if [ "$1" != "$default_docker_network" ]; then
local net_hash
if ! net_hash=$(docker network ls -qf "Name=${1}") || [ -z "$net_hash" ]; then
_log_stderr "Docker network '$1' not found"
return 1
fi
docker_network="br-$net_hash"
fi
ip route show | _grep -v '169.254' | _grep " dev $docker_network" | _awk '{ print $1 }'
}
connect_docker_networks_to_vpn() {
if [ $# -lt 1 ] || [[ "$1" == "" ]]; then
_log_stderr "No docker network(s) specified"
return 1
fi
# vpnSubnet - VPN's CIDR
mapfile -t vpnSubnets < <(ip route list table 220 | _grep -o '^[0-9.]*/[0-9]*')
if [ "${#vpnSubnets}" -eq 0 ]; then
_log_stderr "VPN not active"
return 1
fi
# defaultRouteInterface - the active host network interface connecting to the VPN
defaultRouteInterface="$(ip route show | _grep -e "^default" | _awk '{ print $5 }')"
# virtualIP - virtual IP address of the host in the VPN
# - ignore dockerX and lo iterfaces
# - IP from interfaces that have valid-lifetime 'forever'
virtualIP="$(ip -4 -o addr show | _awk '/ (lo|docker|br-|tun)/ {next;} /valid_lft forever/ {print $4}' | _cut -d/ -f1)"
local docker_net docker_subnet vpn_subnet
for docker_net in "$@"; do
if docker_subnet="$(get_docker_network_subnet "$docker_net")"; then
for vpn_subnet in "${vpnSubnets[@]}"; do
_dry_run sudo iptables \
-j SNAT -t nat -I POSTROUTING 1 \
-o "$defaultRouteInterface" \
-d "$vpn_subnet" \
-s "$docker_subnet" \
--to-source "$virtualIP"
done
fi
done
}
_bash_debug_on
mapfile -t nets_chosen < <(get_docker_networks "$@")
connect_docker_networks_to_vpn "${nets_chosen[@]}"
_bash_debug_off
)
}
: "${default_docker_network:=docker0}"
# check if this script is being sourced or executed
if ! (return 0 2>/dev/null); then
# if not sourced, execute vpn-docker-fix with CLI agrs
vpn-docker-fix "$@"
else
_vpn-docker-fix_completions() {
local net nets=()
while read -r net; do
if [[ " ${COMP_WORDS[*]} " =~ " $net " ]]; then
continue
fi
nets+=("$net")
done < <(command docker network ls --format '{{.Name}}' --filter Driver=bridge | command grep -v ^bridge$)
mapfile -t COMPREPLY < <(compgen -W "${nets[*]}" -- ${COMP_WORDS[COMP_CWORD]})
}
complete -F _vpn-docker-fix_completions vpn-docker-fix
fi
@codemedic
Copy link
Author

This gist was developed based on this SO SF answer.
Once sourced into your bash shell, you can invoke command vpn-docker-fix in the below forms.

# setup an iptables rule to connect default network docker0 to VPN
vpn-docker-fix

# setup an iptables rule to connect the specific-network created via docker-compose or
# using 'docker network create' to VPN
vpn-docker-fix <specific-network>

# turn on debug
debug=1 vpn-docker-fix [...]

# dry run without applying any changes
dry_run=1 vpn-docker-fix [...]

# dry run and debug on
dry_run=1 debug=1 vpn-docker-fix [...]

@rszczypka
Copy link

do you run it on the host or in a docker container?

@codemedic
Copy link
Author

@rszczypka
You run it on the host.

@trytuna
Copy link

trytuna commented Mar 26, 2018

Thank you! That helped me!

@codemedic
Copy link
Author

is_strongswan_vpn_up() {
    local table220
    table220="$(ip route list table 220 2>/dev/null)" &&
        [ -n "$table220" ]
}

You can use the above function to check if StrongSwan / IPSec VPN is "up", in case you want to automate the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment