# network.sh — LAN isolation via iptables IPv4 and IPv6 rules.
# shellcheck shell=bash
# Requires: warn, die (from helpers.sh).
# Runs in foreground (Group C) while Groups A and B run in background.
# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.

# _setup_network_isolation: block RFC 1918 LAN ranges, allow Docker-internal subnets
# and public internet. Batch mode via iptables-restore with individual-call fallback.
# The iptables batch helpers are nested to close over the local subnet arrays.
_setup_network_isolation() {
    if ! command -v iptables >/dev/null 2>&1; then
        die "iptables not available — refusing to start without network isolation"
    fi

    # Collect Docker-internal subnets for ACCEPT rules
    local _docker_subnets=()
    local CIDR _o1 _o2 _o3 _o4 _pfx _prefix
    while IFS= read -r CIDR; do
        if ! [[ "$CIDR" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]]; then
            warn "skipping invalid Docker subnet: $CIDR"
            continue
        fi
        # Validate each octet is <=255
        IFS='./' read -r _o1 _o2 _o3 _o4 _pfx <<< "$CIDR"
        if [ "$_o1" -gt 255 ] || [ "$_o2" -gt 255 ] || [ "$_o3" -gt 255 ] || [ "$_o4" -gt 255 ]; then
            warn "skipping Docker subnet with invalid octet: $CIDR"
            continue
        fi
        _prefix="${CIDR##*/}"
        if [ "$_prefix" = "" ] || ! [[ "$_prefix" =~ ^[0-9]+$ ]]; then
            warn "skipping Docker subnet ${CIDR} (non-numeric prefix '${_prefix}')"
            continue
        fi
        if [ "$_prefix" -ge 16 ]; then
            _docker_subnets+=("$CIDR")
        else
            warn "skipping broad Docker subnet ${CIDR} (prefix < /16)"
        fi
    done < <(ip -4 route | awk '!/default/ && /src/ {print $1}')

    # Nested helper: batch mode via iptables-restore (single kernel transition).
    # --noflush preserves Docker's own bridge/NAT rules.
    # Uses the batch itself as probe for REJECT support — failed batch applies zero rules.
    _iptables_v4_batch() {
        local _reject="$1" _rf
        _rf="$(mktemp /tmp/.devcage-ipt4-XXXXXXXX)" || return 1
        {
            printf '*filter\n'
            printf -- '-A OUTPUT -o lo -d 127.0.0.0/8 -j ACCEPT\n'
            local _subnet
            for _subnet in "${_docker_subnets[@]}"; do
                printf -- '-A OUTPUT -d %s -j ACCEPT\n' "$_subnet"
            done
            printf -- '-A OUTPUT -d 10.0.0.0/8 -j %s\n' "$_reject"
            printf -- '-A OUTPUT -d 172.16.0.0/12 -j %s\n' "$_reject"
            printf -- '-A OUTPUT -d 192.168.0.0/16 -j %s\n' "$_reject"
            printf -- '-A OUTPUT -d 169.254.0.0/16 -j %s\n' "$_reject"
            printf -- '-A OUTPUT -d 100.64.0.0/10 -j %s\n' "$_reject"
            printf -- '-A OUTPUT -d 198.18.0.0/15 -j %s\n' "$_reject"
            printf 'COMMIT\n'
        } > "$_rf"
        if iptables-restore --noflush < "$_rf" 2>/dev/null; then
            rm -f "$_rf"; return 0
        else
            rm -f "$_rf"; return 1
        fi
    }

    local _v4_applied=0
    if command -v iptables-restore >/dev/null 2>&1; then
        local _try
        for _try in "REJECT --reject-with icmp-net-unreachable" "REJECT" "DROP"; do
            if _iptables_v4_batch "$_try"; then
                _v4_applied=1; break
            fi
        done
    fi

    if [ "$_v4_applied" = "0" ]; then
        # Fallback: iptables-restore broken or unavailable. Probe + individual calls.
        local _REJECT_ACTION
        _reject_action() {
            if iptables -A OUTPUT -d 192.0.2.0/24 -j REJECT --reject-with icmp-net-unreachable 2>/dev/null; then
                iptables -D OUTPUT -d 192.0.2.0/24 -j REJECT --reject-with icmp-net-unreachable 2>/dev/null
                echo "REJECT --reject-with icmp-net-unreachable"
            elif iptables -A OUTPUT -d 192.0.2.0/24 -j REJECT 2>/dev/null; then
                iptables -D OUTPUT -d 192.0.2.0/24 -j REJECT 2>/dev/null
                echo "REJECT"
            else
                echo "DROP"
            fi
        }
        read -ra _REJECT_ACTION < <(_reject_action)
        iptables -A OUTPUT -o lo -d 127.0.0.0/8 -j ACCEPT
        local _subnet
        for _subnet in "${_docker_subnets[@]}"; do
            iptables -A OUTPUT -d "$_subnet" -j ACCEPT
        done
        iptables -A OUTPUT -d 10.0.0.0/8 -j "${_REJECT_ACTION[@]}"
        iptables -A OUTPUT -d 172.16.0.0/12 -j "${_REJECT_ACTION[@]}"
        iptables -A OUTPUT -d 192.168.0.0/16 -j "${_REJECT_ACTION[@]}"
        iptables -A OUTPUT -d 169.254.0.0/16 -j "${_REJECT_ACTION[@]}"
        iptables -A OUTPUT -d 100.64.0.0/10 -j "${_REJECT_ACTION[@]}"
        iptables -A OUTPUT -d 198.18.0.0/15 -j "${_REJECT_ACTION[@]}"
    fi

    # Block IPv6 LAN ranges — early-out when no global IPv6 (typical on Docker Desktop for Windows).
    # Mirrors the IPv4 model: block well-known non-public ranges,
    # allow everything else (including public IPv6 internet).
    local _has_global_ipv6=0
    if ip -6 addr show scope global 2>/dev/null | grep -q inet6; then
        _has_global_ipv6=1
    fi

    if [ "$_has_global_ipv6" = "0" ]; then
        # No global IPv6 — try to disable; no ip6tables needed for most cases
        if { echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6; } 2>/dev/null; then
            : # disabled via sysctl
        elif ip -6 addr show 2>/dev/null | grep -q 'inet6 fe80'; then
            # Link-local present — still block for defense-in-depth if ip6tables available
            if command -v ip6tables >/dev/null 2>&1; then
                _has_global_ipv6=1  # reuse the ip6tables setup path below
            else
                warn "ip6tables not available — IPv6 link-local (fe80::) present but unblocked; neighbor discovery possible (low risk, LAN unicast traffic is blocked on IPv4)"
            fi
        fi
    fi

    if [ "$_has_global_ipv6" = "1" ]; then
        if command -v ip6tables >/dev/null 2>&1; then
            # Collect Docker-internal IPv6 subnets
            local _docker_v6_subnets=()
            local _v6_prefix
            while IFS= read -r CIDR; do
                if [[ "$CIDR" == *:*/* ]]; then
                    _v6_prefix="${CIDR##*/}"
                    if [[ "$_v6_prefix" =~ ^[0-9]+$ ]] && [ "$_v6_prefix" -ge 48 ]; then
                        _docker_v6_subnets+=("$CIDR")
                    else
                        warn "skipping broad Docker IPv6 subnet ${CIDR} (prefix < /48)"
                    fi
                fi
            done < <(ip -6 route 2>/dev/null | awk '!/default/ && /dev/ {print $1}' | grep -v '^fe80' | grep -v '^::1')

            # Nested helper: batch mode with retry — same pattern as IPv4.
            _iptables_v6_batch() {
                local _reject="$1" _rf
                _rf="$(mktemp /tmp/.devcage-ipt6-XXXXXXXX)" || return 1
                {
                    printf '*filter\n'
                    printf -- '-A OUTPUT -o lo -d ::1/128 -j ACCEPT\n'
                    local _subnet
                    for _subnet in "${_docker_v6_subnets[@]}"; do
                        printf -- '-A OUTPUT -d %s -j ACCEPT\n' "$_subnet"
                    done
                    printf -- '-A OUTPUT -d fc00::/7 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d fe80::/10 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d ::ffff:10.0.0.0/104 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d ::ffff:172.16.0.0/108 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d ::ffff:192.168.0.0/112 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d ::ffff:169.254.0.0/112 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d ::ffff:100.64.0.0/106 -j %s\n' "$_reject"
                    printf -- '-A OUTPUT -d ::ffff:198.18.0.0/111 -j %s\n' "$_reject"
                    printf 'COMMIT\n'
                } > "$_rf"
                if ip6tables-restore --noflush < "$_rf" 2>/dev/null; then
                    rm -f "$_rf"; return 0
                else
                    rm -f "$_rf"; return 1
                fi
            }

            local _v6_applied=0
            if command -v ip6tables-restore >/dev/null 2>&1; then
                for _try in "REJECT" "DROP"; do
                    if _iptables_v6_batch "$_try"; then
                        _v6_applied=1; break
                    fi
                done
            fi

            if [ "$_v6_applied" = "0" ]; then
                # Fallback: ip6tables-restore broken or unavailable. Probe + individual calls.
                local _v6_reject="DROP"
                if ip6tables -A OUTPUT -d fc00::/7 -j REJECT 2>/dev/null; then
                    ip6tables -D OUTPUT -d fc00::/7 -j REJECT 2>/dev/null
                    _v6_reject="REJECT"
                fi
                ip6tables -A OUTPUT -o lo -d ::1/128 -j ACCEPT
                local _subnet
                for _subnet in "${_docker_v6_subnets[@]}"; do
                    ip6tables -A OUTPUT -d "$_subnet" -j ACCEPT 2>/dev/null || true
                done
                ip6tables -A OUTPUT -d fc00::/7 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d fe80::/10 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d ::ffff:10.0.0.0/104 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d ::ffff:172.16.0.0/108 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d ::ffff:192.168.0.0/112 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d ::ffff:169.254.0.0/112 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d ::ffff:100.64.0.0/106 -j "$_v6_reject" 2>/dev/null || true
                ip6tables -A OUTPUT -d ::ffff:198.18.0.0/111 -j "$_v6_reject" 2>/dev/null || true
            fi
            # Public IPv6 (2000::/3) is allowed — matching IPv4's default-allow model.
        else
            die "ip6tables not available, but container has global IPv6 — refusing to start"
        fi
    fi
}
