Switching between Internet Providers on Debian 7

The latest Linux distributions have quite a few goodies in the / etc folder, but few use them competently. Here I will talk about some of them in relation to the latest stable version of Debian, and give an example of the implementation of switching to the backup channel.
All attempts to find a trivial auto-switch led to a bunch of 1-2 scripts written "for themselves" and not very configurable. Not one of them was attached to dhcp, which means that any manipulations on the provider's side required intervention in the settings. I myself wrote these at one time, but now I decided to design it beautifully on the new system - just as the Debian developers intended, namely, changing the configuration files, adding our scripts and not touching the ones that the system provided to us.

So we have:
- two cables from two providers, both give IP by dhcp
- a freshly assembled server running debian wheezy with three network cards (maybe I'll add more later)
- the desire that the Internet does not disappear (work does not wait!). Balancing, etc. leave for later.

The logic at first glance is simple:
- we ping some host in turn through different interfaces
- if the ping is unstable, we switch to the backup channel.
But only in the implementation the goal was set to do everything as flexible as possible, for example, not to limit the number of potential providers and subsequently make a minimum of body movements for reconfiguration.
First, let's see what kind of buns are already in the system.
In the / etc / network folder, we are interested in the interfaces file and the if-down.d, if-post-down.d, if-pre-up.d, if-up.d folders.
root@ns:/etc/network# cat interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).
# The loopback network interface
iface lo inet loopback
# The primary network interface
iface eth0 inet static
    address 192.168.104.1
    netmask 255.255.255.0
iface eth1 inet dhcp
iface eth3 inet dhcp

When manipulating interfaces through ifup / ifdown, run-parts are set on folders, and the following environment variables are accessible to scripts in them: IFACE, LOGICAL, ADDRFAM, METHOD, MODE, PHASE, MODE, VERBOSITY, PATH
When the system starts, scripts from the if folder are first run -pre-up.d (once for each interface, but before them comes IFACE = ”- all”, then the interfaces rise and scripts from the if-up.d and IFACE = ”- all” folders are run already in At the end, ifup ethX runs only once for ethX (without “–all"). Similarly, if-down.d and if-post-down.d when ifdown and system shutdown.
The system allows you to assign a script for each operation and for each interface, but to make similar changes every 10 out of 20 scripts was not included in my plans, so we will write one large script and arrange symlinks from all four folders on it. You can understand where it is launched from by environment variables.

However, we still need to know the gateway information that came from dhcp. For this case, there are also folders with scripts /etc/dhcp/dhclient-enter-hooks.d and /etc/dhcp/dhclient-exit-hooks.d
The startup sequence is as follows:
- polled the dhcp server
- launched the contents of the dhclient-enter-hooks folder .d
- configured network parameters (ip, DNS, gateway, ...)
- launched the contents of the dhclient-exit-hooks.d folder
The scripts also have access to various useful variables (parameters that came from the dhcp server), some of which we will need to save.

After several evenings, the following happened:
All scripts are in the / etc / network / scripts folder. From different folders there are symlinks
Settings - / etc / default / network-scripts
Temporary files put / var / lib / dhcp
Logs are written to the file /var/log/network-scripts.log
Settings
# cat /etc/default/network-scripts
# Configuration file for /etc/network/scripts/*
# Host to ping for autoroute
HOST_TO_PING="4.2.2.1"
# Number of pings to check the connection
PING_COUNT=5
# list of LAN interfaces
IFLAN="eth0"
# WAN prio from first to last
IFWAN="eth1 eth3"
# open ports from WAN zone
WAN_PORTS_OPEN=""

Everything should be clear here. Interfaces LAN, WAN can be added as much as you like. In the WAN list, the first is the highest priority, then in descending order.
Separate file with functions.
# cat /etc/network/scripts/functions
#!/bin/sh
DHCPLIB="/var/lib/dhcp"
LOGDIR="/var/log"
LOGFILE="$LOGDIR/network-scripts.log"
HOST_TO_PING="4.2.2.1"
PING_COUNT=3
SQUID_PORT="3128"
IFLAN=""
IFWAN=""
WAN_PORTS_OPEN=""
. /etc/default/network-scripts
# Local variables
DEFAULTWAN=${IFWAN% *}
log()
{
    DATE=`date`
    echo "$DATE $@" >> $LOGFILE
}
warn()
{
    log "WARNING: $@"
    echo "WARNING: $@"
}
cmd()
{
    $@
    RES=$?
    log "$RES - $@"
    return $RES
}
get_ip()
{
    IP=`ip addr list $1 | grep "  inet " | head -n 1 | cut -d " " -f 6 | cut -d / -f 1`
}
update_local_redirect()
{
    for i in $IFLAN; do
        cmd iptables -t nat $INS PREROUTING -i $i -p tcp --dport 80 -d $1 -j ACCEPT
    done
}
update_squid()
{
    case $1 in
        start)
            ADD="-A"
            INS="-I"
            ;;
        stop)
            ADD="-D"
            INS="-D"
            ;;
        *)
            ADD="-C"
            INS="-C"
    esac
    for i in $IFLAN; do
        # transparent proxy
        cmd iptables -t nat $ADD PREROUTING -i $i -p tcp --dport 80 -j REDIRECT --to-port $SQUID_PORT
    done
}

