I’m sharing my WireGuard client setup script that supports tunneled IPv4 and/or IPv6. It is working with my Route10 configuration. Tested with Surfshark (IPv4), Proton (IPv4/IPv6), my ISP provided VPN (IPv4/IPv6) with firmware version v1.4k.
| Feature | Status | Description |
|---|---|---|
| Policy-Based Routing (IPv4/IPv6) | Tested working. Routes all IPv4/IPv6 traffic for specified clients (defined by IP or subnet) through the WireGuard tunnel, leaving other traffic unaffected. | |
| Internet Kill Switch (IPv4/IPv6) |
Tested working. Blocks all IPv4/IPv6 internet access for specified clients if the WireGuard tunnel goes down, preventing connection leaks. | |
| IPv6 Leak Prevention |
Tested working. Uses a dedicated firewall chain to block a client’s IPv6 traffic until it can verify that a secure route through the VPN has been established. | |
| IPv4/IPv6 DNS Leak Protection |
Partially working. VPN DNS servers don’t leak to other interfaces. DNS servers used for a direct connection leaks to VPN. |
Running the script
Usage: ./wg-client-setup.sh [-f|--force] <interface_name> -c <config_file> -r <positive_number> -t <IPs_comma_separated> [-n]
<interface_name>: WireGuard interface name (max 11 chars)
-c, --conf <file>: Relative or absolute path to the wg conf file
-r, --routing-table <N>: Positive number for the routing table
-t, --target-ips <IPs>: Comma-separated list of IPv4 addresses or subnets
-n, --no-restart: Optional. Do not restart services (default: restart)
-f, --force: Optional. Force reconfiguration.
By default the script restarts network, firewall and dnsmasq services after a WireGuard interface has been setup. If you want to create a batch of WireGuard interfaces (probably after reboot using your init script), it’s recommended to pass the -n (no-restart) argument and manually restart services after:
./wg-client-setup.sh wg0 --conf conf/wg0.conf --routing-table 110 --target-ips 10.21.5.0/24,10.15.15.4 --no-restart
./wg-client-setup.sh wg1 -c conf/wg1.conf -r 111 -t 10.21.6.0/24 -n
...
./wg-client-setup.sh wg10 -c conf/wg10.conf -r 121 -t 10.21.20.0/24 -n
# restart services after all interfaces have been setup
/etc/init.d/network restart && /etc/init.d/firewall restart && /etc/init.d/dnsmasq reload
The script uses hotplugs. If you plan to modify it, first disable your init script from starting on reboot. Then make sure there’s no circular code executions as it will cause a hotplug storm (usually turning the router off and on will allow you to access your router again, but if you have your init script executing on reboot you’ll have to do a factory reset).
Review the whole script before executing it in your Route10. It has long lines of codes. Supporting IPv6 and DNS leak protection (partially working) introduced a lot of complexities.
Rather than using an external link, I’ll post the code here in 2 splits.
wg-client-setup-v1.0.0 [lines 1-528/1061]
#!/bin/ash
set -e
trap 'rm -f /tmp/neigh_*_$$' EXIT
usage() {
echo "Usage: $0 [-f|--force] <interface_name> -c <config_file> -r <positive_number> -t <IPs_comma_separated> [-n]"
echo " <interface_name>: WireGuard interface name (max 11 chars)"
echo " -c, --conf <file>: Relative or absolute path to the wg conf file"
echo " -r, --routing-table <N>: Positive number for the routing table"
echo " -t, --target-ips <IPs>: Comma-separated list of IPv4 addresses or subnets"
echo " -n, --no-restart: Optional. Do not restart services (default: restart)"
echo " -f, --force: Optional. Force reconfiguration."
exit 1
}
trim() {
echo "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}
FORCE_RECONFIG=0
INTERFACE_NAME=""
CONFIG_FILE=""
ROUTING_TABLE_OVERRIDE=""
VPN_IPS_OVERRIDE=""
RESTART_SERVICES=1
while [ $# -gt 0 ]; do
case "$1" in
-f|--force)
FORCE_RECONFIG=1
shift
;;
-c|--conf)
[ -z "$2" ] && echo "Error: --conf requires a value" && usage
CONFIG_FILE=$(trim "$2")
shift 2
;;
-r|--routing-table)
[ -z "$2" ] && echo "Error: --routing-table requires a value" && usage
ROUTING_TABLE_OVERRIDE=$(trim "$2")
shift 2
;;
-t|--target-ips)
[ -z "$2" ] && echo "Error: --target-ips requires a value" && usage
VPN_IPS_OVERRIDE=$(trim "$2")
shift 2
;;
-n|--no-restart)
RESTART_SERVICES=0
shift
;;
-*)
echo "Error: Unknown option: $1"
usage
;;
*)
# The first non-option argument is the interface name
if [ -z "$INTERFACE_NAME" ]; then
INTERFACE_NAME=$(trim "$1")
else
echo "Error: Unknown positional argument: $1"
usage
fi
shift
;;
esac
done
# Validate arguments
if [ -z "$INTERFACE_NAME" ]; then
echo "Error: WireGuard interface name required"
usage
fi
if [ ${#INTERFACE_NAME} -gt 11 ]; then
echo "Error: Interface name must be 11 characters or less."
usage
fi
if [ -z "$CONFIG_FILE" ]; then
echo "Error: --conf <config_file> is required"
usage
fi
if [ -z "$ROUTING_TABLE_OVERRIDE" ]; then
echo "Error: --routing-table <positive_number> is required"
usage
fi
if [ -z "$VPN_IPS_OVERRIDE" ]; then
echo "Error: --target-ips <IPs_comma_separated> is required"
usage
fi
# Validate routing table is a positive number
if ! echo "$ROUTING_TABLE_OVERRIDE" | grep -Eq '^[1-9][0-9]*$'; then
echo "Error: --routing-table must be a positive number (e.g., 100)"
usage
fi
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: Configuration file not found: $CONFIG_FILE"
exit 1
fi
echo "Setting up WireGuard interface: $INTERFACE_NAME"
echo "Reading configuration from: $CONFIG_FILE"
# Parse configuration file
parse_config() {
local section=""
local line key value value_spaced
while IFS= read -r line || [ -n "$line" ]; do
line="${line%%#*}"; line="$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
[ -z "$line" ] && continue
case "$line" in
\[*\]) section="${line#\[}"; section="${section%]}"; continue;;
esac
case "$line" in
*=*)
key="${line%%=*}"; value="${line#*=}"
key="$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
value="$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
case "$section" in
Interface)
case "$key" in
PrivateKey) PRIVATE_KEY="$value" ;;
Address)
value_spaced=$(echo "$value" | sed 's/,/ /g')
for addr in $value_spaced; do
case "$addr" in
*:*) CLIENT_IP6="$CLIENT_IP6 $addr" ;;
*) CLIENT_IP="$CLIENT_IP $addr" ;;
esac
done
;;
DNS)
value_spaced=$(echo "$value" | sed 's/,/ /g')
for dns in $value_spaced; do
DNS_SERVERS="$DNS_SERVERS $dns"
done
;;
esac;;
Peer)
case "$key" in
PublicKey) PEER_PUBLIC_KEY="$value" ;;
PresharedKey) PRESHARED_KEY="$value" ;;
Endpoint) ENDPOINT="$value" ;;
AllowedIPs) ALLOWED_IPS="$value" ;;
PersistentKeepalive) KEEPALIVE="$value" ;;
esac;;
esac;;
esac
done < "$CONFIG_FILE"
}
parse_config
# Trim whitespace from parsed variables
CLIENT_IP=$(echo "$CLIENT_IP" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
CLIENT_IP6=$(echo "$CLIENT_IP6" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
DNS_SERVERS=$(echo "$DNS_SERVERS" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$PRIVATE_KEY" ] || [ -z "$PEER_PUBLIC_KEY" ] || [ -z "$ENDPOINT" ]; then
echo "Error: PrivateKey, Peer PublicKey, or Endpoint not found in config"
exit 1
fi
ALLOWED_IPS="${ALLOWED_IPS:-0.0.0.0/0, ::/0}"
KEEPALIVE="${KEEPALIVE:-25}"
# --- START VARIABLE OVERRIDE ---
# Set routing table from the required CLI parameter
ROUTING_TABLE="$ROUTING_TABLE_OVERRIDE"
echo "Using Routing Table: $ROUTING_TABLE (from --routing-table)"
ROUTING_TABLE_NAME="${INTERFACE_NAME}_rt"
# Set VPN_IPS from the required CLI parameter
# Convert comma-separated string to space-separated list
VPN_IPS=$(echo "$VPN_IPS_OVERRIDE" | sed 's/,/ /g' | tr -s ' ')
echo "Using Target IPs: $VPN_IPS (from --target-ips)"
# --- END VARIABLE OVERRIDE ---
VPN_DNS_SERVERS="$DNS_SERVERS"
VPN_IP6S=""
DHCP_HOTPLUG_SCRIPT="/etc/hotplug.d/dhcp/99-${INTERFACE_NAME}-pbr"
# Define ipset and dnsmasq config paths
IPSET_NAME="vpn_${INTERFACE_NAME}"
DNSMASQ_DIR="/tmp/dnsmasq.d"
DNSMASQ_CONF="${DNSMASQ_DIR}/99-${INTERFACE_NAME}-dns.conf"
mkdir -p $DNSMASQ_DIR
if [ -n "$VPN_DNS_SERVERS" ]; then
echo "Configuring dnsmasq for selective DNS routing via ipset..."
# Create or flush the ipset
ipset create $IPSET_NAME hash:net 2>/dev/null || ipset flush $IPSET_NAME
# Add all VPN-bound subnets/IPs to the set
for item in $VPN_IPS; do
ipset add $IPSET_NAME $item 2>/dev/null
done
# Create the dnsmasq config file
echo "# Auto-generated by wg-client-setup.sh for $INTERFACE_NAME" > $DNSMASQ_CONF
echo "# This file forces clients in the $IPSET_NAME ipset to use specific DNS servers." >> $DNSMASQ_CONF
for dns in $VPN_DNS_SERVERS; do
# Format: server=<dns_server>@<ipset_name>
echo "server=$dns@$IPSET_NAME" >> $DNSMASQ_CONF
done
else
# If no DNS is set, make sure to remove any old config
echo "No VPN_DNS servers specified; removing any old dnsmasq config."
rm -f $DNSMASQ_CONF
fi
INTERFACE_EXISTS=0
if uci get network.$INTERFACE_NAME >/dev/null 2>&1; then INTERFACE_EXISTS=1; fi
if [ "$INTERFACE_EXISTS" -eq 1 ] && [ "$FORCE_RECONFIG" -eq 0 ]; then
echo "Interface $INTERFACE_NAME already configured. Skipping interface reconfiguration."
SKIP_INTERFACE_CONFIG=1
else
SKIP_INTERFACE_CONFIG=0
if [ "$FORCE_RECONFIG" -eq 1 ]; then
echo "Force reconfiguration enabled - removing existing setup"
while uci -q delete network.@wireguard_$INTERFACE_NAME[0]; do :; done
uci delete network.$INTERFACE_NAME 2>/dev/null || true
for section in $(uci show firewall | grep "\.name='${INTERFACE_NAME}'" | cut -d. -f2 | cut -d= -f1); do uci delete firewall.$section 2>/dev/null || true; done
ZONE_NAME=$(echo "$INTERFACE_NAME" | cut -c1-11)
for section in $(uci show firewall | grep "\.name='${ZONE_NAME}'" | cut -d. -f2 | cut -d= -f1); do uci delete firewall.$section 2>/dev/null || true; done
rm -f "$DHCP_HOTPLUG_SCRIPT" 2>/dev/null
fi
fi
rm -f /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing 2>/dev/null
rm -f /etc/hotplug.d/iface/99-${INTERFACE_NAME}-cleanup 2>/dev/null
if [ "$SKIP_INTERFACE_CONFIG" -eq 0 ]; then
uci set network.$INTERFACE_NAME=interface
uci set network.$INTERFACE_NAME.proto='wireguard'
uci set network.$INTERFACE_NAME.private_key="$PRIVATE_KEY"
if [ -n "$CLIENT_IP" ]; then for ip in $CLIENT_IP; do uci add_list network.$INTERFACE_NAME.addresses="$ip"; done; fi
# Determine IPv6 support from wg conf
IPV6_SUPPORTED=0
if [ -n "$CLIENT_IP6" ]; then
IPV6_SUPPORTED=1
for ip6 in $CLIENT_IP6; do uci add_list network.$INTERFACE_NAME.addresses="$ip6"; done
elif echo "$ALLOWED_IPS" | grep -q "::"; then
IPV6_SUPPORTED=1
echo "INFO: IPv6 routing is enabled via AllowedIPs, but no local IPv6 tunnel address is configured."
fi
uci add network wireguard_$INTERFACE_NAME
uci set network.@wireguard_$INTERFACE_NAME[-1]=wireguard_$INTERFACE_NAME
uci set network.@wireguard_$INTERFACE_NAME[-1].public_key="$PEER_PUBLIC_KEY"
# Parse endpoint (supports both IPv4 and IPv6)
case "$ENDPOINT" in
\[*\]:*)
# IPv6 format: [2401:dc20::50]:51820
ENDPOINT_HOST="${ENDPOINT%]:*}"
ENDPOINT_HOST="${ENDPOINT_HOST#\[}"
ENDPOINT_PORT="${ENDPOINT##*]:}"
;;
*:*)
# IPv4 format: 1.2.3.4:51820
ENDPOINT_HOST="${ENDPOINT%:*}"
ENDPOINT_PORT="${ENDPOINT##*:}"
;;
*)
echo "Error: Invalid endpoint format: $ENDPOINT"
exit 1
;;
esac
uci set network.@wireguard_$INTERFACE_NAME[-1].endpoint_host="$ENDPOINT_HOST"
uci set network.@wireguard_$INTERFACE_NAME[-1].endpoint_port="$ENDPOINT_PORT"
uci set network.@wireguard_$INTERFACE_NAME[-1].persistent_keepalive="$KEEPALIVE"
echo "$ALLOWED_IPS" | tr ',' '\n' | while read -r ip; do ip="$(echo "$ip" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"; [ -n "$ip" ] && uci add_list network.@wireguard_$INTERFACE_NAME[-1].allowed_ips="$ip"; done
if [ -n "$PRESHARED_KEY" ]; then uci set network.@wireguard_$INTERFACE_NAME[-1].preshared_key="$PRESHARED_KEY"; fi
ZONE_NAME=$(echo "$INTERFACE_NAME" | cut -c1-11)
uci set firewall.${INTERFACE_NAME}_zone=zone
uci set firewall.${INTERFACE_NAME}_zone.name="$ZONE_NAME"
uci set firewall.${INTERFACE_NAME}_zone.input='REJECT'
uci set firewall.${INTERFACE_NAME}_zone.output='ACCEPT'
uci set firewall.${INTERFACE_NAME}_zone.forward='ACCEPT'
uci set firewall.${INTERFACE_NAME}_zone.masq='1'
uci add_list firewall.${INTERFACE_NAME}_zone.network="$INTERFACE_NAME"
uci set firewall.${INTERFACE_NAME}_fwd=forwarding
uci set firewall.${INTERFACE_NAME}_fwd.src='lan'
uci set firewall.${INTERFACE_NAME}_fwd.dest="$ZONE_NAME"
else
# Determine IPv6 support from existing config if not reconfiguring
IPV6_SUPPORTED=0
if uci get network.$INTERFACE_NAME.addresses 2>/dev/null | grep -q ':' || \
uci get network.@wireguard_${INTERFACE_NAME}[-1].allowed_ips 2>/dev/null | grep -q ':'; then
IPV6_SUPPORTED=1
fi
fi
echo "Configuring policy-based routing..."
if ! grep -q "^$ROUTING_TABLE[[:space:]]*$ROUTING_TABLE_NAME" /etc/iproute2/rt_tables 2>/dev/null; then
echo "$ROUTING_TABLE $ROUTING_TABLE_NAME" >> /etc/iproute2/rt_tables
fi
# Create ifup script
cat > /etc/hotplug.d/iface/99-${INTERFACE_NAME}-routing << 'EOF_IFACE_ROUTING'
#!/bin/sh
[ "$ACTION" = "ifup" ] || exit 0
[ "$INTERFACE" = "INTERFACE_NAME_PLACEHOLDER" ] || exit 0
trap 'rm -f /tmp/neigh_${WG_INTERFACE}_$$' EXIT
ROUTING_TABLE="ROUTING_TABLE_PLACEHOLDER"
IPV6_SUPPORTED="IPV6_SUPPORTED_PLACEHOLDER"
WG_INTERFACE="INTERFACE_NAME_PLACEHOLDER"
VPN_IPS="VPN_IPS_PLACEHOLDER"
VPN_DNS="VPN_DNS_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}"
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 ))"
}
setup_secure_dns() {
local vpn_dns_list="$1"
local vpn_ips="$2"
local wg_interface="$3"
[ -z "$vpn_dns_list" ] && return 0
local nat_chain="vpn_dns_nat_${wg_interface}"
local filter_chain="vpn_dns_filter_${wg_interface}"
# SIMPLIFIED: Removed isolation_chain. It is handled globally later in this script.
logger -t wireguard "[$wg_interface] Setting up secure VPN DNS redirect to $vpn_dns_list"
# Cleanup old rules first
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
iptables -D FORWARD -j $filter_chain 2>/dev/null
iptables -F $filter_chain 2>/dev/null
iptables -X $filter_chain 2>/dev/null
# SIMPLIFIED: Removed cleanup for redundant isolation_chain
# Create new chains
iptables -t nat -N $nat_chain
iptables -N $filter_chain
# SIMPLIFIED: Removed creation of redundant isolation_chain
for item in $vpn_ips; do
# 1. DNAT rule to redirect DNS queries to VPN DNS
if [ "$(echo $vpn_dns_list | wc -w)" -eq 1 ]; then
iptables -t nat -A $nat_chain -s $item -p udp --dport 53 -j DNAT --to-destination $vpn_dns_list
iptables -t nat -A $nat_chain -s $item -p tcp --dport 53 -j DNAT --to-destination $vpn_dns_list
else
local i=0
local dns_count=$(echo $vpn_dns_list | wc -w)
for dns in $vpn_dns_list; do
iptables -t nat -A $nat_chain -s $item -p udp --dport 53 -m statistic --mode nth --every $((dns_count - i)) --packet 0 -j DNAT --to-destination $dns
iptables -t nat -A $nat_chain -s $item -p tcp --dport 53 -m statistic --mode nth --every $((dns_count - i)) --packet 0 -j DNAT --to-destination $dns
i=$((i + 1))
done
fi
# 2. Block DoT (port 853)
iptables -A $filter_chain -s $item -p tcp --dport 853 -j REJECT --reject-with tcp-reset
iptables -A $filter_chain -s $item -p udp --dport 853 -j REJECT --reject-with icmp-port-unreachable
# 3. Block DoH (port 443) by known providers
iptables -A $filter_chain -s $item -p tcp --dport 443 -m string --algo bm --string "dns.google" -j REJECT --reject-with tcp-reset
iptables -A $filter_chain -s $item -p tcp --dport 443 -m string --algo bm --string "cloudflare-dns.com" -j REJECT --reject-with tcp-reset
done
# SIMPLIFIED: Removed redundant isolation logic. It is handled globally.
# Insert chains
iptables -t nat -I PREROUTING 1 -j $nat_chain
iptables -I FORWARD 1 -j $filter_chain
# SIMPLIFIED: Removed insert for redundant isolation_chain
logger -t wireguard "[$wg_interface] Secure VPN DNS configured with leak prevention"
}
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
}
apply_ipv6_rules() {
# This function only runs if IPV6_SUPPORTED=1
local mac_addr="$1"
local subnet_item="${2:-}"
logger -t wireguard "[$WG_INTERFACE] Starting IPv6 discovery for MAC: $mac_addr"
(
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
# If proactive mode, try to trigger IPv6 discovery
if [ $PING_CLIENT -eq 1 ]; then
client_ip=$(ip neigh show | grep -i "$mac_addr" | awk '{print $1}' | head -1)
if [ -n "$client_ip" ]; then
logger -t wireguard "[$WG_INTERFACE] Proactively pinging $client_ip to discover IPv6..."
ping -c 3 -W 1 "$client_ip" >/dev/null 2>&1 &
fi
fi
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
sleep 2
ipv6_addrs=$(ip -6 neigh show | grep -i "$mac_addr" | 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
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 $mac_addr -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 wireguard "[$WG_INTERFACE] IPv6 unblocked and routed for $mac_addr (${prefix}::/64) in ${elapsed_s}s"
return 0
else
logger -t wireguard "[$WG_INTERFACE] ERROR: Default IPv6 route in table $ROUTING_TABLE is missing or incorrect. IPv6 remains blocked for $mac_addr."
fi
return 1 # 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 wireguard "[$WG_INTERFACE] IPv6 not found for MAC $mac_addr, retrying every 2 seconds..."
fi
done
logger -t wireguard "[$WG_INTERFACE] WARNING: IPv6 not found for MAC $mac_addr after $((MAX_RETRIES * 2))s. IPv6 remains blocked."
) &
}
# Disable Kill Switch
logger -t wireguard "[$WG_INTERFACE] Interface is up. Disabling kill switch."
iptables -F $KS_CHAIN 2>/dev/null; ip6tables -F $KS_CHAIN 2>/dev/null
iptables -D FORWARD -j $KS_CHAIN 2>/dev/null; ip6tables -D FORWARD -j $KS_CHAIN 2>/dev/null
iptables -X $KS_CHAIN 2>/dev/null; ip6tables -X $KS_CHAIN 2>/dev/null
# Create appropriate IPv6 blocking chain (leak prevention OR IPv4-only block)
if [ "$IPV6_SUPPORTED" = "1" ]; then
ip6tables -N $BLOCK_CHAIN 2>/dev/null
ip6tables -C FORWARD -j $BLOCK_CHAIN 2>/dev/null || ip6tables -I FORWARD 1 -j $BLOCK_CHAIN
else
logger -t wireguard "[$WG_INTERFACE] IPv4-only tunnel detected. IPv6 will be blocked for specified clients."
ip6tables -N $BLOCK_IPV4_ONLY_CHAIN 2>/dev/null
ip6tables -C FORWARD -j $BLOCK_IPV4_ONLY_CHAIN 2>/dev/null || ip6tables -I FORWARD 1 -j $BLOCK_IPV4_ONLY_CHAIN
fi
# Setup routing table
logger -t wireguard "[$WG_INTERFACE] Setting up default routes in table $ROUTING_TABLE."
ip route flush table $ROUTING_TABLE 2>/dev/null; ip -6 route flush table $ROUTING_TABLE 2>/dev/null
ip route add default dev $WG_INTERFACE table $ROUTING_TABLE
if [ "$IPV6_SUPPORTED" = "1" ]; then
ip -6 route add default dev $WG_INTERFACE table $ROUTING_TABLE 2>/dev/null
fi
# Create/Flush ipset on every ifup to prevent race condition
logger -t wireguard "[$WG_INTERFACE] Creating/flushing ipset $IPSET_NAME."
ipset create $IPSET_NAME hash:net 2>/dev/null || ipset flush $IPSET_NAME
for item in $VPN_IPS; do
ipset add $IPSET_NAME $item 2>/dev/null
done
# Setup fwmark routing to catch DNS leaks from 'allservers=1'
logger -t wireguard "[$WG_INTERFACE] Setting up DNS leak-prevention firewall mark $MARK_VALUE."
iptables -t mangle -N $MARK_CHAIN 2>/dev/null
iptables -t mangle -F $MARK_CHAIN
# Mark inbound DNS queries from VPN clients
iptables -t mangle -A $MARK_CHAIN -p udp --dport 53 -m set --match-set $IPSET_NAME src -j CONNMARK --set-mark $MARK_VALUE
iptables -t mangle -A $MARK_CHAIN -p tcp --dport 53 -m set --match-set $IPSET_NAME src -j CONNMARK --set-mark $MARK_VALUE
ip6tables -t mangle -N $MARK_CHAIN 2>/dev/null
ip6tables -t mangle -F $MARK_CHAIN
ip6tables -t mangle -A $MARK_CHAIN -p udp --dport 53 -m set --match-set $IPSET_NAME src -j CONNMARK --set-mark $MARK_VALUE
ip6tables -t mangle -A $MARK_CHAIN -p tcp --dport 53 -m set --match-set $IPSET_NAME src -j CONNMARK --set-mark $MARK_VALUE
# Apply mark chain to LAN interfaces
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 -C PREROUTING -i $lan_if -j $MARK_CHAIN 2>/dev/null || iptables -t mangle -A PREROUTING -i $lan_if -j $MARK_CHAIN
ip6tables -t mangle -C PREROUTING -i $lan_if -j $MARK_CHAIN 2>/dev/null || ip6tables -t mangle -A PREROUTING -i $lan_if -j $MARK_CHAIN
done