Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
clayoster committed Sep 15, 2024
0 parents commit ab62f51
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ddns-cloudflare

Self-hosted DDNS service for receiving public IP updates from a router or other local source
and updating a DNS record in Cloudflare.

The primary purposes for building yet another DDNS tool for updating Cloudflare records:
- Many other solutions depend upon using a `Global` API token which is unacceptable for this singular purpose. This solution allows the use of a API token scoped with privileges to edit a single DNS zone.
- Unforunately Cloudflare does not allow limiting the edit privileges to a single DNS record in a zone at this point.
- Self-hosted option for receving DDNS updates from a router or other local source rather than depending on querying an external API for public IP address changes

Valid response codes were gathered from here:
https://github.com/troglobit/inadyn/blob/master/plugins/common.c

This service requires a GET request in the following format

https://<username>:<password>@ddns.example.com/update?hostname=<dns record to update>&ip=<public ip>
114 changes: 114 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/python3

import CloudFlare
import os
from flask import Flask, request
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
auth = HTTPBasicAuth()

# Set variables from environment variables
auth_user = os.environ.get('AUTH_USER', None)
auth_pass = os.environ.get('AUTH_PASS', None)
api_token = os.environ.get('API_TOKEN', None)

users = {
auth_user: generate_password_hash(auth_pass)
}

@auth.verify_password
def verify_password(username, password):
if username in users and \
check_password_hash(users.get(username), password):
return username

@auth.error_handler
def unauthorized():
return "badauth"

@app.route('/update')
@auth.login_required
def main():
# Set hostname variable
if 'hostname' in request.args:
hostname = request.args.get('hostname')
else:
hostname = 'blank'

# Set ip variable
if 'ip' in request.args:
ip = request.args.get('ip')
else:
ip = 'blank'

# Test inputs to determine the next step
if hostname == 'blank':
return "nohost"
elif ip == 'blank':
return "noip"
else:
response = check_cloudflare(hostname, ip)
return(response)

# Cloudflare Functions
def check_cloudflare(hostname, ip):
record_name = hostname
record_type = 'A'

# Determine the DNS zone from the supplied hostname
hostname_split = hostname.split('.')
zone_name = '.'.join(hostname_split[1:])

# Initialize the Cloudflare API
cf = CloudFlare.CloudFlare(token=api_token)

# Get the DNS zone ID
try:
zones = cf.zones.get(params={'name': zone_name})
if len(zones) == 0:
log_msg('Zone not found')
zone_id = zones[0]['id']
except CloudFlare.exceptions.CloudFlareAPIError as e:
log_msg('/zones.get %d %s' % (e, e))

# Get the DNS record ID
try:
dns_records = cf.zones.dns_records.get(zone_id, params={'name': record_name, 'type': record_type})
if len(dns_records) == 0:
log_msg('DNS record not found')
record_id = dns_records[0]['id']
record_content = dns_records[0]['content']
record_ttl = dns_records[0]['ttl']
except CloudFlare.exceptions.CloudFlareAPIError as e:
log_msg('/zones.dns_records.get %d %s' % (e, e))

# Test if the record needs updating
if record_content != ip:
log_msg("A DNS record update is needed for " + record_name)

# Update the DNS record
dns_record = {
'type': record_type,
'name': record_name,
'content': ip,
'ttl': 60
}

try:
cf.zones.dns_records.put(zone_id, record_id, data=dns_record)
log_msg('DNS record updated successfully: ' + record_name + "(" + ip + ")")
except CloudFlare.exceptions.CloudFlareAPIError as e:
log_msg('/zones.dns_records.put %d %s' % (e, e))
return "dnserr"

return("good " + ip)
else:
return("nochg " + ip)

def log_msg(msg):
print(msg)

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)

0 comments on commit ab62f51

Please sign in to comment.