{"id":50781432,"url":"https://github.com/rootwarp/ddns","last_synced_at":"2026-06-12T03:30:32.295Z","repository":{"id":352413898,"uuid":"1214914794","full_name":"rootwarp/ddns","owner":"rootwarp","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-19T12:41:57.000Z","size":257,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-19T14:37:29.678Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rootwarp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T08:13:07.000Z","updated_at":"2026-04-19T12:42:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/rootwarp/ddns","commit_stats":null,"previous_names":["rootwarp/ddns"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/rootwarp/ddns","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootwarp%2Fddns","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootwarp%2Fddns/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootwarp%2Fddns/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootwarp%2Fddns/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rootwarp","download_url":"https://codeload.github.com/rootwarp/ddns/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rootwarp%2Fddns/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34228097,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-12T03:30:31.694Z","updated_at":"2026-06-12T03:30:32.284Z","avatar_url":"https://github.com/rootwarp.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ddns\n\nDynamic DNS updater for Google Cloud DNS. One Go binary, one config file,\none reconcile loop — keep an `A` record pointed at your home's current\npublic IPv4 and stop hand-rolling `curl | gcloud` scripts.\n\n[![CI](https://github.com/rootwarp/ddns/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/rootwarp/ddns/actions/workflows/ci.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/rootwarp/ddns.svg)](https://pkg.go.dev/github.com/rootwarp/ddns)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\n---\n\n## Why ddns\n\n`ddns` is the packaged, correct version of the `curl api.ipify.org | gcloud\ndns record-sets update` shell script that every home-lab operator\neventually writes: it polls three redundant HTTPS echo services, takes a\nquorum, fetches the live Cloud DNS record, and issues a single atomic\n`Changes.Create` only when both a local state file and the live record\ndisagree with the observed IP. It is **narrow on purpose** — Google Cloud\nDNS only, IPv4 `A` records only, single binary, no plugins. If you want a\nmulti-provider framework, look elsewhere; if you already run Cloud DNS and\nwant a correct, observable, scope-locked DDNS updater for it, this is the\ntool.\n\n- **Providers:** Google Cloud DNS only (Cloudflare is Phase 7 / v1.1).\n- **Record types:** `A` only. IPv6 / `AAAA` is an explicit non-goal.\n- **Platforms:** Linux `amd64` and `arm64`. No Windows, no BSD.\n- **Audience:** operators who already know what DDNS and Cloud DNS are.\n\n---\n\n## Install\n\nPick the path that matches your audience:\n\n### Via `go install` — for operators who already have Go\n\n```sh\ngo install github.com/rootwarp/ddns/cmd/ddns@latest\n```\n\nRequires Go 1.26.0 or newer. Installs to `$GOBIN` (default `$HOME/go/bin`).\n\n### Via Docker — recommended deployment\n\n```sh\ndocker pull ghcr.io/rootwarp/ddns:latest\ndocker run --rm ghcr.io/rootwarp/ddns:latest version\n```\n\nSee [Docker deployment](#docker-deployment) below for the compose layout.\n\n### From source — for contributors\n\n```sh\ngit clone https://github.com/rootwarp/ddns.git\ncd ddns\nmake build             # produces ./bin/ddns\n./bin/ddns version\n```\n\n---\n\n## Quick start\n\n```sh\n# 1. Authenticate (one-time, local testing)\ngcloud auth application-default login\n\n# 2. Copy the annotated template and edit it\ncurl -fsSL https://raw.githubusercontent.com/rootwarp/ddns/develop/config.example.yaml \\\n    -o /etc/ddns/config.yaml\n\n# 3. Dry-run: see the reconcile without writing\nddns sync --config /etc/ddns/config.yaml --dry-run\n\n# 4. First real reconcile (creates the record on first run)\nddns sync --config /etc/ddns/config.yaml\n\n# 5. Run as a daemon\nddns run --config /etc/ddns/config.yaml\n```\n\nFull step-by-step walk-through — GCP zone, service-account key, config,\nfirst reconcile, cron vs daemon, troubleshooting — lives in\n[`docs/getting-started-gcp.md`](docs/getting-started-gcp.md). Read that\nbefore your first deployment; the rest of this README is reference.\n\n---\n\n## Minimum config example\n\nThe smallest config that passes validation:\n\n```yaml\n# /etc/ddns/config.yaml\nrecords:\n  - project: my-home-lab\n    managed_zone: example-com\n    name: home.example.com.\n```\n\nDefaults fill in `poll_interval: 5m`, `state_path: ~/.local/state/ddns`,\n`log_format: auto`, the three default echo sources with quorum 2,\n`type: A`, and `ttl: 300`. For the full annotated template — every key,\nevery default, every range — copy [`config.example.yaml`](config.example.yaml).\n\n---\n\n## Docker deployment\n\nRecommended shape, pasted from\n[`deploy/docker/docker-compose.yml`](deploy/docker/docker-compose.yml):\n\n```yaml\nservices:\n  ddns:\n    image: ghcr.io/rootwarp/ddns:latest\n    container_name: ddns\n    restart: unless-stopped\n    environment:\n      DDNS_LOG_FORMAT: json\n      GOOGLE_APPLICATION_CREDENTIALS: /etc/ddns/credentials.json\n    volumes:\n      - ./config.yaml:/etc/ddns/config.yaml:ro\n      - ./credentials.json:/etc/ddns/credentials.json:ro\n      - ddns-state:/var/lib/ddns\n    command: [\"run\", \"--config\", \"/etc/ddns/config.yaml\"]\n\nvolumes:\n  ddns-state:\n```\n\nLayout in the working directory:\n\n```\n./docker-compose.yml       (the file above)\n./config.yaml              (bind-mounted read-only)\n./credentials.json         (GCP service-account JSON, 0600)\n```\n\nSet `state_path: /var/lib/ddns` inside `config.yaml` so per-record state\nwrites land in the named volume and survive container recreation.\n\nBring it up:\n\n```sh\ndocker compose up -d\ndocker logs -f ddns\n```\n\nReload config without a restart:\n\n```sh\ndocker kill -s HUP ddns\n```\n\nOperators who want OS-level supervision wrap the container with their own\nsystemd unit pointing at `docker run` — ddns deliberately does not ship a\nunit file. Day-two ops (resync, key rotation, state migration) are\ndocumented in [`docs/operations.md`](docs/operations.md).\n\n---\n\n## Command reference\n\nFour subcommands, each one-sentence-describable:\n\n- **`ddns run`** — long-lived daemon. Reconciles on startup and every\n  `poll_interval`. Honors `SIGHUP` (config reload), `SIGTERM`/`SIGINT`\n  (clean shutdown). Applies exponential backoff on provider transients.\n- **`ddns sync`** — one-shot reconcile. Exit 0 on success (noop or\n  updated), 1 on resolver quorum failure, 2 on provider transient, 3 on\n  config/auth fatal. Suitable under `cron`.\n- **`ddns status`** — prints the per-record state file contents. Reads\n  local state only, never calls Cloud DNS. `--json` emits the raw state\n  document.\n- **`ddns version`** — prints version, commit, build date. Does not read\n  config; safe on any host.\n\nBoth `run` and `sync` accept `--dry-run`, which performs resolution and\ncomparison but logs the payload it *would* have sent instead of issuing\n`Changes.Create`. Use this to validate config changes without mutating\nthe zone.\n\nFull flag surface: `ddns --help` and `ddns \u003csubcommand\u003e --help`. Exit\ncodes and their mapping to log events are enumerated in\n[`docs/log-events.md`](docs/log-events.md).\n\n---\n\n## Troubleshooting\n\nCommon failure modes and the one-line fix. The full table with worked\nexamples lives in\n[`docs/getting-started-gcp.md#9-troubleshooting`](docs/getting-started-gcp.md#9-troubleshooting).\n\n| You see                                        | Cause                              | Fix                                                                 |\n|------------------------------------------------|------------------------------------|---------------------------------------------------------------------|\n| `auth: authentication failed`                   | ADC unreachable                    | Bind-mount the SA JSON and set `GOOGLE_APPLICATION_CREDENTIALS`.   |\n| `resolver: no quorum among echo services`       | Outbound HTTPS blocked             | Verify `curl -sS https://api.ipify.org` and siblings work.         |\n| `provider upsert: … code=403`                   | SA missing `roles/dns.admin`       | Re-grant via `gcloud projects add-iam-policy-binding …`.           |\n| `provider upsert: … code=404`                   | `managed_zone` typo (not DNS name) | `gcloud dns managed-zones list`; first column is the value needed. |\n| `record type \"AAAA\" not supported in v1`        | IPv6 is out of scope               | v1 only updates `A`. See non-goals.                                |\n\nFor log grep patterns and the full event taxonomy, see\n[`docs/log-events.md`](docs/log-events.md).\n\n---\n\n## FAQ\n\n**Why only Google Cloud DNS?**\nScope. v1 focuses on making one provider correct rather than making many\nproviders shallow. Cloudflare lands in Phase 7 as the proof-of-refactoring\nthat the `DNSProvider` interface was not Cloud-DNS-shaped.\n\n**Why no systemd unit or launchd plist?**\nOperators who want OS-level supervision already have opinions about unit\nfiles, user/group, `ProtectSystem`, `StateDirectory`, journald routing,\netc. Shipping a unit file that's half-right for everyone is worse than\nshipping none. Wrap `docker run` or the installed binary with your own\nunit; see\n[`docs/getting-started-gcp.md#option-b--long-lived-daemon-ddns-run`](docs/getting-started-gcp.md#option-b--long-lived-daemon-ddns-run)\nfor a worked example.\n\n**Why no IPv6 (`AAAA`)?**\nExplicit non-goal. Residential IPv6 deployment is sparse enough, and\nIPv6-reachable home services rare enough, that the feature's carrying\ncost outweighs its value at v1. Re-evaluated in v2 if the landscape\nchanges.\n\n**Can I run it on Windows?**\nNo. Cross-compile works (`GOOS=windows go build`) but none of the\nintegration tests or the deployment shape (Docker) target Windows and\nnothing is validated there. If this matters to you, file an issue with a\nconcrete use case.\n\n**How do I contribute?**\nRead [`CONTRIBUTING.md`](CONTRIBUTING.md) and\n[`plan/bootstrap/project-plan.md`](plan/bootstrap/project-plan.md) to see\nwhere the project is in its phase sequence. Open issues against the phase\nyou're touching; PRs come from `feature/\u003cphase\u003e-\u003cslug\u003e` branches onto\n`develop`, never `main`.\n\n---\n\n## Integration tests\n\nA live-Cloud-DNS integration test is maintained under\n`internal/dnsprovider/gcp/gcp_integration_test.go`, gated behind the\n`integration` Go build tag so it never runs as part of `go test ./...` or in\nCI. The test performs one full reconcile cycle (create/update -\u003e get -\u003e\nidempotent upsert), then cleans up via a pure-deletion `Changes.Create`.\n\n### Prerequisites\n\nAll three environment variables must be set or the test self-skips:\n\n| Variable | Meaning |\n|---|---|\n| `DDNS_INTEGRATION_PROJECT` | GCP project ID that owns the managed zone. |\n| `DDNS_INTEGRATION_ZONE` | Managed-zone name (not the DNS domain). |\n| `DDNS_INTEGRATION_RECORD` | FQDN inside the zone, trailing-dot form (e.g., `ddns-integration.example.com.`). |\n\nCredentials come from Application Default Credentials (ADC) — the same path\nthe daemon uses. The easiest local setup is `gcloud auth\napplication-default login`.\n\n### Running\n\n```sh\nexport DDNS_INTEGRATION_PROJECT=my-home-lab\nexport DDNS_INTEGRATION_ZONE=example-com\nexport DDNS_INTEGRATION_RECORD=ddns-integration.example.com.\nmake integration-test\n```\n\n### Safety\n\nThe test writes addresses from the TEST-NET-3 reserved range (`203.0.113.0/24`,\nRFC 5737). No value written to the record ever directs traffic at a real\nhost, so even if cleanup races the worst-case residual state points at\ndocumentation-only IP space. The cleanup is idempotent, so running the test\ntwice in a row works: the second pass observes \"already absent\" and proceeds.\n\nRunning the test produces exactly two `dns.changes.create` audit-log entries\nper run — one update, one delete — which is the signal to check in the\nCloud Console audit log after a run to confirm behavior end-to-end.\n\n---\n\n## License\n\n[MIT](LICENSE). The `ddns` project is maintained by Joonkyo Kim\n([@rootwarp](https://github.com/rootwarp)).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frootwarp%2Fddns","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frootwarp%2Fddns","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frootwarp%2Fddns/lists"}