WireGuard client setup script with policy based routing

wg-client-setup-v1.0.0 [lines 529-1061/1061]
# Restore the connection mark onto packets generated by dnsmasq (more specific)
iptables -t mangle -C OUTPUT -p udp --sport 53 -j CONNMARK --restore-mark 2>/dev/null || iptables -t mangle -A OUTPUT -p udp --sport 53 -j CONNMARK --restore-mark
iptables -t mangle -C OUTPUT -p tcp --sport 53 -j CONNMARK --restore-mark 2>/dev/null || iptables -t mangle -A OUTPUT -p tcp --sport 53 -j CONNMARK --restore-mark
ip6tables -t mangle -C OUTPUT -p udp --sport 53 -j CONNMARK --restore-mark 2>/dev/null || ip6tables -t mangle -A OUTPUT -p udp --sport 53 -j CONNMARK --restore-mark
ip6tables -t mangle -C OUTPUT -p tcp --sport 53 -j CONNMARK --restore-mark 2>/dev/null || ip6tables -t mangle -A OUTPUT -p tcp --sport 53 -j CONNMARK --restore-mark

# DNS backstop to allow local responses (IPv4)
logger -t wireguard "[$WG_INTERFACE] Adding IPv4 DNS backstop to block leaks."
iptables -t mangle -N $DNS_BLOCK_CHAIN_v4 2>/dev/null
iptables -t mangle -F $DNS_BLOCK_CHAIN_v4
# Only apply this chain to marked packets
iptables -t mangle -C OUTPUT -m mark --mark $MARK_VALUE -j $DNS_BLOCK_CHAIN_v4 2>/dev/null || iptables -t mangle -A OUTPUT -m mark --mark $MARK_VALUE -j $DNS_BLOCK_CHAIN_v4
# Inside the chain:
# Allow if it's correctly going out the WG interface
iptables -t mangle -A $DNS_BLOCK_CHAIN_v4 -o $WG_INTERFACE -j RETURN
# Allow responses back to the VPN client
iptables -t mangle -A $DNS_BLOCK_CHAIN_v4 -m set --match-set $IPSET_NAME dst -j RETURN
# Log before dropping
iptables -t mangle -A $DNS_BLOCK_CHAIN_v4 -p udp --dport 53 -j LOG --log-prefix "DROP_DNS_LEAK_v4: " --log-level 7
iptables -t mangle -A $DNS_BLOCK_CHAIN_v4 -p tcp --dport 53 -j LOG --log-prefix "DROP_DNS_LEAK_v4: " --log-level 7
# Drop any other DNS packet (leaks)
iptables -t mangle -A $DNS_BLOCK_CHAIN_v4 -p udp --dport 53 -j DROP
iptables -t mangle -A $DNS_BLOCK_CHAIN_v4 -p tcp --dport 53 -j DROP
# DNS backstop to allow local responses (IPv6)
logger -t wireguard "[$WG_INTERFACE] Adding IPv6 DNS backstop to block leaks."
ip6tables -t mangle -N $DNS_BLOCK_CHAIN_v6 2>/dev/null
ip6tables -t mangle -F $DNS_BLOCK_CHAIN_v6
# Only apply this chain to marked packets
ip6tables -t mangle -C OUTPUT -m mark --mark $MARK_VALUE -j $DNS_BLOCK_CHAIN_v6 2>/dev/null || ip6tables -t mangle -A OUTPUT -m mark --mark $MARK_VALUE -j $DNS_BLOCK_CHAIN_v6
# Inside the chain:
# Allow if it's correctly going out the WG interface
ip6tables -A $DNS_BLOCK_CHAIN_v6 -o $WG_INTERFACE -j RETURN
# Allow responses back to the VPN client (if ipset supports IPv6)
ip6tables -A $DNS_BLOCK_CHAIN_v6 -m set --match-set $IPSET_NAME dst -j RETURN 2>/dev/null || true
# Log before dropping
ip6tables -t mangle -A $DNS_BLOCK_CHAIN_v6 -p udp --dport 53 -j LOG --log-prefix "DROP_DNS_LEAK_v6: " --log-level 7
ip6tables -t mangle -A $DNS_BLOCK_CHAIN_v6 -p tcp --dport 53 -j LOG --log-prefix "DROP_DNS_LEAK_v6: " --log-level 7
# Drop any other DNS packet (leaks)
ip6tables -A $DNS_BLOCK_CHAIN_v6 -p udp --dport 53 -j DROP
ip6tables -A $DNS_BLOCK_CHAIN_v6 -p tcp --dport 53 -j DROP

# Add new ip rule to force all marked packets (incl. leaks) into the VPN table
ip rule del fwmark $MARK_VALUE table $ROUTING_TABLE 2>/dev/null
ip rule add fwmark $MARK_VALUE table $ROUTING_TABLE priority $((ROUTING_TABLE - 5))
ip -6 rule del fwmark $MARK_VALUE table $ROUTING_TABLE 2>/dev/null
ip -6 rule add fwmark $MARK_VALUE table $ROUTING_TABLE priority $((ROUTING_TABLE - 5))

# Discover and configure existing clients
logger -t wireguard "[$WG_INTERFACE] Scanning for existing clients and applying rules..."
PROCESSED_MACS=""
for item in $VPN_IPS; do
    case "$item" in
        */*) # This is a subnet
            ip rule add from $item table $ROUTING_TABLE priority $ROUTING_TABLE 2>/dev/null

            # 1. DHCP Leases (Primary)
            if [ -f /cfg/dhcp.leases ]; then
                while read -r exp mac ip host; do
                    if is_in_subnet "$ip" "$item" && ! echo "$PROCESSED_MACS" | grep -q "$mac"; then
                        PROCESSED_MACS="$PROCESSED_MACS $mac"
                        logger -t wireguard "[$WG_INTERFACE] Found existing client $ip ($mac) in $item via DHCP lease."
                        ip rule add from $ip table $ROUTING_TABLE priority $ROUTING_TABLE 2>/dev/null
                        if [ "$IPV6_SUPPORTED" = "1" ]; then
                            lan_ifaces=$(ip link show | grep -o 'br-lan[^:]*' | tr '\n' ' ')
                            [ -z "$lan_ifaces" ] && lan_ifaces="br-lan"
                            for lan_if in $lan_ifaces; do ip6tables -I $BLOCK_CHAIN 1 -i $lan_if ! -o $WG_INTERFACE -m mac --mac-source $mac -j DROP; done
                            logger -t wireguard "[$WG_INTERFACE] IPv6 internet blocked for $ip ($mac) until routing configured"
                            apply_ipv6_rules "$mac"
                        else
                            # IPv4-only tunnel: Block IPv6 for this client
                            logger -t wireguard "[$WG_INTERFACE] Blocking IPv6 for $ip ($mac) on IPv4-only tunnel."
                            ip6tables -A $BLOCK_IPV4_ONLY_CHAIN -m mac --mac-source $mac -j DROP
                        fi
                    fi
                done < /cfg/dhcp.leases
            fi

            # 2. ARP/NDP Table (Fallback)
            ip neigh show > /tmp/neigh_${WG_INTERFACE}_$$
            while read -r line; do
                ip=$(echo $line | awk '{print $1}')
                mac=$(echo $line | grep -o -E '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}')
                if [ -n "$mac" ] && is_in_subnet "$ip" "$item" && ! echo "$PROCESSED_MACS" | grep -q "$mac"; then
                    PROCESSED_MACS="$PROCESSED_MACS $mac"
                    logger -t wireguard "[$WG_INTERFACE] Found existing client $ip ($mac) in $item via ARP/NDP table."
                    ip rule add from $ip table $ROUTING_TABLE priority $ROUTING_TABLE 2>/dev/null
                    if [ "$IPV6_SUPPORTED" = "1" ]; then
                        lan_ifaces=$(ip link show | grep -o 'br-lan[^:]*' | tr '\n' ' ')
                        [ -z "$lan_ifaces" ] && lan_ifaces="br-lan"
                        for lan_if in $lan_ifaces; do ip6tables -I $BLOCK_CHAIN 1 -i $lan_if ! -o $WG_INTERFACE -m mac --mac-source $mac -j DROP; done
                        logger -t wireguard "[$WG_INTERFACE] IPv6 internet blocked for $ip ($mac) until routing configured"
                        apply_ipv6_rules "$mac"
                    else
                        # IPv4-only tunnel: Block IPv6 for this client
                        logger -t wireguard "[$WG_INTERFACE] Blocking IPv6 for $ip ($mac) on IPv4-only tunnel."
                        ip6tables -A $BLOCK_IPV4_ONLY_CHAIN -m mac --mac-source $mac -j DROP
                    fi
                fi
            done < /tmp/neigh_${WG_INTERFACE}_$$
            rm -f /tmp/neigh_${WG_INTERFACE}_$$
            ;;
        *) # This is an individual IP
            mac=$(ip neigh show "$item" | grep -o '[0-9a-f:]\{17\}' | head -1)
            if [ -n "$mac" ] && [ "$mac" != "<incomplete>" ] && ! echo "$PROCESSED_MACS" | grep -q "$mac"; then
                PROCESSED_MACS="$PROCESSED_MACS $mac"
                logger -t wireguard "[$WG_INTERFACE] Found existing client $item ($mac)."
                ip rule add from $item table $ROUTING_TABLE priority $ROUTING_TABLE 2>/dev/null
                if [ "$IPV6_SUPPORTED" = "1" ]; then
                    lan_ifaces=$(ip link show | grep -o 'br-lan[^:]*' | tr '\n' ' ')
                    [ -z "$lan_ifaces" ] && lan_ifaces="br-lan"
                    for lan_if in $lan_ifaces; do ip6tables -I $BLOCK_CHAIN 1 -i $lan_if ! -o $WG_INTERFACE -m mac --mac-source $mac -j DROP; done
                    logger -t wireguard "[$WG_INTERFACE] IPv6 internet blocked for $item ($mac) until routing configured"
                    apply_ipv6_rules "$mac"
                else
                    # IPv4-only tunnel: Block IPv6 for this client
                    logger -t wireguard "[$WG_INTERFACE] Blocking IPv6 for $item ($mac) on IPv4-only tunnel."
                    ip6tables -A $BLOCK_IPV4_ONLY_CHAIN -m mac --mac-source $mac -j DROP
                fi
            fi
            ;;
    esac
done

# Setup VPN DNS redirect if DNS servers are configured
if [ -n "$VPN_DNS" ]; then
    setup_secure_dns "$VPN_DNS" "$VPN_IPS" "$WG_INTERFACE"
fi

# ALWAYS block non-VPN DNS from leaking through VPN tunnel (regardless of VPN DNS config)
# This is the global DNS isolation chain.
ISOLATION_CHAIN="vpn_dns_isolate_${WG_INTERFACE}"
iptables -N $ISOLATION_CHAIN 2>/dev/null
iptables -F $ISOLATION_CHAIN 2>/dev/null

# Allow VPN clients' DNS through
for item in $VPN_IPS; do
    iptables -A $ISOLATION_CHAIN -s $item -j RETURN
done

# Block all other DNS from exiting via VPN
iptables -A $ISOLATION_CHAIN -o $WG_INTERFACE -p udp --dport 53 -j REJECT --reject-with icmp-port-unreachable
iptables -A $ISOLATION_CHAIN -o $WG_INTERFACE -p tcp --dport 53 -j REJECT --reject-with tcp-reset

# Insert into FORWARD chain if not already there
iptables -C FORWARD -j $ISOLATION_CHAIN 2>/dev/null || iptables -I FORWARD 1 -j $ISOLATION_CHAIN

logger -t wireguard "[$WG_INTERFACE] DNS isolation active - blocking non-VPN DNS from leaking through tunnel"

ip route flush cache; ip -6 route flush cache 2>/dev/null

# Add a final dnsmasq reload to ensure it binds to the ipset
logger -t wireguard "[$WG_INTERFACE] Performing guaranteed dnsmasq reload."
( /etc/init.d/dnsmasq reload >/dev/null 2>&1 ) &

EOF_IFACE_ROUTING
sed -i "s|INTERFACE_NAME_PLACEHOLDER|$INTERFACE_NAME|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing
sed -i "s|ROUTING_TABLE_PLACEHOLDER|$ROUTING_TABLE|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing
sed -i "s|VPN_IPS_PLACEHOLDER|$VPN_IPS|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing
sed -i "s|IPV6_SUPPORTED_PLACEHOLDER|$IPV6_SUPPORTED|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing
sed -i "s|VPN_DNS_PLACEHOLDER|$VPN_DNS_SERVERS|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing
sed -i "s|IPSET_NAME_PLACEHOLDER|$IPSET_NAME|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing
chmod +x /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing

# Create DHCP hotplug script
if [ ! -f "$DHCP_HOTPLUG_SCRIPT" ] || [ "$FORCE_RECONFIG" -eq 1 ]; then
    echo "Creating/Updating DHCP hotplug script: $DHCP_HOTPLUG_SCRIPT"
    cat > "$DHCP_HOTPLUG_SCRIPT" << 'EOF_DHCP_PBR'
#!/bin/sh
WG_INTERFACE="INTERFACE_NAME_PLACEHOLDER"
ROUTING_TABLE="ROUTING_TABLE_PLACEHOLDER"
VPN_IPS="VPN_IPS_PLACEHOLDER"
IPV6_SUPPORTED="IPV6_SUPPORTED_PLACEHOLDER"
KS_CHAIN="${WG_INTERFACE}_killswitch"
BLOCK_CHAIN="${WG_INTERFACE}_ipv6_block"
BLOCK_IPV4_ONLY_CHAIN="${WG_INTERFACE}_ipv4_only_block"

ip_to_int() {
    local a b c d; IFS=. read -r a b c d <<EOF
$1
EOF
    echo "$(( (a << 24) | (b << 16) | (c << 8) | d ))"
}

is_in_list() {
    local ip_to_check="$1"; local list="$2"; local ip_int
    ip_int=$(ip_to_int "$ip_to_check")
    for item in $list; do
        case "$item" in
            */*)
                local subnet prefix network_int mask
                subnet="${item%/*}"; prefix="${item#*/}"
                network_int=$(ip_to_int "$subnet"); i=0; mask=0
                while [ $i -lt $prefix ]; do
                    mask=$(( (mask >> 1) | 0x80000000 )); i=$((i+1))
                done
                [ $(( ip_int & mask )) -eq $(( network_int & mask )) ] && return 0
                ;;
            *)
                [ "$item" = "$ip_to_check" ] && return 0
                ;;
        esac
    done
    return 1
}

