#!/bin/sh

# the following environment variables are set:
# - 'ACTION' - 'up' or 'down'
# - 'TUN' - tunnel device name
# - 'SETUP_MODE' is 'full_script' or 'firewall_script'
# - 'FWMARK_PROTECTED' - packets allready inspected by network protection
# - 'FWMARK_TUN' - packets that shall enter tun device for inspection
# - 'ROUTING_TABLE_ID' - packets with FWMARK_TUN are routed into this table. When 'SETUP_MODE' is 'full_script' this creates an entry under /etc/iproute2/rt_tables
# - 'ALTERNATE_FIREWALL' - on some systems this variable is set to yes when the nftables lacks certain features.
# When mode is 'simple' or 'default' and network protection fails to configure the firewall via nftables this script
# is called with ALTERNATE_FIREWALL set to 'iptables'
# - 'GROUP_ID' - protected group
# - 'HAVE_IPV6' - configure with IPV6 support
# - "TCP_DSTPORT_BYPASS" - a comma separated list of destination tcp ports to skip
# - "UDP_DSTPORT_BYPASS" - a comma separated list of destination tcp ports to skip

set -e
set -x

oif=$(ls /sys/class/net/ | grep -E "en|eth|wl")
# a list of comma separated network interface names (ex: oif='"eth0","eth1"')
oifset=$(echo $oif | xargs printf '"%s"\n' | paste -s -d ",")

setup_fwbins()
{
    case $ALTERNATE_FIREWALL in
        iptables)
            fw=iptables
            ipt4=`which iptables`
            if [ "$HAVE_IPV6" = "yes" ]; then
                ipt6=`which ip6tables`
            fi
            ;;
        *)            
            fw=nft
            nftcmd=`which nft`
            ;;
    esac    
}

# these are for nftables that have support for inet chain.
# not currently used but available in no-script modes
do_fw_up_nft_inet()
{
    $nftcmd delete table inet wdnetp 2>/dev/null || true
    $nftcmd add table inet wdnetp
    $nftcmd add set inet wdnetp oifset '{ type ifname;}'
    $nftcmd add element inet wdnetp oifset {$oifset}    
    $nftcmd 'add chain inet wdnetp vpnbr { type route hook output priority 0 ; }'
    $nftcmd add rule inet wdnetp vpnbr meta skgid $GROUP_ID return
    $nftcmd add rule inet wdnetp vpnbr meta mark $FWMARK_PROTECTED return
    if [ ! x"$TCP_DSTPORT_BYPASS" = x ]; then
        $nftcmd add rule inet wdnetp vpnbr "tcp dport {$TCP_DSTPORT_BYPASS} return"
    fi
    if [ ! x"$UDP_DSTPORT_BYPASS" = x ]; then
        $nftcmd add rule inet wdnetp vpnbr "udp dport {$UDP_DSTPORT_BYPASS} return"
    fi
    
    $nftcmd add rule inet wdnetp vpnbr "oifname @oifset ip protocol udp ct mark set $FWMARK_TUN counter"
    if [ x"$ENABLE_ICMP" = x"yes" ]; then
        $nftcmd add rule inet wdnetp vpnbr "oifname @oifset ip protocol icmp ct mark set $FWMARK_TUN counter"
        $nftcmd add rule inet wdnetp vpnbr "oifname @oifset ip6 nexthdr icmpv6 ct mark set $FWMARK_TUN counter"
    fi
    $nftcmd add rule inet wdnetp vpnbr "oifname @oifset tcp flags == syn ct mark set $FWMARK_TUN counter"
    $nftcmd add rule inet wdnetp vpnbr "oifname @oifset meta mark set ct mark"
}

do_fw_down_nft_inet()
{
    $nftcmd delete table inet wdnetp 2>/dev/null || true
}

