scripts/vpn-samba.sh
#!/bin/sh
# Made by Jack'lul <jacklul.github.io>
#
# Allow VPN clients to access Samba shares in LAN without reconfiguring each device
#

#jas-update=vpn-samba.sh
#shellcheck shell=ash
#shellcheck disable=SC2155

#shellcheck source=./common.sh
readonly common_script="$(dirname "$0")/common.sh"
if [ -f "$common_script" ]; then . "$common_script"; else { echo "$common_script not found" >&2; exit 1; } fi

VPN_NETWORKS="" # VPN networks (IPv4) to allow access to Samba from, in format '10.6.0.0/24', separated by spaces, empty means auto detect
VPN_NETWORKS6="" # same as VPN_NETWORKS but for IPv6, separated by spaces, no auto detect available
LAN_NETWORK="" # IPv4 LAN network, in format '192.168.0.0/24', empty means auto detect
LAN_NETWORK6="" # IPv6 LAN network, in format 'fd12:3456:789a::/48', if left empty then IPv6 connections will not be handled
BRIDGE_INTERFACE="" # the bridge interface to set rules for, empty means set to LAN bridge interface
USE_LEGACY_PORTS=false # also use legacy NetBIOS ports 137-139 (true/false), set to true only if you really need them
EXECUTE_COMMAND="" # execute a command after firewall rules are applied or removed (receives arguments: $1 = action - add/remove)
RUN_EVERY_MINUTE= # verify that the rules are still set (true/false), empty means false when service-event script is available but otherwise true
RETRY_ON_ERROR=false # retry setting the rules on error (once per run)

load_script_config

readonly CHAIN="jas-${script_name}"

