March 19, 2020

Build Your Own Dynamic DNS Service on AWS

I have been using No-IP.com free services for at least one decade. Recently, I am so tired of clicking the domain verification Email each month. I start looking for alternatives. Dynu looks promising but unfortunately, this service is blocked for unknown reasons in certain areas. I spent several days Googling and trying different methods including existing online or build your own services. Most of them are either too complex or a little bit too expensive.

Luckily, I have several unused domains registered in Amazon Route 53. What I need is a client that can poke the server periodically and update the IP address of a domain. This is a typical client/server application and is not difficult to build using the popular Serverless computing model. By using serverless, I am not paying extra when the client isn’t talking to the server.

Dynamic DNS on AWS

Dynamic DNS Client

The dynamic DNS client is very simple. What it needs is to poke the server through an API endpoint in a certain time interval. I am using Go because I want a cross-platform solution to work on both Linux (especially Raspberry Pi) and Windows and hide the API endpoint. A simple curl command will also do the job I believe.

package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"io/ioutil"
	"log"
	"net/http"
)

func main() {
	var record string
	flag.StringVar(&record, "record", "", "record to update")
	flag.Parse()

	if record == "" {
		log.Fatal("record can't be empty")
	}

	const API = "YOUR_API_ENDPOINT"
	reqBody := map[string]string{"record": record}
	reqJSONBody, _ := json.Marshal(reqBody)

	resp, err := http.Post(API, "application/json", bytes.NewBuffer(reqJSONBody))
	if err != nil {
		log.Fatal(err)
	}

	defer resp.Body.Close()
	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	log.Print(string(respBody))
}

The Go client takes one flag record, which is the domain registered in AWS Route 53, and sends a HTTP POST request to the API endpoint. We don’t really need to send the public IP address in the POST body because the public IP address will be resolved at the server side.

Dynamic DNS API

Here comes the interesting part. My dynamic DNS clients are syncing to the server in a very sparse interval (more than 10 minutes). I don’t want to have a dedicate server listening on requests when clients are not syncing. Serverless model is definitely the way to go. The serverless framework is the de facto choice but I found AWS Chalice is also very interesting.

AWS Chalice is a microframework for writing serverless apps in Python. It allows you to create and deploy applications that use AWS Lambda. After you deploy the app, it will create a Lambda function, an API Gateway endpoint, and also generates an IAM policy automatically for you.

import os

import boto3
from chalice import Chalice

app = Chalice(app_name='my-ddns-app')

r53 = boto3.client('route53')


def _get_source_ip(context):
    """Get request IP address from request context"""

    return context['identity']['sourceIp']


def _update_record(record_name, source_ip):
    """Update record value"""

    return r53.change_resource_record_sets(
        HostedZoneId=os.environ['HOST_ZONE_ID'],
        ChangeBatch={
            'Changes': [
                {
                    'Action': 'UPSERT',
                    'ResourceRecordSet': {
                        'Name': record_name,
                        'ResourceRecords': [
                            {
                                'Value': source_ip,
                            },
                        ],
                        'TTL': 300,
                        'Type': 'A',
                    },
                },
            ],
        },
    )


@app.route('/', methods=['POST'])
def update_record():
    """Update record value"""

    request = app.current_request

    record_name = request.json_body['record']
    source_ip = _get_source_ip(request.context)
    _update_record(record_name, source_ip)

    return {
        'ip': source_ip,
    }

It is also not too difficult to add an Email notification via Amazon SES when the new public IP address is different from the old one. You have to pay to get this feature in some online dynamic DNS services. With Amazon AWS, you get it for free basically. I have been running my own dynamic DNS service for several months and it is stable and costs me nothing. No annoying Emails anymore each month to keep my dynamic DNS alive.

© 2015-2020 Jiawei Huang

Powered by Hugo & Kiss.