add Makefile with clippy/rustfmt linting, fix all warnings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
20
Makefile
Normal file
20
Makefile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.PHONY: all build lint fmt check test clean
|
||||||
|
|
||||||
|
all: lint build
|
||||||
|
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
lint: fmt check
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
cargo fmt --check
|
||||||
|
|
||||||
|
check:
|
||||||
|
cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
clean:
|
||||||
|
cargo clean
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
use crate::{Result};
|
use crate::Result;
|
||||||
|
|
||||||
pub struct BytePacketBuffer {
|
pub struct BytePacketBuffer {
|
||||||
pub buf: [u8; 512],
|
pub buf: [u8; 512],
|
||||||
pub pos: usize,
|
pub pos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for BytePacketBuffer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BytePacketBuffer {
|
impl BytePacketBuffer {
|
||||||
pub fn new() -> BytePacketBuffer {
|
pub fn new() -> BytePacketBuffer {
|
||||||
BytePacketBuffer {
|
BytePacketBuffer {
|
||||||
@@ -63,7 +69,7 @@ impl BytePacketBuffer {
|
|||||||
let res = ((self.read()? as u32) << 24)
|
let res = ((self.read()? as u32) << 24)
|
||||||
| ((self.read()? as u32) << 16)
|
| ((self.read()? as u32) << 16)
|
||||||
| ((self.read()? as u32) << 8)
|
| ((self.read()? as u32) << 8)
|
||||||
| ((self.read()? as u32) << 0);
|
| (self.read()? as u32);
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +150,7 @@ impl BytePacketBuffer {
|
|||||||
self.write(((val >> 24) & 0xFF) as u8)?;
|
self.write(((val >> 24) & 0xFF) as u8)?;
|
||||||
self.write(((val >> 16) & 0xFF) as u8)?;
|
self.write(((val >> 16) & 0xFF) as u8)?;
|
||||||
self.write(((val >> 8) & 0xFF) as u8)?;
|
self.write(((val >> 8) & 0xFF) as u8)?;
|
||||||
self.write(((val >> 0) & 0xFF) as u8)?;
|
self.write((val & 0xFF) as u8)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/cache.rs
12
src/cache.rs
@@ -34,7 +34,7 @@ impl DnsCache {
|
|||||||
self.query_count += 1;
|
self.query_count += 1;
|
||||||
|
|
||||||
// Periodic eviction every 1000 queries
|
// Periodic eviction every 1000 queries
|
||||||
if self.query_count % 1000 == 0 {
|
if self.query_count.is_multiple_of(1000) {
|
||||||
self.evict_expired();
|
self.evict_expired();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,15 +72,19 @@ impl DnsCache {
|
|||||||
.clamp(self.min_ttl, self.max_ttl);
|
.clamp(self.min_ttl, self.max_ttl);
|
||||||
|
|
||||||
let key = (domain.to_string(), qtype);
|
let key = (domain.to_string(), qtype);
|
||||||
self.entries.insert(key, CacheEntry {
|
self.entries.insert(
|
||||||
|
key,
|
||||||
|
CacheEntry {
|
||||||
packet: packet.clone(),
|
packet: packet.clone(),
|
||||||
inserted_at: Instant::now(),
|
inserted_at: Instant::now(),
|
||||||
ttl: Duration::from_secs(min_ttl as u64),
|
ttl: Duration::from_secs(min_ttl as u64),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evict_expired(&mut self) {
|
fn evict_expired(&mut self) {
|
||||||
self.entries.retain(|_, entry| entry.inserted_at.elapsed() < entry.ttl);
|
self.entries
|
||||||
|
.retain(|_, entry| entry.inserted_at.elapsed() < entry.ttl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,36 +126,77 @@ pub fn load_config(path: &str) -> Result<Config> {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_zone_map(zones: &[ZoneRecord]) -> Result<HashMap<(String, QueryType), Vec<DnsRecord>>> {
|
pub fn build_zone_map(
|
||||||
|
zones: &[ZoneRecord],
|
||||||
|
) -> Result<HashMap<(String, QueryType), Vec<DnsRecord>>> {
|
||||||
let mut map: HashMap<(String, QueryType), Vec<DnsRecord>> = HashMap::new();
|
let mut map: HashMap<(String, QueryType), Vec<DnsRecord>> = HashMap::new();
|
||||||
|
|
||||||
for zone in zones {
|
for zone in zones {
|
||||||
let domain = zone.domain.to_lowercase();
|
let domain = zone.domain.to_lowercase();
|
||||||
let (qtype, record) = match zone.record_type.to_uppercase().as_str() {
|
let (qtype, record) = match zone.record_type.to_uppercase().as_str() {
|
||||||
"A" => {
|
"A" => {
|
||||||
let addr: Ipv4Addr = zone.value.parse()
|
let addr: Ipv4Addr = zone
|
||||||
|
.value
|
||||||
|
.parse()
|
||||||
.map_err(|e| format!("invalid A record value '{}': {}", zone.value, e))?;
|
.map_err(|e| format!("invalid A record value '{}': {}", zone.value, e))?;
|
||||||
(QueryType::A, DnsRecord::A { domain: domain.clone(), addr, ttl: zone.ttl })
|
(
|
||||||
|
QueryType::A,
|
||||||
|
DnsRecord::A {
|
||||||
|
domain: domain.clone(),
|
||||||
|
addr,
|
||||||
|
ttl: zone.ttl,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"AAAA" => {
|
"AAAA" => {
|
||||||
let addr: Ipv6Addr = zone.value.parse()
|
let addr: Ipv6Addr = zone
|
||||||
|
.value
|
||||||
|
.parse()
|
||||||
.map_err(|e| format!("invalid AAAA record value '{}': {}", zone.value, e))?;
|
.map_err(|e| format!("invalid AAAA record value '{}': {}", zone.value, e))?;
|
||||||
(QueryType::AAAA, DnsRecord::AAAA { domain: domain.clone(), addr, ttl: zone.ttl })
|
(
|
||||||
}
|
QueryType::AAAA,
|
||||||
"CNAME" => {
|
DnsRecord::AAAA {
|
||||||
(QueryType::CNAME, DnsRecord::CNAME { domain: domain.clone(), host: zone.value.clone(), ttl: zone.ttl })
|
domain: domain.clone(),
|
||||||
}
|
addr,
|
||||||
"NS" => {
|
ttl: zone.ttl,
|
||||||
(QueryType::NS, DnsRecord::NS { domain: domain.clone(), host: zone.value.clone(), ttl: zone.ttl })
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
"CNAME" => (
|
||||||
|
QueryType::CNAME,
|
||||||
|
DnsRecord::CNAME {
|
||||||
|
domain: domain.clone(),
|
||||||
|
host: zone.value.clone(),
|
||||||
|
ttl: zone.ttl,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"NS" => (
|
||||||
|
QueryType::NS,
|
||||||
|
DnsRecord::NS {
|
||||||
|
domain: domain.clone(),
|
||||||
|
host: zone.value.clone(),
|
||||||
|
ttl: zone.ttl,
|
||||||
|
},
|
||||||
|
),
|
||||||
"MX" => {
|
"MX" => {
|
||||||
let parts: Vec<&str> = zone.value.splitn(2, ' ').collect();
|
let parts: Vec<&str> = zone.value.splitn(2, ' ').collect();
|
||||||
if parts.len() != 2 {
|
if parts.len() != 2 {
|
||||||
return Err(format!("MX value must be 'priority host', got '{}'", zone.value).into());
|
return Err(
|
||||||
|
format!("MX value must be 'priority host', got '{}'", zone.value).into(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let priority: u16 = parts[0].parse()
|
let priority: u16 = parts[0]
|
||||||
|
.parse()
|
||||||
.map_err(|e| format!("invalid MX priority '{}': {}", parts[0], e))?;
|
.map_err(|e| format!("invalid MX priority '{}': {}", parts[0], e))?;
|
||||||
(QueryType::MX, DnsRecord::MX { domain: domain.clone(), priority, host: parts[1].to_string(), ttl: zone.ttl })
|
(
|
||||||
|
QueryType::MX,
|
||||||
|
DnsRecord::MX {
|
||||||
|
domain: domain.clone(),
|
||||||
|
priority,
|
||||||
|
host: parts[1].to_string(),
|
||||||
|
ttl: zone.ttl,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
return Err(format!("unsupported record type '{}'", other).into());
|
return Err(format!("unsupported record type '{}'", other).into());
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ impl ResultCode {
|
|||||||
3 => ResultCode::NXDOMAIN,
|
3 => ResultCode::NXDOMAIN,
|
||||||
4 => ResultCode::NOTIMP,
|
4 => ResultCode::NOTIMP,
|
||||||
5 => ResultCode::REFUSED,
|
5 => ResultCode::REFUSED,
|
||||||
0 | _ => ResultCode::NOERROR,
|
_ => ResultCode::NOERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,12 @@ pub struct DnsHeader {
|
|||||||
pub resource_entries: u16,
|
pub resource_entries: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DnsHeader {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DnsHeader {
|
impl DnsHeader {
|
||||||
pub fn new() -> DnsHeader {
|
pub fn new() -> DnsHeader {
|
||||||
DnsHeader {
|
DnsHeader {
|
||||||
@@ -112,7 +118,7 @@ impl DnsHeader {
|
|||||||
| ((self.truncated_message as u8) << 1)
|
| ((self.truncated_message as u8) << 1)
|
||||||
| ((self.authoritative_answer as u8) << 2)
|
| ((self.authoritative_answer as u8) << 2)
|
||||||
| (self.opcode << 3)
|
| (self.opcode << 3)
|
||||||
| ((self.response as u8) << 7) as u8,
|
| ((self.response as u8) << 7),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
buffer.write_u8(
|
buffer.write_u8(
|
||||||
|
|||||||
31
src/main.rs
31
src/main.rs
@@ -31,10 +31,13 @@ async fn main() -> dns_fun::Result<()> {
|
|||||||
.format_timestamp_millis()
|
.format_timestamp_millis()
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let config_path = std::env::args().nth(1).unwrap_or_else(|| "dns_fun.toml".to_string());
|
let config_path = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or_else(|| "dns_fun.toml".to_string());
|
||||||
let config = load_config(&config_path)?;
|
let config = load_config(&config_path)?;
|
||||||
|
|
||||||
let upstream: SocketAddr = format!("{}:{}", config.upstream.address, config.upstream.port).parse()?;
|
let upstream: SocketAddr =
|
||||||
|
format!("{}:{}", config.upstream.address, config.upstream.port).parse()?;
|
||||||
let socket = Arc::new(UdpSocket::bind(&config.server.bind_addr).await?);
|
let socket = Arc::new(UdpSocket::bind(&config.server.bind_addr).await?);
|
||||||
|
|
||||||
let ctx = Arc::new(ServerCtx {
|
let ctx = Arc::new(ServerCtx {
|
||||||
@@ -110,8 +113,14 @@ async fn handle_query(
|
|||||||
(resp, QueryPath::Forwarded)
|
(resp, QueryPath::Forwarded)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{} | {:?} {} | UPSTREAM ERROR | {}", src_addr, qtype, qname, e);
|
error!(
|
||||||
(DnsPacket::response_from(&query, ResultCode::SERVFAIL), QueryPath::UpstreamError)
|
"{} | {:?} {} | UPSTREAM ERROR | {}",
|
||||||
|
src_addr, qtype, qname, e
|
||||||
|
);
|
||||||
|
(
|
||||||
|
DnsPacket::response_from(&query, ResultCode::SERVFAIL),
|
||||||
|
QueryPath::UpstreamError,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,13 +130,19 @@ async fn handle_query(
|
|||||||
|
|
||||||
info!(
|
info!(
|
||||||
"{} | {:?} {} | {} | {} | {}ms",
|
"{} | {:?} {} | {} | {} | {}ms",
|
||||||
src_addr, qtype, qname, path.as_str(),
|
src_addr,
|
||||||
response.header.rescode.as_str(), elapsed.as_millis(),
|
qtype,
|
||||||
|
qname,
|
||||||
|
path.as_str(),
|
||||||
|
response.header.rescode.as_str(),
|
||||||
|
elapsed.as_millis(),
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"response: {} answers, {} authorities, {} resources",
|
"response: {} answers, {} authorities, {} resources",
|
||||||
response.answers.len(), response.authorities.len(), response.resources.len(),
|
response.answers.len(),
|
||||||
|
response.authorities.len(),
|
||||||
|
response.resources.len(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut resp_buffer = BytePacketBuffer::new();
|
let mut resp_buffer = BytePacketBuffer::new();
|
||||||
@@ -137,7 +152,7 @@ async fn handle_query(
|
|||||||
// Record stats and log summary every 1000 queries (single lock acquisition)
|
// Record stats and log summary every 1000 queries (single lock acquisition)
|
||||||
let mut s = ctx.stats.lock().unwrap();
|
let mut s = ctx.stats.lock().unwrap();
|
||||||
let total = s.record(path);
|
let total = s.record(path);
|
||||||
if total % 1000 == 0 {
|
if total.is_multiple_of(1000) {
|
||||||
s.log_summary();
|
s.log_summary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ pub struct DnsPacket {
|
|||||||
pub resources: Vec<DnsRecord>,
|
pub resources: Vec<DnsRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DnsPacket {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DnsPacket {
|
impl DnsPacket {
|
||||||
pub fn new() -> DnsPacket {
|
pub fn new() -> DnsPacket {
|
||||||
DnsPacket {
|
DnsPacket {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ impl DnsRecord {
|
|||||||
((raw_addr >> 24) & 0xFF) as u8,
|
((raw_addr >> 24) & 0xFF) as u8,
|
||||||
((raw_addr >> 16) & 0xFF) as u8,
|
((raw_addr >> 16) & 0xFF) as u8,
|
||||||
((raw_addr >> 8) & 0xFF) as u8,
|
((raw_addr >> 8) & 0xFF) as u8,
|
||||||
((raw_addr >> 0) & 0xFF) as u8,
|
(raw_addr & 0xFF) as u8,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(DnsRecord::A { domain, addr, ttl })
|
Ok(DnsRecord::A { domain, addr, ttl })
|
||||||
@@ -94,13 +94,13 @@ impl DnsRecord {
|
|||||||
let raw_addr4 = buffer.read_u32()?;
|
let raw_addr4 = buffer.read_u32()?;
|
||||||
let addr = Ipv6Addr::new(
|
let addr = Ipv6Addr::new(
|
||||||
((raw_addr1 >> 16) & 0xFFFF) as u16,
|
((raw_addr1 >> 16) & 0xFFFF) as u16,
|
||||||
((raw_addr1 >> 0) & 0xFFFF) as u16,
|
(raw_addr1 & 0xFFFF) as u16,
|
||||||
((raw_addr2 >> 16) & 0xFFFF) as u16,
|
((raw_addr2 >> 16) & 0xFFFF) as u16,
|
||||||
((raw_addr2 >> 0) & 0xFFFF) as u16,
|
(raw_addr2 & 0xFFFF) as u16,
|
||||||
((raw_addr3 >> 16) & 0xFFFF) as u16,
|
((raw_addr3 >> 16) & 0xFFFF) as u16,
|
||||||
((raw_addr3 >> 0) & 0xFFFF) as u16,
|
(raw_addr3 & 0xFFFF) as u16,
|
||||||
((raw_addr4 >> 16) & 0xFFFF) as u16,
|
((raw_addr4 >> 16) & 0xFFFF) as u16,
|
||||||
((raw_addr4 >> 0) & 0xFFFF) as u16,
|
(raw_addr4 & 0xFFFF) as u16,
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(DnsRecord::AAAA { domain, addr, ttl })
|
Ok(DnsRecord::AAAA { domain, addr, ttl })
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ impl QueryPath {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ServerStats {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ServerStats {
|
impl ServerStats {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
ServerStats {
|
ServerStats {
|
||||||
|
|||||||
Reference in New Issue
Block a user