OpenWrt Forum Archive

Topic: firmware-agnostic script to bypass vpn isn't working [SOLVED]

The content of this topic has been archived on 31 Mar 2018. There are no obvious gaps in this topic, but there may still be some posts missing at the end.

I had said I'd tackle this after troubleshooting the vpnbypass package in my other thread, but I just can't resist and will try to do both things concurrently smile I'm really enjoying tinkering with this router and learning new things.

Anyway, here is the script I'd like to try (which I found here):

#!/bin/sh

## CUSTOMIZE YOUR SCRIPT VARIABLES
#
## Uncomment and set value(s) as needed to customize your rules
#
# IP addresses, contiguous range AND/OR individual.
#
ip_addrs_lst="192.168.10.60-192.168.10.69"

##Server ports to bypass VPN
server_ports="5500,22"

#
# Specific destination websites ip range - Spotify , Netflix...
#
#web_range_lst="72.44.32.1-72.44.63.254
#67.202.0.1-67.202.63.254
#207.223.0.1-207.223.15.254
#98.207.0.1-98.207.255.254
#208.85.40.1-208.85.47.254
#78.31.8.1-78.31.15.254
#193.182.8.1-193.182.15.254"

########################################
# NO NEED TO CHANGE BELOW THIS LINE #
########################################

