Record of some of the computer tech I deal with so that it's documented at least somewhere.

Saturday, 9 August 2025

The Net interprets censorship as damage and routes around it - Getting around the GeoFence with Wireguard, for reasons

Motivation: Certain foreign websites are now blocking access to users based on their European location. And Europeans are blocking access to certain websites such as rt.com at quite a fundamental level, not just DNS blocking but de-routeing them. It's not just for porn, it's more pernicious than that - Bitchute and Videy, for insatnce, are choosing to not server UK users because of the Online Safety Act. And I've finally snapped.

Solution: I've rented a Linux VM outside Europe and decide to run a Wireguard VPN

But I don't want to send *every* request because of traffic limitations and whathaveyou.

I got Claude to write me a script to take a list of domain names, resolve their IPs and route traffic to just those IPs to my VM.

I use nftables

Here's the script. It takes the domains from "/etc/wireguard/domains.txt" and routes them through wg0

/usr/local/sbin/wireguard-domain-router.sh #!/bin/bash # Routes specified domains through WireGuard interface set -euo pipefail # Configuration DOMAIN_FILE="/etc/wireguard/domains.txt" WG_INTERFACE="wg0" WG_TABLE="200" WG_TABLE_NAME="wireguard" NFT_MARK="0x00000001" LOG_FILE="/var/log/wireguard-domain-router.log" # Ensure log file exists touch "$LOG_FILE" log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" } # Check if running as root if [[ $EUID -ne 0 ]]; then log "ERROR: This script must be run as root" exit 1 fi # Ensure domain file exists if [[ ! -f "$DOMAIN_FILE" ]]; then log "ERROR: Domain file $DOMAIN_FILE does not exist" exit 1 fi # Setup routing table if it doesn't exist if ! grep -q "$WG_TABLE_NAME" /etc/iproute2/rt_tables; then echo "$WG_TABLE $WG_TABLE_NAME" >> /etc/iproute2/rt_tables log "Added routing table $WG_TABLE_NAME" sleep 1 fi # Verify WireGuard interface exists if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then log "ERROR: WireGuard interface $WG_INTERFACE does not exist" exit 1 fi # Create WireGuard default route in custom table if ! ip route show table "$WG_TABLE" 2>/dev/null | grep -q "default dev $WG_INTERFACE"; then if ip route add default dev "$WG_INTERFACE" table "$WG_TABLE" 2>/dev/null; then log "Added default route for $WG_INTERFACE in table $WG_TABLE_NAME" else log "WARNING: Could not add default route for $WG_INTERFACE (interface may be down)" fi fi # Ensure policy routing rule exists if ! ip rule list | grep -q "fwmark 0x1 lookup $WG_TABLE_NAME"; then ip rule add fwmark 0x1 table "$WG_TABLE_NAME" log "Added policy routing rule for mark 0x1" fi # Create nftables table and chains if they don't exist nft list table ip mangle >/dev/null 2>&1 || nft add table ip mangle # COMPLETELY FLUSH the output chain and recreate it log "Flushing and recreating nftables output chain..." nft delete chain ip mangle output 2>/dev/null || true nft add chain ip mangle output "{ type route hook output priority mangle; }" 2>/dev/null || true # Resolve domains and collect new IPs new_ips=() log "Starting domain resolution..." while IFS= read -r domain || [[ -n "$domain" ]]; do # Skip empty lines and comments [[ -z "$domain" || "$domain" =~ ^[[:space:]]*# ]] && continue # Trim whitespace domain=$(echo "$domain" | xargs) [[ -z "$domain" ]] && continue log "Resolving domain: $domain" # Resolve domain to IPv4 addresses resolved_ips=$(dig +short "$domain" A | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true) if [[ -z "$resolved_ips" ]]; then log "WARNING: No A records found for $domain" continue fi for ip in $resolved_ips; do new_ips+=("$ip") log " $domain -> $ip" done done < "$DOMAIN_FILE" # Remove duplicates and sort if [ ${#new_ips[@]} -gt 0 ]; then new_ips_sorted=$(printf '%s\n' "${new_ips[@]}" | sort -u) log "Found $(echo "$new_ips_sorted" | wc -w) unique IP addresses" else new_ips_sorted="" log "No IP addresses resolved from domains" fi # Clean up old routes that are no longer needed existing_routes=$(ip route show table "$WG_TABLE" | grep -v "default" | awk '{print $1}' | sort || true) if [[ -n "$existing_routes" ]]; then for ip in $existing_routes; do if [[ -z "$new_ips_sorted" ]] || ! echo "$new_ips_sorted" | grep -Fxq "$ip"; then log "Removing old route for $ip" ip route del "$ip" dev "$WG_INTERFACE" table "$WG_TABLE" 2>/dev/null || true fi done fi # Add new rules and routes if [[ -n "$new_ips_sorted" ]]; then log "Adding new rules and routes..." # Use a simple for loop with the string directly for ip in $new_ips_sorted; do [[ -z "$ip" ]] && continue log "Processing IP: $ip" # Add nftables rule to mark packets if nft add rule ip mangle output ip daddr "$ip" mark set "$NFT_MARK"; then log "Added nftables rule for $ip" # Add route via WireGuard if it doesn't exist if ! ip route show table "$WG_TABLE" | grep -q "^$ip "; then if ip route add "$ip" dev "$WG_INTERFACE" table "$WG_TABLE" 2>/dev/null; then log "Added route for $ip" else log "WARNING: Could not add route for $ip" fi else log "Route for $ip already exists" fi else log "ERROR: Could not add nftables rule for $ip" exit 1 fi done fi # Count total rules total_domains=$(grep -v '^[[:space:]]*#\|^[[:space:]]*$' "$DOMAIN_FILE" | wc -l) if [[ -n "$new_ips_sorted" ]]; then total_ips=$(echo "$new_ips_sorted" | wc -w) else total_ips=0 fi log "Update complete: $total_domains domains resolved to $total_ips unique IP addresses" # Final verification final_rules=$(nft list chain ip mangle output 2>/dev/null | grep "mark set 0x00000001" | wc -l) log "Final rule count: $final_rules" # Check for duplicates one more time nft list chain ip mangle output 2>/dev/null | grep "mark set 0x00000001" | sort | while read -r rule; do ip=$(echo "$rule" | sed -n 's/.*ip daddr \([0-9.]*\).*/\1/p') [[ -n "$ip" ]] && log "Active rule: $ip -> WireGuard" done I then have some systemd magic. I have a service file [Unit] Description=WireGuard Domain Router Documentation=man:systemd.service(5) Wants=network-online.target After=network-online.target nss-lookup.target ConditionPathExists=/etc/wireguard/domains.txt [Service] Type=oneshot ExecStart=/usr/local/sbin/wg_domain_script.sh User=root StandardOutput=journal StandardError=journal # Security settings NoNewPrivileges=true PrivateTmp=true ProtectHome=true ProtectSystem=strict ReadWritePaths=/var/log /etc/iproute2 CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW [Install] WantedBy=multi-user.target One type of which I didn't know about which will wait for file changes [Unit] Description=Watch /etc/wireguard/domains.txt and run WireGuard Domain Router on changes Documentation=man:systemd.path(5) ConditionPathExists=/etc/wireguard/domains.txt [Path] PathChanged=/etc/wireguard/domains.txt PathModified=/etc/wireguard/domains.txt Unit=wireguard-domain-router.service [Install] WantedBy=multi-user.target and a timer that runs once a day in case IPs change [Unit] Description=Run WireGuard Domain Router every 15 minutes Documentation=man:systemd.timer(5) Requires=wireguard-domain-router.service [Timer] OnCalendar=*-*-* 04:00:00 Persistent=true [Install] WantedBy=timers.target All I have to do now is add domains to /etc/wireguard/domains.txt and then wait for the country I am using to adopt the same blocks and rent another one!

No comments: