├── LICENSE ├── README.md ├── firefox.sh ├── shell.sh ├── updown.py └── vpn.conf /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Philip Huppert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenVPN Client with a Network Namespace 2 | This repository contains configuration and script files to use the OpenVPN 3 | client with Linux network namespaces. 4 | 5 | Upon connecting with OpenVPN, the VPN's virtual NIC is moved to a dedicated 6 | network namespace named `ns1`. This configuration has the following useful 7 | properties: 8 | * Applications inside `ns1` can only communicate through the VPN's NIC. 9 | * Applications outside `ns1` can not access the VPN's NIC. 10 | * If the VPN connection terminates, `ns1` will loose network access. 11 | 12 | ### Usage 13 | Adjust `vpn.conf` to fit your OpenVPN setup. Then start the OpenVPN client, 14 | i.e. `sudo openvpn --config vpn.conf`. Finally, use `shell.sh` to obtain a 15 | terminal in the network namespace and run some applications. 16 | 17 | ### Contents 18 | * `vpn.conf` is a skeleton OpenVPN config. It only contains the configuration 19 | options to make use of network namespaces. You have to add your own OpenVPN 20 | configuration. 21 | * `updown.py` is the script called by OpenVPN to setup and teardown the 22 | virtual NIC. It sets up the network namespace, virtual NIC, DNS and 23 | iptables. 24 | * `shell.sh` is a utility script to spawn a terminal inside the network 25 | namespace. 26 | * `firefox.sh` is a utility script to spawn Firefox inside the network 27 | namespace. 28 | 29 | ### TODOs 30 | These are some bugs or possible improvements that are not yet addressed: 31 | * Make the name of the network namespace configurable or dynamic. 32 | * Investigate why applications running inside the namespace permanently loose 33 | network access on OpenVPN restarts/reconnects. 34 | * Clean up and document the code. 35 | * Add support for IPv6. 36 | * Add support for routes other than the default gateway. 37 | 38 | -------------------------------------------------------------------------------- /firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | USR=$(whoami) 3 | sudo ip netns exec ns1 sudo -u $USR bash -c 'firefox &> /dev/null &' 4 | -------------------------------------------------------------------------------- /shell.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | USR=$(whoami) 3 | sudo ip netns exec ns1 sudo -iu $USR 4 | -------------------------------------------------------------------------------- /updown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import shlex 6 | import subprocess 7 | 8 | 9 | NAMESPACE = "ns1" 10 | DEBUG_ENV = False 11 | DEBUG_CALLS = False 12 | 13 | 14 | if DEBUG_ENV: 15 | # print all environment variables and exit 16 | # useful for debugging what configuration data is made available by OpenVPN 17 | for key in os.environ.keys(): 18 | sys.stderr.write("%30s %s \n" % (key, os.environ[key])) 19 | sys.exit(1) 20 | 21 | 22 | def call(cmd, args): 23 | """ 24 | Run the command specified in cmd. Arguments consisting of a single ? are 25 | replaced with the corresponding value from args. This function returns if 26 | the command was executed successfully. If the command caused an error, an 27 | exception is thrown. 28 | """ 29 | 30 | # split command into its arguments 31 | assert cmd is not None 32 | cmd = shlex.split(cmd) 33 | 34 | # replace placeholders with contents from args 35 | j = 0 36 | for i in range(len(cmd)): 37 | if cmd[i] == "?": 38 | cmd[i] = str(args[j]) 39 | j += 1 40 | 41 | # run command 42 | if DEBUG_CALLS: 43 | sys.stderr.write("executing: " + " ".join(cmd) + "\n") 44 | subprocess.check_call(cmd) 45 | 46 | 47 | def nsexec(cmd, args=()): 48 | """ 49 | Run a command in a network namespace. 50 | """ 51 | 52 | cmd = "ip netns exec ? " + cmd 53 | args = (NAMESPACE,) + args 54 | call(cmd, args) 55 | 56 | 57 | def mask_to_cidr(mask): 58 | """ 59 | Determine the CIDR suffix for a given dotted decimal IPv4 netmask. 60 | """ 61 | # convert netmask to 32 binary digits 62 | tmp = "".join([format(int(x), "08b") for x in mask.split(".")]) 63 | # count leading ones 64 | return len(tmp) - len(tmp.lstrip("1")) 65 | 66 | 67 | # VPN connection setUP or tearDOWN 68 | script_type = os.getenv("script_type") 69 | assert script_type in ("up", "down") 70 | 71 | # NIC properties 72 | device = os.getenv("dev") 73 | tun_mtu = int(os.getenv("tun_mtu")) 74 | 75 | # IPv4 configuration 76 | v4_addr = os.getenv("ifconfig_local") 77 | v4_mask = os.getenv("ifconfig_netmask") 78 | v4_gateway = os.getenv("route_vpn_gateway") 79 | 80 | # IPv6 configuration 81 | v6_addr = os.getenv("ifconfig_ipv6_local") 82 | v6_netbits = os.getenv("ifconfig_ipv6_netbits") 83 | v6_gateway = os.getenv("ifconfig_ipv6_remote") 84 | v6_available = v6_addr is not None \ 85 | and v6_netbits is not None \ 86 | and v6_gateway is not None 87 | 88 | # DNS configuration 89 | dns_servers = [] 90 | i = 1 91 | while True: 92 | o = os.getenv("foreign_option_%d" % i) 93 | i += 1 94 | 95 | if o is None: 96 | break 97 | 98 | elif o.startswith("dhcp-option DNS"): 99 | dns_ip = o.partition(" DNS ")[2] 100 | dns_servers.append(dns_ip) 101 | 102 | # setup 103 | if script_type == "up": 104 | # create namespace 105 | try: 106 | call("ip netns add ?", (NAMESPACE,)) 107 | except: 108 | # ignore if namespace exists 109 | pass 110 | 111 | # flush routing table 112 | nsexec("ip route flush table all") 113 | 114 | # configure firewall in namespace 115 | nsexec("iptables -F") 116 | nsexec("iptables -I INPUT -j DROP") 117 | nsexec("iptables -I FORWARD -j DROP") 118 | nsexec("iptables -I INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT") 119 | nsexec("iptables -I INPUT -i lo -j ACCEPT") 120 | nsexec("iptables -I OUTPUT -j DROP") 121 | nsexec("iptables -I OUTPUT -o lo -j ACCEPT") 122 | nsexec("iptables -I OUTPUT -o ? -j ACCEPT", (device,)) 123 | nsexec("iptables -I OUTPUT -d 10.0.0.0/8 -j DROP") 124 | nsexec("iptables -I OUTPUT -d 172.16.0.0/12 -j DROP") 125 | nsexec("iptables -I OUTPUT -d 192.168.0.0/16 -j DROP") 126 | nsexec("ip6tables -F") 127 | nsexec("ip6tables -I INPUT -j DROP") 128 | nsexec("ip6tables -I INPUT -i lo -j ACCEPT") 129 | nsexec("ip6tables -I FORWARD -j DROP") 130 | nsexec("ip6tables -I OUTPUT -j DROP") 131 | nsexec("ip6tables -I OUTPUT -o lo -j ACCEPT") 132 | if v6_available: 133 | nsexec("ip6tables -I INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT") 134 | nsexec("ip6tables -I OUTPUT -o ? -j ACCEPT", (device,)) 135 | 136 | # create directory for resolv.conf 137 | namespace_dir = os.path.join("/etc/netns", NAMESPACE) 138 | if not os.path.exists(namespace_dir): 139 | os.makedirs(namespace_dir) 140 | 141 | # create resolv.conf 142 | dns_config = os.path.join(namespace_dir, "resolv.conf") 143 | with open(dns_config, "w") as fh: 144 | for dns_server in dns_servers: 145 | fh.write("nameserver " + dns_server + "\n") 146 | 147 | # create nsswitch.conf 148 | real_nss_config = "/etc/nsswitch.conf" 149 | nss_config = os.path.join(namespace_dir, "nsswitch.conf") 150 | with open(real_nss_config, "r") as ih, open(nss_config, "w") as oh: 151 | for line in ih.readlines(): 152 | if not line.startswith("hosts: "): 153 | oh.write(line) 154 | else: 155 | # prevent DNS resolution via systemd 156 | oh.write("hosts: files mymachines myhostname dns\n") 157 | 158 | # move device to namespace 159 | call("ip link set ? netns ?", (device, NAMESPACE)) 160 | 161 | # start/restart loopback device in namespace 162 | nsexec("ip link set lo down") 163 | nsexec("ip link set lo up") 164 | 165 | # enable device 166 | nsexec("ip link set ? up", (device,)) 167 | 168 | # set mtu 169 | nsexec("ip link set dev ? mtu ?", (device, tun_mtu)) 170 | 171 | # configure v4 172 | address = "%s/%d" % (v4_addr, mask_to_cidr(v4_mask)) 173 | nsexec("ip addr change ? dev ?", (address, device)) 174 | nsexec("ip route add default via ?", (v4_gateway,)) 175 | 176 | # configure v6, if any 177 | if v6_available: 178 | address = "%s/%s" % (v6_addr, v6_netbits) 179 | nsexec("ip -6 addr change ? dev ?", (address, device)) 180 | nsexec("ip -6 route add default via ?", (v6_gateway,)) 181 | 182 | 183 | # teardown 184 | if script_type == "down": 185 | # OpenVPN already removed its NIC 186 | # the namespace is not deleted, because some applications may still use it 187 | 188 | # unconfigure DNS 189 | namespace_dir = os.path.join("/etc/netns", NAMESPACE) 190 | dns_config = os.path.join(namespace_dir, "resolv.conf") 191 | os.remove(dns_config) 192 | nss_config = os.path.join(namespace_dir, "nsswitch.conf") 193 | os.remove(nss_config) 194 | os.rmdir(namespace_dir) 195 | -------------------------------------------------------------------------------- /vpn.conf: -------------------------------------------------------------------------------- 1 | client 2 | 3 | #FIXME insert your OpenVPN config here 4 | 5 | # don't configure NICs 6 | ifconfig-noexec 7 | # don't add routes 8 | route-noexec 9 | # use custom NIC config scripts 10 | script-security 2 11 | up updown.py 12 | down updown.py 13 | --------------------------------------------------------------------------------