Skip to content

Commit 5a8c988

Browse files
authored
feat!: DNS-scoped token auth, per-record IP cache, explicit ip4/ip6 (#166)
* feat!: DNS-scoped token auth, per-record IP cache, ip6 and zone params Closes #151. Auth: the worker now authenticates with the API token alone. Basic credentials still work exactly as UniFi sends them, but the username portion is ignored; raw Bearer tokens are accepted for direct callers. The SDK never receives an email (it only ever paired one with the legacy global key anyway). KV: cache keys move from ip:<email> to ip:<hostname>:<recordType> with JSON {ip, updatedAt} values and a 30-day expirationTtl on every write, so stale identities can no longer accumulate (the original #151 complaint). The skip decision is per record: requests where every record matches its cached IP return without touching the zones API, and partially cached requests only process the pending records. This also fixes adding a new hostname while the IP is unchanged, which the per-user key silently skipped. Updates and notifications key off the actual DNS delta: when the existing record already holds the target IP, no update call is made and no notification fires; the cache is refreshed silently. ntfy messages cover only records that actually changed. New query parameters: ip6 adds an AAAA record per hostname for dual-stack callers (upstream #207 parity); zone optionally restricts record matching to one zone for parity with UniFi's native Cloudflare DDNS scoping. Multi-zone discovery remains the default. Migration: legacy last_ip and ip:<email> KV keys are never read again; sweep them once with wrangler kv key delete (the namespace is a pure cache, so wiping it entirely is also safe). BREAKING CHANGE: Authorization headers must use the Basic scheme (as UniFi does) or a raw Bearer token; Bearer-wrapped base64 credentials are no longer accepted. The JSON response no longer includes previousIp; data.records entries carry per-record updated flags. * feat!: explicit ip4/ip6 parameters, drop legacy ip/myip/hostname ip4 and ip6 are now the only address parameters: literals validate against their family, and auto takes the connecting IP only when it matches the slot's family, silently skipping otherwise so dual-stack UniFi devices can send ip4=%i&ip6=auto without flapping (inadyn has a single %i substitution and no IPv6 variable; verified against inadyn 2.13.0 on the target UDM). hostnames= is the sole hostname parameter. BREAKING CHANGE: ip, myip, and hostname query parameters are removed; the ip=auto 500-on-undeterminable path is gone (auto slots skip, and a request with no resolvable slot is a 422).
1 parent 568cf1f commit 5a8c988

6 files changed

Lines changed: 874 additions & 382 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ the Cloudflare Workers pool, ESLint + prettier, lefthook hooks.
1111
- Deploy (local or CI): `bun run deploy` (see scripts/deploy.ts)
1212
- First-time setup on a clone or fork: `bun run setup`
1313

14+
Cloudflare resource naming: `<type>-<project>-<purpose>-<env>`, e.g.
15+
`kv-uddns-cache-prod`, `kv-uddns-cache-dev`, `d1-uddns-audit-prod`. Binding
16+
names in code stay short (`DDNS_KV`). Workers themselves: `<project>-<env>`.
17+
1418
Rules:
1519

1620
@.claude/rules/github-cli.md

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ A Cloudflare Worker script that enables UniFi devices (e.g., UDM-Pro, USG) to dy
1111

1212
This is a fork from [willswire](https://github.com/willswire/unifi-ddns) with the following enhancements:
1313

14-
- **Smart notifications** - Only sends [ntfy](https://ntfy.sh) alerts when your IP actually changes
14+
- **Smart notifications** - Only sends [ntfy](https://ntfy.sh) alerts when a DNS record actually changes
1515
- **API** - Returns structured JSON responses instead of plain text
1616
- **Multi-hostname support** - Update multiple hostnames in a single request using comma-separated values
17-
- **Multi-zone support** - API tokens can manage DNS records across multiple zones
17+
- **Multi-zone support** - API tokens can manage DNS records across multiple zones, with optional `zone=` scoping
18+
- **Dual-stack support** - Update A and AAAA records together with the `ip6` parameter
19+
- **Token-only auth** - DNS-scoped API tokens; no account email anywhere
1820

1921
## Why Use This?
2022

@@ -25,7 +27,7 @@ This worker exists for what the native client doesn't do:
2527
- **Push notifications** via [ntfy](https://ntfy.sh) when your IP actually changes
2628
- **Multi-hostname updates** in a single entry (comma-separated), including across multiple zones
2729
- **Record preservation**: proxy status, TTL, and comments on existing records survive updates
28-
- **`ip=auto`** for routers behind NAT that would otherwise report a private IP
30+
- **`ip4=auto` / `ip6=auto`** for routers behind NAT that would otherwise report a private IP
2931
- **Audit history** of DNS changes for compliance
3032

3133
## 🚀 **Setup Overview**
@@ -91,10 +93,10 @@ injected at deploy time from the environment.
9193
3. Create New Dynamic DNS with the following information:
9294
- **Service:** `custom`
9395
- **Hostname:** `subdomain.example.com` or `example.com`
94-
- **Username:** Cloudflare Account Email Address (e.g., `you@example.com`)
95-
- **Password:** Cloudflare User API Token _(not an Account API Token)_
96-
- **Server:** `<worker-name>.<worker-subdomain>.workers.dev/update?ip=%i&hostname=%h`
97-
_(Omit `https://`. To update several records at once, use a comma-separated list: `hostnames=example.com,*.example.com`)_
96+
- **Username:** Any value (the field is ignored; the API token does the authenticating)
97+
- **Password:** Cloudflare User API Token scoped to DNS edit _(not an Account API Token)_
98+
- **Server:** `<worker-name>.<worker-subdomain>.workers.dev/update?ip4=%i&ip6=auto&hostnames=%h`
99+
_(Omit `https://`. Comma-separate to update several records at once: `hostnames=example.com,*.example.com`. `ip4`/`ip6` each accept a literal address or `auto`, which uses the connecting IP when it matches that family and skips the slot otherwise; provide at least one. Optional `zone=example.com` restricts matching to one zone.)_
98100

99101
## 🛠️ **Testing & Troubleshooting**
100102

docs/faq.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ The server configuration depends on your UniFi device model:
1111
- **Note:** Do **not** include the path with variables.
1212

1313
- **Newer Gateways (e.g., UDM series, UXG series):**
14-
- **Server:** `<worker-name>.<worker-subdomain>.workers.dev/update?ip=%i&hostname=%h`
14+
- **Server:** `<worker-name>.<worker-subdomain>.workers.dev/update?ip4=%i&ip6=auto&hostnames=%h`
1515
- **Note:** Include the full path with variables. Multiple hostnames are
1616
supported as a comma-separated list, e.g. `hostnames=example.com,*.example.com`.
17+
Drop `ip6=auto` if you have no IPv6.
1718

1819
This distinction is crucial to ensure the DDNS updates function correctly.
1920

@@ -25,8 +26,8 @@ This distinction is crucial to ensure the DDNS updates function correctly.
2526
2. **Create New Dynamic DNS Entry:**
2627
- **Service:** Select `custom`.
2728
- **Hostname:** Enter your desired hostname (e.g., `subdomain.example.com`).
28-
- **Username:** Enter your Cloudflare account email.
29-
- **Password:** Enter your Cloudflare API token.
29+
- **Username:** Any value; the field is ignored.
30+
- **Password:** Enter your DNS-scoped Cloudflare API token.
3031
- **Server:** Enter the appropriate server address based on your device model (see FAQ #1).
3132

3233
3. **Save Configuration:**
@@ -106,9 +107,9 @@ Assign separate DDNS providers to each WAN interface if supported. Using the `cu
106107

107108
## 11. How can I use Unifi devices behind NAT?
108109

109-
In case the Unifi router is deployed behind a NAT gateway (e.g. cable modem in router mode, 5G modem or similar), it will likely get a non-routable RFC 1918 address assigned on the external interface. In this case, the Unifi router would incorrectly update DNS to the non-routable external IP address via `ip=%i`.
110+
In case the Unifi router is deployed behind a NAT gateway (e.g. cable modem in router mode, 5G modem or similar), it will likely get a non-routable RFC 1918 address assigned on the external interface. In this case, the Unifi router would incorrectly update DNS to the non-routable external IP address via `ip4=%i`.
110111

111-
To support scenarios where such a router needs to be externally available (e.g. via port forwarding on the NAT gateway), you can use `ip=auto` instead of `ip=%i` to have the Cloudflare worker automatically determine the Unifi router's NATed IP address and use it for correctly updating the hostname's `A` record.
112+
To support scenarios where such a router needs to be externally available (e.g. via port forwarding on the NAT gateway), you can use `ip4=auto` instead of `ip4=%i` to have the Cloudflare worker automatically determine the Unifi router's NATed IP address and use it for correctly updating the hostname's `A` record. The same applies to `ip6=auto` for AAAA records.
112113

113114
## 12. What should I do if I continue to experience issues with DDNS updates?
114115

0 commit comments

Comments
 (0)