Here we have:
- import settings from / etc / default / network-scripts
- logging (log, warn),
- launching commands with writing to the log parameters and results of work
- update_local_redirect () adds routes to port 80 by transparent proxy
- update_squid () adds a rule for transparent proxy itself (runs in /etc/init.d/squid3 - this is the only system script I had to get into)
Here and thereafter, we use the technology that I invented several years ago with the variables $ ADD and $ INS for iptables . Allows you to write a rule in only one place, and then add-delete it, changing only these variables.
# cat /etc/network/scripts/route-enter
#!/bin/sh
. /etc/network/scripts/functions
log "$0 route-enter ${interface} ${reason} ${new_routers}"
# security bugfix
new_host_name=${new_host_name//[^-.a-zA-Z0-9]/}
# save routers to special file
echo -n ${new_routers} > $DHCPLIB/routers.${interface}
echo -n ${new_ip_address} > $DHCPLIB/ip_address.${interface}
case ${interface} in
   $DEFAULTWAN)
    # by default enable routers only for first WAN interface
    ;;
  *)
    # and clear it for others
    unset new_routers
    ;;
esac

- Save new_routers and new_ip_address to a file (then they will be understood)
- default route is allowed only for the priority interface

# cat /etc/network/scripts/route-exit
#!/bin/sh
. /etc/network/scripts/functions
log "$0 route-exit ${interface} ${reason}"
update_routes()
{
    cmd route $ADD -host $HOST_TO_PING gw ${routers}
    # identyfy providers by DNS addresses
    case $DNS in
        *82.193.96*)
            DESTIP=`resolveip -s stat.ipnet.ua`
            cmd route $ADD -host $DESTIP gw ${routers}
            ;;
        *193.41.63*|*192.168.11.1*)
            DESTIP=`resolveip -s my.kyivstar.ua`
            cmd route $ADD -host $DESTIP gw ${routers}
            ;;
        *)
            warn "route-exit - unknown DNS ${new_domain_name_servers} specified"
            ;;
    esac
}
case ${reason} in
    BOUND)
        ADD="add"
        DNS=${new_domain_name_servers}
        # use saved-to-file value due to $old_routers can be cleared for some interfaces by other script
        routers=`cat $DHCPLIB/routers.${interface}`
        update_routes
        ;;
    RELEASE)
        # No need to delete routes during release
        # ADD="del"
        # routers=${old_routers}`
        # update_routes
        ;;
    PREINIT)
        ;;
    RENEW)
        if [ "$old_routers" != "$new_routers" ]; then
            ADD="del"
            DNS=${old_domain_name_servers}
            routers=${old_routers}
            update_routes
            ADD="add"
            DNS=${new_domain_name_servers}
            routers=`cat $DHCPLIB/routers.${interface}`
            update_routes
        fi
        if [ "$old_ip_address" != "$new_ip_address" ]; then
            ADD="-D"
            INS="-D"
            update_local_redirect ${old_ip_address}
            ADD="-A"
            INS="-I"
            update_local_redirect ${new_ip_address}
        fi
        ;;
    *)
        warn "route-exit - unknown reason ${reason} used"
        ;;
esac

- Add a static route for sites with billing providers. I do not need a provider’s lock, but you can also add it. Authentication by DNS servers.
- added a reconfiguration for the RENEW mode (if suddenly something changes at the provider), but has not yet tested it.

# cat /etc/network/scripts/firewall
#!/bin/bash
. /etc/network/scripts/functions
get_ip $IFACE
log "$0 $IFACE $LOGICAL $ADDRFAM $METHOD $MODE $PHASE $VERBOSITY $IP"
case $MODE in
    start)
        INS="-I"
        ADD="-A"
        echo -n $IP > $DHCPLIB/ip_address.$IFACE
        ;;
    stop)
        INS="-D"
        ADD="-D"
        echo -n > $DHCPLIB/ip_address.$IFACE
        ;;
    *)
        INS="-C"
        ADD="-C"
        warn "Wrong MODE:$MODE"
        ;;