# SHELL COMMANDS FOR MAINTENANCE.
# DO NOT UNCOMMENT, THESE ARE INTENDED TO BE USED IN A SHELL COMMAND LINE
#
#  List Contents by line number
# iptables -L PREROUTING -t mangle -n --line-numbers
#
#  Delete rules from mangle by line number
# iptables -D PREROUTING type-line-number-here -t mangle
#
#  To list the current rules on the router, issue the command:
#     iptables -t mangle -L PREROUTING
#
#  Flush/reset all the rules to default by issuing the command:
#     iptables -t mangle -F PREROUTING
sleep 1
#
# First it is necessary to disable Reverse Path Filtering on all
# current and future network interfaces:
#
for i in /proc/sys/net/ipv4/conf/*/rp_filter ; do
  echo 0 > $i
done

#
# Delete table 100 and flush any existing rules if they exist.
#
ip route flush table 100
ip route del default table 100
ip rule del fwmark 1 table 100
ip route flush cache
iptables -t mangle -F PREROUTING

#
# Let's find out the tunnel interface
#
iface_lst=`route | awk ' {print $8}'`
for tun_if in $iface_lst; do
    if [ $tun_if == "tun11" ] || [ $tun_if == "tun12" ] || [ $tun_if == "ppp0" ]; then
    break
  fi
done

#
# Copy all non-default and non-VPN related routes from the main table into table 100.
# Then configure table 100 to route all traffic out the WAN gateway and assign it mark "1"
#
ip route show table main | grep -Ev ^default | grep -Ev $tun_if \
  | while read ROUTE ; do
     ip route add table 100 $ROUTE
done
ip route add default table 100 via $(ifstatus wan | grep nexthop | egrep -o '[0-9.]+' | grep -v '0.0.0.0') # the output of the command substitution is the wan gateway
ip rule add fwmark 1 table 100
ip route flush cache

# EXAMPLES:
#
#  All LAN traffic will bypass the VPN (Useful to put this rule first,
#  so all traffic bypasses the VPN and you can configure exceptions afterwards)
#    iptables -t mangle -A PREROUTING -i br0 -j MARK --set-mark 1
#
#  Ports 80 and 443 will bypass the VPN
#    iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --dport 80,443 -j MARK --set-mark 1
#
#  All traffic from a particular computer on the LAN will use the VPN
#    iptables -t mangle -A PREROUTING -i br0 -m iprange --src-range 192.168.1.2 -j MARK --set-mark 0
#
#  All traffic to a specific Internet IP address will use the VPN
#    iptables -t mangle -A PREROUTING -i br0 -m iprange --dst-range 216.146.38.70 -j MARK --set-mark 0
#
#  All UDP and ICMP traffic will bypass the VPN
#    iptables -t mangle -A PREROUTING -i br0 -p udp -j MARK --set-mark 1
#    iptables -t mangle -A PREROUTING -i br0 -p icmp -j MARK --set-mark 1

# Default behavior: MARK = 1 all traffic bypasses VPN, MARK = 0 all traffic goes VPN
iptables -t mangle -A PREROUTING -i br0 -j MARK --set-mark 0

# IP_ADDRESSES - RANGE(S) AND/OR INDIVIDUAL(S)
for ip_addrs in $ip_addrs_lst ; do
  iptables -t mangle -A PREROUTING -i br0 -m iprange --src-range $ip_addrs -j MARK --set-mark 1
done

######   Ports that bypass VPN    ######
###### Normal portforwarding will ######
######    need to be applied      ######

iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --dport $server_ports -j MARK --set-mark 1
iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --sport $server_ports -j MARK --set-mark 1

# WEBSITES_IP_RANGES -
#for web_dst_range in $web_range_lst ; do
#  iptables -t mangle -A PREROUTING -i br0 -m iprange --dst-range $web_dst_range -j MARK --set-mark 0
#done

These are the errors that pop up when I try running the script:

# ./vpntoggler-alternative.sh
ip: RTNETLINK answers: No such process
ip: RTNETLINK answers: No such file or directory
iptables v1.4.21: Couldn't load match `iprange':No such file or directory

Try `iptables -h' or 'iptables --help' for more information.

I have no idea what the first two lines mean. As for the third, it seems iptables doesn't like "-m iprange". Any ideas how to modify the script (or my router) so that the script works?

EDIT: The solution is in post #16.

(Last edited by GNUser on 6 Oct 2017, 02:42)

Ok, it is the same principle, as in vpnbypass, as you can see - to mark packets in PREROUTING chain and route them. Try to debug initially, run bash script with code print:

sh -x vpntoggler-alternative.sh

or modify the first line in script:

#!/bin/sh -x

We will see, in which line error occurs.

Here are where the errors occur:

# sh -x vpntoggler-alternative.sh
---snip---
+ ip route flush table 100
+ ip route del default table 100
ip: RTNETLINK answers: No such process
+ ip rule del fwmark 1 table 100
ip: RTNETLINK answers: No such file or directory
---snip---
+ iptables -t mangle -A PREROUTING -i br0 -m iprange --src-range 192.168.10.60-192.168.10.69 -j MARK --set-mark 1
iptables v1.4.21: Couldn't load match `iprange':No such file or directory

Try `iptables -h' or 'iptables --help' for more information.
---snip---

Here's all of the output in case it's helpful:

# sh -x vpntoggler-alternative.sh
+ ip_addrs_lst=192.168.10.60-192.168.10.69
+ server_ports=5500,22
+ sleep 1
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ echo 0
+ ip route flush table 100
+ ip route del default table 100
ip: RTNETLINK answers: No such process
+ ip rule del fwmark 1 table 100
ip: RTNETLINK answers: No such file or directory
+ ip route flush cache
+ iptables -t mangle -F PREROUTING
+ awk  {print $8}
+ route
+ iface_lst=
Iface
tun0
eth1
tun0
tun0
eth1
eth1
eth1
tun0
br-lan
+ [ Iface == tun11 ]
+ [ Iface == tun12 ]
+ [ Iface == ppp0 ]
+ [ tun0 == tun11 ]
+ [ tun0 == tun12 ]
+ [ tun0 == ppp0 ]
+ [ eth1 == tun11 ]
+ [ eth1 == tun12 ]
+ [ eth1 == ppp0 ]
+ [ tun0 == tun11 ]
+ [ tun0 == tun12 ]
+ [ tun0 == ppp0 ]
+ [ tun0 == tun11 ]
+ [ tun0 == tun12 ]
+ [ tun0 == ppp0 ]
+ [ eth1 == tun11 ]
+ [ eth1 == tun12 ]
+ [ eth1 == ppp0 ]
+ [ eth1 == tun11 ]
+ [ eth1 == tun12 ]
+ [ eth1 == ppp0 ]
+ [ eth1 == tun11 ]
+ [ eth1 == tun12 ]
+ [ eth1 == ppp0 ]
+ [ tun0 == tun11 ]
+ [ tun0 == tun12 ]
+ [ tun0 == ppp0 ]
+ [ br-lan == tun11 ]
+ [ br-lan == tun12 ]
+ [ br-lan == ppp0 ]
+ + + + grep -Evgrep -Evread ^default ROUTEip
 route show
 table main
 br-lan
+ ip route add table 100 0.0.0.0/1 via 10.82.10.5 dev tun0
+ read ROUTE
+ ip route add table 100 10.82.10.1 via 10.82.10.5 dev tun0
+ read ROUTE
+ ip route add table 100 10.82.10.5 dev tun0 src 10.82.10.6
+ read ROUTE
+ ip route add table 100 73.226.68.0/22 dev eth1 src 73.226.70.41
+ read ROUTE
+ ip route add table 100 73.226.68.1 dev eth1 src 73.226.70.41
+ read ROUTE
+ ip route add table 100 107.191.33.9 via 73.226.68.1 dev eth1
+ read ROUTE
+ ip route add table 100 128.0.0.0/1 via 10.82.10.5 dev tun0
+ read ROUTE
+ + + grep nexthop
grepegrep -v -o 0.0.0.0
 [0-9.]++ 
ifstatus wan
+ ip route add default table 100 via 73.226.68.1
+ ip rule add fwmark 1 table 100
+ ip route flush cache
+ iptables -t mangle -A PREROUTING -i br0 -j MARK --set-mark 0
+ iptables -t mangle -A PREROUTING -i br0 -m iprange --src-range 192.168.10.60-192.168.10.69 -j MARK --set-mark 1
iptables v1.4.21: Couldn't load match `iprange':No such file or directory

Try `iptables -h' or 'iptables --help' for more information.
+ iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --dport 5500,22 -j MARK --set-mark 1
+ iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --sport 5500,22 -j MARK --set-mark 1
+ ip route del default table 100
ip: RTNETLINK answers: No such process

There is no such table, you can ignore it.

iptables v1.4.21: Couldn't load match `iprange':No such file or directory

There is misusing of iptables parameters. It tries to add IPs from list:

for ip_addrs in $ip_addrs_lst ; do
  iptables -t mangle -A PREROUTING -i br0 -m iprange --src-range $ip_addrs -j MARK --set-mark 1
done

See correct format of command in output of

iptables -S -t mangle

when vpnbypass is active.
Usage of

-m iprange

assumes, iprange was created by ipset earlier.

(Last edited by ulmwind on 23 Sep 2017, 16:47)

Good to know first error is benign.

Here's the output of "iptables -S -t mangle" while vpnbypass is active. I'm not sure what to do with it, though.

# iptables -S -t mangle
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N VPNBYPASS
-A PREROUTING -m mark --mark 0x0/0xff0000 -g VPNBYPASS
-A FORWARD -o eth1 -p tcp -m id --id 0x66773300 -m tcp --tcp-flags SYN,RST SYN -m comment --comment "wan (mtu_fix)" -j TCPMSS --clamp-mss-to-pmtu
-A FORWARD -o tun0 -p tcp -m id --id 0x66773300 -m tcp --tcp-flags SYN,RST SYN -m comment --comment "VPN (mtu_fix)" -j TCPMSS --clamp-mss-to-pmtu
-A VPNBYPASS -s 192.168.10.56/29 -j MARK --set-xmark 0x10000/0xff0000

Like this, what is your question?

iptables -t mangle -A PREROUTING  -s 192.168.10.56/29 -j MARK --set-mark 1

It is command, you should specify subnets in variable.
Compare with your output:

iptables -t mangle -A VPNBYPASS -s 192.168.10.56/29 -j MARK --set-xmark 0x10000/0xff0000

See difference between set-mark and set-xmark.

(Last edited by ulmwind on 23 Sep 2017, 19:28)

Okay, so openvpn is on, vpnbypass is off and disabled.

This is the current form of the script:

#!/bin/sh

## CUSTOMIZE YOUR SCRIPT VARIABLES
#
## Uncomment and set value(s) as needed to customize your rules
#
# IP addresses, contiguous range AND/OR individual.
#
subnet="192.168.10.56/29"

##Server ports to bypass VPN
server_ports="5500,22"

#
# Specific destination websites ip range - Spotify , Netflix...
#
#web_range_lst="72.44.32.1-72.44.63.254
#67.202.0.1-67.202.63.254
#207.223.0.1-207.223.15.254
#98.207.0.1-98.207.255.254
#208.85.40.1-208.85.47.254
#78.31.8.1-78.31.15.254
#193.182.8.1-193.182.15.254"

########################################
# NO NEED TO CHANGE BELOW THIS LINE #
########################################

# SHELL COMMANDS FOR MAINTENANCE.
# DO NOT UNCOMMENT, THESE ARE INTENDED TO BE USED IN A SHELL COMMAND LINE
#
#  List Contents by line number
# iptables -L PREROUTING -t mangle -n --line-numbers
#
#  Delete rules from mangle by line number
# iptables -D PREROUTING type-line-number-here -t mangle
#
#  To list the current rules on the router, issue the command:
#     iptables -t mangle -L PREROUTING
#
#  Flush/reset all the rules to default by issuing the command:
#     iptables -t mangle -F PREROUTING
sleep 1
#
# First it is necessary to disable Reverse Path Filtering on all
# current and future network interfaces:
#
for i in /proc/sys/net/ipv4/conf/*/rp_filter ; do
  echo 0 > $i
