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
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
[[endpoints]]
@@ -176,6 +177,7 @@ ipv4 = ["myip", "ipv4", "ip4"]
ipv6 = ["myip6", "ipv6", "ip6"]
username = ["username", "user"]
password = ["password", "pass", "token"]
notify_change = ["notify_change"]
[[endpoints]]
path = "/nic/update"
@@ -185,6 +187,7 @@ ipv4 = ["myip"]
ipv6 = ["myip6"]
username = ["username"]
password = ["password"]
notify_change = []
```
**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
# username: username, user
# password: password, pass, token
# notify_change: notify_change
#
# Multiple endpoints can be defined with custom parameter names
# [[endpoints]]
# path = "/update"
# [endpoints.params]
@@ -74,6 +77,7 @@ from_address = "ddns@example.com" # required if email.enabled
# ipv6 = ["myip6", "ipv6", "ip6"]
# username = ["username", "user"]
# password = ["password", "pass", "token"]
# notify_change = ["notify_change"]
# [[endpoints]]
# path = "/nic/update"
@@ -83,3 +87,4 @@ from_address = "ddns@example.com" # required if email.enabled
# ipv6 = ["myip6"]
# username = ["username"]
# password = ["password"]
# notify_change = []

View File

@@ -149,7 +149,7 @@ def cmd_hostname_list(args, app):
return 0
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'}"
)
print("-" * 140)

View File

@@ -21,6 +21,7 @@ DEFAULT_ENDPOINT_PARAMS = {
"ipv6": ["myip6", "ipv6", "ip6"],
"username": ["username", "user"],
"password": ["password", "pass", "token"],
"notify_change": ["notify_change"],
}
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}")
return False
def send_changed_notification(
def send_change_notification(
self,
email,
hostname,
@@ -135,8 +135,8 @@ class EmailService:
"expiry_ttl": hostname.expiry_ttl,
}
subject = f"DDNS hostname expired: {fqdn}"
body = self.expiry_template.render(**template_variables)
subject = f"DDNS hostname changed: {fqdn}"
body = self.change_template.render(**template_variables)
return self.send(email, subject, body)

View File

@@ -317,7 +317,8 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
now = utc_now()
changed = False
ipv4_changed = False
ipv6_changed = False
if ipv4:
hostname.last_ipv4_update = now
if ipv4 != hostname.last_ipv4:
@@ -330,7 +331,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname.dns_ttl
)
hostname.last_ipv4 = ipv4
changed = True
ipv4_changed = True
except Exception as e:
hostname.save()
logging.error(
@@ -351,7 +352,7 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname.dns_ttl
)
hostname.last_ipv6 = ipv6
changed = True
ipv6_changed = True
except Exception as e:
hostname.save()
logging.error(
@@ -364,12 +365,12 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
hostname.save()
changed_addrs = ""
if ipv4:
if ipv4_changed:
changed_addrs += f" ipv4={ipv4}"
if ipv6:
if ipv6_changed:
changed_addrs += f" ipv6={ipv6}"
if not changed:
if not ipv4_changed and not ipv6_changed:
logging.info(
f"No change: client={client_ip} hostname={hostname.hostname} "
f"zone={hostname.zone}{changed_addrs}"
@@ -383,6 +384,19 @@ class DDNSRequestHandler(BaseHTTPRequestHandler):
f"Updated: client={client_ip} hostname={hostname.hostname} "
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 (
200, "good",
{"ipv4": hostname.last_ipv4, "ipv6": hostname.last_ipv6}