1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
|
//! DNS SRV record lookup via libc `res_query`.
//!
//! Discovers bootstrap peers by querying
//! `_tesseras._udp.tesseras.net` for SRV records.
//! Falls back to an empty list on any DNS or parse error,
//! logging a warning so the operator knows discovery failed.
//!
//! ## Anti-spoofing
//!
//! Two mitigations against DNS spoofing are applied:
//!
//! - **DNSSEC (AD flag)**: if the resolver validated the response
//! via DNSSEC, the AD bit is set. When AD is absent, a warning
//! is logged so the operator knows the response is unauthenticated.
//!
//! - **Host suffix pinning**: only SRV targets ending in
//! `.tesseras.net` are accepted. This limits spoofed responses
//! to hosts within the trusted domain.
//!
//! These checks only apply to the automatic SRV discovery path.
//! Explicit `-b` peers bypass DNS entirely.
/// Default SRV record name for bootstrap discovery.
const SRV_NAME: &str = "_tesseras._udp.tesseras.net";
/// DNS class: Internet.
const C_IN: i32 = 1;
/// DNS record type: SRV (RFC 2782).
const T_SRV: i32 = 33;
/// Fixed DNS header size (ID + flags + 4 counts).
const HFIXEDSZ: usize = 12;
/// Maximum DNS response buffer. Covers UDP responses
/// (512 bytes standard, up to 4096 with EDNS0).
const MAX_ANSWER: usize = 4096;
/// Sanity cap on qdcount/ancount to avoid looping on
/// malformed responses (no legitimate response has >64 RRs
/// in 4096 bytes).
const MAX_RR_COUNT: usize = 64;
/// Only SRV targets under this domain suffix are accepted.
/// Prevents a spoofed DNS response from directing the daemon
/// to attacker-controlled hosts.
const TRUSTED_SUFFIX: &str = ".tesseras.net";
/// Bit mask for the AD (Authenticated Data) flag in the DNS
/// header flags field (byte 3, bit 5). Set by a validating
/// resolver when DNSSEC verification succeeded.
const DNS_FLAG_AD: u8 = 0x20;
/// A resolved SRV record with target host and port.
pub struct SrvRecord {
pub host: String,
pub port: u16,
}
/// `h_errno` values from `<netdb.h>`.
const HOST_NOT_FOUND: i32 = 1;
const TRY_AGAIN: i32 = 2;
const NO_RECOVERY: i32 = 3;
const NO_DATA: i32 = 4;
unsafe extern "C" {
fn res_query(
dname: *const u8,
class: i32,
rtype: i32,
answer: *mut u8,
anslen: i32,
) -> i32;
fn dn_expand(
msg: *const u8,
eomorig: *const u8,
comp_dn: *const u8,
exp_dn: *mut u8,
length: i32,
) -> i32;
/// Per-thread DNS error code, set by `res_query` on failure.
static h_errno: i32;
}
/// Query DNS for SRV records at `_tesseras._udp.tesseras.net`
/// and return the discovered (host, port) pairs.
/// Returns an empty Vec on any DNS or parsing failure.
pub fn lookup_bootstrap() -> Vec<SrvRecord> {
lookup_srv(SRV_NAME)
}
/// Perform the SRV query and parse the response.
fn lookup_srv(name: &str) -> Vec<SrvRecord> {
let mut buf = vec![0u8; MAX_ANSWER];
// res_query expects a null-terminated C string.
let mut cname = name.as_bytes().to_vec();
cname.push(0);
// SAFETY: cname is a valid null-terminated byte string,
// buf is a properly sized mutable buffer.
let len = unsafe {
res_query(
cname.as_ptr(),
C_IN,
T_SRV,
buf.as_mut_ptr(),
buf.len() as i32,
)
};
if len < 0 {
let reason = match unsafe { h_errno } {
HOST_NOT_FOUND => "host not found",
TRY_AGAIN => "timeout or temporary failure",
NO_RECOVERY => "non-recoverable server error",
NO_DATA => "no SRV records for this name",
other => {
log::warn!(
"dns: SRV query for {name} failed (h_errno={other})"
);
return Vec::new();
}
};
log::warn!("dns: SRV query for {name} failed: {reason}");
return Vec::new();
}
let len = len as usize;
if len < HFIXEDSZ {
log::warn!("dns: SRV response too short ({len} bytes)");
return Vec::new();
}
// res_query returns the full (untruncated) response length,
// which may exceed the buffer when the answer was truncated.
// Cap to the actual buffer size to avoid an out-of-bounds slice.
let len = len.min(buf.len());
parse_srv_response(&buf[..len])
}
/// Read a big-endian u16 from `data[*pos..]`, advancing `*pos` by 2.
/// Returns `None` if there aren't enough bytes remaining.
fn read_u16(data: &[u8], pos: &mut usize) -> Option<u16> {
if *pos + 2 > data.len() {
return None;
}
let val = u16::from_be_bytes([data[*pos], data[*pos + 1]]);
*pos += 2;
Some(val)
}
/// Skip over a compressed domain name in the DNS wire format.
/// Returns `false` if the name is malformed or extends past the buffer.
fn skip_name(data: &[u8], pos: &mut usize) -> bool {
while *pos < data.len() {
let label_len = data[*pos] as usize;
if label_len == 0 {
*pos += 1;
return true;
}
// Compression pointer: top 2 bits set, followed by 1 offset byte.
if label_len & 0xC0 == 0xC0 {
if *pos + 2 > data.len() {
return false;
}
*pos += 2;
return true;
}
if *pos + 1 + label_len > data.len() {
return false;
}
*pos += 1 + label_len;
}
false
}
/// Expand a compressed domain name at `msg[*pos..]` using
/// libc `dn_expand`. Advances `*pos` past the compressed
/// name. Returns `None` on any error.
fn expand_name(msg: &[u8], pos: &mut usize) -> Option<String> {
if *pos >= msg.len() {
return None;
}
// MAXDNAME is 1025 on OpenBSD; 512 is more than enough
// for any valid domain name (max 253 chars + null) and
// gives headroom for malformed names without truncation.
let mut name_buf = [0u8; 512];
// SAFETY: pos is bounds-checked above, eomorig points to
// one past the last byte of msg. dn_expand will not read
// past eomorig and writes at most `length` bytes to exp_dn.
let n = unsafe {
dn_expand(
msg.as_ptr(),
msg.as_ptr().add(msg.len()),
msg.as_ptr().add(*pos),
name_buf.as_mut_ptr(),
name_buf.len() as i32,
)
};
if n < 0 {
return None;
}
*pos += n as usize;
// dn_expand null-terminates the output.
let end = name_buf.iter().position(|&b| b == 0).unwrap_or(0);
String::from_utf8(name_buf[..end].to_vec()).ok()
}
/// Parse a raw DNS response and extract SRV records.
/// Checks the AD flag for DNSSEC validation and rejects
/// SRV targets outside the trusted domain suffix.
fn parse_srv_response(data: &[u8]) -> Vec<SrvRecord> {
if data.len() < HFIXEDSZ {
return Vec::new();
}
// Check DNSSEC AD (Authenticated Data) flag.
// Byte 3 of the header contains AD at bit 5.
if data[3] & DNS_FLAG_AD != 0 {
log::info!("dns: response has AD flag (DNSSEC validated)");
} else {
log::warn!(
"dns: response lacks AD flag (DNSSEC not validated); \
results may be spoofed"
);
}
let mut pos = 4; // skip ID + flags
let qdcount =
(read_u16(data, &mut pos).unwrap_or(0) as usize).min(MAX_RR_COUNT);
let ancount =
(read_u16(data, &mut pos).unwrap_or(0) as usize).min(MAX_RR_COUNT);
pos += 4; // skip nscount + arcount
// Skip question section.
for _ in 0..qdcount {
if !skip_name(data, &mut pos) {
log::debug!("dns: failed to skip question name");
return Vec::new();
}
if pos + 4 > data.len() {
return Vec::new();
}
pos += 4; // qtype + qclass
}
let mut records = Vec::new();
for _ in 0..ancount {
if !skip_name(data, &mut pos) {
break;
}
let rtype = match read_u16(data, &mut pos) {
Some(v) => v,
None => break,
};
// rclass
if read_u16(data, &mut pos).is_none() {
break;
}
// ttl (4 bytes)
if pos + 4 > data.len() {
break;
}
pos += 4;
let rdlength = match read_u16(data, &mut pos) {
Some(v) => v as usize,
None => break,
};
// Guard: rdlength must not extend past the buffer.
if pos + rdlength > data.len() {
log::debug!("dns: rdlength extends past response buffer");
break;
}
if rtype != T_SRV as u16 || rdlength < 6 {
pos += rdlength;
continue;
}
let rdata_start = pos;
// SRV RDATA: priority(2) + weight(2) + port(2) + target
if read_u16(data, &mut pos).is_none() {
break;
}
if read_u16(data, &mut pos).is_none() {
break;
}
let srv_port = match read_u16(data, &mut pos) {
Some(v) => v,
None => break,
};
let target = match expand_name(data, &mut pos) {
Some(name) => name,
None => {
pos = rdata_start + rdlength;
continue;
}
};
// Advance to end of RDATA regardless of how much
// expand_name consumed (defensive against short reads).
pos = rdata_start + rdlength;
// SRV target "." means no service available (RFC 2782).
if target.is_empty() || target == "." {
continue;
}
// Anti-spoofing: only accept targets under the trusted domain.
let lower = target.to_ascii_lowercase();
if !lower.ends_with(TRUSTED_SUFFIX) {
log::warn!(
"dns: rejecting SRV target '{target}' \
(not under {TRUSTED_SUFFIX})"
);
continue;
}
records.push(SrvRecord {
host: target,
port: srv_port,
});
}
records
}
|