# default configure ip and ip4 separate chains. (ex: RHEL8.1)
do_fw_up_nft_ip()
{
    $nftcmd delete table ip wdnetp 2>/dev/null || true
    $nftcmd add table ip wdnetp
    $nftcmd add set ip wdnetp oifset '{ type ifname;}'
    $nftcmd add element ip wdnetp oifset {$oifset}    
    $nftcmd add chain ip 'wdnetp vpnbr { type route hook output priority 0 ; }'
    $nftcmd add rule ip wdnetp vpnbr meta skgid $GROUP_ID return
    $nftcmd add rule ip wdnetp vpnbr meta mark $FWMARK_PROTECTED return
    if [ ! x"$TCP_DSTPORT_BYPASS" = x ]; then
        $nftcmd add rule ip wdnetp vpnbr "tcp dport {$TCP_DSTPORT_BYPASS} return"
    fi
    if [ ! x"$UDP_DSTPORT_BYPASS" = x ]; then
        $nftcmd add rule ip wdnetp vpnbr "udp dport {$UDP_DSTPORT_BYPASS} return"
    fi
    
    $nftcmd add rule ip wdnetp vpnbr "oifname @oifset ip protocol udp ct mark set $FWMARK_TUN counter"
    if [ x"$ENABLE_ICMP" = x"yes" ]; then
        $nftcmd add rule ip wdnetp vpnbr "oifname @oifset ip protocol icmp ct mark set $FWMARK_TUN counter"
    fi
    $nftcmd add rule ip wdnetp vpnbr "oifname @oifset tcp flags == syn ct mark set $FWMARK_TUN counter"
    $nftcmd add rule ip wdnetp vpnbr "oifname @oifset meta mark set ct mark"
}

do_fw_up_nft_ip6()
{
    $nftcmd delete table ip6 wdnetp 2>/dev/null || true
    $nftcmd add table ip6 wdnetp
    $nftcmd add set ip6 wdnetp oifset '{ type ifname;}'
    $nftcmd add element ip6 wdnetp oifset {$oifset}    
    $nftcmd add chain ip6 'wdnetp vpnbr { type route hook output priority 0 ; }'
    $nftcmd add rule ip6 wdnetp vpnbr meta skgid $GROUP_ID return
    $nftcmd add rule ip6 wdnetp vpnbr meta mark $FWMARK_PROTECTED return
    if [ ! x"$TCP_DSTPORT_BYPASS" = x ]; then
        $nftcmd add rule ip6 wdnetp vpnbr "tcp dport {$TCP_DSTPORT_BYPASS} return"
    fi
    if [ ! x"$UDP_DSTPORT_BYPASS" = x ]; then
        $nftcmd add rule ip6 wdnetp vpnbr "udp dport {$UDP_DSTPORT_BYPASS} return"
    fi
    
    $nftcmd add rule ip6 wdnetp vpnbr "oifname @oifset ip6 nexthdr udp ct mark set $FWMARK_TUN counter"
    if [ x"$ENABLE_ICMP" = x"yes" ]; then
        $nftcmd add rule ip6 wdnetp vpnbr "oifname @oifset ip6 nexthdr icmpv6 ct mark set $FWMARK_TUN counter"
    fi
    $nftcmd add rule ip6 wdnetp vpnbr "oifname @oifset tcp flags == syn ct mark set $FWMARK_TUN counter"
    $nftcmd add rule ip6 wdnetp vpnbr "oifname @oifset meta mark set ct mark"
}

do_fw_down_nft_ip()
{
    $nftcmd delete table ip wdnetp 2>/dev/null || true
    if [ "$HAVE_IPV6" = "yes" ]; then
        $nftcmd delete table ip6 wdnetp 2>/dev/null || true
    fi
}

fw_up_nft()
{
    do_fw_up_nft_ip
    if [ "$HAVE_IPV6" = "yes" ]; then
        do_fw_up_nft_ip6
    fi
}

fw_down_nft()
{
    do_fw_down_nft_ip
}

do_fw_up_iptables()
{
    ipt=$1
    chain=$2
    
    do_fw_down_iptables $ipt $chain

    $ipt -N $chain -t mangle
    $ipt -A OUTPUT -t mangle -j $chain
    $ipt -A $chain -t mangle -m owner --gid-owner $GROUP_ID -j RETURN
    $ipt -A $chain -t mangle  -m mark --mark $FWMARK_PROTECTED -j RETURN
    if [ ! x"$TCP_DSTPORT_BYPASS" = x ]; then
        $ipt -A $chain -t mangle  -p tcp --match multiport --dports $TCP_DSTPORT_BYPASS -j RETURN
    fi
    if [ ! x"$UDP_DSTPORT_BYPASS" = x ]; then
        $ipt -A $chain -t mangle  -p udp --match multiport --dports $UDP_DSTPORT_BYPASS -j RETURN
    fi
    
    for eth in $oif; do
        $ipt -A $chain -t mangle -o $eth -p udp  -j CONNMARK --set-mark $FWMARK_TUN
        if [ x"$ENABLE_ICMP" = x"yes" ]; then
            $ipt -A $chain -t mangle -o $eth -p icmp  -j CONNMARK --set-mark $FWMARK_TUN
        fi
        $ipt -A $chain -t mangle -o $eth -p tcp --syn -j CONNMARK --set-mark $FWMARK_TUN
        $ipt -A $chain -t mangle -o $eth -j CONNMARK --restore-mark
    done
    $ipt -A $chain -t mangle -j RETURN

}