done

#
# Delete table 100 and flush any existing rules if they exist.
#
ip route flush table 100
ip route del default table 100
ip rule del fwmark 1 table 100
ip route flush cache
iptables -t mangle -F PREROUTING

#
# Let's find out the tunnel interface
#
iface_lst=`route | awk ' {print $8}'`
for tun_if in $iface_lst; do
    if [ $tun_if == "tun11" ] || [ $tun_if == "tun12" ] || [ $tun_if == "ppp0" ]; then
    break
  fi
done

#
# Copy all non-default and non-VPN related routes from the main table into table 100.
# Then configure table 100 to route all traffic out the WAN gateway and assign it mark "1"
#
ip route show table main | grep -Ev ^default | grep -Ev $tun_if \
  | while read ROUTE ; do
     ip route add table 100 $ROUTE
done
ip route add default table 100 via $(ifstatus wan | grep nexthop | egrep -o '[0-9.]+' | grep -v '0.0.0.0')
ip rule add fwmark 1 table 100
ip route flush cache

# EXAMPLES:
#
#  All LAN traffic will bypass the VPN (Useful to put this rule first,
#  so all traffic bypasses the VPN and you can configure exceptions afterwards)
#    iptables -t mangle -A PREROUTING -i br0 -j MARK --set-mark 1
#
#  Ports 80 and 443 will bypass the VPN
#    iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --dport 80,443 -j MARK --set-mark 1
#
#  All traffic from a particular computer on the LAN will use the VPN
#    iptables -t mangle -A PREROUTING -i br0 -m iprange --src-range 192.168.1.2 -j MARK --set-mark 0
#
#  All traffic to a specific Internet IP address will use the VPN
#    iptables -t mangle -A PREROUTING -i br0 -m iprange --dst-range 216.146.38.70 -j MARK --set-mark 0
#
#  All UDP and ICMP traffic will bypass the VPN
#    iptables -t mangle -A PREROUTING -i br0 -p udp -j MARK --set-mark 1
#    iptables -t mangle -A PREROUTING -i br0 -p icmp -j MARK --set-mark 1

