Add URL parameter to enable notification of IP changes

This commit is contained in:
2026-01-23 00:51:30 +01:00
parent 9f75e5e66b
commit 5744408c92
6 changed files with 36 additions and 13 deletions

View File

@@ -166,6 +166,7 @@ Without TSIG authentication, the DNS server must allow updates based on IP addre
### Endpoints ### Endpoints
Configure one or more HTTP endpoints. If no endpoints are defined, a default endpoint at `/update` is created with standard parameter names. Configure one or more HTTP endpoints. If no endpoints are defined, a default endpoint at `/update` is created with standard parameter names.
Set an empty list of names to disable a parameter.
```toml ```toml
[[endpoints]] [[endpoints]]
@@ -176,6 +177,7 @@ ipv4 = ["myip", "ipv4", "ip4"]
ipv6 = ["myip6", "ipv6", "ip6"] ipv6 = ["myip6", "ipv6", "ip6"]
username = ["username", "user"] username = ["username", "user"]
password = ["password", "pass", "token"] password = ["password", "pass", "token"]
notify_change = ["notify_change"]
[[endpoints]] [[endpoints]]
path = "/nic/update" path = "/nic/update"
@@ -185,6 +187,7 @@ ipv4 = ["myip"]
ipv6 = ["myip6"] ipv6 = ["myip6"]
username = ["username"] username = ["username"]
password = ["password"] password = ["password"]
notify_change = []
``` ```
**Default accepted parameter names** (first match wins): **Default accepted parameter names** (first match wins):

View File

@@ -65,7 +65,10 @@ from_address = "ddns@example.com" # required if email.enabled
# ipv6 (IPv6 address): myip6, ipv6, ip6 # ipv6 (IPv6 address): myip6, ipv6, ip6
# username: username, user # username: username, user
# password: password, pass, token # password: password, pass, token
# notify_change: notify_change
#
# Multiple endpoints can be defined with custom parameter names # Multiple endpoints can be defined with custom parameter names
# [[endpoints]] # [[endpoints]]
# path = "/update" # path = "/update"
# [endpoints.params] # [endpoints.params]
@@ -74,6 +77,7 @@ from_address = "ddns@example.com" # required if email.enabled
# ipv6 = ["myip6", "ipv6", "ip6"] # ipv6 = ["myip6", "ipv6", "ip6"]
# username = ["username", "user"] # username = ["username", "user"]
# password = ["password", "pass", "token"] # password = ["password", "pass", "token"]
# notify_change = ["notify_change"]
# [[endpoints]] # [[endpoints]]
# path = "/nic/update" # path = "/nic/update"
@@ -83,3 +87,4 @@ from_address = "ddns@example.com" # required if email.enabled
# ipv6 = ["myip6"] # ipv6 = ["myip6"]
# username = ["username"] # username = ["username"]
# password = ["password"] # password = ["password"]
# notify_change = []

View File

@@ -149,7 +149,7 @@ def cmd_hostname_list(args, app):
return 0 return 0
print( print(
f"{'Hostname':<35} {'User':<15} {'Zone':<20} " f"\n{'Hostname':<35} {'User':<15} {'Zone':<20} "
f"{'DNS-TTL':<8} {'Exp-TTL':<8} {'Last-Update IPv4':<25} {'Last-Update IPv6'}" f"{'DNS-TTL':<8} {'Exp-TTL':<8} {'Last-Update IPv4':<25} {'Last-Update IPv6'}"
) )
print("-" * 140) print("-" * 140)

View File

@@ -21,6 +21,7 @@ DEFAULT_ENDPOINT_PARAMS = {
"ipv6": ["myip6", "ipv6", "ip6"], "ipv6": ["myip6", "ipv6", "ip6"],
"username": ["username", "user"], "username": ["username", "user"],
"password": ["password", "pass", "token"], "password": ["password", "pass", "token"],
"notify_change": ["notify_change"],
} }
VALID_PARAM_KEYS = frozenset(DEFAULT_ENDPOINT_PARAMS.keys()) VALID_PARAM_KEYS = frozenset(DEFAULT_ENDPOINT_PARAMS.keys())

View File

@@ -94,7 +94,7 @@ class EmailService:
logging.error(f"Email send failed: to={to} error={e}") logging.error(f"Email send failed: to={to} error={e}")
return False return False
def send_changed_notification( def send_change_notification(
self, self,
email, email,
hostname, hostname,
@@ -135,8 +135,8 @@ class EmailService:
"expiry_ttl": hostname.expiry_ttl, "expiry_ttl": hostname.expiry_ttl,
} }
subject = f"DDNS hostname expired: {fqdn}" subject = f"DDNS hostname changed: {fqdn}"
body = self.expiry_template.render(**template_variables) body = self.change_template.render(**template_variables)
return self.send(email, subject, body) return self.send(email, subject, body)

View File

@@ -317,7 +317,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
now = utc_now() now = utc_now()
changed = False ipv4_changed = False
ipv6_changed = False
if ipv4: if ipv4:
hostname.last_ipv4_update = now hostname.last_ipv4_update = now
if ipv4 != hostname.last_ipv4: if ipv4 != hostname.last_ipv4:
@@ -330,7 +331,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname.dns_ttl hostname.dns_ttl
) )
hostname.last_ipv4 = ipv4 hostname.last_ipv4 = ipv4
changed = True ipv4_changed = True
except Exception as e: except Exception as e:
hostname.save() hostname.save()
logging.error( logging.error(
@@ -351,7 +352,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname.dns_ttl hostname.dns_ttl
) )
hostname.last_ipv6 = ipv6 hostname.last_ipv6 = ipv6
changed = True ipv6_changed = True
except Exception as e: except Exception as e:
hostname.save() hostname.save()
logging.error( logging.error(
@@ -364,15 +365,15 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname.save() hostname.save()
changed_addrs = "" changed_addrs = ""
if ipv4: if ipv4_changed:
changed_addrs += f"ipv4={ipv4}" changed_addrs += f" ipv4={ipv4}"
if ipv6: if ipv6_changed:
changed_addrs += f" ipv6={ipv6}" changed_addrs += f" ipv6={ipv6}"
if not changed: if not ipv4_changed and not ipv6_changed:
logging.info( logging.info(
f"No change: client={client_ip} hostname={hostname.hostname} " f"No change: client={client_ip} hostname={hostname.hostname} "
f"zone={hostname.zone} {changed_addrs}" f"zone={hostname.zone}{changed_addrs}"
) )
return ( return (
200, "nochg", 200, "nochg",
@@ -381,8 +382,21 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
logging.info( logging.info(
f"Updated: client={client_ip} hostname={hostname.hostname} " f"Updated: client={client_ip} hostname={hostname.hostname} "
f"zone={hostname.zone} {changed_addrs}" f"zone={hostname.zone}{changed_addrs}"
) )
notify_change = extract_param(params, endpoint["params"]["notify_change"])
if notify_change and notify_change.lower() not in ["0", "no", "off"]:
try:
self.app.email_service.send_change_notification(
hostname.user.email,
hostname,
ipv4_changed,
ipv6_changed
)
except Exception as e:
logging.error(f"Sending change notification error: {e}")
return ( return (
200, "good", 200, "good",
{"ipv4": hostname.last_ipv4, "ipv6": hostname.last_ipv6} {"ipv4": hostname.last_ipv4, "ipv6": hostname.last_ipv6}