[ "$ACTION" = "add" ] || [ "$ACTION" = "new" ] || exit 0

if is_in_list "$IPADDR" "$VPN_IPS"; then
    # Check if tunnel interface exists (avoids race condition)
    if ifconfig | grep -q "$WG_INTERFACE"; then
        logger -t wg-dhcp-hotplug "[$WG_INTERFACE] New client $IPADDR ($MACADDR) detected. Applying VPN routing rules."
        iptables -D $KS_CHAIN -s $IPADDR -j REJECT 2>/dev/null
        ip6tables -D $KS_CHAIN -m mac --mac-source $MACADDR -j REJECT 2>/dev/null
        ip rule del from $IPADDR table $ROUTING_TABLE 2>/dev/null
        ip rule add from $IPADDR table $ROUTING_TABLE priority $ROUTING_TABLE

        if [ "$IPV6_SUPPORTED" = "1" ]; then
            # IPv6 Supported: Start leak prevention
            lan_ifaces=$(ip link show | grep -o 'br-lan[^:]*' | tr '\n' ' ')
            [ -z "$lan_ifaces" ] && lan_ifaces="br-lan"
            for lan_if in $lan_ifaces; do ip6tables -I $BLOCK_CHAIN 1 -i $lan_if ! -o $WG_INTERFACE -m mac --mac-source $MACADDR -j DROP; done
            logger -t wg-dhcp-hotplug "[$WG_INTERFACE] IPv6 internet blocked for $IPADDR ($MACADDR) until routing configured"

            (
                start_time=$(date +%s 2>/dev/null || echo 0)
                start_time_ns=$(date +%s%N 2>/dev/null || echo ${start_time}000000000)
                MAX_RETRIES=10
                RETRY_COUNT=0
                PING_CLIENT=1

                # Proactive ping to trigger IPv6 discovery
                if [ $PING_CLIENT -eq 1 ]; then
                    logger -t wg-dhcp-hotplug "[$WG_INTERFACE] Proactively pinging $IPADDR to discover IPv6..."
                    ping -c 3 -W 1 "$IPADDR" >/dev/null 2>&1 &
                fi

                while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
                    sleep 2
                    ipv6_addrs=$(ip -6 neigh show | grep -i "$MACADDR" | grep -v "fe80:" | awk '{print $1}')
                    if [ -n "$ipv6_addrs" ]; then
                        prefix=$(echo "$ipv6_addrs" | head -n1 | cut -d: -f1-4)
                        if [ -n "$prefix" ]; then
                            # Add routing FIRST
                            ip -6 rule del from ${prefix}::/64 table $ROUTING_TABLE 2>/dev/null
                            ip -6 rule add from ${prefix}::/64 table $ROUTING_TABLE priority $ROUTING_TABLE

                            # Verify default route is correct, THEN unblock
                            if ip -6 route show table "$ROUTING_TABLE" | grep -q "default dev $WG_INTERFACE"; then
                                for lan_if in $lan_ifaces; do
                                    ip6tables -D $BLOCK_CHAIN -i $lan_if ! -o $WG_INTERFACE -m mac --mac-source $MACADDR -j DROP 2>/dev/null
                                done
                                end_time_ns=$(date +%s%N 2>/dev/null || echo ${start_time}000000000)
                                elapsed_ms=$(( (end_time_ns - start_time_ns) / 1000000 ))
                                elapsed_s=$(printf "%d.%03d" $((elapsed_ms / 1000)) $((elapsed_ms % 1000)) 2>/dev/null || echo "$((RETRY_COUNT * 2))")
                                logger -t wg-dhcp-hotplug "[$WG_INTERFACE] IPv6 unblocked and routed for $MACADDR (${prefix}::/64) in ${elapsed_s}s"
                                break
                            else
                                logger -t wg-dhcp-hotplug "[$WG_INTERFACE] ERROR: Default IPv6 route in table $ROUTING_TABLE is missing or incorrect. IPv6 remains blocked for $MACADDR."
                            fi
                            break # Exit on first valid prefix found, even if routing fails
                        fi
                    fi
                    RETRY_COUNT=$((RETRY_COUNT+1))
                    if [ $RETRY_COUNT -eq 1 ]; then
                        logger -t wg-dhcp-hotplug "[$WG_INTERFACE] IPv6 not found for $MACADDR, retrying every 2s..."
                    fi
                done
                if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
                    logger -t wg-dhcp-hotplug "[$WG_INTERFACE] WARNING: IPv6 not found for $MACADDR after $((MAX_RETRIES * 2))s. IPv6 remains blocked."
                fi
            ) &
        else
            # IPv4-only tunnel: Block IPv6 for this client
            logger -t wg-dhcp-hotplug "[$WG_INTERFACE] Blocking IPv6 for $IPADDR ($MACADDR) on IPv4-only tunnel."
            ip6tables -A $BLOCK_IPV4_ONLY_CHAIN -m mac --mac-source $MACADDR -j DROP
        fi
    else
        logger -t wg-dhcp-hotplug "[$WG_INTERFACE] New client $IPADDR ($MACADDR) detected, but tunnel is down. Applying kill switch."
        iptables -N $KS_CHAIN 2>/dev/null; ip6tables -N $KS_CHAIN 2>/dev/null
        iptables -C FORWARD -j $KS_CHAIN 2>/dev/null || iptables -I FORWARD 1 -j $KS_CHAIN
        ip6tables -C FORWARD -j $KS_CHAIN 2>/dev/null || ip6tables -I FORWARD 1 -j $KS_CHAIN
        iptables -A $KS_CHAIN -s $IPADDR -j REJECT --reject-with icmp-host-prohibited
        ip6tables -A $KS_CHAIN -m mac --mac-source $MACADDR -j REJECT --reject-with icmp6-adm-prohibited
    fi
else
    # This client is NOT in the VPN list, so clean up any old rules
    logger -t wg-dhcp-hotplug "[$WG_INTERFACE] Client $IPADDR ($MACADDR) is on a non-VPN network. Cleaning up any lingering rules."

    # 1. Clean up IPv4 routing rule
    ip rule del from $IPADDR table $ROUTING_TABLE 2>/dev/null
    logger -t wg-dhcp-hotplug "[$WG_INTERFACE] Removed IPv4 routing rule for $IPADDR"

    # 2. Clean up IPv6 firewall block rule (from BOTH potential chains)
    lan_ifaces=$(ip link show | grep -o 'br-lan[^:]*' | tr '\n' ' ')
    [ -z "$lan_ifaces" ] && lan_ifaces="br-lan"
    for lan_if in $lan_ifaces; do
        ip6tables -D $BLOCK_CHAIN -i $lan_if ! -o $WG_INTERFACE -m mac --mac-source $MACADDR -j DROP 2>/dev/null
    done
    ip6tables -D $BLOCK_IPV4_ONLY_CHAIN -m mac --mac-source $MACADDR -j DROP 2>/dev/null

    # 3. Find and clean up lingering IPv6 *routing* rule
    ipv6_addrs=$(ip -6 neigh show | grep -i "$MACADDR" | grep -v "fe80:" | awk '{print $1}')
    if [ -n "$ipv6_addrs" ]; then
        prefix=$(echo "$ipv6_addrs" | head -n1 | cut -d: -f1-4)
        if [ -n "$prefix" ]; then
            logger -t wg-dhcp-hotplug "[$WG_INTERFACE] Removing IPv6 route for ${prefix}::/64 from table $ROUTING_TABLE"
            ip -6 rule del from ${prefix}::/64 table $ROUTING_TABLE 2>/dev/null
        fi
    fi
