Last active
October 23, 2025 00:29
-
-
Save boazsegev/0948141073a1e0fe220c6cdc84c85125 to your computer and use it in GitHub Desktop.
Netlify DDNS - Update a Netlify DNS Record Dynamically
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Emulates a Dynamic DNS (DDNS) update command for a Netlify managed domain. | |
| // | |
| // Updates both the DNS A (IPv4) and AAAA (IPv6) records. | |
| // | |
| // Sets DNS for SUBDOMAIN to lead to the computer running the script (or the router in front of it). | |
| // | |
| // Place info in the following environment variables: NETLIFY_USER, NETLIFY_AUTH, NETLIFY_WEBSITE, NETLIFY_SUBDOMAIN | |
| // | |
| // Or fill in the following... | |
| // Helper for env variable / default collection | |
| function get_env(name, default_value) { | |
| return (typeof process !== 'undefined' && process.env && process.env[name]) | |
| ? process.env[name] | |
| : default_value; | |
| } | |
| const USER = get_env('NETLIFY_USER', 'name ([email protected])'); | |
| const WEBSITE = get_env('NETLIFY_WEBSITE', 'example.com'); | |
| const SUBDOMAIN = get_env('NETLIFY_SUBDOMAIN', 'home'); | |
| const AUTH = get_env('NETLIFY_AUTH', 'MY_BIG_SECRET'); | |
| // Helper for getting JSON responses. | |
| async function request_json(url, args, log_name) { | |
| var result = await fetch(url, args); | |
| if (!result.ok) { | |
| console.error(`Error performing ${log_name}`, result); | |
| return false; | |
| } | |
| var j = await result.json(); | |
| if (!j) | |
| console.error(`${log_name} JSON empty?`, j); | |
| return j; | |
| } | |
| // Returns the public IPv4 address, if possible | |
| async function getipv4() { | |
| var j = await request_json('https://api.ipify.org?format=json', {}, | |
| "IPv4 address collection"); | |
| if (!j.ip) { | |
| console.error("IPv4 data missing", result, j); | |
| return false; | |
| } | |
| console.log(`IPv4 detected: ${j.ip}`); | |
| return j.ip; | |
| } | |
| // Returns the public IPv6 address, if possible | |
| async function getipv6() { | |
| var j = await request_json('https://api64.ipify.org?format=json', {}, | |
| "IPv6 address collection"); | |
| if (!j.ip) { | |
| console.error("IPv6 data missing", result, j); | |
| return false; | |
| } | |
| console.log(`IPv6 detected: ${j.ip}`); | |
| return j.ip; | |
| } | |
| // Returns the website ID for named website | |
| async function get_site_id(website) { | |
| let sites = await request_json('https://api.netlify.com/api/v1/sites', { | |
| headers : { | |
| 'User-Agent' : USER, | |
| 'Authorization' : `Bearer ${AUTH}`, | |
| 'Content-Type' : 'application/json', | |
| 'Accept' : 'application/json' | |
| } | |
| }); | |
| // console.log(sites); | |
| while (sites.length) { | |
| var tmp = sites.pop(); | |
| // console.log(tmp); | |
| if (tmp.name == website || tmp.custom_domain == website || | |
| tmp.url == website) | |
| return tmp.id; | |
| } | |
| return false; | |
| } | |
| // Collects information about the DNS status for the WebSite and Subdomain | |
| async function get_dns_info(website, subdomain) { | |
| const site_id = await get_site_id(website); | |
| if (!site_id) { | |
| console.error(`Can't get site ID for ${website}`); | |
| throw new Error("Site ID unknown or missing, can't continue"); | |
| } | |
| var dns = { | |
| 'site_id' : site_id, | |
| 'website' : website, | |
| 'subdomain' : subdomain, | |
| 'zones' : [], | |
| 'records' : [] | |
| }; | |
| var data = await request_json( | |
| `https://api.netlify.com/api/v1/sites/${site_id}/dns`, { | |
| method : 'GET', | |
| headers : { | |
| 'User-Agent' : USER, | |
| 'Authorization' : `Bearer ${AUTH}`, | |
| 'Content-Type' : 'application/json', | |
| 'Accept' : 'application/json' | |
| } | |
| }, | |
| "finding DNS zones for website."); | |
| if (!data || !data.length) { | |
| console.error("Couldn't get DNS zones for website!", data); | |
| throw new Error("DDNS Failed!"); | |
| } | |
| // For each DNS Domain, filter for subdomain. | |
| while (data.length) { | |
| var dmz = data.pop(); | |
| dns.zones.push(dmz); | |
| const root = dmz.domain ? dmz.domain : dmz.name; | |
| const target = `${dns.subdomain}.${root}`; | |
| console.log(`Scanning for ${target} @ ${root}`); | |
| const zone_id = dmz.id; | |
| if (!root || !zone_id) { | |
| console.error("Missing domain root name or zone ID", dmz); | |
| continue; | |
| } | |
| const records = dmz.records; | |
| if (!records) { | |
| console.error("Missing domain zone records", dmz); | |
| continue; | |
| } | |
| // Collect Matches | |
| while (records.length) { | |
| var n = records.pop(); | |
| if (n.hostname != target) | |
| continue; | |
| dns.records.push(n); | |
| console.log(`Found DNS Record ${n.type}: ${n.id}@${target}`); | |
| } | |
| } | |
| return dns; | |
| } | |
| // Deletes existing A and AAAA records for subdomain and writes new ones. | |
| async function delete_existing_records(dns) { | |
| var records = dns.records; | |
| if (!records) { | |
| console.warn( | |
| `No existing DNS records to delete? @ ${dns.subdomain}.${dns.website}`); | |
| return; | |
| } | |
| for (var i = 0; i < records.length; i++) { | |
| const zone_id = records[i].dns_zone_id; | |
| const record_id = records[i].id; | |
| const response = await fetch( | |
| `https://api.netlify.com/api/v1/dns_zones/${zone_id}/dns_records/${ | |
| record_id}`, | |
| { | |
| method : 'DELETE', | |
| headers : { | |
| 'User-Agent' : USER, | |
| 'Authorization' : `Bearer ${AUTH}`, | |
| 'Content-Type' : 'application/json', | |
| 'Accept' : 'application/json' | |
| }, | |
| body : JSON.stringify({"code" : 0, "message" : "DDNS Update needed"}) | |
| }); | |
| if (!response.ok) { | |
| console.error("Delete operation failed!", records[i]); | |
| } else { | |
| console.log(`Delete operation success: ${records[i].hostname}`); | |
| } | |
| } | |
| } | |
| // Create New A and AAAA Records | |
| async function create_records(dns, ipv4, ipv6) { | |
| let to_make = []; | |
| if (ipv4) | |
| to_make.push( | |
| {"type" : "A", "hostname" : false, "value" : ipv4, "ttl" : 1800}); | |
| if (ipv6) | |
| to_make.push( | |
| {"type" : "AAAA", "hostname" : false, "value" : ipv6, "ttl" : 1800}); | |
| if (!to_make.length) { | |
| console.error(`Public IP unknown, can't update ${domain}!`); | |
| throw new Error("Public IP unknown, nothing to do!"); | |
| } | |
| if (!dns) { | |
| console.error(`Missing DNS information, can't create new records!`); | |
| throw new Error("Missing DNS information, can't create new records"); | |
| } | |
| // for each DNS zone, create the records in the to_make array | |
| for (var i = 0; i < dns.zones.length; i++) { | |
| const zone = dns.zones[i]; | |
| const zone_id = zone.id; | |
| const target = `${dns.subdomain}.${zone.domain ? zone.domain : zone.name}`; | |
| if (!zone.domain && !zone.name) { | |
| console.error(`Hostname error for domain record for ${target}`); | |
| throw new Error("Hostname creation error"); | |
| } | |
| console.log(`Creating a new domain record for ${target}`); | |
| for (var j = 0; j < to_make.length; j++) { | |
| let r = to_make[j] | |
| r.hostname = target; | |
| const response = await fetch( | |
| `https://api.netlify.com/api/v1/dns_zones/${zone_id}/dns_records`, { | |
| method : 'POST', | |
| headers : { | |
| 'User-Agent' : USER, | |
| 'Authorization' : `Bearer ${AUTH}`, | |
| 'Content-Type' : 'application/json', | |
| 'Accept' : 'application/json' | |
| }, | |
| body : JSON.stringify(r) | |
| }); | |
| if (!response.ok) { | |
| console.error(`DDNS operation failed! @ ${target}`); | |
| } else { | |
| console.log(`DDNS operation performed @ ${target}`); | |
| } | |
| } | |
| } | |
| } | |
| // Do it all in one function | |
| async function update_dns(dns, ipv4, ipv6) { | |
| if (!dns) { | |
| console.error(`Missing DNS information!`, dns); | |
| throw new Error("Missing DNS information!"); | |
| } | |
| if (!ipv4 && !ipv6) { | |
| console.error(`Missing public IP information!`, dns); | |
| throw new Error("Missing public IP information!"); | |
| } | |
| await delete_existing_records(dns); | |
| await create_records(dns, ipv4, ipv6); | |
| } | |
| // Perform DDNS Update | |
| const ip4 = await getipv4(); | |
| const ip6 = await getipv6(); | |
| const dns = await get_dns_info(WEBSITE, SUBDOMAIN); | |
| await update_dns(dns, ip4, ip6); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment