Skip to content

DB-configured DNS providers + OVH reverse DNS#157

Open
v0l wants to merge 4 commits into
masterfrom
feat/dns-db-refactor-ovh-reverse
Open

DB-configured DNS providers + OVH reverse DNS#157
v0l wants to merge 4 commits into
masterfrom
feat/dns-db-refactor-ovh-reverse

Conversation

@v0l

@v0l v0l commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #78. Also finishes the DB-driven zone-id move started in #16 and lays groundwork for #110.

What & why

DNS provider config was split awkwardly: the provider + credentials + forward zone lived in static config.yaml (Settings.dns), while reverse zones lived per-range in the DB. OVH reverse DNS (issue #78) doesn't fit the Cloudflare zone+record-id model at all (it's per-IP, no zones, no record ids), so the DNS layer needed a refactor to make providers fully DB-configurable.

Changes

DB / model

  • New dns_server table + migration, DnsServer / DnsServerKind model, base-trait CRUD.
  • ip_range gains forward_dns_server_id, reverse_dns_server_id, forward_zone_id (alongside existing reverse_zone_id).
  • New dns_server admin permission resource (AdminResource::DnsServer).

DNS layer (lnvps_api)

  • Generalized the DnsServer trait; zone/record ids are now a DnsRef enum (Implicit | Id(String)) rather than stuffing the IP into a string field.
  • Factory dns::get_dns_server(db, id) dispatches on kind.
  • New ovh DNS provider — reverse DNS via POST/DELETE /ip/{ip}/reverse (reverse only). Shared OVH request signing extracted to crate::ovh and reused by the additional-IP router.
  • vm_network resolves forward/reverse DNS servers per range from the DB. Settings.dns is now legacy (migration-only).

Admin API

  • /api/admin/v1/dns_servers CRUD.
  • IP range create/update/list expose the new DNS fields.
  • POST /api/admin/v1/ip_ranges/{id}/patch_dns — queue a PatchIpRangeDns job to re-apply forward/reverse DNS for a whole range on demand.

Startup migration

  • Imports the legacy Cloudflare config.yaml block into a dns_server row and points existing ranges at it (forward everywhere; reverse where a reverse zone was set).
  • Imports OVH additional-IP routers as Ovh DNS servers (reusing their url + token) and auto-maps reverse DNS onto the ranges they route.
  • Force-refreshes stale Cloudflare reverse records on OVH-routed IPs to real OVH PTRs. Idempotent; best-effort so a missing OVH /ip/*/reverse permission logs a warning instead of blocking startup.

Notes / caveats

  • Deploy with the old dns block still in config.yaml so the migration can read it once; it can be removed afterward.
  • The OVH consumer key reused from the additional-IP router may need /ip/*/reverse permission granted for reverse calls to succeed.
  • OVH IPv6 ranges without an access policy/router can't be auto-detected and need a manual DNS-server assignment.
  • Verified against the live admin API: OVH IPs keep their global Cloudflare forward zone (vm-{id}.lnvps.cloud); only reverse switches to OVH.

Testing

  • Full workspace unit suite green (cargo test --workspace --exclude lnvps_e2e -- --test-threads=1), clippy clean on new code.
  • New tests: DnsRef/stored_ref, record constructors, mock factory, OVH fqdn_with_dot, AdminResource round-trip, migration bootstrap + OVH import + idempotency, reverse force-refresh decision logic, PatchIpRangeDns display.

Move DNS provider configuration out of static config.yaml into the database,
mirroring the router pattern, and add an OVH reverse-DNS provider.

- New `dns_server` table + model (`DnsServer`/`DnsServerKind`), base-trait CRUD,
  and admin API at `/api/admin/v1/dns_servers` (`dns_server` permission resource).
- IP ranges gain `forward_dns_server_id`, `reverse_dns_server_id`, `forward_zone_id`
  (alongside the existing `reverse_zone_id`) selecting the forward/reverse provider
  + zone per range.
- Generalize the `DnsServer` trait: zone/record ids modelled as a `DnsRef` enum
  (`Implicit` | `Id(String)`) instead of stuffing the IP into a string; factory
  `dns::get_dns_server(db, id)` dispatches on kind.
- New `ovh` DNS provider: reverse DNS via `POST/DELETE /ip/{ip}/reverse` (reverse
  only). Shared OVH signing extracted to `crate::ovh` and reused by the router.
- `vm_network` resolves DNS servers per range from the DB; `Settings.dns` is now a
  legacy migration-only field.
- Startup data migration imports the legacy Cloudflare config and OVH additional-IP
  routers into `dns_server` rows, auto-maps reverse DNS on OVH-routed ranges, and
  force-refreshes stale Cloudflare reverse records to real OVH PTRs (idempotent,
  best-effort so a permission error never blocks startup).
- New `PatchIpRangeDns` job + `POST /api/admin/v1/ip_ranges/{id}/patch_dns` to
  re-apply forward/reverse DNS for a whole range on demand.

Fixes #78
@v0l v0l added enhancement New feature or request api User-facing or admin API changes database Migration or schema changes labels Jul 10, 2026
v0l added 3 commits July 10, 2026 13:35
Add docs/agents/testing-db-backups.md describing how to safely restore a prod
mariadb dump into an isolated DB and test branch migrations/startup against it:
swap all hosts to the Dummy/mock kind, wipe user contact preferences, neutralise
ENC: encrypted secrets, repoint external integrations to inert endpoints, seed
the dummy-host state so VMs report running, and apply migrations. Linked from
AGENTS.md.
Ran the full lnvps_api binary against a restored 1577-VM prod snapshot (Polar
regtest LND + redis). It boots cleanly to "Listening on 0.0.0.0:8000" and serves
HTTP 200; the vm_subscription backfill (1577 subs / 3429 payments, 0 errors),
encryption re-encrypt, and the DNS data migration (OVH dns_server rows + reverse
wiring on the OVH-routed ranges) all run.

Key finding folded into the doc: read-only mode does NOT stop startup data
migrations or the worker (expired-VM cleanup, DNS reverse backfill, arp_ref_fixer)
from making real router/DNS API calls, so repointing all router URLs to an inert
endpoint is mandatory. Expanded step 5 with the verified full-run procedure and an
"expected output" section.
test_admin_create_ip_space_persists_company_id sent min_prefix_size=24 /
max_prefix_size=32, which the pre-existing to_available_ip_space() validation
rejects (requires min >= max; for a /24 RIPE block: min=/32, max=/24, matching
dev_setup.sql). The endpoint returned 500 "min_prefix_size must be greater than
or equal to max_prefix_size", so the test asserted != 200 and failed.

This was a latent failure introduced when the test was added (135258c), unrelated
to the DNS changes on this branch; reproduced by POSTing to a locally-run admin
server against a freshly-migrated DB (503 -> 200 after the swap).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api User-facing or admin API changes database Migration or schema changes enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OVH Reverse

1 participant