fi

ip route flush cache
EOF_DHCP_PBR
    sed -i "s|INTERFACE_NAME_PLACEHOLDER|$INTERFACE_NAME|g" "$DHCP_HOTPLUG_SCRIPT"
    sed -i "s|ROUTING_TABLE_PLACEHOLDER|$ROUTING_TABLE|g" "$DHCP_HOTPLUG_SCRIPT"
    sed -i "s|VPN_IPS_PLACEHOLDER|$VPN_IPS|g" "$DHCP_HOTPLUG_SCRIPT"
    sed -i "s|IPV6_SUPPORTED_PLACEHOLDER|$IPV6_SUPPORTED|g" "$DHCP_HOTPLUG_SCRIPT"
    sed -i "s|VPN_DNS_PLACEHOLDER|$VPN_DNS_SERVERS|g" "$DHCP_HOTPLUG_SCRIPT"
    chmod +x "$DHCP_HOTPLUG_SCRIPT"
else
    echo "DHCP hotplug script $DHCP_HOTPLUG_SCRIPT already exists. Skipping (use --force to overwrite)."
fi

# Create cleanup script
cat > /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup << 'EOFCLEANUP'
#!/bin/sh
[ "$ACTION" = "ifdown" ] || exit 0
[ "$INTERFACE" = "INTERFACE_NAME_PLACEHOLDER" ] || exit 0
VPN_IPS="VPN_IPS_PLACEHOLDER"
VPN_DNS="VPN_DNS_PLACEHOLDER"
ROUTING_TABLE="ROUTING_TABLE_PLACEHOLDER"
IPSET_NAME="IPSET_NAME_PLACEHOLDER"
MARK_CHAIN="mark_${WG_INTERFACE}"
DNS_BLOCK_CHAIN_v4="dns_block_v4_${WG_INTERFACE}"
DNS_BLOCK_CHAIN_v6="dns_block_v6_${WG_INTERFACE}"
MARK_VALUE="0x${ROUTING_TABLE}"
WG_INTERFACE="INTERFACE_NAME_PLACEHOLDER"
KS_CHAIN="${WG_INTERFACE}_killswitch"
BLOCK_CHAIN="${WG_INTERFACE}_ipv6_block"
BLOCK_IPV4_ONLY_CHAIN="${WG_INTERFACE}_ipv4_only_block"

ip_to_int() {
    local a b c d; IFS=. read -r a b c d <<EOF
$1
EOF
    echo "$(( (a << 24) | (b << 16) | (c << 8) | d ))"
}
is_in_subnet() {
    local ip_to_check="$1"; local subnet_cidr="$2"
    local ip_int subnet prefix network_int mask
    ip_int=$(ip_to_int "$ip_to_check")
    subnet="${subnet_cidr%/*}"; prefix="${subnet_cidr#*/}"
    network_int=$(ip_to_int "$subnet")
    i=0; mask=0
    while [ $i -lt $prefix ]; do
        mask=$(( (mask >> 1) | 0x80000000 )); i=$((i+1))
    done
    [ $(( ip_int & mask )) -eq $(( network_int & mask )) ] && return 0 || return 1
}
# Cleanup function for DNS rules
cleanup_vpn_dns() {
    local wg_interface="$1"

    local nat_chain="vpn_dns_nat_${wg_interface}"
    local filter_chain="vpn_dns_filter_${wg_interface}"
    local isolation_chain="vpn_dns_isolate_${wg_interface}"

    # Clean up nat chain
    iptables -t nat -D PREROUTING -j $nat_chain 2>/dev/null
    iptables -t nat -F $nat_chain 2>/dev/null
    iptables -t nat -X $nat_chain 2>/dev/null

    # Clean up filter chain
    iptables -D FORWARD -j $filter_chain 2>/dev/null
    iptables -F $filter_chain 2>/dev/null
    iptables -X $filter_chain 2>/dev/null

    # Clean up isolation chain
    iptables -D FORWARD -j $isolation_chain 2>/dev/null
    iptables -F $isolation_chain 2>/dev/null
    iptables -X $isolation_chain 2>/dev/null

    logger -t wireguard "[$wg_interface] DNS rules cleaned up"
}
# Clean up ALL potential IPv6 blocking chains
ip6tables -F $BLOCK_CHAIN 2>/dev/null
ip6tables -D FORWARD -j $BLOCK_CHAIN 2>/dev/null
ip6tables -X $BLOCK_CHAIN 2>/dev/null
ip6tables -F $BLOCK_IPV4_ONLY_CHAIN 2>/dev/null
ip6tables -D FORWARD -j $BLOCK_IPV4_ONLY_CHAIN 2>/dev/null
ip6tables -X $BLOCK_IPV4_ONLY_CHAIN 2>/dev/null

