280 lines
9.3 KiB
Markdown
280 lines
9.3 KiB
Markdown
---
|
|
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 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 --allow-net
|
|
|
|
/**
|
|
* Copyright (c) 2023 Foster Hangdaan <https://www.fosterhangdaan.com>
|
|
* SPDX-License-Identifier: agpl-3.0-or-later
|
|
*
|
|
* A script which synchronizes a GoDaddy A-record with your public IP
|
|
* address.
|
|
*/
|
|
|
|
// Change this value to your GoDaddy 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://api.ipify.org
|
|
const publicIpUrl = "https://ipme.fosterhangdaan.com";
|
|
|
|
const domainParts = domain.split(".");
|
|
const domainRoot = domainParts.slice(-2).join(".");
|
|
const domainName = domainParts.slice(0, -2).join(".") || "@";
|
|
|
|
const goDaddyUrl =
|
|
`https://api.godaddy.com/v1/domains/${domainRoot}/records/A/${domainName}`;
|
|
|
|
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.ts`. Make sure to replace some necessary values:
|
|
|
|
- Replace `example.com` in `const domain = "example.com";` with your GoDaddy
|
|
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.ts
|
|
sudo chmod 700 /etc/cron.hourly/ddns.ts
|
|
```
|
|
|
|
> 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.
|