feat: local Mullvad WireGuard + policy routing for guest traffic

- parahub-mullvad script: setup/status/remove for owner's Mullvad key
- WireGuard packages: kmod-wireguard, wireguard-tools, luci-proto-wireguard
- Policy routing: ip4table='100' + guest subnet rule (fixes guest→VPN flow)
- setup: auto-detects country, registers key, creates WG interface, switches firewall
- remove: reverts to GRE6→VPS gateway

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 15:42:45 +00:00
parent 3b6eb65dc5
commit 859033635b
3 changed files with 309 additions and 0 deletions

View File

@@ -132,6 +132,13 @@ set network.vpn_tunnel.ipaddr='172.16.0.2'
set network.vpn_tunnel.netmask='255.255.255.0' set network.vpn_tunnel.netmask='255.255.255.0'
set network.vpn_tunnel.gateway='172.16.0.1' set network.vpn_tunnel.gateway='172.16.0.1'
set network.vpn_tunnel.mtu='1400' set network.vpn_tunnel.mtu='1400'
set network.vpn_tunnel.ip4table='100'
# --- Policy routing: guest traffic → VPN table 100 ---
add network rule
set network.@rule[-1].src='${GUEST_SUBNET}/24'
set network.@rule[-1].lookup='100'
set network.@rule[-1].priority='100'
NET_EOF NET_EOF
uci commit network uci commit network

297
files/usr/bin/parahub-mullvad Executable file
View File

