-
Notifications
You must be signed in to change notification settings - Fork 0
72 feature add nsupdate support #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
26f65de
434fbf4
535eed9
465826e
a83d76c
27076ab
0553188
29f8f91
03d12c6
a682491
970e074
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,25 @@ | ||
| listen_addr = "127.0.0.1" # Common listen address for all services | ||
| listen_addr = "127.0.0.1" # Common listen address for all services | ||
|
|
||
| [api] | ||
| listen_port = 3000 # HTTP API listen port | ||
| require_authentication = true # Enable API authentication (true/false) | ||
| listen_port = 3000 # HTTP API listen port | ||
| require_authentication = true # Enable API authentication (true/false) | ||
|
|
||
| [database] | ||
| type = "mysql" # Database type: mysql, sqlite, postgresql | ||
| type = "mysql" # Database type: mysql, sqlite, postgresql | ||
|
|
||
| [database.mysql] | ||
| server_url = "mysql://user:password@hostname:port/database" # Mysql server configuration | ||
|
|
||
| [database.sqlite] | ||
| file_path = "bindizr.db" # SQLite database file path | ||
| file_path = "bindizr.db" # SQLite database file path | ||
|
|
||
| [database.postgresql] | ||
| server_url = "postgresql://user:password@hostname:port/database" # PostgreSQL server configuration | ||
|
|
||
| [xfr] | ||
| listen_port = 53 # XFR server listen port (TCP) | ||
| secondary_addrs = "" # Comma-separated secondary DNS server addresses (e.g., "192.168.1.2:53,192.168.1.3:53") for NOTIFY | ||
| [dns] | ||
| listen_port = 53 # DNS server listen port (UDP and TCP) | ||
| secondary_addrs = "" # Comma-separated secondary DNS server addresses for NOTIFY (e.g., "192.168.1.2:53,192.168.1.3:53") | ||
| nsupdate_allowed_ips = "" # Comma-separated list of IPs allowed to perform nsupdate (e.g., "192.168.1.2,192.168.1.3") | ||
|
|
||
| [logging] | ||
| log_level = "debug" # Log level: error, warn, info, debug, trace |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| use crate::{config, log_warn}; | ||
| use std::net::{IpAddr, SocketAddr}; | ||
|
|
||
| pub fn secondary_servers_from_config() -> Vec<IpAddr> { | ||
| parse_ip_list_with_socket_fallback(&config::get_config::<String>("dns.secondary_addrs")) | ||
| } | ||
|
|
||
| pub fn nsupdate_allowed_ips_from_config() -> Vec<IpAddr> { | ||
| parse_ip_list_with_socket_fallback(&config::get_config::<String>("dns.nsupdate_allowed_ips")) | ||
| } | ||
|
|
||
| pub fn is_client_allowed(client_ip: IpAddr, allowed_ips: &[IpAddr]) -> bool { | ||
| allowed_ips.is_empty() || allowed_ips.contains(&client_ip) | ||
| } | ||
|
|
||
| fn parse_ip_list_with_socket_fallback(raw: &str) -> Vec<IpAddr> { | ||
| raw.split(',') | ||
| .filter_map(|item| { | ||
| let trimmed = item.trim(); | ||
| if trimmed.is_empty() { | ||
| return None; | ||
| } | ||
|
|
||
| match trimmed.parse::<SocketAddr>() { | ||
| Ok(addr) => Some(addr.ip()), | ||
| Err(_) => match trimmed.parse::<IpAddr>() { | ||
| Ok(ip) => Some(ip), | ||
| Err(_) => { | ||
| log_warn!("Ignoring invalid IP address in DNS ACL config: {}", trimmed); | ||
| None | ||
| } | ||
| }, | ||
| } | ||
| }) | ||
| .collect() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| pub mod acl; | ||
| pub mod nsupdate; | ||
| pub mod soa; | ||
| pub mod xfr; | ||
|
|
||
| use crate::{config, log_error, log_info, log_warn}; | ||
| use domain::base::iana::Rtype; | ||
| use std::net::{IpAddr, SocketAddr}; | ||
| use std::str::FromStr; | ||
| use tokio::net::{TcpListener, TcpStream, UdpSocket}; | ||
|
|
||
| enum QueryRoute { | ||
| Nsupdate, | ||
| Soa, | ||
| Xfr, | ||
| Other(Rtype), | ||
| } | ||
|
|
||
| pub async fn initialize() { | ||
| xfr::initialize().await; | ||
|
|
||
| let listen_addr_str = config::get_config::<String>("listen_addr"); | ||
| let listen_port = config::get_config::<u16>("dns.listen_port"); | ||
| let listen_addr = SocketAddr::new( | ||
| IpAddr::from_str(&listen_addr_str).expect("Invalid DNS listen address"), | ||
| listen_port, | ||
| ); | ||
|
|
||
| let secondary_servers = acl::secondary_servers_from_config(); | ||
| let tcp_secondary_servers = secondary_servers.clone(); | ||
|
|
||
| tokio::spawn(async move { | ||
| if let Err(e) = run_tcp_server(listen_addr, tcp_secondary_servers).await { | ||
| log_error!("DNS TCP server error: {}", e); | ||
| } | ||
| }); | ||
|
|
||
| tokio::spawn(async move { | ||
| if let Err(e) = run_udp_server(listen_addr, secondary_servers).await { | ||
| log_error!("DNS UDP server error: {}", e); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| async fn run_tcp_server( | ||
| listen_addr: SocketAddr, | ||
| secondary_servers: Vec<IpAddr>, | ||
| ) -> Result<(), String> { | ||
| let listener = TcpListener::bind(listen_addr) | ||
| .await | ||
| .map_err(|e| format!("Failed to bind DNS TCP listener on {}: {}", listen_addr, e))?; | ||
|
|
||
| log_info!("DNS TCP server listening on {}", listen_addr); | ||
|
|
||
| loop { | ||
| match listener.accept().await { | ||
| Ok((stream, client_addr)) => { | ||
| let allowed = secondary_servers.clone(); | ||
| tokio::spawn(async move { | ||
| if let Err(e) = handle_tcp_connection(stream, client_addr, allowed).await { | ||
| log_error!("DNS TCP connection error from {}: {}", client_addr, e); | ||
| } | ||
| }); | ||
| } | ||
| Err(e) => { | ||
| log_error!("Failed to accept DNS TCP connection: {}", e); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async fn handle_tcp_connection( | ||
| mut stream: TcpStream, | ||
| client_addr: SocketAddr, | ||
| secondary_servers: Vec<IpAddr>, | ||
| ) -> Result<(), String> { | ||
| let query_data = xfr::wire::read_tcp_message(&mut stream) | ||
| .await | ||
| .map_err(|e| format!("Failed to read DNS TCP message: {}", e))?; | ||
|
|
||
| match classify_query_route(&query_data) { | ||
| Ok(QueryRoute::Nsupdate) => { | ||
| nsupdate::handle_tcp_nsupdate(&mut stream, &query_data, client_addr).await?; | ||
| } | ||
| Ok(QueryRoute::Soa) => { | ||
| soa::handle_tcp_soa(&mut stream, client_addr, &secondary_servers, &query_data) | ||
| .await | ||
| .map_err(|e| format!("Failed to handle SOA TCP query: {}", e))?; | ||
| } | ||
| Ok(QueryRoute::Xfr) => { | ||
| xfr::handle_tcp_query(&mut stream, client_addr, &secondary_servers, &query_data) | ||
| .await | ||
| .map_err(|e| format!("Failed to handle XFR TCP query: {}", e))?; | ||
| } | ||
| Ok(QueryRoute::Other(qtype)) => { | ||
| log_info!( | ||
| "Ignoring non-XFR DNS TCP query from {} (qtype={:?})", | ||
| client_addr, | ||
| qtype | ||
| ); | ||
| } | ||
| Err(e) => { | ||
| log_warn!("Failed to parse DNS TCP query from {}: {}", client_addr, e); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| async fn run_udp_server( | ||
| listen_addr: SocketAddr, | ||
| secondary_servers: Vec<IpAddr>, | ||
| ) -> Result<(), String> { | ||
| let socket = UdpSocket::bind(listen_addr) | ||
| .await | ||
| .map_err(|e| format!("Failed to bind DNS UDP socket on {}: {}", listen_addr, e))?; | ||
|
|
||
| log_info!("DNS UDP server listening on {}", listen_addr); | ||
|
|
||
| let mut buf = [0u8; 65535]; | ||
|
|
||
| loop { | ||
| let (len, client_addr) = match socket.recv_from(&mut buf).await { | ||
| Ok(v) => v, | ||
| Err(e) => { | ||
| log_error!("Failed to receive DNS UDP packet: {}", e); | ||
| continue; | ||
| } | ||
| }; | ||
|
|
||
| let query_data = &buf[..len]; | ||
|
|
||
| match classify_query_route(query_data) { | ||
| Ok(QueryRoute::Nsupdate) => { | ||
| if let Err(e) = | ||
| nsupdate::handle_udp_nsupdate(&socket, query_data, client_addr).await | ||
| { | ||
| log_error!("NSUPDATE UDP handler failed for {}: {}", client_addr, e); | ||
| } | ||
| } | ||
| Ok(QueryRoute::Soa) => { | ||
| if let Err(e) = | ||
| soa::handle_udp_soa(&socket, client_addr, &secondary_servers, query_data).await | ||
| { | ||
| log_warn!("Failed to handle SOA UDP query from {}: {}", client_addr, e); | ||
| } | ||
| } | ||
| Ok(QueryRoute::Xfr) => { | ||
| if let Err(e) = | ||
| xfr::handle_udp_query(client_addr, &secondary_servers, query_data).await | ||
| { | ||
| log_warn!("Failed to handle XFR UDP query from {}: {}", client_addr, e); | ||
| } | ||
| } | ||
| Ok(QueryRoute::Other(_)) => {} | ||
| Err(_) => {} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn classify_query_route(query_data: &[u8]) -> Result<QueryRoute, String> { | ||
| if nsupdate::is_nsupdate(query_data) { | ||
| return Ok(QueryRoute::Nsupdate); | ||
| } | ||
|
|
||
|
Comment on lines
+161
to
+165
|
||
| let (_zone_name, qtype, _client_serial, _query_id) = | ||
| xfr::wire::parse_query(query_data).map_err(|e| e.to_string())?; | ||
|
|
||
| if qtype == Rtype::SOA { | ||
| Ok(QueryRoute::Soa) | ||
| } else if xfr::is_xfr_query_type(qtype) { | ||
| Ok(QueryRoute::Xfr) | ||
| } else { | ||
| Ok(QueryRoute::Other(qtype)) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UDP server loop awaits the full request handling (including DB work for nsupdate) inline. A slow update or transfer check will block processing of all subsequent UDP packets, which can cause packet loss/timeouts under load. Consider spawning a task per packet (or using a bounded work queue) so the receive loop stays responsive.