Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save socketbox/929378a16b43ed9026a226eb25fabe18 to your computer and use it in GitHub Desktop.
Save socketbox/929378a16b43ed9026a226eb25fabe18 to your computer and use it in GitHub Desktop.
OpenConnect VPN Inside Linux Network Namespace
#!/bin/bash
# start openconnect tunnel and shell inside Linux network namespace
#
# this is a fork of schnouki's script, see original blog post
# https://schnouki.net/posts/2014/12/12/openvpn-for-a-single-application-on-linux/
#
# original script can be found here
# https://gist.github.com/Schnouki/fd171bcb2d8c556e8fdf
# ------------ adjust values below ------------
VPN_ENDPOINT="${VPN_ENDPOINT:-}"
VPN_USER="${VPN_USER:-}"
VPN_GROUP="${VPN_GROUP:-}"
NETNS_NAME="${NETNS_NAME:-}"
NETNS_EXEC="ip netns exec $NETNS_NAME"
WIRED_INTERFACE="enp1s0"
WIRELESS_INTERFACE="wlp2s0"
OUT_IF="${NETNS_NAME}-out"
IN_IF="${NETNS_NAME}-in"
OUT_IP="10.200.200.1"
IN_IP="10.200.200.2"
URLS_TO_FORWARD="${URLS_TO_FORWARD:-}"
set -ueo pipefail
MISSING_VAR=
for VAR_NAME in NETNS_NAME VPN_ENDPOINT VPN_USER VPN_GROUP; do
eval VAR="\${$VAR_NAME}"
if [[ -z $VAR ]]; then
MISSING_VAR=1
echo "Must define env var \$$VAR_NAME"
fi
done
[[ -z $MISSING_VAR ]] || exit 1
if [[ $USER != root ]]; then
echo "You must run this script as root, preferably with sudo."
exit 1
fi
create_ns() {
echo "Add network interface"
# create the network namespace
ip netns add "$NETNS_NAME"
# start the loopback interface in the namespace
$NETNS_EXEC ip addr add 127.0.0.1/8 dev lo
$NETNS_EXEC ip link set lo up
# create virtual network interfaces that will let OpenVPN (in the
# namespace) access the real network, and configure the interface in the
# namespace (${IN_IF}) to use the interface out of the namespace (${OUT_IF})
# as its default gateway
ip link add "${OUT_IF}" type veth peer name "${IN_IF}"
ip link set "${OUT_IF}" up
ip link set "${IN_IF}" netns "${NETNS_NAME}" up
ip addr add "${OUT_IP}"/24 dev "${OUT_IF}"
$NETNS_EXEC ip addr add "${IN_IP}"/24 dev "${IN_IF}"
$NETNS_EXEC ip link set dev "${IN_IF}" mtu 1492
$NETNS_EXEC ip route add default via "${OUT_IP}" dev "${IN_IF}"
# make sure ipv4 forwarding is enabled
sysctl -w net.ipv4.ip_forward=1
# configure the nameserver to use inside the namespace
# TODO use VPN-provided DNS servers in order to prevent leaks
mkdir -p "/etc/netns/${NETNS_NAME}"
cat <<EOF | tee "/etc/netns/${NETNS_NAME}/resolv.conf" || exit 1
nameserver 8.8.8.8
nameserver 8.8.4.4
EOF
# Setup IP masquerading in the default namespace for paquets coming from the network namespace
# so that they can communicate with the internet
iptables -t nat -A POSTROUTING -o "${WIRED_INTERFACE}" -m mark --mark 0x29a -j MASQUERADE
iptables -t nat -A POSTROUTING -o "${WIRELESS_INTERFACE}" -m mark --mark 0x29a -j MASQUERADE
iptables -t mangle -A PREROUTING -i "${OUT_IF}" -j MARK --set-xmark 0x29a/0xffffffff
if which ufw && [[ $(ufw status | head -n 1) == *active ]]; then
# If you have ufw, you need to allow paquets coming from the netns to flow through ufw
ufw route allow in on "${OUT_IF}" out on "${WIRED_INTERFACE}"
ufw route allow in on "${OUT_IF}" out on "${WIRELESS_INTERFACE}"
fi
VPN_PASS=$(systemd-ask-password --keyname=openconnect-$NETNS_NAME --accept-cached "Password for vpn $NETNS_NAME:")
echo -e "${VPN_PASS}\npush" | $NETNS_EXEC /usr/sbin/openconnect \
--interface vpn0 $VPN_ENDPOINT \
-u $VPN_USER --authgroup=$VPN_GROUP --passwd-on-stdin &
sleep 1
while ! $NETNS_EXEC ip link show dev vpn0 &>/dev/null; do sleep 1; done
# Enable IP masquerading for IP paquets comming from default namespace.
# My use case was forwarding IPs for private docker registries since the docker
# daemon runs in the default namespace. I added routes to forward the IPs to
# the vpn namespace, but for it to work you need to masquerade those IP paquets
# being forwarded from the default namespace.
$NETNS_EXEC iptables -t mangle -A PREROUTING -i $IN_IF --src $OUT_IP -j MARK --set-xmark 0x8/0x0
$NETNS_EXEC iptables -t nat -A POSTROUTING -m mark --mark 0x8 -j MASQUERADE
for url in ${URLS_TO_FORWARD}; do
for ip in $(dig +short $url); do
ip route replace $ip via ${IN_IP}
done
done
}
tear_down_ns() {
set +eo pipefail
echo "Stopping VPN"
if NS_PIDS=$(ip netns pids $NETNS_NAME 2> /dev/null) && [[ -n $NS_PIDS ]]; then
echo "$NS_PIDS" | xargs -rd'\n' kill -SIGINT &>/dev/null
fi
echo "Cleaning up iptables"
iptables -t nat -D POSTROUTING -o "${WIRED_INTERFACE}" -m mark --mark 0x29a -j MASQUERADE &>/dev/null
iptables -t nat -D POSTROUTING -o "${WIRELESS_INTERFACE}" -m mark --mark 0x29a -j MASQUERADE &>/dev/null
iptables -t mangle -D PREROUTING -i "${OUT_IF}" -j MARK --set-xmark 0x29a/0xffffffff &>/dev/null
if which ufw && [[ $(ufw status | head -n 1) == *active ]]; then
# cleanup ufw
ufw route delete allow in on "${OUT_IF}" out on "${WIRED_INTERFACE}" &>/dev/null
ufw route delete allow in on "${OUT_IF}" out on "${WIRELESS_INTERFACE}" &>/dev/null
fi
echo "Cleanup network namespace"
rm -rf "/etc/netns/${NETNS_NAME}" &>/dev/null
ip netns delete "${NETNS_NAME}" &>/dev/null
ip link delete "${OUT_IF}" &>/dev/null
}
set -x
trap tear_down_ns EXIT
create_ns
echo "Starting shell inside $NETNS_NAME network namespace"
# start shell in new network namespace
SU_USER=${SUDO_USER:-}
$NETNS_EXEC su ${SU_USER}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment