Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API: Zone Management

Crate: kinetic-daemon Stage: 9 Reading time: 15 minutes Depends on: 01_overview.md, 11_api_identity.md, 12_api_names.md, docs/learn/types/05_dns_zones.md


What Is This?

This file (kinetic-daemon/src/api/zone.rs) provides the HTTP REST API endpoints that allow users, frontends, or automation scripts to manage their DNS zones. In the Kinetic ecosystem, a “zone” is the complete collection of DNS records (like A, AAAA, TXT, CNAME) that define what a specific domain name actually resolves to. However, because Kinetic operates a decentralized DHT rather than a traditional DNS registry, publishing a zone is not just a simple database update. It is a complex cryptographic operation. It requires taking the raw zone data, cryptographically signing it with the identity key that originally registered the name, and then broadcasting that proven payload to the peer-to-peer network. This module acts as the crucial bridge between user-friendly JSON payloads and the complex, cryptographic DHT network layer. It provides three primary operations: retrieving the current local draft of a zone, saving edits to that local draft, and finalizing those edits by signing and publishing them to the global network.


Why Kinetic Needs This

To truly understand why this module exists in its current form, we have to contrast the Kinetic architecture with traditional DNS management platforms. If you buy a domain on Namecheap or Amazon Route53, you log into a web dashboard, add an A record for your IP address, and click “Save”. Behind the scenes, the provider simply updates their centralized PostgreSQL database. That database then pushes changes out to their authoritative nameservers. You don’t have to cryptographically sign anything yourself, because you are logged into their centralized system, and they trust their own database implicitly. Kinetic has no centralized server and no trusted database. The “authoritative nameservers” are the thousands of independent nodes running the Kinetic DHT. If you want to tell the world that saif.kyn points to 192.168.1.100, you cannot just send that IP address to a central server and ask it to remember it.

Instead, you must manually perform the following sequence:

  1. Construct a formal DnsZone data structure in memory.
  2. Serialize that entire data structure into a precise byte array.
  3. Retrieve your original cryptographic registration record (NameRecord) for the domain saif.kyn.
  4. Embed the newly serialized zone bytes into the payload section of that record.
  5. Use your local ML-DSA identity key to generate a post-quantum signature over the updated record.
  6. Package the signed record and flood it into the DHT so peers can verify your signature against the public key and cache your new IP.

If Saif (or any regular user) had to do this manually using command-line tools, building byte arrays by hand, and managing ML-DSA signatures directly, Kinetic would be unusable for daily operations. This API module exists to hide that entire 6-step cryptographic pipeline behind a simple, standard REST interface. A frontend web application can simply send a JSON list of DNS records to the POST /zone/saif.kyn endpoint, and then subsequently call the POST /zone/saif.kyn/publish endpoint. The daemon handles the heavy lifting of the cryptography, the local storage lookups, and the network flooding automatically behind the scenes. Furthermore, by deliberately splitting the process into a two-step “Save” and “Publish” workflow, this module protects the health of the entire Kinetic network. If every minor typo fix or sequential record addition immediately triggered a DHT broadcast, the network would be flooded with redundant signature verifications and propagation traffic. By allowing users to save locally as much as they want, and only publishing when the entire zone is finalized, we conserve massive amounts of network bandwidth and node CPU cycles. In legacy systems, DNS propagation relies on TTLs (Time to Live) and hierarchical caching, meaning you wait hours for caches to expire. Kinetic’s DHT architecture behaves differently. When a signed record is flooded into the network, peers actively receive the update, verify the signature in real-time, and overwrite their local caches because they trust the new signature over the old one. The network achieves near-instantaneous global consistency. But that only works if the cryptographic payload is perfect. This module guarantees that perfection, acting as a strict cryptographic firewall that prevents malformed or improperly serialized payloads from ever leaving the local daemon.


How It Works

This file implements three distinct HTTP handlers using the Axum web framework. Let’s break down the execution flow of each handler, as they represent the complete lifecycle of a domain zone in Kinetic.

1. Retrieving the Local Zone Draft (handle_get_zone)

-> See: kinetic-daemon/src/api/zone.rs — Lines 15 to 43

When a client or frontend wants to see their current DNS records, they send a GET request to the path /zone/{name}. The execution flow is straightforward and local:

  • The daemon uses Axum’s Path extractor to pull the {name} variable from the URL.
  • It normalizes the name (converting it to lowercase and handling trailing dots) to ensure exact, predictable matching.
  • It validates that the requested name is a proper apex name using kinetic_core::types::is_valid_apex_name.
  • A critical architectural note here: you cannot manage a sub-zone (like sub.saif.kyn) directly through this endpoint; you must manage the entire apex zone for saif.kyn.
  • It constructs a filesystem path pointing to the daemon’s local configuration directory: get_zones_dir() / {fqdn}.json.
  • It attempts to read the file.
  • If the file is missing, it returns a 404 Not Found.
  • If it exists, it parses it as JSON.
  • If the JSON is corrupted, it returns a 422 Unprocessable Entity.
  • If everything succeeds, it returns the parsed JSON to the client.

Notice that this endpoint does not query the DHT. It only looks at the local filesystem. This is because the local daemon is the authoritative source of truth for its own domains before they are published. The DHT is just where we publish to, not where we read our drafts from.

2. Saving a Local Draft (handle_post_zone)

-> See: kinetic-daemon/src/api/zone.rs — Lines 50 to 90

When a user adds, modifies, or removes a DNS record in the UI, they send a POST request with the new DnsZone JSON payload to /zone/{name}. This endpoint functions exclusively as a “Save Draft” feature.

  • Authentication Check: The very first operation is an authorization check.
  • The Extension(role): Extension<Role> parameter is an Axum extractor.
  • In Axum, HTTP requests flow through a chain of middleware before hitting this handler.
  • Earlier in the request lifecycle, an authentication middleware inspected the API token, validated it, and injected a Role enum into the request’s internal map.
  • When Axum invokes handle_post_zone, the Extension extractor reaches into that map and pulls out the Role.
  • Only API tokens with the Publish or Admin privileges can modify zone files; others receive a 403 Forbidden.
  • Validation Extractors: The Json(zone): Json<kinetic_core::types::DnsZone> parameter is another vital Axum extractor.
  • When the POST request arrives, the HTTP body is just raw bytes.
  • Axum intercepts these bytes and pipes them through the serde_json deserializer to construct a valid DnsZone Rust struct.
  • If the JSON is missing required fields (like a TTL), Axum catches the error and automatically returns a 400 or 422 error to the client.
  • This means the code inside handle_post_zone is immune to malformed JSON; zone is guaranteed to be semantically valid.
  • Local Persistence: It ensures the local zones/ directory exists.
  • Then, it uses serde_json::to_string_pretty to serialize the DnsZone struct back into a human-readable JSON string.
  • This choice to use a pretty-printed JSON string ensures that sysadmins can manually edit the zone files directly in ~/.kinetic/zones/ if they prefer.
  • Finally, it overwrites the local .json file on the disk with this new content.

Crucially, the execution of this function stops here. It does not interact with the DHT. It does not sign any payloads. It simply writes a text file to the local disk. This architectural choice is vital for enabling complex frontend workflows, allowing users to “Discard Changes” or build up massive, intricate zone files across multiple API calls over hours or days before finally committing them to the permanent network record.

3. Finalizing, Signing, and Publishing (handle_publish_zone)

-> See: kinetic-daemon/src/api/zone.rs — Lines 98 to 245

This is the cryptographic heavy lifter of the module. Once the local JSON file is crafted, the user triggers a publish. This is the exact moment where local, mutable data is transformed into immutable, verifiable network truth.

Step A: Verification and Draft Loading The daemon first verifies the user’s role. Then it attempts to read the .json zone file from the disk. If you haven’t saved a draft zone yet via the POST endpoint, the publish process immediately aborts. It parses the JSON back into a DnsZone struct to ensure it hasn’t been externally corrupted on disk.

Step B: Retrieving the Local Registration Record To publish an update to a domain name, you must cryptographically prove you own it. When you originally registered the name on this daemon, the system saved a NameRecord (often referred to as a “Reveal” transaction) in its local key-value storage engine. The handler fetches this historical record from state.storage using the prefix DB_PREFIX_REVEAL. This specific record contains your original public key and your previous signature.

Step C: Loading the Daemon Identity Key The daemon then loads its core identity.key file from the disk. This file contains the ML-DSA keypair that serves as the identity for the entire daemon. It extracts the public key bytes from this loaded keypair and compares them to the public key stored inside the historical NameRecord. This is a critical, non-negotiable security boundary: You cannot publish a zone for a name if your current daemon identity does not match the identity that originally registered the name. If you manually copied a .json zone file from another node, you cannot trick the Kinetic network into accepting it because you do not possess the corresponding private key required to sign the update.

Step D: The Cryptographic Update Process This is where the actual transformation occurs (Lines 188-210):

  • The DnsZone struct is serialized into a raw byte vector using serde_json::to_vec. This byte array is the new payload.
  • The code matches on the NameRecord enum to determine its type.
  • If it is a standard name (NameRecord::Standard), it replaces the old payload field with the newly generated bytes.
  • It then invokes signable_bytes().
  • This function structures the record in a specific way, combining it with the NETWORK_ID constant.
  • By including the NETWORK_ID in the signed data, the signature is bound to the current Kinetic network (for example, binding it to the testnet vs the mainnet).
  • This cryptographically prevents replay attacks where someone might copy your testnet zone and fraudulently publish it to the mainnet.
  • It utilizes the ML-DSA private key to generate a brand new, post-quantum secure signature over these signable bytes.
  • It overwrites the old signature in the NameRecord with this newly generated one.

Step E: Deep Dive - Network Broadcast Now that the daemon has constructed a freshly signed NameRecord containing the updated DNS zone payload:

  • It saves this updated NameRecord back into its local state.storage.
  • This local caching ensures that if the daemon immediately restarts, the very next publish operation will correctly build upon this latest state rather than reverting to an older sequence.
  • Finally, it serializes the entire signed NameRecord into bytes and hands it off to state.network.publish_redundant_payload().
  • When this network function is called, the daemon calculates the SHA-256 hash of the domain name (saif.kyn).
  • This hash determines the “address” of the data on the DHT.
  • The daemon looks up the closest peers to that hash in its routing table and opens direct connections to them.
  • It transmits the serialized, signed NameRecord bytes to them.
  • Those nodes receive the bytes, extract the public key and signature, and run the ML-DSA verification algorithm.
  • If it passes, they store the record and gossip it to their closest peers, creating a viral cascade.
  • Within seconds, any node looking up saif.kyn will be routed to a node holding this fresh, proven zone data.

Deep Dive: The Cryptographic Security Model

To appreciate the design of this API, we must understand the threat model it defends against. Traditional REST APIs rely on transport layer security (TLS) and bearer tokens. If an attacker compromises the central database of a traditional provider, they can change any DNS record. In Kinetic, the API daemon is merely a convenient interface; it is not the ultimate authority. The ultimate authority is the mathematics of the ML-DSA post-quantum signature scheme. When handle_publish_zone executes, it performs a strict validation of the identity key. Why is this necessary? Imagine an attacker compromises your local zones/ directory and modifies saif.kyn.json. They add a malicious A record pointing to a phishing server. If they then hit the POST /zone/saif.kyn/publish endpoint, the daemon will dutifully read the modified file. However, if the daemon does not possess the correct identity.key that originally registered saif.kyn, the publish will fail. The DHT peers do not care what the API daemon says; they only care about the signature attached to the payload. When the payload is serialized via serde_json::to_vec, it is converted into a deterministic stream of bytes. This exact stream of bytes is what the ML-DSA private key signs. If even a single bit in the JSON changes (for example, an IP address changes from .100 to .101), the entire byte stream changes. Because the byte stream changes, the old signature becomes invalid. Therefore, the daemon must generate a new signature every single time a zone is published. Furthermore, the signable_bytes function embeds the NETWORK_ID into the signed data payload. This is a defense against replay attacks across different Kinetic environments. Without the NETWORK_ID, an attacker could monitor the testnet, copy your signed testnet zone update, and broadcast it to the mainnet DHT. Because the signature would be valid for the payload, the mainnet nodes might accept it. By forcing the signature to cover both the payload AND the specific network identifier, this cross-network pollution becomes impossible.


Technical Concept: Axum Extractors in Practice

Throughout this API, you will see function parameters like Path(name), Extension(role), and Json(zone). These are known in the Rust Axum framework as “Extractors”. They are a powerful, declarative way to pull data out of an incoming HTTP request. Instead of writing boilerplate code to read the request body, parse JSON, and handle errors manually, you simply declare what you want. If you declare Path(name): Path<String>, Axum automatically parses the URL, extracts the segment matching the route variable {name}, and provides it as a Rust String. If you declare Json(zone): Json<DnsZone>, Axum automatically reads the request body stream. It pipes that stream directly into the serde_json deserializer. If the JSON is malformed, or if it doesn’t match the DnsZone struct schema, Axum intercepts the error. It prevents the function from ever executing. It automatically generates an appropriate HTTP 400 or 422 response and sends it back to the client. This means that by the time the first line of your function runs, you have absolute mathematical certainty that zone is a fully valid, correctly shaped Rust data structure. The Extension extractor works similarly but for server-side state. Earlier in the request lifecycle, authentication middleware validates the user’s API token. It creates a Role enum and stashes it in a type-mapped dictionary attached to the request. When your function asks for Extension(role): Extension<Role>, Axum reaches into that dictionary and pulls it out. This eliminates the need for global variables or passing context objects down a long chain of functions.


Key Pieces

handle_get_zone

  • What it does: Reads and returns a domain’s local JSON zone file draft from the daemon’s disk.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 15 to 43
  • Why it matters: This endpoint allows the frontend dashboard to accurately display the current state of a domain’s DNS records so the user can see exactly what they are editing before they publish.

handle_post_zone

  • What it does: Accepts a JSON DnsZone payload over HTTP and saves it directly to the local filesystem without interacting with the network.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 50 to 90
  • Why it matters: Acts as a vital staging area for DNS changes. It prevents the peer-to-peer network from being spammed with incomplete, intermediate, or broken updates while a user is in the middle of configuring multiple complex records.

handle_publish_zone

  • What it does: Reads the staged local zone file, embeds the payload into a NameRecord, cryptographically signs it with the daemon’s ML-DSA identity key, updates local storage, and broadcasts the finalized record to the DHT.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 98 to 245
  • Why it matters: This is the singular, core mechanism for actually updating the global Kinetic state. It bridges the gap between local, mutable configuration drafts and immutable, proven network consensus.

The ML-DSA Signing Block

  • What it does: Generates the cryptographic proof of ownership for the new DNS zone payload.
  • Where it lives: kinetic-daemon/src/api/zone.rs — Lines 202 to 205
  • Why it matters: Without this explicit signing step, every single peer on the DHT would immediately reject the updated payload. They would have no mathematical proof that the update actually originated from the true, verified owner of the domain.

How This Connects to the Rest of Kinetic

  • CROSS-CRATE: kinetic_core::types::DnsZone — Defined in the kinetic-core crate, this struct dictates the exact, required schema of the JSON payload.
  • CROSS-CRATE: kinetic_core::types::NameRecord — Defined in the kinetic-core crate, this is the wrapper struct that holds the signature, the public key, and the actual zone payload bytes.
  • CROSS-CRATE: kinetic_core::config — Provides the exact directory paths for where the zones/ folder and the core identity.key are physically stored on the local disk.
  • CROSS-CRATE: kinetic-network — The publish_redundant_payload function, which is called at the very end of the publish handler, belongs to the network crate. That crate is responsible for correctly routing the data to the appropriate DHT peers based on the domain’s hash.

Quick Reference

  • GET /zone/{name}: Fetch the currently saved local draft of a zone. This operation is purely local and does not touch the network.
  • POST /zone/{name}: Save a new draft of the zone to the local disk. Requires the Publish role. Validates the JSON schema against the Rust structs but does not touch the network or sign anything.
  • POST /zone/{name}/publish: Read the local draft, serialize it, cryptographically sign it with the daemon’s ML-DSA identity key, update the local key-value database, and broadcast the signed record to the DHT network.
  • File Storage Path: ~/.kinetic/zones/{fqdn}.json
  • DB Storage Prefix: reveal_ (used to lookup original registration data).
  • Authentication: All modifying endpoints require Role::Admin or Role::Publish.

Open Questions / Things to Revisit

  • Premium Name Branching: Inside handle_publish_zone, there is an explicit branch handling for NameRecord::Premium (Line 208). It updates the raw payload but it does not generate a new signature. Is this intentional behavior? Do Premium names use an different mechanism for update validation on the DHT, or is this a missing signature generation step that will eventually cause DHT peers to reject any Premium name updates?
  • Concurrency Race Conditions: If an automated script or a fast-clicking admin triggers POST /zone/{name}/publish twice in rapid succession, is it possible for the file read operation and the database read operation to interleave? This could cause a dangerous race condition where an older zone gets signed with a newer sequence number, corrupting the timeline. A simple file-level or name-level mutex around the publish operation might be necessary in high-throughput environments.
  • Semantic Record Validation: The POST endpoint verifies that the incoming data matches the DnsZone Rust struct shape, but it does not appear to perform deep semantic validation. For example, it does not verify that an A record actually contains a valid, well-formed IPv4 address string, rather than just returning any random string. Should this semantic validation happen directly here in the API daemon before allowing the save, or is that solely the responsibility of the frontend dashboard?
  • Error Verbosity: The API currently returns detailed internal error messages directly to the client (e.g., "File write failed: {e}"). While useful for debugging, in a production environment, exposing raw filesystem or DHT networking errors to an API client might be a minor information disclosure risk. We may want to sanitize these errors before returning them over HTTP.