CloudFlare Dynamic DNS using OpenWRT

I use dynamic DNS for my home internet connection so that I can access the machines from anywhere on the internet. And I use OpenWRT on my router. Earlier I was using Namecheap for managing DNS but I switched to CloudFlare for performance and security reasons of the website.

Unfortunately CloudFlare doesn’t support updating IP via shell script — well, it sort of does but the JSON stuff gets very messy with quoting in shell scripts, so I wrote a Lua script to update my IP whenever my PPPoE connection starts up; I have dropped the script in /etc/ppp/ip-up.d  so it gets executed by pppd whenever my connection comes up. You can run this script via cron or put it /etc/hotplug  if you wish to. This script uses LuaSocket, LuaSec, JSON4Lua and libubus-lua libraries that are easily installable on an OpenWRT router with 4 MB flash memory.

Now I can have the benefits of CloudFlare without losing out on DDNS :D. Here’s the code:

#!/usr/bin/lua

-- For use with openwrt since openwrt supports LUA.
-- Prerequisites:

-- luasec
-- luasocket
-- libubus-lua
-- json4lua

-- I have a PPPoE connection so I just drop this script in /etc/ppp/ip-up.d
-- You can run via crontab or put it in interface hotplug 🙂

update_records = {"ddns"}
domain_name = "domain.com"

function log(msg)
	os.execute("logger -t updateip '" .. msg .. "'")
end

require ("os")
json = require ("json")
require ("socket")
https = require ("ssl.https")
require ("ltn12")

reqheaders = {
	["X-Auth-Email"] = "EMAIL_ID",
	["X-Auth-Key"] = "API_KEY",
	["Content-Type"] = "application/json",
}

zones = {}

success = https.request({
	url = "https://api.cloudflare.com/client/v4/zones?name=" .. domain_name,
	sink = ltn12.sink.table(zones),
	method = "GET",
	headers = reqheaders,
})

if not success then
	log('Failed to fetch zones')
	os.exit(1)	
end

zones = json.decode(zones[1])

if not zones.success then
	log('Zone fetch failed')
	os.exit(1)
end

zone_id = zones.result[1].id

for i, name in ipairs(update_records) do
	if name ~= domain_name then
		update_records[i] = string.format("%s.%s", name, domain_name)
	end
end

record_filter = "name=" .. table.concat(update_records, ",")

records = {}
success = https.request({
	url = "https://api.cloudflare.com/client/v4/zones/" .. zone_id .. "/dns_records?type=A&" .. record_filter,
	sink = ltn12.sink.table(records),
	method = "GET",
	headers = reqheaders
})

if not success then
	log('Failed to fetch dns records')
	os.exit(1)
end

records = json.decode(records[1])

if not records.success then
	log('Record fetch failed')
	os.exit(1)
end

update_records_1 = {}

require ("ubus")
u = ubus.connect()

if not u then
	log('Ubus connect failed')
	os.exit(1)
end

status = u:call("network.interface.wan", "status", {})
ip = status["ipv4-address"][1]["address"]

for _, record in pairs(records.result) do
	for _, name in pairs(update_records) do
		if name == record.name and record.content ~= ip then
			record.content = ip
			table.insert(update_records_1, record)
		end
	end
end

for _, record in pairs(update_records_1) do
	encoded_update = json.encode(record)	
	response = {}
	reqheaders["content-length"] = string.len(encoded_update)
	success = https.request({
		url = "https://api.cloudflare.com/client/v4/zones/" .. zone_id .. "/dns_records/" .. record.id,
		method = "PUT",
		headers = reqheaders,
		sink = ltn12.sink.table(response),
		source = ltn12.source.string(encoded_update)
	})
	if not success then
		log("Failed to update " .. record.name)
	else
		response = json.decode(response[1])
		if not response.success then
			log("Failed to update " .. record.name)
		else
			log("Updated " .. record.name)
		end
	end
end

Suggestions? Post in comments or fork on GitHub.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: