bindy/
ddns.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4#![allow(clippy::must_use_candidate)]
5
6//! Dynamic DNS update utilities for BIND9 via RFC 2136.
7//!
8//! This module provides functionality for updating DNS records in BIND9 using:
9//! - Direct DNS updates via hickory-client (TCP port 53, RFC 2136)
10//! - Zone operations via bindcar HTTP API
11//!
12//! It includes:
13//! - Hash calculation for change detection
14//! - Direct DNS update functions using hickory-client
15//! - Zone transfer triggering via bindcar API
16//!
17//! # Architecture
18//!
19//! Record reconcilers use this module to:
20//! 1. Calculate hash of current record data
21//! 2. Compare with last known hash in `status.record_hash`
22//! 3. If hash changed, send RFC 2136 update to BIND9 via hickory-client
23//! 4. Update sent directly to primary BIND9 instance (TCP port 53)
24//! 5. BIND9 handles zone transfer to secondaries automatically
25//!
26//! # Example
27//!
28//! ```rust
29//! use bindy::ddns::calculate_record_hash;
30//! use bindy::crd::ARecordSpec;
31//!
32//! # fn main() {
33//! let spec = ARecordSpec {
34//!     name: "www".to_string(),
35//!     ipv4_addresses: vec!["192.0.2.1".to_string()],
36//!     ttl: Some(300),
37//! };
38//!
39//! // Calculate current hash
40//! let current_hash = calculate_record_hash(&spec);
41//! // hash is a 64-character hex string
42//! assert_eq!(current_hash.len(), 64);
43//! # }
44//! ```
45
46// Record types are used in tests and by the calculate_record_hash function via Serialize trait
47use serde::Serialize;
48use sha2::{Digest, Sha256};
49
50/// Calculate SHA-256 hash of a record's data fields.
51///
52/// This function serializes the record's spec to JSON and calculates a SHA-256
53/// hash. The hash is used to detect actual data changes and avoid unnecessary
54/// DNS updates.
55///
56/// # Arguments
57///
58/// * `record` - The record to hash (must implement Serialize)
59///
60/// # Returns
61///
62/// A hexadecimal string representation of the SHA-256 hash.
63///
64/// # Example
65///
66/// ```rust
67/// use bindy::ddns::calculate_record_hash;
68/// use bindy::crd::{ARecord, ARecordSpec};
69/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
70///
71/// let record = ARecord {
72///     metadata: ObjectMeta::default(),
73///     spec: ARecordSpec {
74///         name: "www".to_string(),
75///         ipv4_addresses: vec!["192.0.2.1".to_string()],
76///         ttl: Some(300),
77///     },
78///     status: None,
79/// };
80///
81/// let hash = calculate_record_hash(&record.spec);
82/// assert_eq!(hash.len(), 64); // SHA-256 produces 64 hex chars
83/// ```
84pub fn calculate_record_hash<T: Serialize>(data: &T) -> String {
85    let json = serde_json::to_string(data).unwrap_or_default();
86    let mut hasher = Sha256::new();
87    hasher.update(json.as_bytes());
88    // sha2 0.11 returns `Array<u8, _>` from hybrid-array, which does not
89    // implement `LowerHex` like the old `GenericArray<u8, U32>` did.
90    hasher
91        .finalize()
92        .iter()
93        .map(|b| format!("{b:02x}"))
94        .collect::<String>()
95}
96
97/// Generate nsupdate commands for an A record.
98///
99/// Creates DNS update commands in nsupdate format to delete existing records
100/// and add the new record.
101///
102/// # Arguments
103///
104/// * `record` - The A record to update
105/// * `zone_fqdn` - Fully qualified domain name of the zone (e.g., "example.com.")
106///
107/// # Returns
108///
109/// A string containing nsupdate commands.
110///
111/// # Example
112///
113/// ```rust
114/// use bindy::ddns::generate_a_record_update;
115/// use bindy::crd::{ARecord, ARecordSpec};
116/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
117///
118/// let record = ARecord {
119///     metadata: ObjectMeta::default(),
120///     spec: ARecordSpec {
121///         name: "www".to_string(),
122///         ipv4_addresses: vec!["192.0.2.1".to_string()],
123///         ttl: Some(300),
124#[cfg(test)]
125#[path = "ddns_tests.rs"]
126mod ddns_tests;