const SRV_NAME: &str = "_tesseras._udp.tesseras.net";
const C_IN: i32 = 1;
const T_SRV: i32 = 33;
const HFIXEDSZ: usize = 12;
const MAX_ANSWER: usize = 4096;
const MAX_RR_COUNT: usize = 64;
const TRUSTED_SUFFIX: &str = ".tesseras.net";
const DNS_FLAG_AD: u8 = 0x20;
pub struct SrvRecord {
pub host: String,
pub port: u16,
}
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;
static h_errno: i32;
}
pub fn lookup_bootstrap() -> Vec<SrvRecord> {
lookup_srv(SRV_NAME)
}
fn lookup_srv(name: &str) -> Vec<SrvRecord> {
let mut buf = vec![0u8; MAX_ANSWER];
let mut cname = name.as_bytes().to_vec();
cname.push(0);
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();
}
let len = len.min(buf.len());
parse_srv_response(&buf[..len])
}
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)
}
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;
}
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
}
fn expand_name(msg: &[u8], pos: &mut usize) -> Option<String> {
if *pos >= msg.len() {
return None;
}
let mut name_buf = [0u8; 512];
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;
let end = name_buf.iter().position(|&b| b == 0).unwrap_or(0);
String::from_utf8(name_buf[..end].to_vec()).ok()
}
fn parse_srv_response(data: &[u8]) -> Vec<SrvRecord> {
if data.len() < HFIXEDSZ {
return Vec::new();
}
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; 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;
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; }
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,
};
if read_u16(data, &mut pos).is_none() {
break;
}
if pos + 4 > data.len() {
break;
}
pos += 4;
let rdlength = match read_u16(data, &mut pos) {
Some(v) => v as usize,
None => break,
};
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;
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;
}
};
pos = rdata_start + rdlength;
if target.is_empty() || target == "." {
continue;
}
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
}