-
-
Save IsoLinearCHiP/d6bc594143102e6acd131fc65c080f64 to your computer and use it in GitHub Desktop.
DIY dDNS service script using Hetzner's DNS API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env ruby | |
| # on OpenRC systems, place into e.g. /etc/periodic/15min | |
| # (or create /etc/periodic/1min and add line to root's crontab) | |
| # on systemd systems, install it as a timer | |
| # create/find these in Hetzner's DNS admin interface at https://dns.hetzner.com/ | |
| API_TOKEN_NAME="..." | |
| API_TOKEN="..." | |
| ZONES=["...", "..."] # TLD; must be registered at https://robot.hetzner.com/domain | |
| RECORDS=["...", "..."] # list of subdomains, also specified there | |
| API_BASE = "https://dns.hetzner.com/api/v1" | |
| # will make the script return a little faster if specified | |
| # ZONE_ID="..." | |
| # RECORD_ID="..." | |
| require 'json' | |
| require 'socket' | |
| require 'net/http' | |
| require 'optparse' | |
| def GET(uri) | |
| headers = { | |
| "Auth-API-Token" => API_TOKEN, | |
| "Accept" => "application/json" | |
| } | |
| Net::HTTP.get(URI(uri), headers) | |
| end | |
| def POST(uri, body) | |
| headers = { | |
| "Auth-API-Token" => API_TOKEN, | |
| "Accept" => "application/json", | |
| "Content-Type" => "application/json" | |
| } | |
| res = Net::HTTP.post(URI(uri), body, headers) | |
| res.body | |
| end | |
| def PUT(uri, body) | |
| uri = URI(uri) | |
| hostname = uri.hostname | |
| req = Net::HTTP::Put.new(uri) | |
| req["Auth-API-Token"] = API_TOKEN | |
| req["Accept"] = "application/json" | |
| req.body = body | |
| req.content_type = 'application/json' | |
| res = Net::HTTP.start(hostname, uri.port, :use_ssl => true) do |http| | |
| http.request(req) | |
| end | |
| res.body | |
| end | |
| def main(zone_id, record_id) | |
| ZONES.each do |zone| | |
| zone_id ||= ( | |
| json = JSON.parse(GET("https://dns.hetzner.com/api/v1/zones")) | |
| zones_json = json.delete('zones') | |
| zones_json.find{|x| x['name'] == zone}['id'] | |
| ) | |
| json = JSON.parse(GET("https://dns.hetzner.com/api/v1/records?zone_id=#{zone_id}")) | |
| records_json = json.delete('records') | |
| 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] | |
| if record_id then | |
| data = JSON.generate({ | |
| "value": "#{current_wan_ip}", | |
| "ttl": 60, | |
| "type": "A", | |
| "name": "#{record}", | |
| "zone_id": "#{zone_id}" | |
| }) | |
| PUT("https://dns.hetzner.com/api/v1/records/#{record_id}", data) | |
| else | |
| RECORDS.each do |record| | |
| record_json = records_json.find{|x| x['name']==record} | |
| url = "https://dns.hetzner.com/api/v1/records" | |
| data = JSON.generate({ | |
| "value": "#{current_wan_ip}", | |
| "ttl": 60, | |
| "type": "A", | |
| "name": "#{record}", | |
| "zone_id": "#{zone_id}" | |
| }) | |
| record_id ||= record_json['id'] # must exist! | |
| if record_id then | |
| PUT("https://dns.hetzner.com/api/v1/records/#{record_id}", data) | |
| else | |
| POST("https://dns.hetzner.com/api/v1/records", data) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| def test(zone_id, record_id) | |
| puts zone_id | |
| puts record_id | |
| puts GET("https://httpbin.org/get") | |
| puts POST("https://httpbin.org/post", JSON.generate({foo: "bar"})) | |
| puts PUT("https://httpbin.org/put", JSON.generate({foo: "bar"})) | |
| end | |
| options = {} | |
| OptionParser.new do |parser| | |
| parser.banner = "Usage: update-ddns.rb [options]" | |
| parser.on("-t", "--test", "Run tests") do |v| | |
| options[:test] = v | |
| end | |
| parser.on("-z ZONE", "--zone ZONE", "Specify zone id") do |v| | |
| options[:zone_id] = v | |
| end | |
| parser.on("-r RECORD", "--record RECORD", "Specify record id") do |v| | |
| options[:record_id] = v | |
| end | |
| end.parse! | |
| if options[:test] then | |
| test(options[:zone_id], options[:record_id]) | |
| else | |
| main(options[:zone_id], options[:record_id]) | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment