diff --git a/blog/posts/2023-12-02_make-your-own-ddns/index.md b/blog/posts/2023-12-02_make-your-own-ddns/index.md new file mode 100644 index 0000000..c26b5ac --- /dev/null +++ b/blog/posts/2023-12-02_make-your-own-ddns/index.md @@ -0,0 +1,221 @@ +--- +title: Make Your Own DDNS +description: A guide on how to setup a DDNS-like system using Cron and a Deno script. +tags: + - linux + - guide + - automation +--- + +Ever tried hosting a server in your home network but annoyed at the fact that your public IP address keeps changing? Imagine for a moment that you're running a [Nextcloud](https://nextcloud.com) server located at `nextcloud.example.com`. All of a sudden, the public IP changes and your domain is still pointing to the old IP address. As a result, connectivity to your server gets disrupted. The change can happen at any time and without warning. It can take some time for you to find out. Once you do, you'll have to manually update the [A-record](https://en.wikipedia.org/wiki/List_of_DNS_record_types#A) of `nextcloud.example.com` to restore connectivity. + +As you can see, spontaneous IP address changes are disruptive events; a [static IP address](https://en.wikipedia.org/wiki/IP_address#IP_address_assignment) or a [DDNS](https://en.wikipedia.org/wiki/Dynamic_DNS) service is essential for running public servers. Internet service providers usually offer static IP addresses only in their business plans. So this leaves out the vast majority of internet users. The other option, DDNS, is a feature supported by some routers. However, utilizing this feature typically involves having to sign-up for a hostname on a DDNS provider like [No-IP](https://www.noip.com). If you get one of No-IP's free accounts, then you must [manually confirm hostnames](https://www.noip.com/support/knowledgebase/confirm-my-hostname-free-account-support-question-day) every 30 days. + +So, the options we have thus far require us to either: + +- Pay for a more expensive internet plan. +- Utilize a router feature that requires creating a hostname on a third-party DDNS provider. + +If only there was another way... + +## The Solution + +The answer is to create our own DDNS. We can achieve a DDNS-like system with a [shell script](https://en.wikipedia.org/wiki/Shell_script) that runs every hour; scheduled via [Cron](https://en.wikipedia.org/wiki/Cron). The script performs the following tasks everytime it runs: + +1. Obtain the current public IP address and the domain's A-record. +1. Compare the public IP and the A-record's value. +1. If they are not the same, it updates the A-record to the value of the public IP address. + +This system provides some major advantages: + +- Can be deployed on any computer with a modern [Linux](https://en.wikipedia.org/wiki/Linux) installation. +- Does not depend on a DDNS provider. +- No need for a router with DDNS support. +- The script is flexible and can be changed to use another [domain registrar](https://en.wikipedia.org/wiki/Domain_name_registrar). + +## Requirements + +- Basic proficiency with Linux and its command line. +- A computer with a [Debian](https://www.debian.org)-based distribution installed. + + This computer will be referred to as the *Host Computer*. It will be online 24/7 to keep the scheduled script running. + +- Domain (or subdomain) with its DNS A-record set. + + Your domain registrar must have an [API](https://en.wikipedia.org/wiki/API) which provides the ability to read and update records. This guide will be using the [GoDaddy API](https://developer.godaddy.com/doc) to update an A-record on [GoDaddy](https://www.godaddy.com). + +- **Optional**: Familiarity with [JavaScript](https://en.wikipedia.org/wiki/JavaScript) or [TypeScript](https://en.wikipedia.org/wiki/TypeScript). + + Having some basic TypeScript knowledge allows you to modify or update the shell script. + +## Preparation + +### Host Computer + +Perform these steps on the Host Computer. + +#### Update the System + +Open a terminal then enter the command below to update the system. + +``` sh +sudo apt -y update && sudo apt -y upgrade +``` + +#### Install Cron + +Cron is included in most Linux distributions but we will install it just to be sure: + +``` sh +sudo apt install cron +``` + +#### Install Deno + +[Deno](https://deno.com) is a secure runtime environment for JavaScript and TypeScript. It is the environment for which the TypeScript-programmed shell script will be running under. + +At the time of writing, Deno is not available in the [APT](https://en.wikipedia.org/wiki/APT_(software)) repositories; we will be using the installation script instead. + +> Refer to the Deno documentation for [additional installation options](https://docs.deno.com/runtime/manual/getting_started/installation). +{.info} + +Run the following command to install Deno: + +``` sh +curl -fsSL https://deno.land/x/install/install.sh | sh +``` + +Confirm that Deno is properly installed by checking its version: + +``` sh +deno --version +``` + +An output like the one below indicates that Deno was successfully installed: + +``` txt +deno 1.38.3 (release, x86_64-unknown-linux-gnu) +v8 12.0.267.1 +typescript 5.2.2 +``` + +### GoDaddy API Keys + +Follow the [GoDaddy API setup](https://developer.godaddy.com/getstarted#setup) to create the API secret and key. Take note of the key and secret as they will be needed shortly. + +> Ensure that you get the API secret and key for the **production environment**; not the ones for the test environment. +{.warning} + +## The Script {#the-script} + +> The script sends requests to two endpoints from GoDaddy's Domains API: +> +> - [GET /v1/domains/{domain}/records/{type}/{name}](https://developer.godaddy.com/doc/endpoint/domains#/v1/recordGet) +> - [PUT /v1/domains/{domain}/records/{type}/{name}](https://developer.godaddy.com/doc/endpoint/domains#/v1/recordReplaceTypeName) +> +> Refer to the [GoDaddy Domains API documentation](https://developer.godaddy.com/doc/endpoint/domains) for the complete list of endpoints. +{.info} + + +> The public IP address is obtained from an instance of [IpMe](https://code.fosterhangdaan.com/foster/ipme) at [https://ipme.fosterhangdaan.com](https://ipme.fosterhangdaan.com). [Ipify](https://www.ipify.org) can be used as an alternative. +{.info} + +Here is the content of the script: + +``` ts +#!/usr/bin/env -S deno run --ext=ts --allow-net + +/** + * Copyright (c) 2023 Foster Hangdaan + * SPDX-License-Identifier: agpl-3.0-or-later + * + * A script which synchronizes a GoDaddy A-record with the public IP + * address. +*/ + +// Change this value to your domain. +const domain = "example.com"; + +// Change this value to your GoDaddy API key. +const GODADDY_API_KEY = "key"; + +// Change this value to your GoDaddy API secret. +const GODADDY_API_SECRET = "secret"; + +// If you prefer to use Ipify instead, change this URL to: +// https://www.ipify.org +const publicIpUrl = "https://ipme.fosterhangdaan.com"; + +const goDaddyUrl = `https://api.godaddy.com/v1/domains/${domain}/records/A/@`; + +const goDaddyRequestHeaders = new Headers({ + Authorization: `sso-key ${GODADDY_API_KEY}:${GODADDY_API_SECRET}`, +}); + +const [ publicIpResponse, goDaddyResponse ] = await Promise.all([ + fetch(publicIpUrl), + fetch(goDaddyUrl, { headers: goDaddyRequestHeaders }), +]); + +if (!publicIpResponse.ok) { + throw new Error(`Failed to fetch public IP address from ${publicIpUrl}.`); +} else if (!goDaddyResponse.ok) { + throw new Error("Failed to fetch A-records from GoDaddy."); +} + +const goDaddyJsonResponse = await goDaddyResponse.json(); + +if (goDaddyJsonResponse.length === 0) { + throw new Error("No GoDaddy A-records found."); +} + +const publicIP = await publicIpResponse.text(); +const goDaddyIP = goDaddyJsonResponse[0]["data"]; + +if (publicIP !== goDaddyIP) { + console.log(`The public IP address has changed. Public IP: ${publicIP}; Old IP: ${goDaddyIP}`); + goDaddyRequestHeaders.append("Content-Type", "application/json"); + const response = await fetch(goDaddyUrl, { + method: "PUT", + headers: goDaddyRequestHeaders, + body: JSON.stringify([ + { + data: publicIP, + } + ]), + }); + if (!response.ok) { + throw new Error("Failed to update GoDaddy A-record."); + } else { + console.log(`GoDaddy A-record successfully updated to ${publicIP}.`); + } +} +``` + +## Schedule the Script to Run Every Hour with Cron + +On the Host Computer, save [the script](#the-script) as a file at `/etc/cron.hourly/ddns`. Make sure to replace some necessary values: + +- Replace `example.com` in `const domain = "example.com";` with your domain. +- Replace `key` in `const GODADDY_API_KEY = "key";` with your GoDaddy API key. +- Replace `secret` in `const GODADDY_API_SECRET = "secret";` with your GoDaddy API secret. + +Grant read, write, and execute permissions only to the `root` user: + +``` sh +sudo chown root:root /etc/cron.hourly/ddns +sudo chmod 700 /etc/cron.hourly/ddns +``` + +> Access to the script should be granted only to authorized users (such as `root`) since the script contains sensitive information: your GoDaddy API secret and key. +{.warning} + +The script should now be scheduled to run every hour by Cron. + +## Conclusion + +In this guide, we have setup an hourly Cron script that updates a GoDaddy domain's A-record if the public IP address changed. You can now rest assured that your hosted services will be protected from disruptions caused by public IP address changes. + +Feel free to modify the script to your needs; especially if you are using a domain registrar other than GoDaddy. + +As always, [contact me](/#contact-me) for any comments or suggestions regarding this guide.