esac
case $IFACE in
    --all)
        case $PHASE in
            pre-down|post-up)
                # skip proxy for local addresses
                for j in $IFLAN $IFWAN; do
                    get_ip $j
                    update_local_redirect $IP
                done
                ;;
            post-up|pre-down)
                ;;
        esac
        ;;
    lo)
        ;;
    *)
        if [[ "$IFLAN" == *$IFACE* ]]; then
                # LAN
                case $PHASE in
                    pre-up|post-down)
                        cmd iptables $INS INPUT -p tcp -i $IFACE --dport 22 -j ACCEPT
                        ;;
                    post-up|pre-down)
                        ;;
                    *)
                        warn "Wrong PHASE:$PHASE"
                        ;;
                esac
        fi
        if [[ "$IFWAN" == *$IFACE* ]]; then
                # WAN
                case $PHASE in
                    pre-up|post-down)
                        # by default close all input connections
                        cmd iptables $ADD INPUT -p tcp -i $IFACE --dport 1:10000 -j DROP
                        cmd iptables $ADD INPUT -p udp -i $IFACE --dport 1:10000 -j DROP
                        # open ports from list
                        for PORT in $WAN_PORTS_OPEN; do
                            cmd iptables $INS INPUT -p tcp -i $IFACE --dport $PORT -j ACCEPT
                        done
                        ;;
                    post-up|pre-down)
                        cmd iptables -t nat $ADD POSTROUTING -o $IFACE -j MASQUERADE
                        ;;
                    *)
                        warn "Wrong PHASE:$PHASE"
                        ;;
                esac
        fi
        ;;
esac

Firewall rules. For general tables, we write at the time of pre-up and post-down, for NAT - in post-up and pre-down.

# cat /etc/network/scripts/autoroute
#!/bin/sh
# Script for cron to monitor WAN interfaces
# and (in future) SQUID status
. /etc/network/scripts/functions
CURRENT_ROUTE_DEV=`ip route show | grep default | awk '{print $5}'`
unset ROUTE_GOOD
PING_RESULTS=""
for i in $IFWAN; do
    if [ -z $ROUTE_GOOD ]; then
        PING_RESULT=`ping -c$PING_COUNT -q $HOST_TO_PING -I $i | grep 'packet loss' | awk '{print $6}'`
        # If no route t host then set to 100% loss
        if [ -z $PING_RESULT ]; then
            warn "$0 No route to host $HOST_TO_PING on $i"
            PING_RESULT='100%'
        fi
        if [ $PING_RESULT = '0%' ]; then
            ROUTE_GOOD=$i
            if [ -z $CURRENT_ROUTE_DEV ]; then
                log "$0 Adding default route to $i"
                cmd route add default gw `cat $DHCPLIB/routers.$i`
            elif [ $CURRENT_ROUTE_DEV != $i ]; then
                log "$0 Change default route from $CURRENT_ROUTE_DEV to $i"
                cmd route del default
                cmd route add default gw `cat $DHCPLIB/routers.$i`
            fi
        else
            log "$0 loss $PING_RESULT on $i"
        fi
    fi
    PING_RESULTS="$PING_RESULTS $PING_RESULT"
done
if [ -z $ROUTE_GOOD ]; then
    warn "$0 lost all internet connections ($PING_RESULTS loss)"
fi

It's simple: we ping in order of priority. Found the best - switch. If anything, write to the log.

And finally
# cat /etc/cron.d/autoroute
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
*/5 * * * * root /etc/network/scripts/autoroute

# cat /etc/logrotate.conf  | tail
# system-specific logs may be configured here
/var/log/network-scripts.log {
        weekly
        missingok
        rotate 7
        compress
}


Simlink
/etc/dhcp/dhclient-enter-hooks.d/route-enter -> ../../network/scripts/route-enter
/etc/dhcp/dhclient-exit-hooks.d/route-exit -> ../../network/scripts/route-exit
/etc/network/if-pre-up.d/firewall -> ../scripts/firewall
/etc/network/if-down.d/firewall -> ../scripts/firewall
/etc/network/if-up.d/firewall -> ../scripts/firewall
/etc/network/if-post-down.d/firewall -> ../scripts/firewall 

Also popular now: