├── COPYING ├── README.md ├── TODO ├── example-hooks ├── post.d │ ├── limit.sh │ └── unvpn.sh └── pre.d │ ├── limit.sh │ └── unvpn.sh └── vpnify /COPYING: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2019 Olga Ustiuzhanina 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vpnify 2 | == 3 | 4 | This tool can be used to transparently route traffic of certain programs through VPN, while keeping the rest of it routed normally. It is protocol-agnostic and can work with any VPN software. 5 | 6 | For example: 7 | 8 | vpnify sudo openvpn --config vpn.conf 9 | 10 | Creates an isolated VPN connection. To make a program use this connection, you can use 11 | 12 | vpnify 13 | 14 | That's all. No configuration needed. It creates network namespace and configures it on the first run and deletes it once the last process using it exits. 15 | 16 | Installation 17 | -- 18 | 19 | Just copy to /usr/local/bin/ 20 | 21 | sudo cp $HOME/vpnify/vpnify /usr/local/bin/vpnify 22 | 23 | Multiple VPN's 24 | -- 25 | To create two or more distinct VPN connections, you just need to create a new symlink. 26 | 27 | ln -s /usr/local/bin/vpnify /usr/local/bin/vpnify2 28 | 29 | Now you can do this: 30 | 31 | vpnify sudo openvpn --config vpn.conf 32 | vpnify2 sudo openvpn --config vpn2.conf 33 | 34 | Programs run with vpnify2 will use different connection from programs run with vpnify. 35 | 36 | Custom resolv.conf and hosts 37 | -- 38 | 39 | You can put your custom hosts and resolv.conf file to /etc/vpnify/ (or /etc/vpnify/\ for a symlinked version). 40 | 41 | Also you can create folders named "pre.d" and "post.d" in the same folder with custom hooks that will be executed before running the supplied command inside the namespace and after the cleanup respectively. 42 | 43 | Advanced features: Limiting clearnet access 44 | -- 45 | You can use hooks to limit clearnet access by the applications run inside vpnify. First let's create a folder /etc/vpnify/pre.d/: 46 | 47 | mkdir -p /etc/vpnify/pre.d/ 48 | 49 | Or, if you want to setup a symlinked version, 50 | 51 | mkdir -p /etc/vpnify//pre.d/ 52 | 53 | Now we need to create a hook that will execute firewall commands: 54 | 55 | vim /etc/vpnify/pre.d/limit.sh 56 | 57 | Contents of this file can be something like: 58 | 59 | iptables -I FORWARD -i $VETH0 -j DROP # Drop all outgoing traffic 60 | iptables -I FORWARD -i $VETH0 -d 198.51.100.157 -p udp --destination-port 1024 -j ACCEPT # Allow ONLY packets going to your VPN server 61 | 62 | Where 198.51.100.157 is IP address of your VPN server. Replace udp/1024 with transport protocol your VPN uses it's port. 63 | This forbids all outgoing traffic from inside vpnify except for traffic going to 198.51.100.157 udp:1024. 64 | 65 | Don't forget to make the hook executable! 66 | 67 | chmod +x /etc/vpnify/pre.d/limit.sh 68 | 69 | If your VPN configuration uses a hostname you need to add this hostname to /etc/vpnify/hosts (which will be /etc/hosts inside the namespace). 70 | Otherwise it will fail to resolve, since all traffic outside is blocked. 71 | 72 | Take a look at files in example-hooks/\*.d/limit.sh for a better explanation and a clean-up hook! 73 | 74 | unVpnify 75 | --- 76 | You can use this script to route all the traffic on your machine through a VPN *except* for applications running inside (un)vpnify! 77 | 78 | To do this, let's create a symlink: 79 | 80 | ln -s /usr/local/bin/vpnify /usr/local/bin/unvpn 81 | 82 | Then, we create the configuration folders 83 | 84 | mkdir -p /etc/vpnify/unvpnify/pre.d/ 85 | mkdir -p /etc/vpnify/unvpnify/post.d/ 86 | 87 | And now, create a hook that does some routing magic. Look [here](https://www.thomas-krenn.com/en/wiki/Two_Default_Gateways_on_One_System) for a deeper explanation of routing commands used in this hook. 88 | 89 | vim /etc/vpnify/unvpnify/pre.d/unvpn.sh 90 | 91 | ip rule add iif $VETH0 table rt2 # Route all traffic from our namespace through a second routing table 92 | ip route add default via 192.168.1.1 table rt2 # Set up the default gateway on our second table 93 | 94 | chmod +x /etc/vpnify/unvpnify/unvpn.sh 95 | 96 | Also we need to add the 'rt2' routing table to our system: 97 | 98 | echo '1 rt2' >> /etc/iproute2/rt_tables 99 | 100 | Check out example-hooks/\*.d/unvpn.sh for more information and a clean-up hook. 101 | 102 | bashrc 103 | --- 104 | 105 | You might want your bash prompt to change when you are using vpnify, so you can easily tell apart which consoles are runing through vpnify and which are not. Luckily it is very easy to do. Just put something like this in your bashrc: 106 | 107 | netns() { 108 | [[ -z "$NETNS" ]] || echo "[$NETNS]" 109 | } 110 | 111 | PS1="$(netns)$PS1" 112 | 113 | sudoers 114 | --- 115 | 116 | If you want to use this script without having to type your password every time, you can add this line to your /etc/sudoers 117 | 118 | %wheel ALL=(ALL) NOPASSWD:SETENV: /usr/local/bin/vpnify 119 | 120 | **Warning: this might not be secure, use with caution.** 121 | 122 | Compatibility 123 | -- 124 | 125 | This script should work on any modern linux that supports network and mount namespaces and has nsenter command available. I have tested it on Void Linux, Ubuntu 16.04, CentOS 6.5 and 7. 126 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 1. Check for availability of binaries 2 | 2. Use a workaround if nsenter is absent 3 | 3. Figure out how to deal with background tasks 4 | 4. Use a better way to get default gateway, iptables rules etc 5 | -------------------------------------------------------------------------------- /example-hooks/post.d/limit.sh: -------------------------------------------------------------------------------- 1 | # Clean up our rules 2 | iptables -D FORWARD -i "$VETH0" -j DROP 3 | iptables -D FORWARD -i "$VETH0" -d 198.51.100.157 -p udp --destination-port 1024 -j ACCEPT 4 | -------------------------------------------------------------------------------- /example-hooks/post.d/unvpn.sh: -------------------------------------------------------------------------------- 1 | # Try to find a second routing table 2 | TABLE="$(egrep '^1\s+\w+' /etc/iproute2/rt_tables | sed -E 's/1\s+//g')" 3 | 4 | # Drop the routing rule 5 | ip rule del iif "$VETH0" table rt2 6 | 7 | # Flush routes 8 | ip route flush table rt2 9 | -------------------------------------------------------------------------------- /example-hooks/pre.d/limit.sh: -------------------------------------------------------------------------------- 1 | # Drop all outgoing traffic from our namespace 2 | iptables -I FORWARD -i "$VETH0" -j DROP 3 | # Expect for traffic going to your VPN server 4 | # Replace 198.51.100.157 with the correct ip 5 | # And tcp/1024 with your VPN transport protocol and port 6 | iptables -I FORWARD -i "$VETH0" -d 198.51.100.157 -p udp --destination-port 1024 -j ACCEPT 7 | -------------------------------------------------------------------------------- /example-hooks/pre.d/unvpn.sh: -------------------------------------------------------------------------------- 1 | # Try to find a second routing table 2 | TABLE="$(egrep '^1\s+\w+' /etc/iproute2/rt_tables | sed -E 's/1\s+//g')" 3 | 4 | # Create it if it doesn't exist 5 | if [[ -z "$TABLE" ]]; then 6 | TABLE=rt2 7 | echo '1 rt2' >> /etc/iproute2/rt_tables 8 | fi 9 | 10 | # Use it for traffic coming out of our namespace 11 | ip rule add iif "$VETH0" table rt2 12 | 13 | # Replace this with appropriate default gateway 14 | ip route add default via 192.168.1.1 table rt2 15 | -------------------------------------------------------------------------------- /vpnify: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2019 Olga Ustiuzhanina 5 | # 6 | # This program is free software. It comes without any warranty, to 7 | # the extent permitted by applicable law. You can redistribute it 8 | # and/or modify it under the terms of the Do What The Fuck You Want 9 | # To Public License, Version 2, as published by Sam Hocevar. See 10 | # the COPYING file for more details. 11 | # 12 | 13 | # Escalate priveleges 14 | if [ "$(id -u)" != 0 ]; then 15 | exec sudo -E -- "$0" "$@" 16 | fi 17 | 18 | # Prevent nested execution 19 | if [ -n "$NETNS" ]; then 20 | exit 1 21 | fi 22 | 23 | # Setup main variables: 24 | # Name of network namespace 25 | NETNS=$(basename "$0") 26 | export NETNS 27 | 28 | # Use 10.118.0.0/16 subnet 29 | MNUM=118 30 | export NNUM 31 | 32 | # Customization path 33 | if [ "$NETNS" = "vpnify" ]; then 34 | CUSTOM_PATH="/etc/vpnify" 35 | else 36 | CUSTOM_PATH="/etc/vpnify/$NETNS" 37 | fi 38 | 39 | export CUSTOM_PATH 40 | 41 | # Names of veth interfaces 42 | VETH0="${NETNS}_veth0" 43 | VETH1="${NETNS}_veth1" 44 | 45 | export VETH0 46 | export VETH1 47 | 48 | # Cleanup function 49 | clean() { 50 | # Find all pids using this NS 51 | PIDS=$(find -L /proc/[1-9]*/task/*/ns/net -samefile /run/netns/"$NETNS" 2> /dev/null) 52 | 53 | if [ -z "$PIDS" ]; then 54 | # Remove NS and drop iptables rule 55 | ip netns del "$NETNS" 56 | iptables -t nat -D POSTROUTING -s "$SUBNET.0/24" \ 57 | -m comment --comment "for $NETNS" -j MASQUERADE 2> /dev/null 58 | 59 | iptables -D FORWARD -s "$SUBNET.0/24" -j ACCEPT -m comment --comment "for $NETNS" 60 | 61 | # If mount namespace was created and is unsued now - remove it 62 | if [ -e "/run/mntns/$NETNS" ]; then 63 | # Remove mount namespace if it was created 64 | umount "/run/mntns/$NETNS" 65 | rm "/run/mntns/$NETNS" 66 | fi 67 | fi 68 | 69 | # Execute custom user (post) hooks 70 | for hook in "$CUSTOM_PATH"/post.d/*; do 71 | [ -x "$hook" ] && "$hook" 72 | done 73 | } 74 | 75 | # Run cleanup on exit 76 | trap 'clean;' 0 77 | 78 | # Check if this namespace exists already 79 | EXISTING=$(ip netns show | cut -d' ' -f1 | grep "^$NETNS\$"; true) 80 | 81 | # Network NS argument for nsenter 82 | NETWORK_NS="-n/run/netns/$NETNS" 83 | 84 | # Create mount namespace for /etc/resolv.conf and /etc/hosts if needed 85 | if [ -e "$CUSTOM_PATH/resolv.conf" ] || [ -e "$CUSTOM_PATH/hosts" ] ; then 86 | MOUNT_NS="--mount=/run/mntns/$NETNS" 87 | 88 | if [ ! -e "/run/mntns/$NETNS" ]; then 89 | mkdir -p /run/mntns 90 | fi 91 | 92 | if ! mountpoint -q /run/mntns; then 93 | mount --bind /run/mntns /run/mntns 94 | # Fix for systemd based systems 95 | mount --make-rprivate /run/mntns 96 | fi 97 | 98 | if [ ! -e "/run/mntns/$NETNS" ]; then 99 | touch /run/mntns/"$NETNS" 100 | 101 | # This doesn't work with older unshare 102 | # unshare $MOUNT_NS echo -n 103 | 104 | # Ugly workaround for old unshare versions 105 | unshare -m sleep 0.1 & 106 | 107 | # Avoids a race condition 108 | sleep 0.01 109 | 110 | # Bind mount a namespace to make it permanent 111 | mount --bind /proc/$!/task/$!/ns/mnt "/run/mntns/$NETNS" 112 | mount --make-shared "/run/mntns/$NETNS" 113 | 114 | # Kill backround process 115 | kill $! 116 | wait $! 2>/dev/null 117 | 118 | [ -e "$CUSTOM_PATH/hosts" ] && nsenter "$MOUNT_NS" mount --bind \ 119 | "$CUSTOM_PATH/hosts" /etc/hosts 120 | [ -e "$CUSTOM_PATH/resolv.conf" ] && nsenter "$MOUNT_NS" mount \ 121 | --bind "$CUSTOM_PATH/resolv.conf" /etc/resolv.conf 122 | fi 123 | fi 124 | 125 | if [ -n "$EXISTING" ]; then 126 | # Get the existing subnet number 127 | NNUM=$(iptables-save | grep "for $NETNS" | head -n1 | cut -d' ' -f4 | cut -d'.' -f3) 128 | export NNUM 129 | 130 | SUBNET="10.$MNUM.$NNUM" 131 | export SUBNET 132 | else 133 | # Find an unused subnet starting from 10.*.0.0/24 to 10.*.255.0/24 134 | # Where * is $MNUM 135 | NNUM=0 136 | while [ $NNUM -le 255 ]; do 137 | COUNT=$(iptables-save | grep -F -c "10.$MNUM.$NNUM.0" ; true) 138 | 139 | if [ "$COUNT" = 0 ]; then 140 | break 141 | fi 142 | 143 | NNUM=$((NNUM + 1)) 144 | done 145 | 146 | # No free subnets - probably something's wrong with iptables 147 | [ $NNUM = 256 ] && exit 1 148 | 149 | export NNUM 150 | 151 | SUBNET="10.$MNUM.$NNUM" 152 | export SUBNET 153 | 154 | # Create network namespace 155 | ip netns add "$NETNS" 156 | 157 | # Not using 'ip -n' because it's not supported 158 | # by old versions of iproute2 159 | ip netns exec "$NETNS" ip link set dev lo up 160 | 161 | # Create veth pair 162 | ip link add "$VETH0" type veth peer name "$VETH1" 163 | ip link set "$VETH1" netns "$NETNS" 164 | 165 | # Setup the pair 166 | ip netns exec "$NETNS" ip addr add "$SUBNET.1/24" dev "$VETH1" 167 | ip addr add "$SUBNET.2/24" dev "$VETH0" 168 | ip netns exec "$NETNS" ip link set "$VETH1" up 169 | ip link set "$VETH0" up 170 | 171 | # Add default route inside NS 172 | ip netns exec "$NETNS" ip route add default via "$SUBNET.2" dev "$VETH1" 173 | 174 | # Enable routing 175 | echo 1 > /proc/sys/net/ipv4/ip_forward 176 | 177 | # Allow forwarding of packages form our subnet 178 | iptables -A FORWARD -s "$SUBNET.0/24" -j ACCEPT -m comment --comment "for $NETNS" 179 | 180 | # Create nat rule 181 | iptables -t nat -A POSTROUTING -s "$SUBNET.0/24" \ 182 | -m comment --comment "for $NETNS" -j MASQUERADE 183 | 184 | # Execute custom user hooks 185 | for hook in "$CUSTOM_PATH"/pre.d/*; do 186 | [ -x "$hook" ] && "$hook" 187 | done 188 | fi 189 | 190 | # Workaround pwd bug 191 | if [ "$*" = "" ]; then 192 | COMMAND="cd $PWD; exec $SHELL" 193 | else 194 | COMMAND="cd $PWD; exec " 195 | 196 | # Passing arguments containing a whitespace character 197 | # doesn't work without this workaround 198 | for arg in "$@"; do 199 | COMMAND="$COMMAND \"$arg\"" 200 | done 201 | fi 202 | 203 | # Clear sudo credential cache 204 | sudo -k 205 | 206 | NSENTER_CMD='nsenter' 207 | 208 | # Add mount namespace for custom resolv.conf and hosts 209 | # Thanks to @ischoonover for noticing this bug and providing a solution 210 | if [ "$MOUNT_NS" ] ; then 211 | NSENTER_CMD="$NSENTER_CMD $MOUNT_NS" 212 | fi 213 | 214 | # Use sudo to drop priveleges inside the namespace 215 | $NSENTER_CMD "$NETWORK_NS" sudo -Eu "$SUDO_USER" -- "$SHELL" -c "$COMMAND" 216 | --------------------------------------------------------------------------------