This article is incomplete as of yet. It describes how to setup a network namespace based seperation of concerns for arbitrary downloaders on a Debian system without leaking traffic from downloading via the main network/internet facing interfaces.
Install Packages
$ apt install jq bc openvpn
#Download netns-ctl from $SOURCE (not published yet)
cd netns-ctl
$ sudo make install
$ sudo -i
mkdir -p /etc/netns/{protonvpn-ch,vm-down}/{network/{if-down.d,if-post-down.d,if-pre-up.d,if-up.d,netns-scripts,interfaces.d},iptables}
vim /etc/network/interfaces.d/pvpn-ch
allow-hotplug pvpn-ch
auto pvpn-ch
iface pvpn-ch inet static
address 10.33.0.1
netmask 255.255.255.252
vim /etc/network/interfaces.d/vm-down
allow-hotplug vm-down
auto vm-down
iface vm-down inet static
address 10.33.0.9
netmask 255.255.255.252
vim /etc/netns/protonvpn-ch/network/interfaces
auto main
iface main inet static
address 10.33.0.2
netmask 255.255.255.252
up ip route add 185.159.157.0/24 via 10.33.0.1 dev main
up ip route add 193.36.117.0/24 via 10.33.0.1 dev main
allow-hotplug vm-down
auto vm-down
iface vm-down inet static
address 10.33.0.5
netmask 255.255.255.252
auto lo
iface lo inet loopback
vim /etc/netns/vm-down/network/interfaces
allow-hotplug main
auto main
iface main inet static
address 10.33.0.10
netmask 255.255.255.252
allow-hotplug pvpn-ch
auto pvpn-ch
iface pvpn-ch inet static
address 10.33.0.6
netmask 255.255.255.252
up ip route add default via 10.33.0.5 dev pvpn-ch
touch /etc/netns/protonvpn-ch/resolv.conf.vpn
vim /etc/netns/protonvpn-ch/resolv.conf
nameserver 127.0.0.1
vim /etc/netns/vm-down/resolv.conf
nameserver 127.0.0.1
vim /etc/netns/vm-down/resolv.conf.vpn
nameserver 10.33.0.5
vim /etc/netns/protonvpn-ch/iptables/rules.v4
# Generated by xtables-save v1.8.2 on Tue Oct 20 19:35:40 2020
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
COMMIT
# Completed on Tue Oct 20 19:35:40 2020
# Generated by xtables-save v1.8.2 on Tue Oct 20 19:35:40 2020
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A POSTROUTING -o tun0 -j MASQUERADE
COMMIT
# Completed on Tue Oct 20 19:35:40 2020
touch /etc/netns/protonvpn-ch/iptables/rules.v6
touch /etc/netns/vm-down/iptables/rules.v4
touch /etc/netns/vm-down/iptables/rules.v6
vim /etc/netns/protonvpn-ch/network/netns-scripts/protonvpn-ch
#!/bin/sh
set -e;
set -x;
TASK="$1";
NS="$2";
case "$TASK" in
up-outer)
ifup pvpn-ch
;;
down-outer)
;;
up-inner)
echo "DUMMY"
iptables-restore < /etc/netns/protonvpn-ch/iptables/rules.v4
ip6tables-restore < /etc/netns/protonvpn-ch/iptables/rules.v6
;;
esac
chmod +x /etc/netns/protonvpn-ch/network/netns-scripts/protonvpn-ch
vim /etc/network/netns-ctl.conf
netns main
pid 1
auto link main-protonvpn-ch
auto link main-vm-down
end
netns protonvpn-ch
pid foreign
auto link main-protonvpn-ch
auto link protonvpn-ch-vm-down
end
netns vm-down
pid foreign
auto link main-vm-down
auto link protonvpn-ch-vm-down
end
link main-protonvpn-ch
iface main in protonvpn-ch
iface pvpn-ch in main
end
link protonvpn-ch-vm-down
iface vm-down in protonvpn-ch
iface pvpn-ch in vm-down
end
link main-vm-down
iface main in vm-down
iface vm-down in main
TODO: Maybe have netns-ctl balk if there are auto links but no link tags?
vim /etc/systemd/system/netns\@.service
[Unit]
Description=Network Namespace %i
Wants=network-pre.target
Before=network-pre.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=bash -c 'mkdir -p /run/netns && touch /run/netns/"%i" && mount --bind /proc/self/ns/net /run/netns/"%i"'
ExecStop=/usr/bin/ip netns delete "%i"
KillMode=none
[Install]
WantedBy=multi-user.target
vim /etc/systemd/system/netns-ctl\@.service
[Unit]
Description=Network Namespace - CTL %i
Wants=network-pre.target
Before=network-pre.target
After=netns@%i.service
BindsTo=netns@%i.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/netns-ctl netnsup %i
ExecStop=/usr/local/sbin/netns-ctl netnsdown %i
KillMode=none
[Install]
WantedBy=multi-user.target
mkdir -p /etc/systemd/system/netns@{protonvpn-ch,vm-down}.service.d/
$ vim /etc/systemd/system/netns@protonvpn-ch.service.d/override.conf
[Service]
PrivateNetwork=yes
[Unit]
After=netns@protonvpn-ch.service
BindsTo=netns@protonvpn-ch.service
$ vim /etc/systemd/system/netns@vm-down.service.d/override.conf
[Service]
PrivateNetwork=yes
[Unit]
After=netns@protonvpn-ch.service
mkdir /etc/systemd/system/openvpn@protonvpn-ch.service.d/
$ vim /etc/systemd/system/openvpn@protonvpn-ch.service.d/override.conf
[Unit]
BindsTo = netns@protonvpn-ch.service
JoinsNamespaceOf = netns@protonvpn-ch.service
After = netns@protonvpn-ch.service
[Service]
PrivateNetwork = true
BindPaths=/etc/netns/protonvpn-ch/resolv.conf:/etc/resolv.conf
BindPaths=/etc/netns/protonvpn-ch/resolv.conf.vpn:/etc/resolv.conf.vpn
mkdir /etc/openvpn/protonvpn/
touch /etc/openvpn/protonvpn/login.conf
chmod 600 /etc/openvpn/protonvpn/login.conf
vim /etc/openvpn/protonvpn/login.conf
username
password
mkdir /etc/openvpn/protonvpn/ch/
vim /etc/openvpn/protonvpn/ch/ch.ovpn (modified ProtonVPN CH Config)
#
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# ==============================================================================
client
dev tun
proto udp
remote ch.protonvpn.com 80
remote ch.protonvpn.com 443
remote ch.protonvpn.com 4569
remote ch.protonvpn.com 1194
remote ch.protonvpn.com 5060
remote-random
resolv-retry infinite
nobind
cipher AES-256-CBC
auth SHA512
comp-lzo no
verb 3
tun-mtu 1500
tun-mtu-extra 32
mssfix 1450
persist-key
persist-tun
reneg-sec 0
remote-cert-tls server
auth-user-pass
pull
fast-io
<ca>
...
</ca>
key-direction 1
<tls-auth>
...
</tls-auth>
cd /etc/openvpn/protonvpn/
pull-filter ignore redirect-gateway
redirect-gateway local
auth-user-pass login.conf
script-security 2
up ch-test
down ch-test
ifconfig-noexec
route-noexec
route-up ch-test
keepalive 10 120
status /etc/openvpn/protonvpn/ch/openvpn-status.log
#route-down ch-test
#
#
auth-retry nointeract
vim /etc/openvpn/protonvpn/ch/ch-test
#!/bin/bash
#
# Parses DHCP options from openvpn to update resolv.conf
# To use set as 'up' and 'down' script in your openvpn *.conf:
# up /etc/openvpn/update-resolv-conf
# down /etc/openvpn/update-resolv-conf
#
# Used snippets of resolvconf script by Thomas Hood and Chris Hanson.
# Licensed under the GNU GPL. See /usr/share/common-licenses/GPL.
#
# Example envs set from openvpn:
#
# foreign_option_1='dhcp-option DNS 193.43.27.132'
# foreign_option_2='dhcp-option DNS 193.43.27.133'
# foreign_option_3='dhcp-option DOMAIN be.bnc.ch'
#
set -ex
[ "$script_type" ] || exit 0
[ "$dev" ] || exit 0
split_into_parts()
{
part1="$1"
part2="$2"
part3="$3"
}
ROUTING_TABLE=666
echo "MODE START: $script_type"
case "$script_type" in
up)
NMSRVRS=""
SRCHS=""
foreign_options=$(printf '%s\n' ${!foreign_option_*} | sort -t _ -k 3 -g)
for optionvarname in ${foreign_options} ; do
option="${!optionvarname}"
echo "$option"
split_into_parts $option
if [ "$part1" = "dhcp-option" ] ; then
if [ "$part2" = "DNS" ] ; then
NMSRVRS="${NMSRVRS:+$NMSRVRS }$part3"
elif [ "$part2" = "DOMAIN" ] ; then
SRCHS="${SRCHS:+$SRCHS }$part3"
fi
fi
done
R=""
[ "$SRCHS" ] && R="search $SRCHS
"
for NS in $NMSRVRS ; do
R="${R}nameserver $NS
"
done
echo -n "$R" > /etc/resolv.conf.vpn # ip netns exec `ip netns identify` tee /etc/resolv.conf
cat /etc/resolv.conf.vpn
#ip link set dev "$dev" up netns "vm-down" mtu "$tun_mtu" 2>&1 | logger
#ip netns exec "vm-down" date 2>&1 | logger
ip link set dev "$dev" up mtu "$tun_mtu"
# set device address
netmask4="${ifconfig_netmask:-30}"
netbits6="${ifconfig_ipv6_netbits:-112}"
if [ -n "$ifconfig_local" ]; then
if [ -n "$ifconfig_remote" ]; then
/sbin/ip -4 addr add \
local "$ifconfig_local" \
peer "$ifconfig_remote/$netmask4" \
${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
dev "$dev"
else
/sbin/ip -4 addr add \
local "$ifconfig_local/$netmask4" \
${ifconfig_broadcast:+broadcast "$ifconfig_broadcast"} \
dev "$dev"
fi
#ip -4 route
#ip -4 route | grep "src $ifconfig_local" || true
#ip -4 route | grep "src $ifconfig_local" | xargs -iIII -n 1 echo ip -4 route add III table $ROUTING_TABLE
#ip -4 route | grep "src $ifconfig_local" | xargs -iIII -n 1 ip -4 route add III table $ROUTING_TABLE
#10.0.1.4/30 dev vm-down proto kernel scope link src 10.0.1.6
fi
if [ -n "$IPV6" -a -n "$ifconfig_ipv6_local" ]; then
if [ -n "$ifconfig_ipv6_remote" ]; then
/sbin/ip -6 addr add \
local "$ifconfig_ipv6_local" \
peer "$ifconfig_ipv6_remote/$netbits6" \
dev "$dev"
else
/sbin/ip -6 addr add \
local "$ifconfig_ipv6_local/$netbits6" \
dev "$dev"
fi
fi
;;
route-up)
i=1
while
eval net=\"\$route_network_$i\"
eval mask=\"\$route_netmask_$i\"
eval gw=\"\$route_gateway_$i\"
eval mtr=\"\$route_metric_$i\"
[ -n "$net" ]
do
/sbin/ip -4 route add "$net/$mask" via "$gw" ${mtr:+metric "$mtr"} table $ROUTING_TABLE
i=$(( i + 1 ))
done
if [ -n "$route_vpn_gateway" ]; then
/sbin/ip -4 route add default via "$route_vpn_gateway" table $ROUTING_TABLE
fi
if [ -n "$IPV6" ]; then
i=1
while
# There doesn't seem to be $route_ipv6_metric_<n>
# according to the manpage.
eval net=\"\$route_ipv6_network_$i\"
eval gw=\"\$route_ipv6_gateway_$i\"
[ -n "$net" ]
do
/sbin/ip -6 route add "$net" via "$gw" metric 100 table $ROUTING_TABLE
i=$(( i + 1 ))
done
# There's no $route_vpn_gateway for IPv6. It's not
# documented if OpenVPN includes default route in
# $route_ipv6_*. Set default route to remote VPN
# endpoint address if there is one. Use higher metric
# than $route_ipv6_* routes to give preference to a
# possible default route in them.
if [ -n "$ifconfig_ipv6_remote" ]; then
/sbin/ip -6 route add default \
via "$ifconfig_ipv6_remote" metric 200 table $ROUTING_TABLE
fi
fi
ip rule add iif vm-down lookup $ROUTING_TABLE
;;
down)
ip rule del iif vm-down lookup $ROUTING_TABLE
echo > /etc/resolv.conf.vpn
;;
esac
echo "MODE END: $script_type"
chmod +x /etc/openvpn/protonvpn/ch-test
ln -s /etc/openvpn/protonvpn/ch/ch.ovpn /etc/openvpn/protonvpn-ch.conf
vim /etc/systemd/system/openvpn\@protonvpn-ch.service.d/override.conf
[Unit]
BindsTo = netns@protonvpn-ch.service
JoinsNamespaceOf = netns@protonvpn-ch.service
After = netns-ctl@protonvpn-ch.service
[Service]
PrivateNetwork = true
BindPaths=/etc/netns/protonvpn-ch/resolv.conf:/etc/resolv.conf
BindPaths=/etc/netns/protonvpn-ch/resolv.conf.vpn:/etc/resolv.conf.vpn
TODO: Upstream "foreign" mode for netns-ctl
systemctl enable --now netns@main netns-ctl@main
systemctl enable --now netns@protonvpn-ch netns-ctl@protonvpn-ch
systemctl enable --now netns@vm-down netns-ctl@vm-down
vim /etc/systemd/system/dnsmasq-netns\@.service
[Unit]
Description=dnsmasq - A lightweight DHCP and caching DNS server
Requires=network.target
Wants=netns-ctl@%i.service
After=network.target netns-ctl@%i.service
BindsTo = netns-ctl@%i.service
JoinsNamespaceOf = netns@%i.service
[Service]
Type=forking
PIDFile=/run/dnsmasq/dnsmasq-netns-%i.pid
# Test the config file and refuse starting if it is not valid.
ExecStartPre=/usr/sbin/dnsmasq --conf-dir /etc/dnsmasq/netns/%i/,*.conf --resolv=/etc/netns/%i/resolv.conf.vpn --test
# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a
# wrapper picking up extra configuration files and then execs dnsmasq
# itself, when called with the "systemd-exec" function.
ExecStart=/usr/sbin/dnsmasq --conf-dir /etc/dnsmasq/netns/%i/,*.conf --resolv=/etc/netns/%i/resolv.conf.vpn -x /run/dnsmasq/dnsmasq-netns-%i.pid
# The systemd-*-resolvconf functions configure (and deconfigure)
# resolvconf to work with the dnsmasq DNS server. They're called like
# this to get correct error handling (ie don't start-resolvconf if the
# dnsmasq daemon fails to start.
#ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf
#ExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf
ExecReload=/bin/kill -HUP $MAINPID
PrivateNetwork = true
BindPaths=/etc/netns/%i/resolv.conf:/etc/resolv.conf
BindPaths=/etc/netns/%i/resolv.conf.vpn:/etc/resolv.conf.vpn
[Install]
WantedBy=multi-user.target
mkdir /etc/dnsmasq/netns/{protonvpn-ch,vm-down}
vim /etc/dnsmasq/netns/protonvpn-ch/dnsmasq.conf
interface=main
interface=vm-down
interface=lo
no-dhcp-interface=main
no-dhcp-interface=tun0
no-dhcp-interface=lo
no-hosts
vim /etc/dnsmasq/netns/vm-down/dnsmasq.conf
interface=eth1-down
interface=lo
no-dhcp-interface=pvpn-ch
no-hosts
vim /etc/hosts
#PVPN-CH START
127.0.0.1 ch.protonvpn.com ch-12.protonvpn.com
127.0.0.1 ch.protonvpn.com node-ch-03.protonvpn.net
127.0.0.1 ch.protonvpn.com node-ch-02.protonvpn.net
#PVPN-CH STOP
$ vim /root/newpvpnch.sh
#!/bin/bash
SERVERS="`curl https://account.protonvpn.com/api/vpn/logicals | jq -r '[.LogicalServers[] | select( .ExitCountry == "CH" and .Tier >= 2 and .Load < 40 and .Features == 4 and .Status == 1) | { "entryIP": .Servers[0].EntryIP, "exitIP": .Servers[0].ExitIP, "score": .Score, "load": .Load, obj: ., "domain": .Servers[0].Domain }] | sort_by(.score)[:3] | .[] | "\(.entryIP) ch.protonvpn.com \(.domain)"' | sort -R ` "
echo -e "$SERVERS"
perl -e '$/=undef; my $string = <STDIN>; $string =~ s/#PVPN-CH START.+#PVPN-CH STOP/#PVPN-CH START\n'"$SERVERS"'\n#PVPN-CH STOP/igs; print $string;' < /etc/hosts > /etc/hosts.tmp
cat /etc/hosts.tmp
cp /etc/hosts{,.bak}
mv /etc/hosts.tmp /etc/hosts
systemctl restart openvpn@protonvpn-ch
journalctl --follow -u openvpn@protonvpn-ch
$ chmod +x /root/newpvpnch.sh
systemctl enable --now dnsmasq-netns@vm-down.service
systemctl enable --now dnsmasq-netns@protonvpn-ch.service
TODO: persist firewall masquerade for outgoing traffic for the vpn connection
bash
iptables -t nat -I POSTROUTING -s 10.33.0.2 -o br0 -j MASQUERADE
TODO: create firewall rules on all NS to limit traffic between main and protonvpn-ch/vm-down
/root/new/pvpnch.sh
Wait until VPN is connected(if it does not connect, debug) and then this should work:
bash
ip netns exec vm-down ping google.de
ip netns exec vm-down curl ipinfo.io