Files
ospab.network/osds/src/dns.rs

140 lines
3.8 KiB
Rust

//! Stealth DNS resolver - DoH/DoT with anti-hijacking
use bytes::{BufMut, BytesMut};
use std::net::SocketAddr;
use thiserror::Error;
use tokio::net::UdpSocket;
#[derive(Error, Debug)]
pub enum DnsError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("invalid query")]
InvalidQuery,
#[error("upstream timeout")]
Timeout,
#[error("hijacking detected")]
HijackDetected,
}
/// DNS query types
#[derive(Debug, Clone, Copy)]
pub enum QueryType {
A = 1,
AAAA = 28,
CNAME = 5,
TXT = 16,
}
/// Minimal DNS query builder
pub fn build_dns_query(domain: &str, qtype: QueryType) -> BytesMut {
let mut buf = BytesMut::with_capacity(512);
// Transaction ID (random)
let txid: u16 = rand::random();
buf.put_u16(txid);
// Flags: standard query, recursion desired
buf.put_u16(0x0100);
// Questions: 1, Answers: 0, Authority: 0, Additional: 0
buf.put_u16(1);
buf.put_u16(0);
buf.put_u16(0);
buf.put_u16(0);
// QNAME
for label in domain.split('.') {
buf.put_u8(label.len() as u8);
buf.put_slice(label.as_bytes());
}
buf.put_u8(0); // null terminator
// QTYPE and QCLASS (IN)
buf.put_u16(qtype as u16);
buf.put_u16(1); // IN class
buf
}
/// DNS forwarder that tunnels queries through OSTP
pub struct StealthDnsForwarder {
listen_addr: SocketAddr,
/// Upstream resolver (will be tunneled through OSTP)
upstream: SocketAddr,
}
impl StealthDnsForwarder {
pub fn new(listen: SocketAddr, upstream: SocketAddr) -> Self {
Self {
listen_addr: listen,
upstream,
}
}
/// Start DNS listener (intercepts local queries)
pub async fn run(&self) -> Result<(), DnsError> {
let socket = UdpSocket::bind(self.listen_addr).await?;
tracing::info!("DNS forwarder listening on {}", self.listen_addr);
let mut buf = [0u8; 512];
loop {
let (len, src) = socket.recv_from(&mut buf).await?;
let query = &buf[..len];
// TODO: Encrypt and forward through OSTP tunnel instead of direct
// For now, direct forward (to be replaced with tunnel)
match self.forward_query(query).await {
Ok(response) => {
let _ = socket.send_to(&response, src).await;
}
Err(e) => {
tracing::warn!("DNS forward failed: {}", e);
}
}
}
}
async fn forward_query(&self, query: &[u8]) -> Result<Vec<u8>, DnsError> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.send_to(query, self.upstream).await?;
let mut response = vec![0u8; 512];
let timeout = tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.recv_from(&mut response),
)
.await;
match timeout {
Ok(Ok((len, _))) => {
response.truncate(len);
Ok(response)
}
Ok(Err(e)) => Err(DnsError::Io(e)),
Err(_) => Err(DnsError::Timeout),
}
}
}
/// Anti-hijacking: verify response matches known-good resolver fingerprint
pub fn detect_hijack(response: &[u8], expected_patterns: &[[u8; 4]]) -> bool {
// Check if response contains known hijack IPs (ISP redirect pages, etc.)
// Simplified: check for common hijack patterns
if response.len() < 12 {
return true; // Suspicious short response
}
// Check for NXDOMAIN being rewritten (common hijack)
let rcode = response[3] & 0x0F;
if rcode == 0 {
// NOERROR - check if IP is in known hijack list
for pattern in expected_patterns {
if response.windows(4).any(|w| w == pattern) {
return true;
}
}
}
false
}