Astronaut with laptop Astronaut
Custom DynDNS for FirtzBox

Custom DynDNS for FirtzBox

3 min read

💡 Idea

So basically at home I have a fritzbox router, and I have a home server, and a homeassistant that runs on a raspberry pi. And since i don’t have a static IP, i want to somehow have the ability to access the server/pi from outside via a dns query. The problem is that for that to work i need to have a dynamic dns record which updates whenever the ip of the router changes. Thankfully my router comes with a built-in dyndns function, sadly it doesn’t work with cloudflare. But since all it does is fire a get request to a specific url, I can just use a serverless function to handle the request and update the dns record. The code for this is quite simple, and can be seen here fritz-dns

I copied the idea from the old php code I used before, to host on my server, but since I didn’t want to pay for a server, I decided to use vercel serverless functions. Original idea

The only thing I had to change was migrate it to js, and then use the cloudflare library to update the dns record. I use a random token, to validate that only I can send the request, and then I just update the dns record with the new ip.

That’s it. This week was kinda short, but that’s because I didn’t do that much, since it took like 2 hours to find out what the problem was 😂


🛠️ Code

import { NextRequest, NextResponse } from "next/server";
import Cloudflare from "cloudflare";
const client = new Cloudflare({
apiEmail: process.env["CLOUDFLARE_EMAIL"],
apiKey: process.env["CLOUDFLARE_API_KEY"],
});
function isValidIPv4(ip: string) {
return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip);
}
function isValidIPv6(ip: string) {
return /^[a-fA-F0-9:]+$/.test(ip);
}
export async function GET(req: NextRequest) {
const params = Object.fromEntries(req.nextUrl.searchParams.entries());
const { cf_key, domain, ipv4, ipv6, log, proxy } = params;
if (cf_key !== process.env["CLOUDFLARE_TOKEN"]) {
return NextResponse.json(
{ error: "Invalid Cloudflare token" },
{ status: 403 },
);
}
// Logging helper (console only)
function wlog(level: string, msg: string) {
if (log === "true") {
// Optionally, write to a file or external log here
console.log(`${new Date().toISOString()} - ${level} - ${msg}`);
}
}
wlog("INFO", "===== Starting Script =====");
if (!cf_key || !domain) {
wlog("ERROR", "Parameter(s) missing or invalid");
wlog("INFO", "Script aborted");
return NextResponse.json(
{ error: "Parameter(s) missing or invalid" },
{ status: 400 },
);
}
let validIPv4 = ipv4 && isValidIPv4(ipv4) ? ipv4 : null;
let validIPv6 = ipv6 && isValidIPv6(ipv6) ? ipv6 : null;
if (!validIPv4) {
wlog("ERROR", "Neither IPv4 nor IPv6 available.");
wlog("INFO", "Script aborted");
return NextResponse.json(
{ error: "Neither IPv4 nor IPv6 available." },
{ status: 400 },
);
}
const proxied = proxy === "true";
wlog("INFO", `Record will${proxied ? "" : " not"} be proxied by Cloudflare`);
// Automatically fetches more pages as needed.
for await (const zone of client.zones.list()) {
if (domain.includes(zone.name)) {
const list = await client.dns.records.list({ zone_id: zone.id });
const record = list.result.find((r: any) => r.name === domain);
if (!record) {
wlog("ERROR", `Record ${domain} not found`);
wlog("INFO", "Script aborted");
return NextResponse.json(
{ error: `Record ${domain} not found` },
{ status: 404 },
);
}
await client.dns.records.update(record.id, {
zone_id: zone.id,
type: "A",
name: domain,
content: validIPv4,
ttl: 2 * 60,
});
break;
}
}
wlog("INFO", "===== Script completed =====");
return NextResponse.json(
{ message: "IP updated successfully" },
{ status: 200 },
);
}