2015-08-11

Force only rTorrent over OpenVPN interface

In this tutorial we will force only rTorrent to use OpenVPN, and prevent network access if the VPN goes down, while the rest of the system can access internet normally.

This will be achieved by restricting network access for the user running rTorrent to the VPN interface only. OpenVPN will be configured not to add default routing and instead we will only route the VPN traffic needed. rTorrent will be bound to the VPN interface. We will also add tools to restart OpenVPN if down and automatic update rTorrents bind settings if VPN IP changes.
There are probably better ways to achieve this but this works for me. Note that all script are made for my setup and will probably need to be modified to work for your setup. Maybe you will find some useful information here.

Tested on Ubuntu Server 14.04 connected to a local network.

Pre-Requirements:
* Working OpenVPN interface (tap0 in this tutorial)
* Dedicated user running rTorrent (named torrent in this tutorial)



First we are going to lock down network access for the torrent user to tap0, lannet and local. This script should be run at startup or when your network interface is up. I've chosen to place it in /etc/network/if-up.d/vpn-lockdown.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/bin/bash
## vpn-lockdown - v1.0
## Function: Limit a user's network access to VPN and LAN only

## Installation:
## Place this script in /etc/network/if-up.d/vpn-lockdown

#Settings
export LANIF="eth0"
export LANNET="192.168.0.0/24"
export VPNIF="tap0"
export VPNUSER="torrent"
export TORRENTPORT="57225"
export ENABLEDHT="1"
#/Settings

# only run when triggered from $LANIF (use -f parameter to force run)
if [[ "$IFACE" != "$LANIF" ]]; then
  if [[ "$1" != "-f" ]]; then
    exit 0
  fi
fi

# only run from ifup
if [[ "$MODE" != start ]]; then
  if [[ "$1" != "-f" ]]; then
    exit 0
  fi
fi

sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.conf.all.rp_filter=0
sysctl -w net.ipv4.conf.default.rp_filter=0
sysctl -w net.ipv4.conf.lo.rp_filter=0
sysctl -w net.ipv4.conf.eth0.rp_filter=0

# clear iptables
iptables -F -t nat
iptables -X -t nat
iptables -F -t mangle
iptables -X -t mangle
iptables -F -t filter
iptables -X -t filter
iptables -F -t raw
iptables -X -t raw

# allow responses
iptables -A INPUT -i $VPNIF -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# allow incoming $TORRENTPORT
iptables -A INPUT -i $VPNIF -p tcp --dport $TORRENTPORT -j ACCEPT
iptables -A INPUT -i $VPNIF -p udp --dport $TORRENTPORT -j ACCEPT

# allow incoming DHT
if [[ "$ENABLEDHT" = "1" ]]; then
  iptables -A INPUT -i $VPNIF -p udp --dport 6881 -j ACCEPT
  iptables -t raw -I PREROUTING -i $VPNIF -p udp --dport 6881 -j NOTRACK
  iptables -t raw -I OUTPUT -o $VPNIF -p udp --sport 6881 -j NOTRACK
fi

# block everything else incoming on $VPNIF
iptables -A INPUT -i $VPNIF -j DROP

# all packets on $VPNIF needs to be masqueraded
iptables -t nat -A POSTROUTING -o $VPNIF -j MASQUERADE

# allow $VPNUSER outgoing on lo, to $LANNET and on $VPNIF
iptables -A OUTPUT -o lo -m owner --uid-owner $VPNUSER -j ACCEPT
iptables -A OUTPUT -d $LANNET -m owner --uid-owner $VPNUSER -j ACCEPT
iptables -A OUTPUT -o $VPNIF -m owner --uid-owner $VPNUSER -j ACCEPT

# reject everything else outgoing for $VPNUSER that is not going over $VPNIF
iptables -A OUTPUT ! -o $VPNIF -m owner --uid-owner $VPNUSER -j DROP

When this script i executed the torrent user should only be able to access your LAN and internet via VPN (not yet).
Verify this by trying to connect to the internet as the torrent user. For example executing curl -s "icanhazip.com" should time out.



Next step is to configure OpenVPN not to add the default routing since we don't want the whole system to use the VPN. We also need to supply a script that adds the necessary routing for when the VPN is used.

First add a new table ($IPTABLE) in /etc/iproute2/rt_tables (e.g. echo "200 torrent" >> /etc/iproute2/rt_tables)
Then add the following settings to your OpenVPN config:
route-noexec
route-up /etc/openvpn/vpn-route

Place this script in /etc/openvpn/vpn-route

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/bin/bash
## vpn-route - v1.0
## Function: Set routing for OpenVPN network

## Installation:
## Add a new table ($IPTABLE) in /etc/iproute2/rt_tables (e.g. echo "200 torrent" >> /etc/iproute2/rt_tables)
## Add settings to OpenVPN config:
## route-noexec
## route-up /path/to/this-script

#echo "$dev: $ifconfig_local mask: $ifconfig_netmask gw: $route_vpn_gateway"

#Settings
VPNIF=$dev
MASK=$ifconfig_netmask
VPNGW=$route_vpn_gateway
IPTABLE="torrent"
LANGW="192.168.0.1"
LANNET="192.168.0.0/24"
#/Settings

if [[ -z "$VPNIF" ]]; then
  echo "Error: No VPN interface"
  exit 1
fi

if [[ -z "$MASK" ]]; then
  echo "Error: No VPN mask"
  exit 1
fi

if [[ -z "$VPNGW" ]]; then
  echo "Error: No VPN gateway"
  exit 1
fi

if [[ `ip rule list | grep -c $VPNGW` == 0 ]]; then
  #Add ip rule for VPN traffic to use $IPTABLE table
  ip rule add from $VPNGW/$MASK table $IPTABLE
fi

#Set default route to $VPNGW in $IPTABLE table
ip route replace default via $VPNGW dev $VPNIF table $IPTABLE
#Add routing for lo
ip route append default via 127.0.0.1 dev lo table $IPTABLE
#Add routing for $LANNET via $LANGW
ip route add $LANNET via $LANGW table $IPTABLE

#Commit changes
ip route flush cache

Restarting OpenVPN will execute the script. The torrent user is now able to access to the internet via the tap0 interface, and your LAN via eth0, but not access internet via eth0. Verify this by running curl --interface "tap0" -s "icanhazip.com". Running curl -s "icanhazip.com" should not work as it uses eth0.

From here you can run rTorrent with the -b 1.2.3.4 parameter or bind = 1.2.3.4 setting in .rtorrent.rc, replacing 1.2.3.4 with the IP of tap0, and you are good to go!



Since there is a possibility that tap0 will change IP and we want rTorrent to rebind the new IP if this happens we will use the schedule setting in .rtorrent.rc.

First we need to add an entry in the hosts file that will resolve the tap0 IP (e.g. echo "10.0.0.1 vpniphost" >> /etc/hosts).
Add the script below to root crontab (*/5 * * * * /root/vpn-check-hostname). The script will update to host entry with the tap0 IP if it has changed

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash
## vpn-check-hostname - v1.0
## Function: Set hostname entry to VPN IP

## Installation:
## Add a new hostname ($HOST) in your hosts file ($HOSTFILE) (e.g. echo "10.0.0.1 vpniphost" >> /etc/hosts)
## Add this to script to root crontab (*/5 * * * * /path/to/this-script)
## Add bind settings to .rtorrent.rc:
## bind = vpniphost
## schedule = bind_tick,30,30,bind=vpniphost

#Settings
VPNIF="tap0"
HOSTFILE="/etc/hosts"
HOST="vpniphost"
#/Settings

# get VPN IP
VPNIP=$(ifconfig $VPNIF 2>/dev/null | awk '/inet addr/{print substr($2,6)}')

if [[ -z "$VPNIP" ]]; then
  if [[ "$1" = "-v" ]]; then
    echo "Cannot find IP for $VPNIF. Terminating."
  fi
  exit
fi

# get current hostname IP
FINDLINE=$(cat $HOSTFILE | egrep -n "\ $HOST$" | tail -n 1)
FINDLINENR=$(echo $FINDLINE | egrep -o "[0-9]+:" | egrep -o "[0-9]+")
FINDOLDIP=$(echo $FINDLINE | egrep -o "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}")
if [[ -z "$FINDLINENR" ]] || [[ -z "$FINDOLDIP" ]]; then
  if [[ "$1" = "-v" ]]; then
    echo "Couldn't find '$HOST' host in $HOSTFILE. Terminating."
  fi
  exit
fi

# check if IP has changed
if [[ "$VPNIP" = "$FINDOLDIP" ]]; then
  if [[ "$1" = "-v" ]]; then
    echo "Update not required"
  fi
else
  echo "Setting '$HOST' to $VPNIP (old: $FINDOLDIP)"
  sed -i.bak -e "$FINDLINENR s/$FINDOLDIP/$VPNIP/" $HOSTFILE
fi

Executing ping vpniphost should get a response from the tap0 IP.

We now tell rTorrent to bind to whatever interface 'vpniphost' is pointing to, and check every 30 seconds if it has changed. Add the following settings to .rtorrent.rc:
bind = vpniphost
schedule = bind_tick,30,30,bind=vpniphost

Thats it!



Finally I use these optional scripts to restart OpenVPN and rTorrent if they go down.

OpenVPN.
Add this script to root crontab (*/10 * * * * /root/vpn-check-interface).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash
## vpn-check-interface - v1.0
## Function: Make sure OpenVPN is running

## Installation:
## Add this script to root crontab (*/10 * * * * /path/to/this-script)

#Settings
VPNIF="tap0"
#/Settings

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

UPTIME=$(cat /proc/uptime | cut -d . -f 1)
UPTIMEM=$(expr $UPTIME / 60)

# wait a couple of minutes if recently booted
if [ $UPTIMEM -lt 5 ]; then
  if [ "$1" = "-v" ]; then
    echo "Uptime only $UPTIMEM m. Terminating."
  fi
  exit 0
fi

if [ -f /sys/class/net/$VPNIF/operstate ]; then
  if [ "$1" = "-v" ]; then
    echo "OpenVPN seems to be running. Terminating."
  fi
  exit 0
fi

echo "OpenVPN is down. Restarting service."
/etc/init.d/openvpn restart

rTorrent.
Add this script to torrent users crontab (*/10 * * * * /home/torrent/rtorrent-check).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/bin/bash
## rtorrent-check - v1.0
## Function: Make sure rTorrent is running

## Installation:
## Add this script to crontab (*/10 * * * * /path/to/this-script)

#Settings
#Home folder
LOC="/home/torrent"
#Conf file in home folder
CONFFILE=".rtorrent.rc"
#rTorrent working folder
WORKINGFOLDER="$LOC/download"
#Name the screen session
SCREENNAME="torrent"
#rTorrent incoming folder mount check (OPTIONAL)
#MOUNTCHECK="$WORKINGFOLDER/incoming"
#/Settings

if [[ ! -z "$MOUNTCHECK" ]]; then
  testmount=$(/bin/mount | grep "on $MOUNTCHECK")
  if [[ -z "$testmount" ]]; then
    if [[ "$1" = "-v" ]]; then
      echo "$MOUNTCHECK is not mounted. Terminating."
    fi
    exit
  fi
fi

if [[ ! -f $LOC/$CONFFILE ]]; then
  echo "Conf file not found. Terminating."
  exit
fi
if [[ ! -s $LOC/$CONFFILE ]]; then
  echo "Conf file empty. Terminating."
  exit
fi

TESTSCREEN=$(screen -ls | egrep "[0-9]+.$SCREENNAME")
if [[ -n "$TESTSCREEN" ]]; then
  if [[ "$1" = "-v" ]]; then
    echo "rTorrent is already running. Terminating."
  fi
  exit
fi

echo "Starting rTorrent"
cd "$WORKINGFOLDER"
sleep 1
stty stop undef 2>/dev/null
stty start undef 2>/dev/null
screen -A -m -d -S $SCREENNAME -t $SCREENNAME /usr/bin/rtorrent -o http_capath=/etc/ssl/certs

4 comments :

  1. Nice work :-D

    But for some reason vpn-check-hostname dont work in hostname.
    i just run the script it works. but in crontab it dont

    ReplyDelete
    Replies
    1. Maybe cron can't find all utils? Try adding full path to the utils. For example replace: "VPNIP=$(ifconfig $VPNIF ..." with "VPNIP=$(/path/to/ifconfig $VPNIF ..."

      Or you could try adding PATH to the script like this:
      PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Used this to make a continuis running script add it to rc.local (/home/ubuntu/vpn.sh > /dev/null 2>&1 &)

      http://pastebin.com/YF4gRyVA

      Delete