Skip to content

Instantly share code, notes, and snippets.

@sixtyfive
Forked from IsoLinearCHiP/update-ddns.rb
Last active October 20, 2025 15:30
Show Gist options
  • Select an option

  • Save sixtyfive/6afd5440d7ca2e80d4ad73f307431d7f to your computer and use it in GitHub Desktop.

Select an option

Save sixtyfive/6afd5440d7ca2e80d4ad73f307431d7f to your computer and use it in GitHub Desktop.
DIY dDNS service script using Hetzner's DNS API
#!/usr/bin/env ruby
#
# Original, including IsoLinearCHiP's improvements: https://gist.github.com/IsoLinearCHiP/d6bc594143102e6acd131fc65c080f64
# Current version, adapted to Hetzner phasing out dns.h.d for console.h.d
# now maintained at https://gist.weitnahbei.de/sixtyfive/7b090861f8e44a8ba3b37c9cde93c3af
API="https://api.hetzner.cloud/v1"
require 'json'
require 'socket'
require 'net/http'
require 'fileutils'
require 'pstore'
FileUtils.mkdir_p "#{Dir.home}/.local/share/update-ddns"
@pstore = PStore.new "#{Dir.home}/.local/share/update-ddns/cache.pstore"
def api(path, records=nil)
if records
cmd = "curl -s -X POST -H 'Authorization: Bearer #{@token}' -H 'Content-Type: application/json' -d '{\"records\": #{records.to_json}}' #{API}#{path}"
`#{cmd}`
else
cmd = "curl -s -H 'Authorization: Bearer #{@token}' #{API}#{path}"
JSON.parse `#{cmd}`
end
end
def cache(key, value=nil)
if value
@pstore.transaction{@pstore[key] = value}
end
@pstore.transaction(true){return @pstore[key]}
end
def current_wan_ip
Socket.getifaddrs
.map{|iface| [iface.name, iface.addr.ip_address] if iface.addr.ipv4? if iface.addr}
.compact
.find{|x| x[0]=='ppp0'}[1]
end
def query
@token = cache('api_token')
zone = ARGV[0]
rrset_name = ARGV[1]
zone_id = cache('zone_id')
zone_id ||= cache('zone_id', api('/zones')['zones'].find{|x| x['name'] == @zone}['id'])
wan_ip = cache('current_wan_ip', current_wan_ip)
puts wan_ip
ips_on_record = api("/zones/#{zone_id}/rrsets/#{rrset_name}/A")['rrset']['records'].collect{|x| x['value']}
if ips_on_record.last != wan_ip
api("/zones/#{zone_id}/rrsets/#{rrset_name}/A/actions/set_records", [{value: wan_ip, comment: 'dynamically updated'}])
puts "record updated"
else
puts "no update necessary"
end
end
case ARGV.size
when 1
cache('api_token', ARGV[0])
puts "API token set"
when 2
(warn "Error: API token not set"; exit!) unless cache('api_token')
query
else
warn "Usage: update-ddns <zone> <record> | update-ddns <api token>"
exit!
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment