WAN IP Guard: Automatische DNS-updates bij wisselend IP-adres

· 8 min lezen

Wie een thuisserver draait — voor Nextcloud, Forgejo, of een andere self-hosted dienst — stuit vroeg of laat op hetzelfde probleem: je internetprovider geeft je geen vast IP-adres. Het publieke IP wijzigt onvoorspelbaar, en opeens is je server onbereikbaar onder zijn domeinnaam. In dit artikel beschrijf ik hoe ik dit geautomatiseerd heb opgelost met een lichtgewicht Node.js script, een systemd timer, en de DirectAdmin API.

Het probleem: dynamisch WAN IP

De meeste consumenten-internetverbindingen gebruiken een dynamisch IP-adres. Je provider wijst je een publiek IP toe, maar behoudt zich het recht voor om het te wijzigen — na een herstart van de modem, bij netwerkonderhoud, of gewoon zonder aanleiding.

Voor de gemiddelde internetgebruiker is dit onzichtbaar. Voor iemand met een thuisserver is het een nachtmerrie:

  1. Domeinnamen wijzen naar het verkeerde IP — je A-records in DNS wijzen nog naar het oude adres. Bezoekers krijgen een timeout of een “site kan niet worden bereikt” foutmeldking.
  2. Handmatig bijwerken is onpraktisch — je weet niet wánneer het IP wijzigt. Het kan midden in de nacht zijn, tijdens je vakantie, of precies wanneer je een belangrijk bestand van je Nextcloud nodig hebt.
  3. Subdomeinen vermenigvuldigen het probleem — één domein is nog te doen, maar als je git., cloud., analytics. en www. allemaal handmatig moet bijwerken, wordt het een vervelende klus.

De traditionele oplossing is Dynamic DNS (DDNS): een client op je server detecteert een IP-wisseling en update de DNS records automatisch. Bekende diensten zoals DuckDNS of No-IP bieden dit gratis aan, maar dan draai je weer afhankelijk van een derde partij. Bovendien ondersteunen ze vaak alleen hun eigen subdomeinen, niet je eigen domeinnaam bij je hostingprovider.

De oplossing: WAN IP Guard

Ik bouwde een eenvoudige maar robuuste oplossing die drie dingen doet:

  1. Elke 5 minuten het publieke WAN IP ophalen
  2. Bij wijziging: alle relevante DNS A-records updaten via de hosting provider’s API
  3. WhatsApp notificatie sturen zodat ik altijd op de hoogte ben

Het script draait als een systemd timer op mijn server en is volledig autonoom.

Architectuur

┌──────────────────────────────────────────┐
│            systemd timer (5 min)          │
│                    │                      │
│                    ▼                      │
│         ┌─────────────────────┐          │
│         │   WAN IP Guard       │          │
│         │   (Node.js script)   │          │
│         └──────┬──────┬───────┘          │
│                │      │                   │
│     ┌──────────┘      └──────────┐       │
│     ▼                            ▼       │
│ ┌────────┐              ┌────────────┐  │
│ │ WAN IP │              │ DirectAdmin │  │
│ │ check  │              │   API       │  │
│ └───┬────┘              └─────┬──────┘  │
│     │                         │          │
│     ▼                         ▼          │
│ ifconfig.me            DNS records       │
│ api.ipify.org           bijwerken        │
│ icanhazip.com                          │
│                                        │
│              ┌──────────────┐           │
│              │  WhatsApp     │          │
│              │  notificatie  │          │
│              └──────────────┘           │
└──────────────────────────────────────────┘

Implementatie

WAN IP detectie met fallback

Het eerste wat het script doet is het huidige publieke IP ophalen. Daarvoor gebruikt het drie externe diensten met fallback — als één service down is, probeert de volgende:

async function getWanIp() {
  const services = [
    'https://ifconfig.me',
    'https://api.ipify.org',
    'https://icanhazip.com',
  ];
  for (const url of services) {
    try {
      const resp = await fetch(url, { signal: AbortSignal.timeout(10000) });
      if (resp.ok) {
        const ip = (await resp.text()).trim();
        if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) return ip;
      }
    } catch (e) {
      log(`Could not get IP from ${url}: ${e.message}`);
    }
  }
  throw new Error('Could not determine WAN IP from any service');
}

De regex validatie zorgt ervoor dat alleen geldige IPv4-adressen worden geaccepteerd — geen HTML-foutpagina’s of lege responses.

State management: weten dát er iets veranderd is

Het script bewaart het laatste bekende IP in een simpel tekstbestand op de server. Bij elke run vergelijkt het het huidige IP met de opgeslagen waarde:

function readState() {
  try {
    return fs.readFileSync(STATE_FILE, 'utf8').trim();
  } catch {
    return null; // Eerste run — geen state
  }
}

Als het IP gelijk is, stopt het script direct. Geen onnodige API calls, geen onnodige WhatsApp berichten. Een oneshot systemd service is hier perfect voor: het script draait, doet zijn ding, en stopt.

DNS updates via de DirectAdmin API

Mijn domein is gehost bij een provider die DirectAdmin gebruikt. DirectAdmin heeft een REST API die DNS records kan beheren — ideaal voor automatisering.

De authenticatie verloopt via een Login Key: een token dat je aanmaakt in het panel met beperkte rechten (alleen DNS-beheer). Geen wachtwoorden in scripts, geen 2FA-problemen.

Een DNS-update bestaat uit twee stappen: het oude record verwijderen en het nieuwe toevoegen:

async function deleteDnsRecord(name, ip) {
  const recordValue = `${name}=${ip}`;
  const endpoint = `/CMD_API_DNS_CONTROL?domain=${DOMAIN}&action=select&arecs0=${encodeURIComponent(recordValue)}`;
  await daRequest(endpoint);
}

async function addDnsRecord(name, ip) {
  const endpoint = `/CMD_API_DNS_CONTROL?domain=${DOMAIN}&action=add&type=A&name=${encodeURIComponent(name)}&value=${encodeURIComponent(ip)}`;
  await daRequest(endpoint);
}

Voor het apex-domein (example.com zonder subdomein) is de name parameter een lege string. Voor subdomeinen vul je de naam in (git, www, etc.).

Het script loopt door alle te beheren records en werkt ze één voor één bij:

for (const record of DNS_RECORDS) {
  const name = record === DOMAIN ? '' : record;
  const label = name || '(apex)';
  const oldIp = dnsRecords[name];

  try {
    if (oldIp) await deleteDnsRecord(name, oldIp);
    await addDnsRecord(name, currentIp);
    results.push(`✅ ${label}`);
  } catch (e) {
    results.push(`❌ ${label}: ${e.message}`);
  }
}

Per record wordt bijgehouden of het gelukt is, zodat het WhatsApp-bericht een compleet overzicht geeft.

WhatsApp notificatie

Als het IP gewijzigd is, stuurt het script een WhatsApp bericht naar de serverbeheerder. Dit verloopt via de wwebjs-api — een REST wrapper rond whatsapp-web.js die op dezelfde server draait:

async function sendWhatsApp(message) {
  const url = `${WHATSAPP_API}/client/sendMessage/${SESSION}`;
  const chatId = `${ADMIN_PHONE}@c.us`;
  await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ chatId, content: message, contentType: 'string' }),
  });
}

Het bericht bevat het oude en nieuwe IP, plus een overzicht van welke records zijn bijgewerkt:

🔄 WAN IP Gewijzigd

*Oud:* 84.10.20.30
*Nieuw:* 84.10.20.31

*DNS Updates:*
✅ (apex)
✅ git
✅ nextcloud
✅ plausible
✅ www

5 gelukt, 0 mislukt

Zo weet je binnen 5 minuten dat je IP gewijzigd is — én dat de DNS al is bijgewerkt.

systemd timer

De scheduling is een standaard systemd timer die elke 5 minuten triggert:

[Unit]
Description=Run WAN IP Guard every 5 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true betekent dat als de server herstart is, de timer de eerste run direct uitvoert in plaats van te wachten op het volgende interval. Handig als je modem en server tegelijk herstarten.

De service zelf is een oneshot:

[Service]
Type=oneshot
User=server-user
EnvironmentFile=/path/to/.env
WorkingDirectory=/path/to/wan-ip-guard
ExecStart=/usr/bin/node /path/to/wan-ip-guard/index.js

De EnvironmentFile zorgt ervoor dat credentials uit een .env bestand worden geladen — geen hardcoded secrets in de service file.

Beveiligingsoverwegingen

Bij het bouwen van dit script waren er een paar beveiligingsaspecten om rekening mee te houden:

Login Key in plaats van wachtwoord

Het DirectAdmin panel heeft tweestapsverificatie (2FA) ingeschakeld, wat interactieve login via een script onmogelijk maakt. In plaats daarvan gebruik ik een Login Key — een token met beperkte rechten die alleen DNS-beheer toestaat. Deze key staat in een .env bestand dat niet in de git repo terechtkomt.

Credentials scheiding

Alle gevoelige data staat in een .env bestand:

VariabeleDoel
DA_LOGIN_KEYDirectAdmin API token
DA_USERNAMEDirectAdmin gebruiker
WHATSAPP_API_ENDPOINTwwebjs-api URL
ADMIN_PHONENotificatie nummer

De .env.example in de repository bevat alleen placeholder waarden. Het echte .env bestand staat alleen op de server.

Foutafhandeling

Het script is zo gebouwd dat het nooit “half” iets doet:

  • WAN IP check faalt → script stopt, geen DNS update (liever geen update dan een verkeerde)
  • DNS update faalt voor 1 record → andere records worden nog wel bijgewerkt, fout wordt gerapporteerd in WhatsApp
  • WhatsApp send faalt → DNS update is al gedaan, fout wordt gelogd

Resultaat

Sinds de installatie draait de timer stabiel. Het script gebruikt verwaarloosbaar CPU (het draait maximaal een paar seconden per 5 minuten) en de enige output is een regel in de systemd journal per check:

[2026-06-21T06:30:00Z] WAN IP Guard check starting.
[2026-06-21T06:30:01Z] WAN IP obtained from https://api.ipify.org: 84.10.20.31
[2026-06-21T06:30:01Z] IP unchanged (84.10.20.31). Nothing to do.

Bij een IP-wisseling duurt het maximaal 5 minuten voordat de DNS is bijgewerkt en de domeinnamen weer naar de juiste server wijzen. Voor de gemiddelde bezoeker is dat nauwelij merkbaar — en jezelf ben je via de WhatsApp notificatie direct op de hoogte.

Wat ik heb geleerd

  • API boven browser automation — mijn eerste gedacht was om Puppeteer te gebruiken om in te loggen op het hosting panel en de DNS records via de webinterface te wijzigen. Dat werkt, maar is broos, langzaam, en breekt bij elke UI-wijziging. Een API met een Login Key is robuuster, sneller, en veiliger.
  • Fallback bij externe diensten — één IP-detectie service kan down zijn. Drie services met fallback maakt het script betrouwbaar zonder ingewikkelde monitoring.
  • systemd timers boven cron — systemd biedt Persistent=true, betere logging via journalctl, en integratie met service dependencies (After=network-online.target).
  • State in een simpel bestand — geen database nodig voor één IP-adres. Een tekstbestand met één regel is voldoende en makkelijker te debuggen.

Zelf gebruiken

De volledige source code staat openbaar op mijn Forgejo. Het script is geschreven voor DirectAdmin hosting panels maar het patroon — IP ophalen, vergelijken, API call maken, notificatie sturen — is universeel toepasbaar. Clone, vul je .env in, en je hebt je eigen dynamic DNS updater draaien.