# Default behavior: MARK = 1 all traffic bypasses VPN, MARK = 0 all traffic goes VPN
iptables -t mangle -A PREROUTING -i br0 -j MARK --set-mark 0

# IP_ADDRESSES - RANGE(S) AND/OR INDIVIDUAL(S)
iptables -t mangle -A PREROUTING  -s "$subnet" -j MARK --set-mark 1

######   Ports that bypass VPN    ######
###### Normal portforwarding will ######
######    need to be applied      ######

iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --dport $server_ports -j MARK --set-mark 1
iptables -t mangle -A PREROUTING -i br0 -p tcp -m multiport --sport $server_ports -j MARK --set-mark 1

# WEBSITES_IP_RANGES -
#for web_dst_range in $web_range_lst ; do
#  iptables -t mangle -A PREROUTING -i br0 -m iprange --dst-range $web_dst_range -j MARK --set-mark 0
#done

Running script has stopped spitting out ugly errors:

# ./vpnbypass-alternative.sh 
ip: RTNETLINK answers: No such process

However, script seems to have no effect. Whether my local IP is 192.168.10.61 or 192.168.10.144, my public IP is the same (that of my VPN provider). I checked the public IP both by running "wget http://ipinfo.io -q -O -" on my laptop and by vising any of myriad "what is my ip" websites.

