Skip to main content

dns_lattice_core/
error.rs

1use std::fmt;
2
3/// The single error type surfaced across the DNS Lattice workspace.
4///
5/// DNS message and domain-pattern operations return `Result<T, Error>` —
6/// never a bare `String`, panic, or ad hoc per-module error type. See
7/// `ARCHITECTURE.md`'s cross-cutting concerns for why.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum Error {
10    /// The message buffer ended before a length-prefixed field or a
11    /// declared record/section could be fully read.
12    Truncated {
13        /// The number of bytes actually available.
14        len: usize,
15    },
16    /// A DNS name compression pointer is out of bounds, or two pointers
17    /// form a loop.
18    BadNamePointer {
19        /// The byte offset of the offending pointer.
20        offset: usize,
21    },
22    /// A name label's length byte exceeds the 63-byte maximum (RFC 1035
23    /// §2.3.4).
24    LabelTooLong {
25        /// The byte offset of the offending label.
26        offset: usize,
27    },
28    /// An encoded or decoded name exceeds the 255-byte maximum (RFC 1035
29    /// §2.3.4).
30    NameTooLong,
31    /// A name label was empty (two consecutive dots) or contained bytes
32    /// not permitted in that position.
33    InvalidName,
34    /// The header's declared section count does not match the number of
35    /// records actually present after parsing.
36    CountMismatch,
37    /// A resource record's declared `RDLENGTH` does not match the number
38    /// of bytes actually available or consumed for its `RDATA`.
39    RDataLengthMismatch {
40        /// The declared `RDLENGTH` value.
41        declared: u16,
42    },
43    /// A question or resource record's class value is not a value this
44    /// crate recognizes as well-formed.
45    InvalidClass(u16),
46    /// Encoding a message would exceed the 65535-byte maximum a DNS
47    /// message can address.
48    MessageTooLong,
49    /// A domain pattern's wildcard label (`*`) appeared somewhere other
50    /// than the leftmost position.
51    WildcardPosition,
52    /// No split-DNS rule matched the queried name and no default upstream
53    /// group is configured.
54    NoRoute,
55    /// An upstream backend did not produce a response within its
56    /// configured timeout (stage 0.3, `upstream::UpstreamBackend`).
57    Timeout,
58    /// An upstream backend's transport (socket connect/send/receive)
59    /// failed. Carries a human-readable description of the underlying I/O
60    /// error; not `PartialEq`-sensitive to that error's exact kind since
61    /// `std::io::Error` is not itself comparable (stage 0.3,
62    /// `upstream::UpstreamBackend`).
63    Transport(String),
64    /// A TLS handshake, certificate validation, or hostname verification
65    /// failure while establishing or maintaining an encrypted upstream
66    /// connection (stage 0.3, DoT `upstream::DotBackend`/DoH
67    /// `upstream::DohBackend`). Carries a human-readable description; not
68    /// `PartialEq`-sensitive to the underlying TLS library's exact error
69    /// type, matching `Transport`'s existing precedent.
70    Tls(String),
71    /// A caller-supplied dynamic route-selection hook failed. Carries the
72    /// hook's human-readable failure message; resolver integration does not
73    /// fall back to static routing, retry an upstream, or cache this error.
74    Hook(String),
75    /// A Fake IP pool range was empty because its start address was greater
76    /// than its end address.
77    InvalidFakeIpRange,
78    /// A Fake IP pool was built without an IPv4 or IPv6 range.
79    FakeIpPoolUnconfigured,
80    /// An operation requested an address family not configured on a Fake IP
81    /// pool.
82    FakeIpFamilyDisabled,
83    /// A Fake IP pool mapping lifetime was zero or did not contain a whole
84    /// positive number of seconds.
85    InvalidFakeIpTtl,
86    /// A valid Fake IP TTL could not be represented as either a pool
87    /// monotonic-clock expiry instant or a 32-bit DNS record TTL.
88    FakeIpTtlOutOfRange,
89    /// A caller-provided Fake IP pool snapshot was structurally invalid or
90    /// incompatible with its declared pool configuration.
91    InvalidFakeIpSnapshot,
92}
93
94impl fmt::Display for Error {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self {
97            Error::Truncated { len } => write!(f, "message truncated: {len} bytes available"),
98            Error::BadNamePointer { offset } => {
99                write!(f, "invalid name compression pointer at offset {offset}")
100            }
101            Error::LabelTooLong { offset } => {
102                write!(f, "label exceeds 63 bytes at offset {offset}")
103            }
104            Error::NameTooLong => write!(f, "name exceeds 255 bytes"),
105            Error::InvalidName => write!(f, "invalid domain name"),
106            Error::CountMismatch => {
107                write!(f, "header section count does not match parsed records")
108            }
109            Error::RDataLengthMismatch { declared } => {
110                write!(
111                    f,
112                    "rdlength {declared} does not match available rdata bytes"
113                )
114            }
115            Error::InvalidClass(class) => write!(f, "invalid class value {class}"),
116            Error::MessageTooLong => write!(f, "encoded message would exceed 65535 bytes"),
117            Error::WildcardPosition => {
118                write!(f, "wildcard label may only appear as the leftmost label")
119            }
120            Error::NoRoute => write!(
121                f,
122                "no split-dns rule matched and no default upstream group is configured"
123            ),
124            Error::Timeout => write!(f, "upstream backend timed out"),
125            Error::Transport(message) => write!(f, "upstream transport error: {message}"),
126            Error::Tls(message) => write!(f, "upstream tls error: {message}"),
127            Error::Hook(message) => write!(f, "route hook error: {message}"),
128            Error::InvalidFakeIpRange => write!(f, "fake ip range start exceeds its end"),
129            Error::FakeIpPoolUnconfigured => {
130                write!(f, "fake ip pool requires an ipv4 or ipv6 range")
131            }
132            Error::FakeIpFamilyDisabled => write!(f, "fake ip address family is not configured"),
133            Error::InvalidFakeIpTtl => {
134                write!(f, "fake ip ttl must be a non-zero whole number of seconds")
135            }
136            Error::FakeIpTtlOutOfRange => {
137                write!(
138                    f,
139                    "fake ip ttl cannot be represented by the pool clock or dns wire ttl"
140                )
141            }
142            Error::InvalidFakeIpSnapshot => write!(f, "invalid fake ip pool snapshot"),
143        }
144    }
145}
146
147impl std::error::Error for Error {}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn every_variant_has_a_stable_non_empty_display_message() {
155        let cases = [
156            (
157                Error::Truncated { len: 3 },
158                "message truncated: 3 bytes available",
159            ),
160            (
161                Error::BadNamePointer { offset: 12 },
162                "invalid name compression pointer at offset 12",
163            ),
164            (
165                Error::LabelTooLong { offset: 5 },
166                "label exceeds 63 bytes at offset 5",
167            ),
168            (Error::NameTooLong, "name exceeds 255 bytes"),
169            (Error::InvalidName, "invalid domain name"),
170            (
171                Error::CountMismatch,
172                "header section count does not match parsed records",
173            ),
174            (
175                Error::RDataLengthMismatch { declared: 200 },
176                "rdlength 200 does not match available rdata bytes",
177            ),
178            (Error::InvalidClass(9999), "invalid class value 9999"),
179            (
180                Error::MessageTooLong,
181                "encoded message would exceed 65535 bytes",
182            ),
183            (
184                Error::WildcardPosition,
185                "wildcard label may only appear as the leftmost label",
186            ),
187            (
188                Error::NoRoute,
189                "no split-dns rule matched and no default upstream group is configured",
190            ),
191            (Error::Timeout, "upstream backend timed out"),
192            (
193                Error::Transport("connection refused".to_string()),
194                "upstream transport error: connection refused",
195            ),
196            (
197                Error::Tls("certificate expired".to_string()),
198                "upstream tls error: certificate expired",
199            ),
200            (
201                Error::Hook("policy service unavailable".to_string()),
202                "route hook error: policy service unavailable",
203            ),
204            (
205                Error::InvalidFakeIpRange,
206                "fake ip range start exceeds its end",
207            ),
208            (
209                Error::FakeIpPoolUnconfigured,
210                "fake ip pool requires an ipv4 or ipv6 range",
211            ),
212            (
213                Error::FakeIpFamilyDisabled,
214                "fake ip address family is not configured",
215            ),
216            (
217                Error::InvalidFakeIpTtl,
218                "fake ip ttl must be a non-zero whole number of seconds",
219            ),
220            (
221                Error::FakeIpTtlOutOfRange,
222                "fake ip ttl cannot be represented by the pool clock or dns wire ttl",
223            ),
224            (
225                Error::InvalidFakeIpSnapshot,
226                "invalid fake ip pool snapshot",
227            ),
228        ];
229        for (error, expected) in cases {
230            assert_eq!(error.to_string(), expected);
231        }
232    }
233
234    #[test]
235    fn error_implements_std_error() {
236        fn assert_std_error<E: std::error::Error>(_: &E) {}
237        assert_std_error(&Error::NameTooLong);
238    }
239}