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}