🛡️ WireGuard Guardian – MikroTik RouterOS 7.20.x LT

🔧 DynDNS + Anti-Zombie + Conntrack Cleanup + Ping-Gate

🎯 OBIETTIVO

Questa configurazione rende WireGuard stabile e autonomo su MikroTik:

🟩 Aggiorna DynDNS automaticamente
🟩 Rileva tunnel “UP ma morto”
🟩 Pulisce solo le connessioni WireGuard (flush chirurgico)
🟩 Evita reset inutili con Ping-Gate
🟩 Riduce interventi manuali e ticket dei clienti

Risultato: VPN resiliente.

📌 PARAMETRI STANDARD (nel tuo caso)

Voce Valore
Interfaccia WG wireguard2
Porta WG 51822
Ping Target 192.168.1.100
Ping Source (IP su WG) 192.168.253.2
Address List endpoint WG_ENDPOINTS
Connection Mark WG_OUTER
Script wg-guardian
Scheduler wg-guardian

🟨 Se su altri router cambiano nomi/porte/IP, modifica questi valori nei blocchi di configurazione.

1️⃣ Convenzione commenti peer (DynDNS)

Per i peer con endpoint DynDNS, aggiungi nel commento del peer:

WG_DNS=tuodominio.dyndns.org

✅ Esempio:

SedeRoma WG_DNS=roma.miodominio.it

🔎 Lo script leggerà questo tag e aggiornerà endpoint-address quando cambia l’IP.

2️⃣ Address-List endpoint WG_ENDPOINTS

La lista WG_ENDPOINTS serve per:

  • far sì che il mangle marchi solo WireGuard
  • permettere un flush conntrack “chirurgico”
  • centralizzare FQDN/IP (DynDNS e statici)

2.1 Aggiungi FQDN DynDNS

/ip firewall address-list
add list=WG_ENDPOINTS address=roma.miodominio.it comment="SedeRoma"
add list=WG_ENDPOINTS address=casa.miodominio.it comment="Casa"

2.2 Aggiungi endpoint con IP statico (opzionale ma consigliato)

/ip firewall address-list
add list=WG_ENDPOINTS address=2.33.207.149 comment="PeerStatico"

2.3 Verifica lista

/ip firewall address-list print where list="WG_ENDPOINTS"

🟨 Nota: se usi FQDN, RouterOS li risolve internamente. È normale vedere aggiornamenti nel tempo.

3️⃣ Mangle: tagging conntrack WireGuard (porta 51822)

🎯 Obiettivo: applicare connection-mark=WG_OUTER al traffico UDP WireGuard outer (WAN), così il flush rimuove solo quello.

3.1 Regola IN (prerouting) — consigliata

/ip firewall mangle
add chain=prerouting action=mark-connection new-connection-mark=WG_OUTER passthrough=yes 
protocol=udp dst-port=51822 src-address-list=WG_ENDPOINTS comment="Mark WG outer IN dst-port 51822"

3.2 Regola OUT (output) — consigliata

/ip firewall mangle
add chain=output action=mark-connection new-connection-mark=WG_OUTER passthrough=yes 
protocol=udp dst-port=51822 dst-address-list=WG_ENDPOINTS comment="Mark WG outer OUT dst-port 51822"

3.3 Metti le regole in cima (best practice)

/ip firewall mangle move [find comment="Mark WG outer IN dst-port 51822"] destination=0
/ip firewall mangle move [find comment="Mark WG outer OUT dst-port 51822"] destination=0

3.4 Verifica contatori

/ip firewall mangle print stats where comment~"Mark WG outer"

✅ Devi vedere i contatori crescere su entrambe (IN/OUT).

3.5 Verifica conntrack marcato

/ip firewall connection print where connection-mark="WG_OUTER"

✅ Dovresti vedere entry UDP legate alla porta 51822 verso gli endpoint.

4️⃣ Script wg-guardian con Ping-Gate (versione “robusta”)

Lo script fa:
1) setta UDP timeouts del conntrack (idempotente)
2) aggiorna endpoint DynDNS (WG_DNS= nel commento)
3) rileva sospetto zombie (last-handshake vecchio + WireGuard “in uso” = conntrack marcato presente)
4) fa ping di verifica (Ping-Gate)
5) se ping FAIL → flush conntrack marcato + bounce peer

4.1 Rimuovi script precedente (se esiste)

/system script remove [find name="wg-guardian"]

4.2 Crea lo script (⚠️ usa source={ ... })

/system script add name=wg-guardian policy=read,write,test source={

:local wgIf "wireguard3"
:local maxHandshakeAge 180
:local udpTimeout "2m"
:local udpStreamTimeout "10m"
:local logPrefix "WG-GUARD: "

# Ping-gate
:local pingTarget "192.168.57.1"
:local pingSrc "192.223.223.3"

# Conntrack tuning (idempotente)
:do {
/ip firewall connection tracking set udp-timeout=$udpTimeout udp-stream-timeout=$udpStreamTimeout
} on-error={
    :log warning ($logPrefix . "Conntrack UDP timeout non impostati (CT disabilitato?)")
}

# helper: estrai WG_DNS=... dal commento
:local getDnsFromComment do={
    :local c $1
    :local p [:find $c "WG_DNS="]
    :if ($p = -1) do={ :return "" }
    :local s ($p + 7)
    :local rest [:pick $c $s [:len $c]]
    :local sp [:find $rest " "]
    :if ($sp = -1) do={ :return $rest }
    :return [:pick $rest 0 $sp]
}

# helper: ping robusto -> ritorna numero risposte (0..count)
:local safePing do={
    :local target $1
    :local src $2
    :local cnt $3
    :local replies 0

    :do {
        :set replies [:ping $target count=$cnt src-address=$src interval=500ms]
    } on-error={
        :set replies 0
    }

    # se per qualche motivo non è numerico, azzera
    :if ([:typeof $replies] != "num") do={ :set replies 0 }

    :return $replies
}

# helper: last-handshake robusto -> ritorna secondi, o 999999 se "never"/non numerico
:local hsAgeSeconds do={
    :local v $1
    :if ([:typeof $v] = "num") do={ :return $v }
    :if ([:typeof $v] = "time") do={ :return $v }
    :return 999999
}

# WG "in uso" se esistono connessioni marcate
:local wgInUse false
:if ([:len [/ip firewall connection find where protocol=udp and connection-mark="WG_OUTER"]] > 0) do={
    :set wgInUse true
}

:foreach pid in=[/interface wireguard peers find where interface=$wgIf] do={

    :local comment [/interface wireguard peers get $pid comment]
    :local endpointOld [/interface wireguard peers get $pid endpoint-address]

    # 1) DynDNS refresh (solo se comment contiene WG_DNS=host)
    :local dnsHost [$getDnsFromComment $comment]
    :if ([:len $dnsHost] > 0) do={
        :local newIp ""
        :do { :set newIp [:resolve $dnsHost] } on-error={ :set newIp "" }

        :if ([:len $newIp] > 0 && $newIp != $endpointOld) do={
            /interface wireguard peers set $pid endpoint-address=$newIp
            :log info ($logPrefix . "DynDNS update: " . $comment . " -> " . $newIp . " (prima: " . $endpointOld . ")")
        }
    }

    # 2) Zombie detect: handshake vecchio + wgInUse
    :local hsRaw [/interface wireguard peers get $pid last-handshake]
    :local hsAge [$hsAgeSeconds $hsRaw]
    :local rxNow [/interface wireguard peers get $pid rx]
    :local txNow [/interface wireguard peers get $pid tx]

    :if (($hsAge > $maxHandshakeAge) && ($wgInUse = true)) do={

        :log warning ($logPrefix . "SUSPECT: " . $comment . " hsAge=" . $hsAge . "s rx=" . $rxNow . " tx=" . $txNow)

        # Ping-gate: se ping OK, non fare recovery
        :local replies [$safePing $pingTarget $pingSrc 3]

        :if ($replies > 0) do={
            :log info ($logPrefix . "Ping OK (" . $replies . "/3) " . $pingTarget . " src " . $pingSrc . " -> skip recovery per " . $comment)
        } else={
            :log warning ($logPrefix . "Ping FAIL (0/3) " . $pingTarget . " src " . $pingSrc . " -> recovery per " . $comment)

            # 3) Flush conntrack SOLO marcate WG_OUTER
            :local removed 0
            :foreach cid in=[/ip firewall connection find where protocol=udp and connection-mark="WG_OUTER"] do={
                /ip firewall connection remove $cid
                :set removed ($removed + 1)
            }
            :log warning ($logPrefix . "Conntrack flush WG_OUTER: rimossi " . $removed)

            # 4) Bounce peer
            /interface wireguard peers disable $pid
            :delay 2
            /interface wireguard peers enable $pid
            :log warning ($logPrefix . "Peer bounce: " . $comment)
        }
    }
}
}

5️⃣ Test manuale

5.1 Esegui lo script

/system script run wg-guardian

5.2 Controlla i log

/log print where message~"WG-GUARD"

🟩 Se tutto è sano potresti non vedere log (a parte eventuali DynDNS update).
🟥 Se c’è un problema vedrai SUSPECT, poi Ping FAIL e recovery.

6️⃣ Scheduler (automazione)

Esegui lo script ogni 1 minuto:

/system scheduler add name=wg-guardian interval=1m on-event=wg-guardian

Verifica:

/system scheduler print where name="wg-guardian"

7️⃣ Verifiche finali (checklist)

7.1 Mangle IN/OUT attivi e contatori in aumento

/ip firewall mangle print stats where comment~"Mark WG outer"

7.2 Conntrack marcato

/ip firewall connection print where connection-mark="WG_OUTER"

7.3 Ping manuale (deve rispondere quando la VPN è ok)

/ping address=192.168.1.100 src-address=192.168.253.2 count=3

8️⃣ Troubleshooting rapido

🟨 Non vedi conntrack marcato (WG_OUTER):

  • controlla che gli endpoint siano in WG_ENDPOINTS
  • controlla i contatori delle regole mangle

🟨 Ping sempre FAIL anche quando la VPN “sembra ok”:

  • verifica che 192.168.1.100 risponda davvero
  • verifica che 192.168.253.2 esista sul MikroTik ed è l’IP corretto sulla WG

🟨 Recovery troppo frequenti:

  • alza maxHandshakeAge (es. 300 o 600)
  • aumenta count ping o interval
  • usa un host target ping “sempre acceso” (gateway, server, NAS)

9️⃣ Replicazione su altre RouterBoard

🟩 Cosa devi modificare per ogni router:

  • wgIf (nome interfaccia WG)
  • porta 51822 nelle regole mangle (se diversa)
  • address-list WG_ENDPOINTS (FQDN/IP endpoint)
  • pingTarget e pingSrc

🟩 Best practice: salva questa guida in repo (Git) e tieni una versione per “template cliente”.

✅ Risultato

Con questa configurazione:
✅ DynDNS sempre aggiornato
✅ Recovery automatico solo quando serve (Ping-Gate)
✅ Flush conntrack “chirurgico” grazie a WG_OUTER
✅ WireGuard stabile anche in scenari “maledetti” di NAT/ISP

Autore: Frank – WireGuard Guardian System