Is there anything wrong with this line?

ip route add default table 100 via $(ifstatus wan | grep nexthop | egrep -o '[0-9.]+' | grep -v '0.0.0.0')

(Last edited by GNUser on 28 Sep 2017, 01:13)

Got it to work perfectly! Just 4 steps:

1. Put this script somewhere (e.g., as /etc/openvpn/vpnbypass-alternative.sh) and make it executable:

#!/bin/sh

## CUSTOMIZE YOUR SCRIPT VARIABLES
#
## Uncomment and set value(s) as needed to customize your rules
#
# IP addresses, contiguous range AND/OR individual.
#
subnet="192.168.10.56/29"

# These interface names can be different depending on your configuration.
# Check the output of ifconfig to ensure these values are correct for your router:
vpn_if=tun0
lan_if=br-lan

##Server ports to bypass VPN
#server_ports="5500,22"

#
# Specific destination websites ip range - Spotify , Netflix...
#
#web_range_lst="72.44.32.1-72.44.63.254
#67.202.0.1-67.202.63.254
#207.223.0.1-207.223.15.254
#98.207.0.1-98.207.255.254
#208.85.40.1-208.85.47.254
#78.31.8.1-78.31.15.254
#193.182.8.1-193.182.15.254"

########################################
# NO NEED TO CHANGE BELOW THIS LINE #
########################################

# Wait for vpn to be fully up before proceeding:
while true; do
    hits=$(route | grep "$vpn_if" | wc -l)
    [ $hits -eq 4 ] && break
    sleep 1
done

# SHELL COMMANDS FOR MAINTENANCE.
# DO NOT UNCOMMENT, THESE ARE INTENDED TO BE USED IN A SHELL COMMAND LINE
#
#  List Contents by line number
# iptables -L PREROUTING -t mangle -n --line-numbers
#
#  Delete rules from mangle by line number
# iptables -D PREROUTING type-line-number-here -t mangle
#
#  To list the current rules on the router, issue the command:
#     iptables -t mangle -L PREROUTING
#
#  Flush/reset all the rules to default by issuing the command:
#     iptables -t mangle -F PREROUTING
sleep 1
#
# First it is necessary to disable Reverse Path Filtering on all
# current and future network interfaces:
#
for i in /proc/sys/net/ipv4/conf/*/rp_filter ; do
  echo 0 > $i
done

#
# Delete table 100 and flush any existing rules if they exist.
#
ip route flush table 100
ip route del default table 100
ip rule del fwmark 1 table 100
ip route flush cache
iptables -t mangle -F PREROUTING


#
# Copy all non-default and non-VPN related routes from the main table into table 100.
# Then configure table 100 to route all traffic out the WAN gateway and assign it mark "1"
#
ip route show table main | grep -Ev ^default | grep -Ev "$vpn_if" \
  | while read ROUTE ; do
     ip route add table 100 $ROUTE
done
ip route add default table 100 via $(ifstatus wan | grep nexthop | egrep -o '[0-9.]+' | grep -v '0.0.0.0')
ip rule add fwmark 1 table 100
ip route flush cache

# EXAMPLES:
#
#  All LAN traffic will bypass the VPN (Useful to put this rule first,
#  so all traffic bypasses the VPN and you can configure exceptions afterwards)
#    iptables -t mangle -A PREROUTING -i $lan_if -j MARK --set-mark 1
#
#  Ports 80 and 443 will bypass the VPN
#    iptables -t mangle -A PREROUTING -i $lan_if -p tcp -m multiport --dport 80,443 -j MARK --set-mark 1
#
#  All traffic from a particular computer on the LAN will use the VPN
#    iptables -t mangle -A PREROUTING -i $lan_if -m iprange --src-range 192.168.1.2 -j MARK --set-mark 0
#
#  All traffic to a specific Internet IP address will use the VPN
#    iptables -t mangle -A PREROUTING -i $lan_if -m iprange --dst-range 216.146.38.70 -j MARK --set-mark 0
#
#  All UDP and ICMP traffic will bypass the VPN
#    iptables -t mangle -A PREROUTING -i $lan_if -p udp -j MARK --set-mark 1
#    iptables -t mangle -A PREROUTING -i $lan_if -p icmp -j MARK --set-mark 1

# Default behavior: MARK = 1 all traffic bypasses VPN, MARK = 0 all traffic goes VPN
iptables -t mangle -A PREROUTING -i "$lan_if" -j MARK --set-mark 0

# IP_ADDRESSES - RANGE(S) AND/OR INDIVIDUAL(S)
iptables -t mangle -A PREROUTING  -s "$subnet" -j MARK --set-mark 1

######   Ports that bypass VPN    ######
###### Normal portforwarding will ######
######    need to be applied      ######

#iptables -t mangle -A PREROUTING -i "$lan_if" -p tcp -m multiport --dport $server_ports -j MARK --set-mark 1
#iptables -t mangle -A PREROUTING -i "$lan_if" -p tcp -m multiport --sport $server_ports -j MARK --set-mark 1

# WEBSITES_IP_RANGES -
#for web_dst_range in $web_range_lst ; do
#  iptables -t mangle -A PREROUTING -i "$lan_if" -m iprange --dst-range $web_dst_range -j MARK --set-mark 0
#done

2. To execute the script at boot, simply add script's full path to /etc/rc.local (before the final "exit 0" line, of course):

/etc/openvpn/vpnbypass-alternative.sh
exit 0

3. Make sure vpnbypass package, if installed, is disabled (e.g., enter at command line: /etc/init.d/vpnbypass disable)

4. Reboot the router

Now any device on 192.168.10.56/29 subnet will bypass the vpn. This script is not affected by the problem I was having with the vpnbypass package. In other words, my machine on the subnet (in my case it's the laptop at 192.168.10.61) can be reached by ssh both from lan and wan. From lan, I can reach it by using its local ip or by using its public (ISP's) ip.

Note: In addition to running the script at boot (via /etc/rc.local), you should also run it after restarting the firewall for any reason. Stopping or restarting the firewall flushes all rules on all standard chains (including the prerouting chain of mangle table), necessitating script to be re-run for vpnbypass to kick in again.

(Last edited by GNUser on 2 Oct 2017, 00:19)

Congratulations! Very powerful progress!

Thanks! As I wrestle with this script, more and more things are becoming clearer to me.

One thing I don't understand is the order of these two commands:

# Default behavior: MARK = 1 all traffic bypasses VPN, MARK = 0 all traffic goes VPN
iptables -t mangle -A PREROUTING -i "$lan_if" -j MARK --set-mark 0

# IP_ADDRESSES - RANGE(S) AND/OR INDIVIDUAL(S)
iptables -t mangle -A PREROUTING  -s "$subnet" -j MARK --set-mark 1

Since a) the -A appends the rule to the end of the chain and b) processing of rules in a chain stops once a rule matches, I would expect that all packets entering the router from lan would get the "0" mark--and no packets would ever ever get the "1" mark to bypass vpn.

Since the script works, there must be an error in my thinking. Where am I wrong?

(Last edited by GNUser on 28 Sep 2017, 19:10)

I found this somewhere:

Packets traverse a chain until they hit ACCEPT, DROP, REJECT, or RETURN. They do not stop on a match unless that match contains a terminating action.

If this is true, then I understand my error.

Yes, MARK is not terminating target. Packets transverse it, the action is to mark or not to mark, after passing packets go to next rule in the chain, not changing sequence of rules.

(Last edited by ulmwind on 28 Sep 2017, 22:12)

I cleaned up the code a bit--got rid of some comments and all the features I don't need. Now it's a lot more clear what the script does. Here's the version of the script I've been using:

#!/bin/sh

subnet="192.168.10.56/29" # 192.168.10.56 to 192.168.10.63
vpn_if=tun0 # name of vpn interface varies by router. check output of ifconfig
lan_if=br-lan # name of lan interface varies by router. check output of ifconfig

# A. Wait for VPN to have kicked in before proceeding:
while true; do
    hits=$(route | grep "$vpn_if" | wc -l)
    [ $hits -eq 4 ] && break
    sleep 1
done
sleep 1

# B. First, disable Reverse Path Filtering on all network interfaces:
for i in /proc/sys/net/ipv4/conf/*/rp_filter; do
    echo 0 > $i
done

# C. Delete table 100 if it exists, flush any existing rules in mangle table's prerouting chain:
ip route flush table 100; ip route del default table 100; ip rule del fwmark 1 table 100
ip route flush cache; iptables -t mangle -F PREROUTING

# D. Copy non-openvpn routes from main routing table to user-defined routing table 100, assign it mark "1":
ip route show table main | grep -Ev "$vpn_if" | while read ROUTE; do
    ip route add table 100 $ROUTE
done
ip rule add fwmark 1 table 100
ip route flush cache

# E. Establish default behavior (mark 1 = all traffic bypasses VPN; mark 0 = all traffic goes VPN):
iptables -t mangle -A PREROUTING -i "$lan_if" -j MARK --set-mark 0

# F. Now set non-default behavior for subnet:
iptables -t mangle -A PREROUTING  -s "$subnet" -j MARK --set-mark 1

Some comments:

1. Section D: It seemed silly to exclude the default wan route (with this: grep -Ev ^default) when setting up table 100, only to have to add it back in later (with this: ip route add default table 100 via $(ifstatus wan | grep nexthop | egrep -o '[0-9.]+' | grep -v '0.0.0.0')). Since openvpn doesn't change anything in the routing table--it just adds four lines--it is sufficient to simply exclude the added lines when constructing the "no-vpn" table (table 100).

2. Section A: I'm 99.9% sure it's not needed, but it doesn't hurt to leave it in.

3. Section B: I'm not exactly sure what reverse path filtering has to do with what we're trying to accomplish, but disabling it doesn't seem to hurt anything so I left it in.

(Last edited by GNUser on 4 Oct 2017, 18:39)

1. I don't understand first statement, you've written, that rules, not containing openvpn interface, are copied in table 100. These rules were before openvpn connection.
2. It is trivial section, it just waits, while routes with openvpn interface are excluded. If the route with openvpn interface exists, it will be infinite loop.
3. It has effect in case of asymmetric routes, e.g. when response goes through another interface. For our case it is reproduced, when you ping wan-isp IP of your router from Internet. Ping comes to wan interface, but answer comes through openvpn interface. You should get or not to get response depending on this setting. You can check it.

My question is why to use 2 marks, because just one mark should suffice, to my mind.

(Last edited by ulmwind on 5 Oct 2017, 22:54)

1. Never mind. I understand it now.
2. Got it.
3. You are absolutely correct. It is necessary for asymmetric routes like the request arriving via wan-isp but reply leaving via vpn interface. Thank you.

Indeed, one mark will do. Openvpn makes sure that default behavior is via vpn, so it's only necessary to mark "1" the outgoing traffic to bypass vpn. Nice.

I discovered that by installing iptables-mod-range in the router I can use ip address ranges in the script instead of subnet with CIDR notation. This is much more human-friendly.

Here is the current version of the script:

#!/bin/sh

vpnbypass_ips="192.168.10.60-192.168.10.69"
vpn_if=tun0

# No need to change anything below this line

# A. Wait for VPN to have kicked in before proceeding (not sure if necessary, but doesn't hurt):
while true; do
    hits=$(route | grep "$vpn_if" | wc -l)
    [ $hits -eq 4 ] && break
    sleep 1
done
sleep 1

# B. First, disable Reverse Path Filtering on all network interfaces:
for i in /proc/sys/net/ipv4/conf/*/rp_filter; do
    echo 0 > $i
done

# C. Delete table 100 if it exists, flush any existing rules in mangle table's prerouting chain:
ip route flush table 100; ip route del default table 100; ip rule del fwmark 1 table 100
ip route flush cache; iptables -t mangle -F PREROUTING

# D. Copy non-openvpn routes from main routing table to user-defined routing table 100, assign mark "1":
ip route show table main | grep -Ev "$vpn_if" | while read ROUTE; do
    ip route add table 100 $ROUTE
done
ip rule add fwmark 1 table 100
ip route flush cache

# E. Mark packets to bypass vpn:
iptables -t mangle -A PREROUTING -m iprange --src-range "$vpnbypass_ips" -j MARK --set-mark 1

Again, just make sure to run script automatically after boot (put script's absolute path in /etc/rc.local) and manually anytime router's firewall is restarted.

(Last edited by GNUser on 6 Oct 2017, 02:36)

The discussion might have continued from here.