# Clean up policy routing rules
while ip rule | grep -q "lookup $ROUTING_TABLE"; do
    ip rule del $(ip rule | grep "lookup $ROUTING_TABLE" | head -n1)
done
while ip -6 rule | grep -q "lookup $ROUTING_TABLE"; do
    ip -6 rule del $(ip -6 rule | grep "lookup $ROUTING_TABLE" | head -n1)
done

# Clean up firewall mark rules
logger -t wireguard "[$WG_INTERFACE] Cleaning up DNS leak-prevention firewall mark $MARK_VALUE."
ip rule del fwmark $MARK_VALUE table $ROUTING_TABLE 2>/dev/null
ip -6 rule del fwmark $MARK_VALUE table $ROUTING_TABLE 2>/dev/null

lan_ifaces=$(ip link show | grep -o 'br-lan[^:]*' | tr '\n' ' ')
[ -z "$lan_ifaces" ] && lan_ifaces="br-lan"
for lan_if in $lan_ifaces; do
    iptables -t mangle -D PREROUTING -i $lan_if -j $MARK_CHAIN 2>/dev/null
    ip6tables -t mangle -D PREROUTING -i $lan_if -j $MARK_CHAIN 2>/dev/null
done
iptables -t mangle -F $MARK_CHAIN 2>/dev/null
iptables -t mangle -X $MARK_CHAIN 2>/dev/null
ip6tables -t mangle -F $MARK_CHAIN 2>/dev/null
ip6tables -t mangle -X $MARK_CHAIN 2>/dev/null

# Clean up specific CONNMARK restore rules
iptables -t mangle -D OUTPUT -p udp --sport 53 -j CONNMARK --restore-mark 2>/dev/null
iptables -t mangle -D OUTPUT -p tcp --sport 53 -j CONNMARK --restore-mark 2>/dev/null
ip6tables -t mangle -D OUTPUT -p udp --sport 53 -j CONNMARK --restore-mark 2>/dev/null
ip6tables -t mangle -D OUTPUT -p tcp --sport 53 -j CONNMARK --restore-mark 2>/dev/null

# *** CORRECTED: Clean up DNS backstop rules ***
logger -t wireguard "[$WG_INTERFACE] Cleaning up DNS backstop rules."
iptables -t mangle -D OUTPUT -m mark --mark $MARK_VALUE -j $DNS_BLOCK_CHAIN_v4 2>/dev/null
iptables -t mangle -F $DNS_BLOCK_CHAIN_v4 2>/dev/null
iptables -t mangle -X $DNS_BLOCK_CHAIN_v4 2>/dev/null

ip6tables -t mangle -D OUTPUT -m mark --mark $MARK_VALUE -j $DNS_BLOCK_CHAIN_v6 2>/dev/null
ip6tables -t mangle -F $DNS_BLOCK_CHAIN_v6 2>/dev/null
ip6tables -t mangle -X $DNS_BLOCK_CHAIN_v6 2>/dev/null

# Activate Kill Switch
logger -t wireguard "[$WG_INTERFACE] Interface is down. Activating kill switch."
iptables -N $KS_CHAIN 2>/dev/null; ip6tables -N $KS_CHAIN 2>/dev/null
iptables -C FORWARD -j $KS_CHAIN 2>/dev/null || iptables -I FORWARD 1 -j $KS_CHAIN
ip6tables -C FORWARD -j $KS_CHAIN 2>/dev/null || ip6tables -I FORWARD 1 -j $KS_CHAIN

for item in $VPN_IPS; do
    logger -t wireguard "[$WG_INTERFACE] Blocking all traffic from $item."
    iptables -A $KS_CHAIN -s $item -j REJECT --reject-with icmp-host-prohibited
    case "$item" in
        */*) # For subnets, find all known MACs and block them for IPv6
            if [ -f /cfg/dhcp.leases ]; then
                while read -r exp mac ip host; do
                    if is_in_subnet "$ip" "$item"; then
                        ip6tables -A $KS_CHAIN -m mac --mac-source $mac -j REJECT --reject-with icmp6-adm-prohibited
                    fi
                done < /cfg/dhcp.leases
            fi
            ;;
        *) # For individual IPs, get MAC directly
            mac=$(ip neigh show "$item" | grep -o '[0-9a-f:]\{17\}' | head -1)
            if [ -n "$mac" ] && [ "$mac" != "<incomplete>" ]; then
                ip6tables -A $KS_CHAIN -m mac --mac-source $mac -j REJECT --reject-with icmp6-adm-prohibited
            fi
            ;;
    esac
done

ip route flush table $ROUTING_TABLE 2>/dev/null
ip -6 route flush table $ROUTING_TABLE 2>/dev/null
ip route flush cache; ip -6 route flush cache 2>/dev/null
logger -t wireguard "[$WG_INTERFACE] Interface down and routing cleaned up"

cleanup_vpn_dns "$WG_INTERFACE"

# ADDED: Clean up dnsmasq config and ipset
IPSET_NAME="vpn_${WG_INTERFACE}"
DNSMASQ_CONF="/tmp/dnsmasq.d/99-${WG_INTERFACE}-dns.conf"

logger -t wireguard "[$WG_INTERFACE] Cleaning up ipset and dnsmasq config."
rm -f $DNSMASQ_CONF

# *** FIX 1: Do NOT destroy the ipset. It will be flushed by the ifup script. ***
# ipset destroy $IPSET_NAME 2>/dev/null

# Reload dnsmasq to apply changes (remove config)
( /etc/init.d/dnsmasq reload >/dev/null 2>&1 ) &
EOFCLEANUP
sed -i "s|INTERFACE_NAME_PLACEHOLDER|$INTERFACE_NAME|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup
sed -i "s|VPN_IPS_PLACEHOLDER|$VPN_IPS|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup
sed -i "s|ROUTING_TABLE_PLACEHOLDER|$ROUTING_TABLE|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup
sed -i "s|IPSET_NAME_PLACEHOLDER|$IPSET_NAME|g" /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup
chmod +x /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup

if [ "$SKIP_INTERFACE_CONFIG" -eq 0 ]; then
    uci commit network
    uci commit firewall
fi

# Reload dnsmasq to apply selective DNS rules
if [ "$RESTART_SERVICES" -eq 1 ]; then
    if [ -n "$VPN_DNS_SERVERS" ] && [ -f "$DNSMASQ_CONF" ]; then
       echo "Reloading dnsmasq to apply selective DNS rules..."
       /etc/init.d/dnsmasq reload
    fi
fi

echo "Configuration applied successfully!"

if [ "$RESTART_SERVICES" -eq 1 ]; then
    if [ "$SKIP_INTERFACE_CONFIG" -eq 0 ]; then
        echo "Restarting network and firewall services..."
        /etc/init.d/network restart
        /etc/init.d/firewall restart
    else
        echo "Applying routing configuration..."
        # Reload dnsmasq to apply selective DNS rules
        if [ "$RESTART_SERVICES" -eq 1 ]; then
            if [ -n "$VPN_DNS_SERVERS" ] && [ -f "$DNSMASQ_CONF" ]; then
                echo "Reloading dnsmasq to apply selective DNS rules..."
                /etc/init.d/dnsmasq reload
            fi
        fi
        ifdown $INTERFACE_NAME 2>/dev/null || true
        sleep 1
        ifup $INTERFACE_NAME
    fi
else
    echo "Skipping service restarts (--no-restart set). Please restart services manually."
    echo "To apply changes, run:"
    echo "/etc/init.d/network restart && /etc/init.d/firewall restart && /etc/init.d/dnsmasq reload"
    echo "Or, if interface already exists:"
    echo "ifdown $INTERFACE_NAME && ifup $INTERFACE_NAME"
fi

echo ""
echo "✅ WireGuard client setup complete!"
echo "Interface: $INTERFACE_NAME"
echo "Policy routing: Dynamically managing clients in '$VPN_IPS'"
echo ""