#!/usr/bin/perl

use strict;
use warnings FATAL => 'all';
use Net::Ping;
use Data::Dumper;
use Try::Tiny;

$ENV{PATH} = '/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin';

my @instances;
my %routes;

sub
connection_sort_helper
{
        return $b->{score} <=> $a->{score} || $a->{priority} <=> $b->{priority} ;
}

push(@instances,
{
        dsts => [
                '1.1.1.1',
                '8.8.8.8',
                '88.198.237.218',
        ],
        connections => [
        # KEVAG - 700 Mbit/s
        {
                bind => '212.7.174.82',
                priority => 1,
                routes => [
                        'default via 212.7.174.81 dev ens19',
                ],
        },
        # DTAG - 50/20 Mbit/s
        {
                bind => '192.168.222.222',
                priority => 2,
                routes => [
                        'default via 192.168.222.1 dev ens20',
                ],
        },
        ],
},
);

my @routes = `ip route show`;
chomp(@routes);

for (@routes) {
        $_ =~ s/\s+$//;
        $routes{$_} = 1;
}

for my $i (@instances) {
        for my $c (@{$i->{connections}}) {
                $c->{score} = 0;
                my $p = Net::Ping->new('icmp');
                $p->bind($c->{bind});
                for my $d (@{$i->{dsts}}) {
                        try {
                                $c->{score} += $p->ping($d, 5);
                        } catch {
                                # just keep going
                        };
                }
        }

        my @queue = sort connection_sort_helper (@{$i->{connections}});

        my $active = shift @queue;

        for my $c (@queue) {
                for my $r (@{$c->{routes}}) {
                        if (exists($routes{$r})) {
                                system("ip route delete $r");
                        }
                }
        }

        for my $r (@{$active->{routes}}) {
                unless (exists($routes{$r})) {
                        system("ip route add $r");
                }
        }
}

__END__

# Configuration

- Run this script every minute via cron

- Create a route table for each uplink:

        echo '1 kevag' >> /etc/iproute2/rt_tables
        echo '2 detag' >> /etc/iproute2/rt_tables

- Create the necessary routes and rules for each uplink in
  /etc/network/interfaces associated to the table:

        - Route for the network or point2point peer
        - Default or network route
        - Rule to put any traffic with src ip
        - Rule to put any traffic with dst ip
        - If you have wireguard tunnels that need to be bound to an interface
          add a rule as well.
        - Flush the rules when the interface is deconfigured

auto ens19
iface ens19 inet static
        address 212.7.174.82
        netmask 255.255.255.240
        post-up /usr/local/sbin/firewall.sh || true
        post-up ip route add 212.7.174.80/28 dev ens19 src 212.7.174.82 table kevag || true
        post-up ip route add default via 212.7.174.81 dev ens19 table kevag || true
        post-up ip rule add from 212.7.174.82/32 table kevag || true
        post-up ip rule add to 212.7.174.82/32 table kevag || true
        post-down ip rule flush table kevag || true

auto ens20
iface ens20 inet static
        address 192.168.222.222/24
        post-up ip route add 192.168.222.0/24 dev ens20 src 192.168.222.222 table dtag || true
        post-up ip route add default via 192.168.222.1 dev ens20 table dtag || true
        post-up ip rule add from 192.168.222.222/32 table dtag || true
        post-up ip rule add to 192.168.222.222/32 table dtag || true
        post-up ip rule add to 88.198.237.218/32 ipproto udp dport 51822 table dtag || true
        post-down ip rule flush table dtag || true
