DynDNS
Dynamic DNS helps turning dynamic IP adresses into static DNS Names and thus making systems permanently available. This page gives an insight into how to set things up with Hetzner DNS console and Opnsense. Even though Opnsense has a DynDNS client included, it does currently not support the Hetzner DNS API keys.
Hetzner
Step one is to create the actual record we went to regularly update. The easiest way is to do this via the DNS console.
Warning
The Hetzner DNS API does not support any ACLs at his point (January 2024), thus having access to the access token exposes ALL DNS settings you have in your Hetzner DNS console!
Hetzner DNS API
The Hetzner DNS API is documented here . The API tokens can be created in the DNS Console. The first code snippet gives you a list of all your zones in Hetzner’s DNS Console. The only necessary setting is the API Token. We use this to get the Zone ID.
# Install the Python Requests library:
# pip install requests
import requests
def send_request():
# Get Zones
# GET https://dns.hetzner.com/api/v1/zones
try:
response = requests.get(
url="https://dns.hetzner.com/api/v1/zones",
headers={
"Auth-API-Token": "**API TOKEN**",
},
)
print('Response HTTP Status Code: {status_code}'.format(
status_code=response.status_code))
print('Response HTTP Response Body: {content}'.format(
content=response.content))
except requests.exceptions.RequestException:
print('HTTP Request failed')
send_request()
The second snippet is used to fetch the Record ID.
# Install the Python Requests library:
# pip install requests
import requests
def send_request():
# Get Records
# GET https://dns.hetzner.com/api/v1/records
try:
response = requests.get(
url="https://dns.hetzner.com/api/v1/records",
params={
"zone_id": "**Zone ID**",
},
headers={
"Auth-API-Token": "**API Token**",
},
)
print('Response HTTP Status Code: {status_code}'.format(
status_code=response.status_code))
print('Response HTTP Response Body: {content}'.format(
content=response.content))
except requests.exceptions.RequestException:
print('HTTP Request failed')
send_request()
The third snippet is just for verification, that we have the correct ID. Now, this request is a little mean, as the Record ID has to be passed as part of the URL!
# Install the Python Requests library:
# pip install requests
import requests
def send_request():
# Get Record
# GET https://dns.hetzner.com/api/v1/records/{RecordID}
try:
response = requests.get(
url="https://dns.hetzner.com/api/v1/records/**Record ID**",
headers={
"Auth-API-Token": "**API TOKEN**",
},
)
print('Response HTTP Status Code: {status_code}'.format(
status_code=response.status_code))
print('Response HTTP Response Body: {content}'.format(
content=response.content))
except requests.exceptions.RequestException:
print('HTTP Request failed')
send_request()
Set Record
In the final snippet we need all parameters together: API Token, Zone ID, Record ID, the target DNS Name and the target IP.
import requests
import json
def send_request():
# Update Record
# PUT https://dns.hetzner.com/api/v1/records/{RecordID}
try:
response = requests.put(
url="https://dns.hetzner.com/api/v1/records/**Record ID**",
headers={
"Content-Type": "application/json",
"Auth-API-Token": "**API Token**",
},
data=json.dumps({
"value": "**IP**",
"ttl": 300,
"type": "A",
"name": "**DNS Name**",
"zone_id": "**Zone ID**"
})
)
print('Response HTTP Status Code: {status_code}'.format(
status_code=response.status_code))
print('Response HTTP Response Body: {content}'.format(
content=response.content))
except requests.exceptions.RequestException:
print('HTTP Request failed')
send_request()
Opnsense
Opnsense exposes various functions, not all, some sligthly strange, via the API. Access is possible with individual API keys, which can be created on the users page. It also supports ACLs, which are more or less good.
Warning
The ACL for /diagnostics/interface/getinterfaceconfig
seems to be slightly broken. It only seems to be accessible when the API key has ALL permissions.
Fetching the Interface IP
The following snippet requires the API key and secret and returns the IP address of ppppoe0. The interface name will have to be adjusted. Also ipv4 can be replaced with ipv6 if necessary. Don’t forget to import the server’s certificate beforehand, for testing purposes verify=False
can be addedd to the request.
import json
import requests
key='**Key**'
secret='**Secret**'
url='https://**IP**:**Port**/api/diagnostics/interface/getinterfaceconfig'
r = requests.get(url, auth=(key,secret))
if r.status_code == 200:
response = json.loads(r.text)
print(response["pppoe0"]["ipv4"][0]["ipaddr"])
The Full Script
The full script will fetch the IP from the local Opnsense instance and then pass it on to Hetzner.
import json
import requests
opnsense_key = ''
opnsense_secret = ''
opnsense_url = 'https://ip:port'
opnsense_api_endpoint = '/api/diagnostics/interface/getinterfaceconfig'
opnsense_api_url = opnsense_url+opnsense_api_endpoint
hetzner_api_key = ''
hetzner_api_url = 'https://dns.hetzner.com/api/v1/records/'
hetzner_record_id = ''
hetzner_url = hetzner_api_url + hetzner_record_id
hetzner_zone_id = ''
domain_record_name = 'dyndns.system'
dns_ttl = 300
def get_external_ip():
try:
r = requests.get(opnsense_api_url, auth=(opnsense_key,opnsense_secret))
if r.status_code == 200:
response = json.loads(r.text)
return response["pppoe0"]["ipv4"][0]["ipaddr"]
except requests.exceptions.RequestException:
print('Opnsense: HTTP Request failed')
def set_ip(ip):
try:
response = requests.put(
url = hetzner_url,
headers={
"Content-Type": "application/json",
"Auth-API-Token": hetzner_api_key,
},
data=json.dumps({
"value": ip,
"ttl": dns_ttl,
"type": "A",
"name": domain_record_name,
"zone_id": hetzner_zone_id
})
)
print('Response HTTP Status Code: {status_code}'.format(
status_code=response.status_code))
print('Response HTTP Response Body: {content}'.format(
content=response.content))
except requests.exceptions.RequestException:
print('Hetzner: HTTP Request failed')
set_ip(get_external_ip())