I plan on creating a résumé website/GitLab server that will run on some old hardware I have laying around.

A common problem that arises when trying to run servers off a residential connection is that the ip address changes preiodically. The ip address is dynamic. There are good software solutions out there but I decided to build my own.

Google Domains does give it's users a Rest API which makes the construction of a small python script to keep it updated easy. So, let us take a look at it's documentation. (link)

The Rest endpoint is: https://domains.google.com/nic/update

The endpoint requires Basic Authentication were the username and password are concatenated in a specific format, username:password, is encoded into base64 and sent with the https packet. Another requirement is the User-Agent http header. The ip address to be updated to and the host name are both sent through url query, domains.google.com/nic/update?hostname=subdomain.yourdomain.com&myip=1.2.3.4. The username and password is provided to you when creating the Dynamic DNS in the Google Domains console.

The server only returns good 1.2.3.4 or nochg 1.2.3.4 when the request has a success and the ip address the host points to is changed or the ip address is already is pointing to the ip address provided.

Python Code

Firstly, we need to store our cridentials. Now, this is obviously not the best way to store our credentials. In plain text and in a JSON format. I'll leave it to the reader to figure out a better way. Anyway, here is the format in the conf.json file.

{
  "username":"username goes here",
  "password":"password goes here",
  "hostname":"your hostname goes here",
  "email":"your account email goes here",
  "interval":30
}

Python provides a package to parse this is called json. The interval variable will be the refresh rate for our ip checking. Here is the python code:

# parse.py
import json


def read_config_file(path):
    file = open(path, 'r').read()
    js = json.loads(file)
    return js["username"], js["password"], js["hostname"], js["email"], js["interval"]

Now, in another module we need a function that gets the current ip address from an online ip service and another to update the ip. Python provides a low level http client in http.client that allows us to modularly add the required headers. Firstly, in a module called wclient in the file wclient.py we add the following imports.

# wclient.py
import http.client
from base64 import b64encode

Creating the function to get the ip address from the service in the domain ipv4bot.whatismyipaddress.com is pretty simple since it returns your ip address in plain text requiring to parsing.

def get_ip_address():
    h = http.client.HTTPSConnection('ipv4bot.whatismyipaddress.com', 443)
    h.request('GET', '/')
    response = h.getresponse()
    return str(response.read(), "UTF-8")

Now for the function that updates the ip address we first need to encode the username and password in base64, add it and the required headers to the header of the https request, add the email to the body of the 'POST' request, send it, and get the return.

def set_ip_address(username, password, hostname, ip_address, email):
    h = http.client.HTTPSConnection('domains.google.com', 443)
    user_pass = b64encode(bytes(f"{username}:{password}", "UTF-8")).decode("UTF-8")
    headers = {'Authorization': 'Basic %s' % user_pass, 'User-Agent': 'GoogleDynamicUpdater/1.0',
               'accept': 'text/html'}
    h.request("POST", f"/nic/update?hostname={hostname}&myip={ip_address}", email, headers)
    response = h.getresponse()
    return str(response.read(), "UTF-8")

Now for running these functon we need to use an infinite loop while sleeping a specified amount of minutes on every interval, getting the ip address, and checking it against the last acquired ip address. We could set it up as a daemon with python-daemon or manually, I'll go ahead and do that in a future post.

We call the function we created and check the returned from the set_ip_address function.

import parse
import wclient
import time


def work():
    (username, password, hostname, email, interval) = parse.read_config_file("conf.json")
    n_minutes = int(interval) * 60
    new_ip = ""
    while True:
        ip = wclient.get_ip_address()
        if new_ip != ip:
            ret = wclient.set_ip_address(username, password, hostname, ip, email)
            check = {f"good {ip}", f"nochg {ip}"}
            if ret not in check:
                print(f"Error: no good  - {ret}")
                exit()
            new_ip = ip
        time.sleep(n_minutes)


work()