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    format!("{:x}", hasher.finalize())
89}
90
91/// Generate nsupdate commands for an A record.
92///
93/// Creates DNS update commands in nsupdate format to delete existing records
94/// and add the new record.
95///
96/// # Arguments
97///
98/// * `record` - The A record to update
99/// * `zone_fqdn` - Fully qualified domain name of the zone (e.g., "example.com.")
100///
101/// # Returns
102///
103/// A string containing nsupdate commands.
104///
105/// # Example
106///
107/// ```rust
108/// use bindy::ddns::generate_a_record_update;
109/// use bindy::crd::{ARecord, ARecordSpec};
110/// use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
111///
112/// let record = ARecord {
113///     metadata: ObjectMeta::default(),
114///     spec: ARecordSpec {
115///         name: "www".to_string(),
116///         ipv4_addresses: vec!["192.0.2.1".to_string()],
117///         ttl: Some(300),
118#[cfg(test)]
119#[path = "ddns_tests.rs"]
120mod ddns_tests;