Dynamic DNS with CPanel

By

Occasionally I need to SSH into my home computer while I’m out and about, so to make things easy I set up a DNS entry to point to my home network. My router, like most routers, supports a service called “dynamic DNS” where the router periodically updates a DNS entry when it notices that it has a new IP from my ISP. And it works: as long as that service is dyndns.org. If you’re familiar with DynDNS, you may know that they shut down their free tier earlier this year and since I have absolutely no interest (nor need) to join their paid tier, I’ve been doing without.

I’d like to get that up an running again, so I decided to do some digging into whether or not CPanel has its own API. The official documentation was down when I did my initial research (I assumed it was offline for good), but apparently it’s back up now. In any case, I was able to use Google Cache to find the Zone Edit feature. Next, all you have to do is place this script on a server within the network and register it with cron / Windows Scheduler and your DNS entry will be periodically updated with your public IP address.

As usual, you can test the following code in LINQPad with the “C# Program” language:

async void Main()
{
	var user = "";
	var pass = "";
	var cpanelDomain = "www.myhost.com";
	var baseDomain = "nem.ec";
	var domainToEdit = "example.nem.ec.";  // keep the trailing .
	var newAddress = await GetCurrentIpAddress();
	
	var baseUrl = String.Format(
        "https://{0}:2083/xml-api/cpanel?cpanel_xmlapi_module=ZoneEdit&domain={1}",
        cpanelDomain, 
        baseDomain);
	
	var client = new HttpClient();
	client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
        Convert.ToBase64String(
            Encoding.ASCII.GetBytes(String.Format("{0}:{1}", user, pass))));
	var url = baseUrl + "&cpanel_xmlapi_func=fetchzone";
	
	var resp = await client.GetAsync(url);
	resp.EnsureSuccessStatusCode();
	var stream = await resp.Content.ReadAsStreamAsync();
	
	var doc = XDocument.Load(stream);
	
    // Grab the first result where the domain name is the name
    // we're looking for.
	var record = doc
		.Element("cpanelresult")
		.Element("data")
		.Elements("record")
		.FirstOrDefault(r => 
			r.Elements("name").Any(e => 
				e.Value == domainToEdit));
	
	if(record != null)
	{
		Console.WriteLine("Replacing old address '{0}' with '{1}'", 
			record.Element("address").Value,
			newAddress);
		var lineNumber = record.Element("line").Value;
        
        // Line number is the "primary key" of the record,
        // at least as far as the API is concerned.
		var editUrl = baseUrl + String.Format(
			"&cpanel_xmlapi_func=edit_zone_record&line={0}&address={1}",
			lineNumber,
			newAddress);
		resp = await client.GetAsync(editUrl);
		resp.EnsureSuccessStatusCode();
		Console.WriteLine("Changed address.");
	}
	else
	{
		Console.WriteLine("No record available.");
	}
}

// Polls icanhazip for the current public IP address.
async Task<string> GetCurrentIpAddress()
{
	var client = new HttpClient();
	var resp = await client.GetAsync("https://icanhazip.com/");
	return await resp.Content.ReadAsStringAsync();
}