do_fw_down_iptables()
{
    ipt=$1
    chain=$2

    if $ipt -L $chain -t mangle; then
        $ipt -D OUTPUT -t mangle -j $chain || true
        $ipt -t mangle --flush $chain
        $ipt -t mangle -X $chain
    fi
}

fw_up_iptables()
{
    do_fw_up_iptables $ipt4 out_wdnetp4

    if [ "$HAVE_IPV6" = "yes" ]; then
        do_fw_up_iptables $ipt6 out_wdnetp6
    fi
}

fw_down_iptables()
{
    do_fw_down_iptables $ipt4 out_wdnetp4

    if [ "$HAVE_IPV6" = "yes" ]; then
        do_fw_down_iptables $ipt6 out_wdnetp6
    fi
}

tun_up()
{
    ip address add 10.9.0.1/24 dev $TUN
    if [ "$HAVE_IPV6" = "yes" ]; then
        ip address add fd00::0/64 dev $TUN
    fi
    ip link set dev $TUN up
}

tun_down()
{
    :
}

# make sure the /etc/iproute2/rt_tables has a custom entry named wdnissrv
rt_tables_fix()
{
    if  ! grep -q -E '[[:space:]]+wdnissrv$' /etc/iproute2/rt_tables ; then
        printf "%d\twdnissrv\n" $ROUTING_TABLE_ID >> /etc/iproute2/rt_tables
    fi
}

iprules_clean()
{
    for r in $(ip rule show table $ROUTING_TABLE_ID | awk '{ print $1;}' | cut -d ':' -f 1); do
        ip rule del pref $r
    done
    if [ "$HAVE_IPV6" = "yes" ]; then
        for r in $(ip -6 rule show table $ROUTING_TABLE_ID | awk '{ print $1;}' | cut -d ':' -f 1); do
            ip -6 rule del pref $r
        done
    fi
}

iprules_up()
{
    iprules_clean
    ip rule add fwmark $FWMARK_TUN table wdnissrv
    ip route add default dev $TUN table wdnissrv
    ip route flush cache
    if [ "$HAVE_IPV6" = "yes" ]; then
        ip -6 rule add fwmark $FWMARK_TUN table wdnissrv
        ip -6 route add default dev $TUN table wdnissrv
        ip -6 route flush cache
    fi
}

iprules_down()
{
    iprules_clean
}

up()
{
    # if in 'full_script' mode setup the tun device and routing rules
    if [ "$SETUP_MODE" = "full_script" ]; then
        tun_up
        rt_tables_fix
        iprules_up
    fi
    fw_up_${fw}
}

down()
{
    fw_down_${fw}
    
    if [ "$SETUP_MODE" = "full_script" ]; then
        iprules_down
        tun_down
    fi
}

fwreset()
{
    fw_down_${fw}
    fw_up_${fw}
}

echo "ACTION               = $ACTION"
echo "SETUP_MODE           = $SETUP_MODE"
echo "TUN                  = $TUN"
echo "ALTERNATE_FIREWALL   = $ALTERNATE_FIREWALL"
echo "FWMARK_PROTECTED     = $FWMARK_PROTECTED"
echo "FWMARK_TUN           = $FWMARK_TUN"
echo "ROUTING_TABLE_ID     = $ROUTING_TABLE_ID"
echo "GROUP_ID             = $GROUP_ID"
echo "NFT_MODE             = $NFT_MODE"
echo "HAVE_IPV6            = $HAVE_IPV6"
echo "TCP_DSTPORT_BYPASS   = $TCP_DSTPORT_BYPASS"
echo "UDP_DSTPORT_BYPASS   = $UDP_DSTPORT_BYPASS"

setup_fwbins

case "$ACTION" in
    up)
        up
        ;;
    down)
        down
        ;;
    fwreset)
        fwreset
        ;;
    *)
        exit 1
esac

echo "DONE $ACTION"