firewall_rules() {
    if { [ -z "$VPN_NETWORKS" ] && [ -z "$VPN_NETWORKS6" ] ; }; then
        if [ "$(nvram get wgs_enable)" = "1" ]; then
            local _wgs_addr="$(nvram get wgs_addr)" # WireGuard - 10.6.0.1/32

            if [ -n "$_wgs_addr" ]; then
                VPN_NETWORKS="$VPN_NETWORKS $(echo "$_wgs_addr" | cut -d '.' -f -3).0/24"
            fi
        fi

        if [ "$(nvram get vpn_server_state)" = "2" ]; then
            local _vpn_server_sn="$(nvram get vpn_server_sn)" # OpenVPN - 10.8.0.0
            local _vpn_server_nm="$(nvram get vpn_server_nm)" # OpenVPN - 255.255.255.0

            if [ -n "$_vpn_server_sn" ] && [ -n "$_vpn_server_nm" ]; then
                local _openvpn_network="$_vpn_server_sn/$(mask_to_cidr "$_vpn_server_nm")"
                [ -n "$_openvpn_network" ] && VPN_NETWORKS="$VPN_NETWORKS $_openvpn_network"
            fi
        fi

        if [ "$(nvram get ipsec_server_enable)" = "1" ]; then
            local _ipsec_profile=s"$(nvram get ipsec_profile_1)" # IPSec - after 14th '>' is 10.10.10

            if [ -n "$_ipsec_profile" ]; then
                VPN_NETWORKS="$VPN_NETWORKS $(echo "$_ipsec_profile" | cut -d '>' -f 15).0/24"
            fi
        fi

        # PPTP has build-in toggle for Samba access, so this is not needed
        #if [ "$(nvram get pptpd_enable)" = "1" ]; then
        #    local _pptpd_clients="$(nvram get pptpd_clients)"
        #
        #    if [ -n "$_pptpd_clients" ]; then
        #        VPN_NETWORKS="$VPN_NETWORKS $(echo "$_pptpd_clients" | cut -d '.' -f -3).0/24"
        #    fi
        #fi

        { [ -z "$VPN_NETWORKS" ] && [ -z "$VPN_NETWORKS6" ] ; } && return # silently exit
    fi

    [ -z "$BRIDGE_INTERFACE" ] && BRIDGE_INTERFACE="$(nvram get lan_ifname)"

    lockfile lockwait

    modprobe xt_comment

    local _for_iptables="iptables"
    [ "$(nvram get ipv6_service)" != "disabled" ] && _for_iptables="$_for_iptables ip6tables"

    local _iptables _rules_action _rules_error _vpn_networks _lan_network _lan_ipaddr _lan_netmask _cidr _network _vpn_network

    for _iptables in $_for_iptables; do
        if [ "$_iptables" = "ip6tables" ]; then
            _vpn_networks="$VPN_NETWORKS6"
            _lan_network="$LAN_NETWORK6"
        else
            _vpn_networks="$VPN_NETWORKS"
            _lan_network="$LAN_NETWORK"

            if [ -z "$_lan_network" ]; then
                _lan_ipaddr="$(nvram get lan_ipaddr)"
                _lan_netmask="$(nvram get lan_netmask)"
                _cidr="$(mask_to_cidr "$_lan_netmask")"
                _network="$(calculate_network "$_lan_ipaddr" "$_lan_netmask")"

                { [ -z "$_cidr" ] || [ -z "$_network" ] ; } && { logecho "Error: Failed to calculate destination IPv4 network" error; exit 1; }

                _lan_network="$_network/$_cidr"
            fi
        fi

        [ -z "$_vpn_networks" ] && continue
        [ -z "$_lan_network" ] && continue

        case "$1" in
            "add")
                if ! $_iptables -t nat -nL "$CHAIN" > /dev/null 2>&1; then
                    $_iptables -t nat -N "$CHAIN"
                    $_iptables -t nat -A "$CHAIN" -p tcp --dport 445 -j MASQUERADE # SMB

                    if [ "$USE_LEGACY_PORTS" = true ]; then # NetBIOS, legacy ports
                        $_iptables -t nat -A "$CHAIN" -p tcp --dport 139 -j MASQUERADE
                        $_iptables -t nat -A "$CHAIN" -p udp --dport 138 -j MASQUERADE
                        $_iptables -t nat -A "$CHAIN" -p udp --dport 137 -j MASQUERADE
                    fi

                    for _vpn_network in $_vpn_networks; do
                        $_iptables -t nat -A POSTROUTING -s "$_vpn_network" -d "$_lan_network" -o "$BRIDGE_INTERFACE" -j "$CHAIN" \
                            -m comment --comment "jas-$script_name" \
                                && _rules_action=1 || _rules_error=1
                    done
                fi
            ;;
            "remove")
                remove_iptables_rules_by_comment "$_iptables" "nat" && _rules_action=-1

                if $_iptables -t nat -nL "$CHAIN" > /dev/null 2>&1; then
                    $_iptables -t nat -F "$CHAIN"
                    $_iptables -t nat -X "$CHAIN" || _rules_error=1
                fi
            ;;
        esac
    done

    [ "$_rules_error" = 1 ] && logecho "Errors detected while modifying firewall rules ($1)" error

    if [ -n "$_rules_action" ]; then
        if [ "$_rules_action" = 1 ]; then
            logecho "Masquerading Samba connections coming from VPN networks: $(echo "$VPN_NETWORKS $VPN_NETWORKS6" | awk '{$1=$1};1')" alert
        else
            logecho "Stopped masquerading Samba connections coming from VPN networks: $(echo "$VPN_NETWORKS $VPN_NETWORKS6" | awk '{$1=$1};1')" alert
        fi
    fi

    [ -n "$EXECUTE_COMMAND" ] && [ -n "$_rules_action" ] && eval "$EXECUTE_COMMAND $1"

    lockfile unlock
    [ -z "$_rules_error" ] && return 0 || return 1
}

case "$1" in
    "run")
        firewall_rules add || { [ "$RETRY_ON_ERROR" = true ] && firewall_rules add; }
    ;;
    "start")
        firewall_rules add

        # Set value of empty RUN_EVERY_MINUTE depending on situation
        execute_script_basename "service-event.sh" check && service_event_active=true
        [ -z "$RUN_EVERY_MINUTE" ] && [ -z "$service_event_active" ] && RUN_EVERY_MINUTE=true

        if [ "$RUN_EVERY_MINUTE" = true ]; then
            crontab_entry add "*/1 * * * * $script_path run"
        fi
    ;;
    "stop")
        crontab_entry delete
        firewall_rules remove
    ;;
    "restart")
        sh "$script_path" stop
        sh "$script_path" start
    ;;
    *)
        echo "Usage: $0 run|start|stop|restart"
        exit 1
    ;;
esac