@@ -0,0 +1,297 @@
#!/bin/sh
# Parahub Mesh — Local Mullvad WireGuard Manager
#
# Allows the node owner to run Mullvad directly on the router.
# Guest traffic routes through the local WireGuard tunnel instead of
# the default GRE6→VPS path, giving lower latency and using the
# nearest Mullvad server.
#
# Usage:
# parahub-mullvad setup <account_key> [country_code]
# parahub-mullvad status
# parahub-mullvad remove
set -e
PARAHUB_DIR="/etc/parahub"
ACCOUNT_FILE="$PARAHUB_DIR/mullvad_account"
# ============================================================================
# Helpers
# ============================================================================
# Find the firewall forwarding index where src=guest
find_guest_forwarding() {
local idx=0
while uci -q get "firewall.@forwarding[$idx]" >/dev/null 2>&1; do
local src=$(uci -q get "firewall.@forwarding[$idx].src")
if [ "$src" = "guest" ]; then
echo "$idx"
return 0
fi
idx=$((idx + 1))
done
return 1
}
# Find firewall zone index by name
find_zone_index() {
local name="$1"
local idx=0
while uci -q get "firewall.@zone[$idx]" >/dev/null 2>&1; do
local zname=$(uci -q get "firewall.@zone[$idx].name")
if [ "$zname" = "$name" ]; then
echo "$idx"
return 0
fi
idx=$((idx + 1))
done
return 1
}
# ============================================================================
# setup <account_key> [country_code]
# ============================================================================
cmd_setup() {
local ACCOUNT="$1"
local COUNTRY="$2"
if [ -z "$ACCOUNT" ]; then
echo "Usage: parahub-mullvad setup <account_key> [country_code]"
echo ""
echo "Examples:"
echo " parahub-mullvad setup 1234567890123456 pt # Portugal"
echo " parahub-mullvad setup 1234567890123456 de # Germany"
echo " parahub-mullvad setup 1234567890123456 # Auto-detect"
echo ""
echo "Countries: us gb de nl se pt fr es ch at it jp sg br ..."
exit 1
fi
# Auto-detect country from WAN IP
if [ -z "$COUNTRY" ]; then
echo "Auto-detecting country from WAN IP..."
COUNTRY=$(curl -s --max-time 5 https://ipinfo.io/country 2>/dev/null | tr 'A-Z' 'a-z')
if [ -z "$COUNTRY" ] || [ ${#COUNTRY} -ne 2 ]; then
echo "Error: Could not detect country."
echo "Specify manually: parahub-mullvad setup $ACCOUNT <country>"
exit 1
fi
echo "Detected: $COUNTRY"
fi
COUNTRY=$(echo "$COUNTRY" | tr 'A-Z' 'a-z')
# --- Step 1: Generate WireGuard keys ---
echo "Generating WireGuard keys..."
umask 077
wg genkey > "$PARAHUB_DIR/wg_private.key"
wg pubkey < "$PARAHUB_DIR/wg_private.key" > "$PARAHUB_DIR/wg_public.key"
PRIVKEY=$(cat "$PARAHUB_DIR/wg_private.key")
PUBKEY=$(cat "$PARAHUB_DIR/wg_public.key")
# --- Step 2: Register with Mullvad API ---
echo "Registering key with Mullvad..."
RESULT=$(curl -s --max-time 15 -X POST https://api.mullvad.net/wg/ \
-d "account=$ACCOUNT" \
-d "pubkey=$PUBKEY")
if echo "$RESULT" | grep -q "^[0-9]"; then
MULLVAD_IPV4=$(echo "$RESULT" | cut -d',' -f1)
echo "Mullvad IP: $MULLVAD_IPV4"
else
echo "Error from Mullvad: $RESULT"
exit 1
fi
# --- Step 3: Find server in target country ---
echo "Finding Mullvad server in '$COUNTRY'..."
curl -s --max-time 15 https://api.mullvad.net/www/relays/wireguard/ | \
tr '{' '\n' | \
grep "\"country_code\":\"${COUNTRY}\"" | \
grep '"active":true' | \
head -1 > /tmp/mullvad_relay.tmp
if [ ! -s /tmp/mullvad_relay.tmp ]; then
echo "Error: No active WireGuard server for country '$COUNTRY'"
rm -f /tmp/mullvad_relay.tmp
exit 1
fi
SERVER_IP=$(sed 's/.*"ipv4_addr_in":"\([^"]*\)".*/\1/' /tmp/mullvad_relay.tmp)
SERVER_PUBKEY=$(sed 's/.*"pubkey":"\([^"]*\)".*/\1/' /tmp/mullvad_relay.tmp)
SERVER_HOST=$(sed 's/.*"hostname":"\([^"]*\)".*/\1/' /tmp/mullvad_relay.tmp)
rm -f /tmp/mullvad_relay.tmp
echo "Server: $SERVER_HOST ($SERVER_IP)"
# --- Step 4: Clean previous config ---
uci -q delete network.mullvad_local 2>/dev/null || true
while uci -q delete network.@wireguard_mullvad_local[0] 2>/dev/null; do :; done
# --- Step 5: Create WireGuard interface (routes in table 100) ---
echo "Configuring WireGuard..."
uci batch <<-WG_EOF
set network.mullvad_local=interface
set network.mullvad_local.proto='wireguard'
set network.mullvad_local.private_key='${PRIVKEY}'
add_list network.mullvad_local.addresses='${MULLVAD_IPV4}'
set network.mullvad_local.mtu='1420'
set network.mullvad_local.ip4table='100'
add network wireguard_mullvad_local
set network.@wireguard_mullvad_local[-1].public_key='${SERVER_PUBKEY}'
set network.@wireguard_mullvad_local[-1].endpoint_host='${SERVER_IP}'
set network.@wireguard_mullvad_local[-1].endpoint_port='51820'
add_list network.@wireguard_mullvad_local[-1].allowed_ips='0.0.0.0/0'
set network.@wireguard_mullvad_local[-1].route_allowed_ips='1'
set network.@wireguard_mullvad_local[-1].persistent_keepalive='25'
WG_EOF
# Disable GRE6 tunnel (WG replaces it in table 100)
uci set network.vpn_tunnel.auto='0'
uci commit network
# --- Step 6: Firewall zone for mullvad_local ---
local zone_idx
if zone_idx=$(find_zone_index "mullvad_local"); then
uci delete "firewall.@zone[$zone_idx]"
fi
uci batch <<-FW_EOF
add firewall zone
set firewall.@zone[-1].name='mullvad_local'
set firewall.@zone[-1].input='REJECT'
set firewall.@zone[-1].output='ACCEPT'
set firewall.@zone[-1].forward='REJECT'
set firewall.@zone[-1].masq='1'
set firewall.@zone[-1].mtu_fix='1'
add_list firewall.@zone[-1].network='mullvad_local'
FW_EOF
# Switch guest forwarding to mullvad_local
local fwd_idx
if fwd_idx=$(find_guest_forwarding); then
uci set "firewall.@forwarding[$fwd_idx].dest=mullvad_local"
fi
uci commit firewall
# --- Step 7: Save config ---
cat > "$ACCOUNT_FILE" <<-ACCT_EOF
MULLVAD_ACCOUNT=${ACCOUNT}
MULLVAD_COUNTRY=${COUNTRY}
MULLVAD_SERVER=${SERVER_HOST}
MULLVAD_SERVER_IP=${SERVER_IP}
MULLVAD_LOCAL_IP=${MULLVAD_IPV4}
ACCT_EOF
chmod 600 "$ACCOUNT_FILE"
# --- Step 8: Apply ---
echo "Restarting network..."
/etc/init.d/network restart
/etc/init.d/firewall restart
echo ""
echo "Done! Guest traffic now routes directly through Mullvad."
echo "Server: $SERVER_HOST ($COUNTRY)"
echo "Test: connect to Parahub_Free, visit https://am.i.mullvad.net"
}
# ============================================================================
# status
# ============================================================================
cmd_status() {
echo "=== Parahub Mesh VPN Status ==="
echo ""
if [ -f "$ACCOUNT_FILE" ]; then
echo "Mode: LOCAL MULLVAD (direct)"
cat "$ACCOUNT_FILE"
echo ""
wg show mullvad_local 2>/dev/null || echo "WireGuard interface: not up"
else
echo "Mode: VPS GATEWAY (GRE6 tunnel)"
echo "VPS: 91.98.123.238 -> Mullvad Portugal"
fi
echo ""
echo "Guest forwarding:"
local fwd_idx
if fwd_idx=$(find_guest_forwarding); then
echo " guest -> $(uci -q get "firewall.@forwarding[$fwd_idx].dest")"
fi
}
# ============================================================================
# remove
# ============================================================================
cmd_remove() {
if [ ! -f "$ACCOUNT_FILE" ]; then
echo "No local Mullvad configured. Nothing to remove."
exit 0
fi
echo "Removing local Mullvad, reverting to VPS gateway..."
# Remove WireGuard interface
uci -q delete network.mullvad_local 2>/dev/null || true
while uci -q delete network.@wireguard_mullvad_local[0] 2>/dev/null; do :; done
# Re-enable GRE6 tunnel
uci -q delete network.vpn_tunnel.auto 2>/dev/null || true
uci commit network
# Remove firewall zone
local zone_idx
if zone_idx=$(find_zone_index "mullvad_local"); then
uci delete "firewall.@zone[$zone_idx]"
fi
# Switch guest forwarding back to vpn_tunnel
local fwd_idx
if fwd_idx=$(find_guest_forwarding); then
uci set "firewall.@forwarding[$fwd_idx].dest=vpn_tunnel"
fi
uci commit firewall
# Clean up files
rm -f "$PARAHUB_DIR/wg_private.key" "$PARAHUB_DIR/wg_public.key" "$ACCOUNT_FILE"
echo "Restarting network..."
/etc/init.d/network restart
/etc/init.d/firewall restart
echo "Done! Guest traffic now routes through VPS gateway."
}
# ============================================================================
# Main
# ============================================================================
case "${1:-}" in
setup)
cmd_setup "$2" "${3:-}"
;;
status)
cmd_status
;;
remove)
cmd_remove
;;
*)
echo "Parahub Mesh — Mullvad WireGuard Manager"
echo ""
echo "Commands:"
echo " parahub-mullvad setup <account_key> [country]"
echo " parahub-mullvad status"
echo " parahub-mullvad remove"
echo ""
echo "By default, guest traffic goes through the VPS gateway."
echo "With 'setup', it routes directly through your own Mullvad"
echo "account — faster, and uses the nearest server."
;;
esac

View File

@@ -64,6 +64,11 @@ PACKAGES_CORE=(
# GRE6 tunnel (guest traffic → VPS gateway) # GRE6 tunnel (guest traffic → VPS gateway)
kmod-gre6 kmod-gre6
# WireGuard (optional local Mullvad via parahub-mullvad script)
kmod-wireguard
wireguard-tools
luci-proto-wireguard
# DNS-over-HTTPS for guest privacy # DNS-over-HTTPS for guest privacy
https-dns-proxy https-dns-proxy