diff --git a/Cargo.lock b/Cargo.lock index 79fde39..9b79cbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1079,6 +1079,7 @@ dependencies = [ "tracing-subscriber", "url", "webpki-roots 0.26.11", + "windows-service", ] [[package]] @@ -2416,6 +2417,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2431,6 +2438,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-service" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" +dependencies = [ + "bitflags 2.10.0", + "widestring", + "windows-sys 0.61.2", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 29f7577..490e7f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,10 @@ rustls = { version = "0.23", features = ["ring"] } webpki-roots = "0.26" url = "2" quinn = { version = "0.11", features = ["runtime-tokio", "rustls"] } + +# Windows service support (Windows only, harmless on other platforms) +# Windows 服务支持(仅 Windows,其他平台无害) +windows-service = { version = "0.8" } [dev-dependencies] criterion = "0.5" ctor = "0.2" @@ -97,4 +101,3 @@ strip = true # 详情请参考 GitHub Actions 工作流 # [target.aarch64-unknown-freebsd] # linker = "aarch64-unknown-freebsd12.0-gcc" - diff --git a/README.md b/README.md index 6fd0a4f..5423e69 100644 --- a/README.md +++ b/README.md @@ -90,32 +90,212 @@ OPTIONS: -V, --version Show version ``` -### systemd Service +### Service Management (Cross-Platform) -Create `/etc/systemd/system/kixdns.service`: +KixDNS includes built-in service management that supports all major init systems. +The `service` subcommand works on Windows, Linux, and FreeBSD. + +#### Quick Commands + +```bash +# Install as a system service (auto-detects init system) +sudo ./target/release/kixdns service install + +# Install with custom config +sudo ./target/release/kixdns service install \ + --config /etc/kixdns/pipeline.json \ + --listener-label edge-internal + +# Uninstall the service +sudo ./target/release/kixdns service uninstall + +# Run as service (used by init system internally) +kixdns service run +``` + +> **Note**: `service install` and `service uninstall` require root/Administrator +> privileges. `service run` is called automatically by the init system. + +#### Auto-Detection Logic + +The init system is auto-detected in the following order: + +| Condition | Detected System | +|-----------|----------------| +| `#[cfg(target_os = "windows")]` | Windows SCM | +| `/sbin/procd` exists | Procd (OpenWrt) | +| `/usr/lib/systemd/systemd` or `/lib/systemd/systemd` exists | systemd | +| `/sbin/openrc-run` exists | OpenRC | +| `#[cfg(target_os = "freebsd")]` | BSD rc.d | +| Nothing matched | Unknown (manual setup required) | + +#### Service Scripts + +
+systemd (Linux — most distributions) + +Installed to: `/etc/systemd/system/kixdns.service` ```ini [Unit] -Description=KixDNS +Description=KixDNS DNS Server +Documentation=https://github.com/kixdns/kixdns After=network.target +Wants=nss-lookup.target [Service] Type=simple -ExecStart=/usr/local/bin/kixdns --config /etc/kixdns/pipeline.json +ExecStart=/usr/local/bin/kixdns run \ + --config /etc/kixdns/pipeline.json \ + --listener-label default Restart=on-failure +RestartSec=5 +User=nobody +Group=nogroup +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true LimitNOFILE=65536 +PrivateTmp=true +ProtectSystem=full +ProtectHome=true [Install] WantedBy=multi-user.target ``` ```bash -sudo install -m 0755 target/release/kixdns /usr/local/bin/kixdns -sudo mkdir -p /etc/kixdns -sudo cp config/pipeline.json /etc/kixdns/ -sudo systemctl daemon-reload -sudo systemctl enable --now kixdns +# Manual commands +systemctl daemon-reload +systemctl enable --now kixdns +systemctl status kixdns +journalctl -u kixdns -f +``` +
+ +
+OpenRC (Gentoo, Alpine Linux) + +Installed to: `/etc/init.d/kixdns` + +```sh +#!/sbin/openrc-run + +name="KixDNS DNS Server" +description="High-performance async DNS server" +command="/usr/local/bin/kixdns" +command_args="run --config /etc/kixdns/pipeline.json --listener-label default" +command_user="nobody:nogroup" +pidfile="/run/${RC_SVCNAME}.pid" +command_background=false + +depend() { + need net + use dns logger +} +``` + +```bash +# Manual commands +rc-update add kixdns default +rc-service kixdns start +rc-service kixdns status +``` +
+ +
+Procd (OpenWrt) + +Installed to: `/etc/init.d/kixdns` + +```sh +#!/bin/sh /etc/rc.common + +USE_PROCD=1 + +start_service() { + procd_open_instance + procd_set_param command "/usr/local/bin/kixdns" + procd_append_param command run --config /etc/kixdns/pipeline.json --listener-label default + procd_set_param user nobody + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "kixdns" +} +``` + +```bash +# Manual commands +/etc/init.d/kixdns enable +/etc/init.d/kixdns start +/etc/init.d/kixdns status +``` +
+ +
+BSD rc.d (FreeBSD) + +Installed to: `/usr/local/etc/rc.d/kixdns` + +```sh +#!/bin/sh +# +# PROVIDE: kixdns +# REQUIRE: NETWORKING SERVERS +# KEYWORD: shutdown + +. /etc/rc.subr + +name="kixdns" +rcvar="kixdns_enable" + +load_rc_config $name + +: ${kixdns_enable:="NO"} +: ${kixdns_config:="/usr/local/etc/kixdns/pipeline.json"} +: ${kixdns_listener_label:="default"} +: ${kixdns_user:="nobody"} + +command="/usr/local/bin/kixdns" +command_args="run --config ${kixdns_config} --listener-label ${kixdns_listener_label}" +command_user="${kixdns_user}" +pidfile="/var/run/kixdns.pid" + +run_rc_command "$1" +``` + +```bash +# Manual commands +echo 'kixdns_enable="YES"' >> /etc/rc.conf +service kixdns start +service kixdns status +``` +
+ +
+Windows (SCM — Service Control Manager) + +```bash +# Install (Administrator PowerShell) +kixdns.exe service install --config config/pipeline.json + +# Start +sc start KixDNS + +# Query status +sc query KixDNS + +# Stop +sc stop KixDNS + +# Uninstall (Administrator PowerShell) +kixdns.exe service uninstall ``` +
## Configuration diff --git a/README.zh-CN.md b/README.zh-CN.md index 80f6f7f..9010351 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -90,32 +90,211 @@ OPTIONS: -V, --version 显示版本 ``` -### systemd 服务 +### 服务管理(跨平台) -创建 `/etc/systemd/system/kixdns.service`: +KixDNS 内置服务管理功能,支持所有主流 init 系统。 +`service` 子命令在 Windows、Linux 和 FreeBSD 上均可使用。 + +#### 快速命令 + +```bash +# 安装为系统服务(自动检测 init 系统) +sudo ./target/release/kixdns service install + +# 使用自定义配置安装 +sudo ./target/release/kixdns service install \ + --config /etc/kixdns/pipeline.json \ + --listener-label edge-internal + +# 卸载服务 +sudo ./target/release/kixdns service uninstall + +# 以服务方式运行(供 init 系统内部调用) +kixdns service run +``` + +> **注意**:安装和卸载需要 root/管理员权限。`service run` 由 init 系统自动调用。 + +#### 自动检测逻辑 + +init 系统按如下顺序自动检测: + +| 检测条件 | 判定结果 | +|----------|----------| +| `#[cfg(target_os = "windows")]` | Windows SCM | +| `/sbin/procd` 存在 | Procd (OpenWrt) | +| `/usr/lib/systemd/systemd` 或 `/lib/systemd/systemd` 存在 | systemd | +| `/sbin/openrc-run` 存在 | OpenRC | +| `#[cfg(target_os = "freebsd")]` | BSD rc.d | +| 以上均不匹配 | 未知(需要手动配置) | + +#### 服务脚本 + +
+systemd(Linux — 大多数发行版) + +安装到:`/etc/systemd/system/kixdns.service` ```ini [Unit] -Description=KixDNS +Description=KixDNS DNS Server +Documentation=https://github.com/kixdns/kixdns After=network.target +Wants=nss-lookup.target [Service] Type=simple -ExecStart=/usr/local/bin/kixdns --config /etc/kixdns/pipeline.json +ExecStart=/usr/local/bin/kixdns run \ + --config /etc/kixdns/pipeline.json \ + --listener-label default Restart=on-failure +RestartSec=5 +User=nobody +Group=nogroup +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true LimitNOFILE=65536 +PrivateTmp=true +ProtectSystem=full +ProtectHome=true [Install] WantedBy=multi-user.target ``` ```bash -sudo install -m 0755 target/release/kixdns /usr/local/bin/kixdns -sudo mkdir -p /etc/kixdns -sudo cp config/pipeline.json /etc/kixdns/ -sudo systemctl daemon-reload -sudo systemctl enable --now kixdns +# 手动命令 +systemctl daemon-reload +systemctl enable --now kixdns +systemctl status kixdns +journalctl -u kixdns -f +``` +
+ +
+OpenRC(Gentoo、Alpine Linux) + +安装到:`/etc/init.d/kixdns` + +```sh +#!/sbin/openrc-run + +name="KixDNS DNS Server" +description="High-performance async DNS server" +command="/usr/local/bin/kixdns" +command_args="run --config /etc/kixdns/pipeline.json --listener-label default" +command_user="nobody:nogroup" +pidfile="/run/${RC_SVCNAME}.pid" +command_background=false + +depend() { + need net + use dns logger +} +``` + +```bash +# 手动命令 +rc-update add kixdns default +rc-service kixdns start +rc-service kixdns status +``` +
+ +
+Procd(OpenWrt) + +安装到:`/etc/init.d/kixdns` + +```sh +#!/bin/sh /etc/rc.common + +USE_PROCD=1 + +start_service() { + procd_open_instance + procd_set_param command "/usr/local/bin/kixdns" + procd_append_param command run --config /etc/kixdns/pipeline.json --listener-label default + procd_set_param user nobody + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "kixdns" +} +``` + +```bash +# 手动命令 +/etc/init.d/kixdns enable +/etc/init.d/kixdns start +/etc/init.d/kixdns status +``` +
+ +
+BSD rc.d(FreeBSD) + +安装到:`/usr/local/etc/rc.d/kixdns` + +```sh +#!/bin/sh +# +# PROVIDE: kixdns +# REQUIRE: NETWORKING SERVERS +# KEYWORD: shutdown + +. /etc/rc.subr + +name="kixdns" +rcvar="kixdns_enable" + +load_rc_config $name + +: ${kixdns_enable:="NO"} +: ${kixdns_config:="/usr/local/etc/kixdns/pipeline.json"} +: ${kixdns_listener_label:="default"} +: ${kixdns_user:="nobody"} + +command="/usr/local/bin/kixdns" +command_args="run --config ${kixdns_config} --listener-label ${kixdns_listener_label}" +command_user="${kixdns_user}" +pidfile="/var/run/kixdns.pid" + +run_rc_command "$1" +``` + +```bash +# 手动命令 +echo 'kixdns_enable="YES"' >> /etc/rc.conf +service kixdns start +service kixdns status +``` +
+ +
+Windows(SCM — 服务控制管理器) + +```bash +# 安装(管理员 PowerShell) +kixdns.exe service install --config config/pipeline.json + +# 启动 +sc start KixDNS + +# 查询状态 +sc query KixDNS + +# 停止 +sc stop KixDNS + +# 卸载(管理员 PowerShell) +kixdns.exe service uninstall ``` +
## 配置 diff --git a/src/lib.rs b/src/lib.rs index c1b5ccc..5f6d46e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,897 @@ pub mod cache; pub mod config; pub mod engine; +pub mod error_utils; pub mod lock; pub mod matcher; pub mod proto_utils; -pub mod watcher; +pub mod service; pub mod socket_utils; -pub mod error_utils; +pub mod watcher; + +// ============================================================================ +// Re-exports & shared server logic +// ============================================================================ + +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream, UdpSocket}; +use tokio::sync::oneshot; +use tracing::{error, info, debug, warn}; +use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; + +use crate::config::load_config; +use crate::engine::{Engine, FastPathResponse}; +use crate::matcher::RuntimePipelineConfig; + +// ============================================================================ +// run_dns_server — console mode (no external shutdown signal) +// ============================================================================ + +/// 运行 DNS 服务器(控制台模式)/ Run DNS server (console mode) +pub async fn run_dns_server( + config: PathBuf, + listener_label: String, + debug: bool, + udp_workers_count: usize, +) -> anyhow::Result<()> { + init_tracing(debug); + + // Install default CryptoProvider for rustls + // 安装 rustls 的默认 CryptoProvider + // This is required for rustls 0.23+ when multiple crypto backends are available + // 当有多个加密后端可用时,rustls 0.23+ 需要此调用 + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install rustls crypto provider"); + + let cfg = load_config(&config).context("load initial config")?; + let cfg = RuntimePipelineConfig::from_config(cfg).context("compile matchers")?; + let bind_addr: SocketAddr = cfg.settings.bind_udp.parse().context("parse bind addr")?; + let bind_tcp: SocketAddr = cfg.settings.bind_tcp.parse().context("parse tcp bind addr")?; + + let engine = Engine::new(cfg, listener_label.clone()); + + watcher::spawn(config.clone(), engine.clone()); + + // UDP worker 数量:默认为 CPU 核心数,最少 1 个 / UDP worker count: defaults to CPU core count, minimum 1 + let udp_workers_final = if udp_workers_count > 0 { + udp_workers_count + } else { + num_cpus::get() + }; + + info!(bind_udp = %bind_addr, bind_tcp = %bind_tcp, udp_workers_count = udp_workers_final, "dns server started"); + + let mut all_handles: Vec> = Vec::new(); + + #[cfg(unix)] + { + // ✅ OpenBSD 兼容性方案:双 socket(IPv4 + IPv6)+ 零拷贝 recv_buf_from + // ✅ OpenBSD compatibility: dual sockets (IPv4 + IPv6) + zero-copy recv_buf_from + // 为每个地址族创建独立的 socket 和 workers,避免 sockaddr 大小断言失败 + // Create separate sockets and workers for each address family to avoid sockaddr size assertion failures + + // 根据配置地址决定创建哪种 socket / Determine which socket type to create based on config + // IPv6 unspecified address (::) 需要同时创建 IPv4 和 IPv6 socket + // IPv6 other addresses 只创建 IPv6 socket + // IPv4 addresses 只创建 IPv4 socket + let needs_ipv4 = bind_addr.is_ipv4() + || (bind_addr.is_ipv6() && bind_addr.ip().is_unspecified()); + let needs_ipv6 = bind_addr.is_ipv6(); + + if needs_ipv4 { + let workers_per_family = if needs_ipv6 { + udp_workers_final.div_ceil(2) + } else { + udp_workers_final + }; + spawn_ipv4_udp_workers(bind_addr, workers_per_family, engine.clone(), &mut all_handles)?; + } + + if needs_ipv6 { + let workers_per_family = if needs_ipv4 { + udp_workers_final.div_ceil(2) + } else { + udp_workers_final + }; + spawn_ipv6_udp_workers(bind_addr, workers_per_family, engine.clone(), &mut all_handles)?; + } + } + + #[cfg(not(unix))] + { + // Non-Unix: create a single shared socket and spawn workers that share it / 非 Unix:创建单个共享套接字并生成共享它的工作线程 + // Use socket2 to set buffer sizes / 使用 socket2 设置缓冲区大小 + use socket2::{Domain, Protocol, Socket, Type}; + let domain = if bind_addr.is_ipv4() { + Domain::IPV4 + } else { + Domain::IPV6 + }; + let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)).context("create socket")?; + + // ✅ Windows 上设置 IPV6_V6ONLY=0 以支持双栈,与 Linux 行为一致 + // ✅ On Windows, set IPV6_V6ONLY=0 for dual-stack support, consistent with Linux behavior + if domain == Domain::IPV6 { + if let Err(e) = socket.set_only_v6(false) { + debug!( + "failed to set IPV6_V6ONLY=0: {}, IPv4 may not work on [::] bind", + e + ); + } else { + info!("UDP IPv6 socket set to dual-stack mode (IPV6_V6ONLY=0)"); + } + } + + // Set buffer sizes to prevent packet loss under load + // Try 4MB first, then fall back to 1MB if it fails + let desired_size = 4 * 1024 * 1024; + let fallback_size = 1024 * 1024; + + if let Err(e) = socket.set_recv_buffer_size(desired_size) { + debug!( + "failed to set udp recv buffer to {} bytes: {}, trying {}", + desired_size, e, fallback_size + ); + let _ = socket.set_recv_buffer_size(fallback_size); + } + if let Err(e) = socket.set_send_buffer_size(desired_size) { + debug!( + "failed to set udp send buffer to {} bytes: {}, trying {}", + desired_size, e, fallback_size + ); + let _ = socket.set_send_buffer_size(fallback_size); + } + + socket.set_nonblocking(true).context("set nonblocking")?; + socket.bind(&bind_addr.into()).context("bind socket")?; + + let udp_socket = Arc::new(UdpSocket::from_std(socket.into()).context("from_std")?); + for worker_id in 0..udp_workers_final { + let engine = engine.clone(); + let socket = Arc::clone(&udp_socket); + let handle = tokio::spawn(async move { + if let Err(err) = run_udp_worker(worker_id, socket, engine).await { + error!(worker_id, error = %err, "udp worker exited"); + } + }); + all_handles.push(handle); + } + } + + // TCP listener / TCP 监听器 + // ✅ 双 socket 方案,与 UDP 行为一致 / Dual-socket approach, consistent with UDP + let needs_ipv4_tcp = bind_tcp.is_ipv4() + || (bind_tcp.is_ipv6() && bind_tcp.ip().is_unspecified()); + + // --- 启动 IPv4 TCP 监听 / Start IPv4 TCP listener --- + if needs_ipv4_tcp { + let addr = if bind_tcp.is_ipv4() { + bind_tcp + } else { + SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), + bind_tcp.port(), + ) + }; + // 纯 IPv4 绑定,不受 bindv6only 影响 / Pure IPv4 bind, unaffected by bindv6only + let listener = TcpListener::bind(addr).await.context("bind ipv4 tcp")?; + let engine = engine.clone(); + let h = tokio::spawn(async move { + if let Err(err) = run_tcp(listener, engine).await { + error!(error = %err, "ipv4 tcp server exited"); + } + }); + all_handles.push(h); + } + + // --- 启动 IPv6 TCP 监听 / Start IPv6 TCP listener --- + if bind_tcp.is_ipv6() { + use socket2::{Domain, Protocol, Socket, Type}; + let socket = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?; + + // ⭐️ 核心:强制 IPV6_V6ONLY=1,避免和 IPv4 监听器冲突 + // ⭐️ Key: force IPV6_V6ONLY=1 to avoid conflict with IPv4 listener + socket.set_only_v6(true).context("set ipv6 only for kixdns")?; + socket.set_reuse_address(true)?; + + socket.bind(&bind_tcp.into()).context("bind ipv6 tcp socket")?; + socket.listen(128)?; + socket.set_nonblocking(true)?; + + let listener = TcpListener::from_std(socket.into())?; + let engine = engine.clone(); + let h = tokio::spawn(async move { + if let Err(err) = run_tcp(listener, engine).await { + error!(error = %err, "ipv6 tcp server exited"); + } + }); + all_handles.push(h); + } + + // 等待所有任务 / Wait for all tasks + for h in all_handles { + let _ = h.await; + } + + Ok(()) +} + +// ============================================================================ +// run_dns_server_with_shutdown — service mode (can be stopped via oneshot) +// ============================================================================ + +/// 运行 DNS 服务器并支持通过 oneshot 通道优雅关闭 +/// Run DNS server with graceful shutdown via a oneshot channel +/// +/// This function is called by the service module when running as a Windows +/// service. The `shutdown_rx` receiver is signalled when SCM requests stop. +/// 此函数在作为 Windows 服务运行时由 service 模块调用。 +/// 当 SCM 请求停止时,`shutdown_rx` 接收器会收到信号。 +pub async fn run_dns_server_with_shutdown( + config: PathBuf, + listener_label: String, + shutdown_rx: oneshot::Receiver<()>, +) -> anyhow::Result<()> { + // Init tracing — no debug flag, use env var or default to info + // 初始化 tracing —— 无 debug 标志,使用环境变量或默认 info 级别 + init_tracing(false); + + let cfg = load_config(&config).context("load initial config")?; + let cfg = RuntimePipelineConfig::from_config(cfg).context("compile matchers")?; + let bind_addr: SocketAddr = cfg.settings.bind_udp.parse().context("parse bind addr")?; + let bind_tcp: SocketAddr = cfg.settings.bind_tcp.parse().context("parse tcp bind addr")?; + + let engine = Engine::new(cfg, listener_label.clone()); + + watcher::spawn(config.clone(), engine.clone()); + + let udp_workers_final = num_cpus::get().max(1); + + info!( + bind_udp = %bind_addr, + bind_tcp = %bind_tcp, + udp_workers_count = udp_workers_final, + "dns server started (service mode)" + ); + + let mut all_handles: Vec> = Vec::new(); + + #[cfg(unix)] + { + let needs_ipv4 = bind_addr.is_ipv4() + || (bind_addr.is_ipv6() && bind_addr.ip().is_unspecified()); + let needs_ipv6 = bind_addr.is_ipv6(); + + if needs_ipv4 { + let workers_per_family = if needs_ipv6 { + udp_workers_final.div_ceil(2) + } else { + udp_workers_final + }; + spawn_ipv4_udp_workers(bind_addr, workers_per_family, engine.clone(), &mut all_handles)?; + } + + if needs_ipv6 { + let workers_per_family = if needs_ipv4 { + udp_workers_final.div_ceil(2) + } else { + udp_workers_final + }; + spawn_ipv6_udp_workers(bind_addr, workers_per_family, engine.clone(), &mut all_handles)?; + } + } + + #[cfg(not(unix))] + { + use socket2::{Domain, Protocol, Socket, Type}; + let domain = if bind_addr.is_ipv4() { + Domain::IPV4 + } else { + Domain::IPV6 + }; + let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)).context("create socket")?; + + if domain == Domain::IPV6 { + if let Err(e) = socket.set_only_v6(false) { + debug!("failed to set IPV6_V6ONLY=0: {}, IPv4 may not work on [::] bind", e); + } else { + info!("UDP IPv6 socket set to dual-stack mode (IPV6_V6ONLY=0)"); + } + } + + let desired_size = 4 * 1024 * 1024; + let fallback_size = 1024 * 1024; + + if let Err(e) = socket.set_recv_buffer_size(desired_size) { + debug!("failed to set udp recv buffer to {} bytes: {}, trying {}", desired_size, e, fallback_size); + let _ = socket.set_recv_buffer_size(fallback_size); + } + if let Err(e) = socket.set_send_buffer_size(desired_size) { + debug!("failed to set udp send buffer to {} bytes: {}, trying {}", desired_size, e, fallback_size); + let _ = socket.set_send_buffer_size(fallback_size); + } + + socket.set_nonblocking(true).context("set nonblocking")?; + socket.bind(&bind_addr.into()).context("bind socket")?; + + let udp_socket = Arc::new(UdpSocket::from_std(socket.into()).context("from_std")?); + for worker_id in 0..udp_workers_final { + let engine = engine.clone(); + let socket = Arc::clone(&udp_socket); + let handle = tokio::spawn(async move { + if let Err(err) = run_udp_worker(worker_id, socket, engine).await { + error!(worker_id, error = %err, "udp worker exited"); + } + }); + all_handles.push(handle); + } + } + + // TCP listener / TCP 监听器 + let needs_ipv4_tcp = bind_tcp.is_ipv4() + || (bind_tcp.is_ipv6() && bind_tcp.ip().is_unspecified()); + + if needs_ipv4_tcp { + let addr = if bind_tcp.is_ipv4() { + bind_tcp + } else { + SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), + bind_tcp.port(), + ) + }; + let listener = TcpListener::bind(addr).await.context("bind ipv4 tcp")?; + let engine = engine.clone(); + let h = tokio::spawn(async move { + if let Err(err) = run_tcp(listener, engine).await { + error!(error = %err, "ipv4 tcp server exited"); + } + }); + all_handles.push(h); + } + + if bind_tcp.is_ipv6() { + use socket2::{Domain, Protocol, Socket, Type}; + let socket = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?; + socket.set_only_v6(true).context("set ipv6 only for kixdns")?; + socket.set_reuse_address(true)?; + socket.bind(&bind_tcp.into()).context("bind ipv6 tcp socket")?; + socket.listen(128)?; + socket.set_nonblocking(true)?; + let listener = TcpListener::from_std(socket.into())?; + let engine = engine.clone(); + let h = tokio::spawn(async move { + if let Err(err) = run_tcp(listener, engine).await { + error!(error = %err, "ipv6 tcp server exited"); + } + }); + all_handles.push(h); + } + + // Wait for shutdown signal OR all tasks to complete + // 等待关闭信号或所有任务完成 + tokio::select! { + _ = shutdown_rx => { + info!("Service shutdown signal received, stopping server..."); + // Tasks will be dropped when we return, so no need to explicitly cancel + // 返回时任务会被自动丢弃,无需显式取消 + } + _ = async { + for h in &mut all_handles { + let _ = h.await; + } + } => { + info!("All server tasks completed (unexpected)"); + } + } + + Ok(()) +} + +// ============================================================================ +// Tracing initialisation +// ============================================================================ + +fn init_tracing(debug: bool) { + // 默认仅保留错误日志以平衡性能与可观测性,除非显式指定 + // Default to error-level logging to balance performance with observability unless explicitly enabled + let fmt_layer = fmt::layer() + .with_target(false) + .with_ansi(false) + .with_level(debug); + + let level = if debug { "debug" } else { "error" }; + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)); + tracing_subscriber::registry() + .with(filter) + .with(fmt_layer) + .init(); +} + +// ============================================================================ +// UDP worker spawning (Unix — SO_REUSEPORT) +// ============================================================================ + +// 为 IPv4 地址创建并启动 UDP workers / Create and spawn UDP workers for IPv4 address +#[cfg(unix)] +fn spawn_ipv4_udp_workers( + bind_addr: SocketAddr, + worker_count: usize, + engine: Engine, + all_handles: &mut Vec>, +) -> anyhow::Result<()> { + let ipv4_addr: SocketAddr = if bind_addr.is_ipv4() { + bind_addr + } else { + // 预编译的常量地址,避免 unwrap / Precompiled constant address, avoid unwrap + // 使用配置中的端口号而非硬编码 / Use port from config instead of hardcoded + SocketAddr::new( + std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), + bind_addr.port(), + ) + }; + + info!(bind_addr = %ipv4_addr, workers = worker_count, "Starting IPv4 UDP workers"); + + for worker_id in 0..worker_count { + let engine = engine.clone(); + let std_socket = create_reuseport_udp_socket(ipv4_addr) + .with_context(|| format!("create ipv4 udp socket for worker {}", worker_id))?; + let socket = UdpSocket::from_std(std_socket)?; + let handle = tokio::spawn(async move { + if let Err(err) = run_udp_worker(worker_id, Arc::new(socket), engine).await { + error!(worker_id, error = %err, "IPv4 udp worker exited"); + } + }); + all_handles.push(handle); + } + + Ok(()) +} + +// 为 IPv6 地址创建并启动 UDP workers / Create and spawn UDP workers for IPv6 address +#[cfg(unix)] +fn spawn_ipv6_udp_workers( + bind_addr: SocketAddr, + worker_count: usize, + engine: Engine, + all_handles: &mut Vec>, +) -> anyhow::Result<()> { + let ipv6_addr: SocketAddr = if bind_addr.is_ipv6() { + bind_addr + } else { + // 预编译的常量地址,避免 unwrap / Precompiled constant address, avoid unwrap + // 使用配置中的端口号而非硬编码 / Use port from config instead of hardcoded + SocketAddr::new( + std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), + bind_addr.port(), + ) + }; + + info!(bind_addr = %ipv6_addr, workers = worker_count, "Starting IPv6 UDP workers"); + + for worker_id in 0..worker_count { + let engine = engine.clone(); + let std_socket = create_reuseport_udp_socket(ipv6_addr) + .with_context(|| format!("create ipv6 udp socket for worker {}", worker_id))?; + let socket = UdpSocket::from_std(std_socket)?; + let handle = tokio::spawn(async move { + if let Err(err) = run_udp_worker(worker_id, Arc::new(socket), engine).await { + error!(worker_id, error = %err, "IPv6 udp worker exited"); + } + }); + all_handles.push(handle); + } + + Ok(()) +} + +// 在 Unix 上创建带 SO_REUSEPORT 的 UDP socket;非 Unix 使用标准绑定 / Create UDP socket with SO_REUSEPORT on Unix; use standard binding on non-Unix +#[cfg(unix)] +fn create_reuseport_udp_socket(addr: SocketAddr) -> anyhow::Result { + use socket2::{Domain, Protocol, Socket, Type}; + let domain = if addr.is_ipv4() { + Domain::IPV4 + } else { + Domain::IPV6 + }; + let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?; + socket.set_reuse_address(true)?; + + // ✅ OpenBSD/FreeBSD 安全措施:为 IPv6 socket 显式设置 IPV6_V6ONLY=1 + // ✅ OpenBSD/FreeBSD safety: explicitly set IPV6_V6ONLY=1 for IPv6 sockets + // 双 socket 方案下,IPv6 socket 只处理 IPv6 流量,确保地址族一致性,避免 sockaddr 大小断言失败 + // With dual-socket approach, IPv6 socket only handles IPv6 traffic, ensuring address family consistency + // 这使得我们可以安全地使用零拷贝的 recv_buf_from + // This allows us to safely use zero-copy recv_buf_from + if domain == Domain::IPV6 { + if let Err(e) = crate::socket_utils::set_ipv6_v6only(&socket, true) { + tracing::warn!( + "Failed to set IPV6_V6ONLY=1: {}, this may cause issues on OpenBSD", + e + ); + } + } + + // Try to set SO_REUSEPORT via safe wrapper / 尝试通过安全封装设置 SO_REUSEPORT + if let Err(e) = crate::socket_utils::set_reuseport(&socket, true) { + // Log warning if SO_REUSEPORT fails / SO_REUSEPORT 失败时记录警告 + tracing::warn!("SO_REUSEPORT failed: {}, falling back to shared socket", e); + } + + // Set buffer sizes to prevent packet loss under load + // Try 4MB first, then fall back to 1MB if it fails + let desired_size = 4 * 1024 * 1024; + let fallback_size = 1024 * 1024; + + if let Err(e) = socket.set_recv_buffer_size(desired_size) { + debug!( + "failed to set udp recv buffer to {} bytes: {}, trying {}", + desired_size, e, fallback_size + ); + let _ = socket.set_recv_buffer_size(fallback_size); + } + if let Err(e) = socket.set_send_buffer_size(desired_size) { + debug!( + "failed to set udp send buffer to {} bytes: {}, trying {}", + desired_size, e, fallback_size + ); + let _ = socket.set_send_buffer_size(fallback_size); + } + + socket.set_nonblocking(true)?; + socket.bind(&addr.into())?; + Ok(socket.into()) +} + +// ============================================================================ +// UDP worker +// ============================================================================ + +/// 高性能 UDP worker:直接在接收循环中处理请求,避免 spawn 开销 / High-performance UDP worker: process requests directly in receive loop, avoiding spawn overhead +async fn run_udp_worker( + worker_id: usize, + socket: Arc, + engine: Engine, +) -> anyhow::Result<()> { + // 预分配缓冲区 / Pre-allocate buffer + // 使用 BytesMut 避免 Bytes::copy_from_slice 的内存分配 / Use BytesMut to avoid memory allocation in Bytes::copy_from_slice + use bytes::BytesMut; + let mut buf = BytesMut::with_capacity(4096); + // 复用发送缓冲区:用于缓存命中时 patch TXID,避免每包堆分配 / Reuse send buffer to patch TXID on cache hits, avoiding per-packet heap allocation + let mut send_buf = BytesMut::with_capacity(512); + + // 自适应流控:每 100 个请求检查一次是否需要调整 permits + // Adaptive flow control: check if adjustment needed every 100 requests + let mut request_count = 0u32; + + info!(worker_id, "UDP worker started"); + + loop { + // 确保有足够的空间 / Ensure sufficient space + if buf.capacity() < 4096 { + buf.reserve(4096 - buf.len()); + } + + // ✅ 使用 tokio 的 recv_buf_from 配合 BytesMut,实现零拷贝的高性能接收 + // ✅ Use tokio's recv_buf_from with BytesMut for zero-copy high-performance reception + // 由于使用双 socket 方案(IPv4 + IPv6 分离),不会出现混合地址族的 sockaddr 问题 + // Since we use dual-socket approach (IPv4 + IPv6 separated), no mixed address family sockaddr issues + match socket.recv_buf_from(&mut buf).await { + Ok((_len, peer)) => { + // 零拷贝获取 Bytes / Zero-copy obtain Bytes + let packet_bytes = buf.split().freeze(); + + // 每 100 个请求检查一次流控调整 / Check flow control adjustment every 100 requests + request_count += 1; + if request_count >= 100 { + request_count = 0; + engine.adjust_flow_control(); // Now synchronous with atomic CAS + } + + // ✅ 优化:使用 handle_packet_fast 避免重复解析 + // ✅ Optimization: Use handle_packet_fast to avoid re-parsing + // 如果缓存命中,直接返回;如果缓存未命中,返回预解析的数据 + // If cache hit, return directly; if cache miss, return pre-parsed data + match engine.handle_packet_fast(&packet_bytes, peer) { + Ok(Some(FastPathResponse::Direct(bytes))) => { + // 已包含正确 TXID,可直接发送 / Already contains correct TXID + let _ = socket.send_to(&bytes, peer).await; + } + Ok(Some(FastPathResponse::CacheHit { + cached, + tx_id, + inserted_at, + })) => { + // 复用 send_buf:copy + patch TXID / Reuse send_buf: copy + patch TXID + send_buf.clear(); + if send_buf.capacity() < cached.len() { + send_buf.reserve(cached.len() - send_buf.capacity()); + } + send_buf.extend_from_slice(&cached); + + // RFC 1035 §5.2: Patch TTL based on residence time / 根据停留时间修正 TTL + let elapsed = inserted_at.elapsed().as_secs() as u32; + if elapsed > 0 { + crate::proto_utils::patch_all_ttls(&mut send_buf, elapsed); + } + + if send_buf.len() >= 2 { + let id_bytes = tx_id.to_be_bytes(); + send_buf[0] = id_bytes[0]; + send_buf[1] = id_bytes[1]; + } + let _ = socket.send_to(&send_buf, peer).await; + } + Ok(Some(FastPathResponse::AsyncNeeded { + qname, + qtype, + qclass, + tx_id, + edns_present, + pipeline_id, + })) => { + // 缓存未命中,使用预解析的数据避免重复解析 + // Cache miss, use pre-parsed data to avoid re-parsing + let permit_mgr = Arc::clone(&engine.permit_manager); + let timeout_ms = engine.get_request_timeout_ms(); + let timeout_dur = Duration::from_millis(timeout_ms); + + // 非阻塞式 try_acquire,避免在接收循环中 await / Non-blocking try_acquire to avoid await in receive loop + if let Some(permit) = permit_mgr.try_acquire() { + let engine = engine.clone(); + let socket = Arc::clone(&socket); + let packet_bytes = packet_bytes.clone(); + tokio::spawn(async move { + let _permit = permit; // 自动释放 / Auto-release on drop + + // ✅ 传递预解析数据给 handle_packet_internal,避免重复解析 + // ✅ Pass pre-parsed data to handle_packet_internal to avoid re-parsing + match tokio::time::timeout( + timeout_dur, + engine.handle_packet_internal_with_pre_parsed( + &packet_bytes, + peer, + false, + qname, + qtype, + qclass, + tx_id, + edns_present, + pipeline_id, + ), + ) + .await + { + Ok(Ok(resp)) => { + let _ = socket.send_to(&resp, peer).await; + } + Ok(Err(e)) => { + debug!(error = %e, "handle_packet error"); + } + Err(_) => { + warn!( + timeout_ms, + upstream_timeout_ms = engine.get_upstream_timeout_ms(), + "request timeout after hedge and fallback exhausted" + ); + } + } + }); + } + } + Ok(None) => { + // 快速解析失败,回退到完整处理 + // Fast parse failed, fallback to full processing + let permit_mgr = Arc::clone(&engine.permit_manager); + let timeout_ms = engine.get_request_timeout_ms(); + let timeout_dur = Duration::from_millis(timeout_ms); + + // 非阻塞式 try_acquire,避免在接收循环中 await / Non-blocking try_acquire to avoid await in receive loop + if let Some(permit) = permit_mgr.try_acquire() { + let engine = engine.clone(); + let socket = Arc::clone(&socket); + let packet_bytes = packet_bytes.clone(); + tokio::spawn(async move { + let _permit = permit; // 自动释放 / Auto-release on drop + match tokio::time::timeout( + timeout_dur, + engine.handle_packet(&packet_bytes, peer), + ) + .await + { + Ok(Ok(resp)) => { + let _ = socket.send_to(&resp, peer).await; + } + Ok(Err(e)) => { + debug!(error = %e, "handle_packet error"); + } + Err(_) => { + warn!( + timeout_ms, + upstream_timeout_ms = engine.get_upstream_timeout_ms(), + "request timeout" + ); + } + } + }); + } + } + Err(_) => { + // 解析错误,忽略 / Parse error, ignore + } + } + } + Err(e) => { + // 接收错误,清除缓冲区并继续 / Receive error, clear buffer and continue + debug!(error = %e, "UDP recv error"); + buf.clear(); + } + } + } +} + +// ============================================================================ +// TCP +// ============================================================================ + +async fn run_tcp(listener: TcpListener, engine: Engine) -> anyhow::Result<()> { + loop { + let (stream, peer) = listener.accept().await?; + let engine = engine.clone(); + tokio::spawn(async move { + let _ = handle_tcp_conn(stream, peer, engine).await; + }); + } +} + +async fn handle_tcp_conn( + mut stream: TcpStream, + peer: SocketAddr, + engine: Engine, +) -> anyhow::Result<()> { + const MAX_TCP_FRAME: usize = 64 * 1024; + let mut len_buf = [0u8; 2]; + + // ✅ 获取整体请求超时(包含 hedge + TCP fallback) + // ✅ Get overall request timeout (including hedge + TCP fallback) + let timeout_ms = engine.get_request_timeout_ms(); + + // Reusable buffer to avoid per-frame heap allocation / 可复用缓冲区,避免每帧堆分配 + // 使用 BytesMut 以支持零拷贝操作 / Use BytesMut for zero-copy operations + let mut buf = bytes::BytesMut::with_capacity(MAX_TCP_FRAME); + + loop { + if let Err(err) = stream.read_exact(&mut len_buf).await { + if err.kind() != std::io::ErrorKind::UnexpectedEof { + return Err(err.into()); + } + return Ok(()); + } + let frame_len = u16::from_be_bytes(len_buf) as usize; + if frame_len == 0 || frame_len > MAX_TCP_FRAME { + return Ok(()); + } + + // Reuse buffer: resize to exact frame length / 复用缓冲区:调整到精确帧长度 + // resize() is safe and efficient - it only initializes new bytes if growing + buf.clear(); + buf.resize(frame_len, 0); + if stream.read_exact(&mut buf).await.is_err() { + return Ok(()); + } + + // ✅ 优化:使用 handle_packet_fast 进行快速路径检查 + // ✅ Optimization: Use handle_packet_fast for fast path check + // 统一 UDP 和 TCP 的行为,避免重复解析 + // Unify UDP and TCP behavior to avoid re-parsing + let packet_bytes = buf.split().freeze(); + let timeout_dur = Duration::from_millis(timeout_ms); + + let resp = match engine.handle_packet_fast(&packet_bytes, peer) { + Ok(Some(FastPathResponse::Direct(bytes))) => { + // 快速路径命中:直接返回 / Fast path hit: return directly + bytes + } + Ok(Some(FastPathResponse::CacheHit { + cached, + tx_id, + inserted_at, + })) => { + // 缓存命中:patch TXID / Cache hit: patch TXID + let mut resp_buf = bytes::BytesMut::with_capacity(cached.len()); + resp_buf.extend_from_slice(&cached); + + // RFC 1035 §5.2: Patch TTL based on residence time / 根据停留时间修正 TTL + let elapsed = inserted_at.elapsed().as_secs() as u32; + if elapsed > 0 { + crate::proto_utils::patch_all_ttls(&mut resp_buf, elapsed); + } + + if resp_buf.len() >= 2 { + let id_bytes = tx_id.to_be_bytes(); + resp_buf[0] = id_bytes[0]; + resp_buf[1] = id_bytes[1]; + } + resp_buf.freeze() + } + Ok(Some(FastPathResponse::AsyncNeeded { + qname, + qtype, + qclass, + tx_id, + edns_present, + pipeline_id, + })) => { + // 缓存未命中:使用预解析数据避免重复解析 + // Cache miss: use pre-parsed data to avoid re-parsing + match tokio::time::timeout( + timeout_dur, + engine.handle_packet_internal_with_pre_parsed( + &packet_bytes, + peer, + false, + qname, + qtype, + qclass, + tx_id, + edns_present, + pipeline_id, + ), + ) + .await + { + Ok(Ok(r)) => r, + Ok(Err(_)) => return Ok(()), + Err(_) => { + warn!( + timeout_ms, + upstream_timeout_ms = engine.get_upstream_timeout_ms(), + "TCP request timeout after hedge and fallback exhausted" + ); + return Ok(()); // 关闭连接 / Close connection + } + } + } + Ok(None) => { + // 快速解析失败,回退到完整处理 + // Fast parse failed, fallback to full processing + match tokio::time::timeout(timeout_dur, engine.handle_packet(&packet_bytes, peer)) + .await + { + Ok(Ok(r)) => r, + Ok(Err(_)) => return Ok(()), + Err(_) => { + warn!( + timeout_ms, + upstream_timeout_ms = engine.get_upstream_timeout_ms(), + "TCP request timeout" + ); + return Ok(()); // 关闭连接 / Close connection + } + } + } + Err(_) => { + // 解析错误,关闭连接 / Parse error, close connection + return Ok(()); + } + }; + if resp.len() <= u16::MAX as usize { + let len_bytes = (resp.len() as u16).to_be_bytes(); + if stream.write_all(&len_bytes).await.is_err() { + return Ok(()); + } + if stream.write_all(&resp).await.is_err() { + return Ok(()); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 6e167c3..cfe7e93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,12 @@ -use std::net::SocketAddr; use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; use anyhow::Context; use clap::{Parser, Subcommand}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream, UdpSocket}; -use tracing::{error, info, debug, warn}; -use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}; +use tracing::error; -use kixdns::config::load_config; -use kixdns::engine::{Engine, FastPathResponse}; -use kixdns::matcher::RuntimePipelineConfig; -use kixdns::watcher; +// ============================================================================ +// CLI argument definitions +// ============================================================================ #[derive(Parser, Debug)] #[command(author, version, about = "KixDNS async DNS with hot-reload pipelines", long_about = None)] @@ -51,668 +44,128 @@ enum Commands { #[arg(short = 'f', long = "filter")] filter: Option, }, + /// Manage system service (install/uninstall/run) / 系统服务管理(安装/卸载/运行) + Service { + #[command(subcommand)] + action: ServiceAction, + }, } -#[tokio::main] -async fn main() -> anyhow::Result<()> { +#[derive(Subcommand, Debug)] +enum ServiceAction { + /// Install KixDNS as a system service / 安装为系统服务 + Install { + /// 配置文件路径(JSON) / Config file path (JSON) + #[arg(short = 'c', long = "config", default_value = "config/pipeline.json")] + config: PathBuf, + /// 监听实例标签 / Listener instance label + #[arg(long = "listener-label", default_value = "default")] + listener_label: String, + }, + /// Uninstall KixDNS system service / 卸载系统服务 + Uninstall, + /// Run as system service (used by init system, not for direct use) / 以服务方式运行(供 init 系统内部使用) + Run { + /// 配置文件路径(JSON) / Config file path (JSON) + #[arg(short = 'c', long = "config", default_value = "config/pipeline.json")] + config: PathBuf, + /// 监听实例标签 / Listener instance label + #[arg(long = "listener-label", default_value = "default")] + listener_label: String, + }, +} + +// ============================================================================ +// Entry point +// ============================================================================ +// +// We do NOT use #[tokio::main] here because the Windows service path must call +// `service_dispatcher::start` from the main thread (it blocks forever). We +// manually create the tokio runtime only when running in console mode. +// +// 我们在此处不使用 #[tokio::main],因为 Windows 服务路径须在主线程调用 +// `service_dispatcher::start`(它会永久阻塞)。仅在控制台模式下才手动创建 tokio 运行时。 +// ============================================================================ +fn main() -> anyhow::Result<()> { let args = Args::parse(); match args.command { - Some(Commands::ConvertGeoIp { input, output, filter }) => { - // Convert GeoIP .dat to MMDB - let filter_countries: Option> = filter - .map(|f| f.split(',').map(|s| s.trim().to_uppercase()).collect()); - - let filter_slice = filter_countries.as_deref(); - - match kixdns::matcher::geoip::GeoIpManager::convert_dat_to_mmdb(&input, &output, filter_slice) { - Ok(stats) => { - println!("Conversion completed successfully:\n{}", stats); - Ok(()) - } - Err(e) => { - error!("Conversion failed: {}", e); - Err(e) - } - } - } - Some(Commands::Run { config, listener_label, debug, udp_workers_count }) => { - run_dns_server(config, listener_label, debug, udp_workers_count).await + // ---- System service management ---- + Some(Commands::Service { action }) => match action { + ServiceAction::Install { + config, + listener_label, + } => kixdns::service::install_service(&config, &listener_label), + ServiceAction::Uninstall => kixdns::service::uninstall_service(), + ServiceAction::Run { + config, + listener_label, + } => { + // Blocks on Windows (SCM dispatcher), sets up signal handlers on Unix. + // Windows:阻塞主线程(SCM 分发); + // Unix:设置信号处理。 + kixdns::service::run_service(config, listener_label) + } + }, + + // ---- Convert GeoIP ---- + Some(Commands::ConvertGeoIp { + input, + output, + filter, + }) => convert_geoip(input, output, filter), + + // ---- Run DNS server (console mode) ---- + Some(Commands::Run { + config, + listener_label, + debug, + udp_workers_count, + }) => { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime")?; + rt.block_on(kixdns::run_dns_server( + config, + listener_label, + debug, + udp_workers_count, + )) } + + // ---- Default: no subcommand → run DNS server ---- None => { - // No subcommand provided - run DNS server with defaults - run_dns_server( + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime")?; + rt.block_on(kixdns::run_dns_server( PathBuf::from("config/pipeline.json"), "default".to_string(), false, 0, - ).await + )) } } } -/// 运行 DNS 服务器 / Run DNS server -/// 提取公共逻辑以消除代码重复 / Extract common logic to eliminate code duplication -async fn run_dns_server( - config: PathBuf, - listener_label: String, - debug: bool, - udp_workers_count: usize, -) -> anyhow::Result<()> { - // Run DNS server - init_tracing(debug); - - // Install default CryptoProvider for rustls - // 安装 rustls 的默认 CryptoProvider - // This is required for rustls 0.23+ when multiple crypto backends are available - // 当有多个加密后端可用时,rustls 0.23+ 需要此调用 - rustls::crypto::ring::default_provider() - .install_default() - .expect("failed to install rustls crypto provider"); - - let cfg = load_config(&config).context("load initial config")?; - let cfg = RuntimePipelineConfig::from_config(cfg).context("compile matchers")?; - let bind_addr: SocketAddr = cfg.settings.bind_udp.parse().context("parse bind addr")?; - let bind_tcp: SocketAddr = cfg - .settings - .bind_tcp - .parse() - .context("parse tcp bind addr")?; - - let engine = Engine::new(cfg, listener_label.clone()); - - watcher::spawn(config.clone(), engine.clone()); - - // UDP worker 数量:默认为 CPU 核心数,最少 1 个 / UDP worker count: defaults to CPU core count, minimum 1 - let udp_workers_final = if udp_workers_count > 0 { - udp_workers_count - } else { - num_cpus::get() - }; - - info!(bind_udp = %bind_addr, bind_tcp = %bind_tcp, udp_workers_count = udp_workers_final, "dns server started"); - - let mut all_handles: Vec> = Vec::new(); - - #[cfg(unix)] - { - // ✅ OpenBSD 兼容性方案:双 socket(IPv4 + IPv6)+ 零拷贝 recv_buf_from - // ✅ OpenBSD compatibility: dual sockets (IPv4 + IPv6) + zero-copy recv_buf_from - // 为每个地址族创建独立的 socket 和 workers,避免 sockaddr 大小断言失败 - // Create separate sockets and workers for each address family to avoid sockaddr size assertion failures - - // ✅ OpenBSD 兼容性方案:双 socket(IPv4 + IPv6)+ 零拷贝 recv_buf_from - // ✅ OpenBSD compatibility: dual sockets (IPv4 + IPv6) + zero-copy recv_buf_from - // 为每个地址族创建独立的 socket 和 workers,避免 sockaddr 大小断言失败 - // Create separate sockets and workers for each address family to avoid sockaddr size assertion failures - - // 根据配置地址决定创建哪种 socket / Determine which socket type to create based on config - // IPv6 unspecified address (::) 需要同时创建 IPv4 和 IPv6 socket - // IPv6 other addresses 只创建 IPv6 socket - // IPv4 addresses 只创建 IPv4 socket - let needs_ipv4 = bind_addr.is_ipv4() || - (bind_addr.is_ipv6() && bind_addr.ip().is_unspecified()); - let needs_ipv6 = bind_addr.is_ipv6(); - - if needs_ipv4 { - let workers_per_family = if needs_ipv6 { - udp_workers_final.div_ceil(2) - } else { - udp_workers_final - }; - spawn_ipv4_udp_workers(bind_addr, workers_per_family, engine.clone(), &mut all_handles)?; - } - - if needs_ipv6 { - let workers_per_family = if needs_ipv4 { - udp_workers_final.div_ceil(2) - } else { - udp_workers_final - }; - spawn_ipv6_udp_workers(bind_addr, workers_per_family, engine.clone(), &mut all_handles)?; - } - } - - #[cfg(not(unix))] - { - // Non-Unix: create a single shared socket and spawn workers that share it / 非 Unix:创建单个共享套接字并生成共享它的工作线程 - // Use socket2 to set buffer sizes / 使用 socket2 设置缓冲区大小 - use socket2::{Domain, Protocol, Socket, Type}; - let domain = if bind_addr.is_ipv4() { - Domain::IPV4 - } else { - Domain::IPV6 - }; - let socket = - Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)).context("create socket")?; - - // ✅ Windows 上设置 IPV6_V6ONLY=0 以支持双栈,与 Linux 行为一致 - // ✅ On Windows, set IPV6_V6ONLY=0 for dual-stack support, consistent with Linux behavior - if domain == Domain::IPV6 { - if let Err(e) = socket.set_only_v6(false) { - debug!("failed to set IPV6_V6ONLY=0: {}, IPv4 may not work on [::] bind", e); - } else { - info!("UDP IPv6 socket set to dual-stack mode (IPV6_V6ONLY=0)"); - } - } - - // Set buffer sizes to prevent packet loss under load - // Try 4MB first, then fall back to 1MB if it fails - let desired_size = 4 * 1024 * 1024; - let fallback_size = 1024 * 1024; - - if let Err(e) = socket.set_recv_buffer_size(desired_size) { - debug!("failed to set udp recv buffer to {} bytes: {}, trying {}", desired_size, e, fallback_size); - let _ = socket.set_recv_buffer_size(fallback_size); - } - if let Err(e) = socket.set_send_buffer_size(desired_size) { - debug!("failed to set udp send buffer to {} bytes: {}, trying {}", desired_size, e, fallback_size); - let _ = socket.set_send_buffer_size(fallback_size); - } - - socket.set_nonblocking(true).context("set nonblocking")?; - socket.bind(&bind_addr.into()).context("bind socket")?; - - let udp_socket = Arc::new(UdpSocket::from_std(socket.into()).context("from_std")?); - for worker_id in 0..udp_workers_final { - let engine = engine.clone(); - let socket = Arc::clone(&udp_socket); - let handle = tokio::spawn(async move { - if let Err(err) = run_udp_worker(worker_id, socket, engine).await { - error!(worker_id, error = %err, "udp worker exited"); - } - }); - all_handles.push(handle); - } - } - - // TCP listener / TCP 监听器 - // ✅ 双 socket 方案,与 UDP 行为一致 / Dual-socket approach, consistent with UDP - let needs_ipv4_tcp = bind_tcp.is_ipv4() || (bind_tcp.is_ipv6() && bind_tcp.ip().is_unspecified()); - - // --- 启动 IPv4 TCP 监听 / Start IPv4 TCP listener --- - if needs_ipv4_tcp { - let addr = if bind_tcp.is_ipv4() { - bind_tcp - } else { - SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), bind_tcp.port()) - }; - // 纯 IPv4 绑定,不受 bindv6only 影响 / Pure IPv4 bind, unaffected by bindv6only - let listener = TcpListener::bind(addr).await.context("bind ipv4 tcp")?; - let engine = engine.clone(); - let h = tokio::spawn(async move { - if let Err(err) = run_tcp(listener, engine).await { - error!(error = %err, "ipv4 tcp server exited"); - } - }); - all_handles.push(h); - } - - // --- 启动 IPv6 TCP 监听 / Start IPv6 TCP listener --- - if bind_tcp.is_ipv6() { - use socket2::{Domain, Protocol, Socket, Type}; - let socket = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))?; - - // ⭐️ 核心:强制 IPV6_V6ONLY=1,避免和 IPv4 监听器冲突 - // ⭐️ Key: force IPV6_V6ONLY=1 to avoid conflict with IPv4 listener - socket.set_only_v6(true).context("set ipv6 only for kixdns")?; - socket.set_reuse_address(true)?; - - socket.bind(&bind_tcp.into()).context("bind ipv6 tcp socket")?; - socket.listen(128)?; - socket.set_nonblocking(true)?; - - let listener = TcpListener::from_std(socket.into())?; - let engine = engine.clone(); - let h = tokio::spawn(async move { - if let Err(err) = run_tcp(listener, engine).await { - error!(error = %err, "ipv6 tcp server exited"); - } - }); - all_handles.push(h); - } - - // 等待所有任务 / Wait for all tasks - for h in all_handles { - let _ = h.await; - } - +// ============================================================================ +// GeoIP conversion (synchronous helper) +// ============================================================================ +fn convert_geoip(input: PathBuf, output: PathBuf, filter: Option) -> anyhow::Result<()> { + let filter_countries: Option> = + filter.map(|f| f.split(',').map(|s| s.trim().to_uppercase()).collect()); + let filter_slice = filter_countries.as_deref(); + + match kixdns::matcher::geoip::GeoIpManager::convert_dat_to_mmdb(&input, &output, filter_slice) { + Ok(stats) => { + println!("Conversion completed successfully:\n{}", stats); Ok(()) -} - -fn init_tracing(debug: bool) { - // 默认仅保留错误日志以平衡性能与可观测性,除非显式指定 - // Default to error-level logging to balance performance with observability unless explicitly enabled - let fmt_layer = fmt::layer() - .with_target(false) - .with_ansi(false) - .with_level(debug); - - let level = if debug { "debug" } else { "error" }; - let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)); - tracing_subscriber::registry() - .with(filter) - .with(fmt_layer) - .init(); -} - -// 为 IPv4 地址创建并启动 UDP workers / Create and spawn UDP workers for IPv4 address -#[cfg(unix)] -fn spawn_ipv4_udp_workers( - bind_addr: SocketAddr, - worker_count: usize, - engine: Engine, - all_handles: &mut Vec>, -) -> anyhow::Result<()> { - let ipv4_addr: SocketAddr = if bind_addr.is_ipv4() { - bind_addr - } else { - // 预编译的常量地址,避免 unwrap / Precompiled constant address, avoid unwrap - // 使用配置中的端口号而非硬编码 / Use port from config instead of hardcoded - SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), bind_addr.port()) - }; - - info!(bind_addr = %ipv4_addr, workers = worker_count, "Starting IPv4 UDP workers"); - - for worker_id in 0..worker_count { - let engine = engine.clone(); - let std_socket = create_reuseport_udp_socket(ipv4_addr) - .with_context(|| format!("create ipv4 udp socket for worker {}", worker_id))?; - let socket = UdpSocket::from_std(std_socket)?; - let handle = tokio::spawn(async move { - if let Err(err) = run_udp_worker(worker_id, Arc::new(socket), engine).await { - error!(worker_id, error = %err, "IPv4 udp worker exited"); - } - }); - all_handles.push(handle); - } - - Ok(()) -} - -// 为 IPv6 地址创建并启动 UDP workers / Create and spawn UDP workers for IPv6 address -#[cfg(unix)] -fn spawn_ipv6_udp_workers( - bind_addr: SocketAddr, - worker_count: usize, - engine: Engine, - all_handles: &mut Vec>, -) -> anyhow::Result<()> { - let ipv6_addr: SocketAddr = if bind_addr.is_ipv6() { - bind_addr - } else { - // 预编译的常量地址,避免 unwrap / Precompiled constant address, avoid unwrap - // 使用配置中的端口号而非硬编码 / Use port from config instead of hardcoded - SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), bind_addr.port()) - }; - - info!(bind_addr = %ipv6_addr, workers = worker_count, "Starting IPv6 UDP workers"); - - for worker_id in 0..worker_count { - let engine = engine.clone(); - let std_socket = create_reuseport_udp_socket(ipv6_addr) - .with_context(|| format!("create ipv6 udp socket for worker {}", worker_id))?; - let socket = UdpSocket::from_std(std_socket)?; - let handle = tokio::spawn(async move { - if let Err(err) = run_udp_worker(worker_id, Arc::new(socket), engine).await { - error!(worker_id, error = %err, "IPv6 udp worker exited"); - } - }); - all_handles.push(handle); - } - - Ok(()) -} - -// 在 Unix 上创建带 SO_REUSEPORT 的 UDP socket;非 Unix 使用标准绑定 / Create UDP socket with SO_REUSEPORT on Unix; use standard binding on non-Unix -#[cfg(unix)] -fn create_reuseport_udp_socket(addr: SocketAddr) -> anyhow::Result { - use socket2::{Domain, Protocol, Socket, Type}; - let domain = if addr.is_ipv4() { - Domain::IPV4 - } else { - Domain::IPV6 - }; - let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?; - socket.set_reuse_address(true)?; - - // ✅ OpenBSD/FreeBSD 安全措施:为 IPv6 socket 显式设置 IPV6_V6ONLY=1 - // ✅ OpenBSD/FreeBSD safety: explicitly set IPV6_V6ONLY=1 for IPv6 sockets - // 双 socket 方案下,IPv6 socket 只处理 IPv6 流量,确保地址族一致性,避免 sockaddr 大小断言失败 - // With dual-socket approach, IPv6 socket only handles IPv6 traffic, ensuring address family consistency - // 这使得我们可以安全地使用零拷贝的 recv_buf_from - // This allows us to safely use zero-copy recv_buf_from - if domain == Domain::IPV6 { - if let Err(e) = kixdns::socket_utils::set_ipv6_v6only(&socket, true) { - tracing::warn!("Failed to set IPV6_V6ONLY=1: {}, this may cause issues on OpenBSD", e); } - } - - // Try to set SO_REUSEPORT via safe wrapper / 尝试通过安全封装设置 SO_REUSEPORT - if let Err(e) = kixdns::socket_utils::set_reuseport(&socket, true) { - // Log warning if SO_REUSEPORT fails / SO_REUSEPORT 失败时记录警告 - tracing::warn!("SO_REUSEPORT failed: {}, falling back to shared socket", e); - } - - // Set buffer sizes to prevent packet loss under load - // Try 4MB first, then fall back to 1MB if it fails - let desired_size = 4 * 1024 * 1024; - let fallback_size = 1024 * 1024; - - if let Err(e) = socket.set_recv_buffer_size(desired_size) { - debug!("failed to set udp recv buffer to {} bytes: {}, trying {}", desired_size, e, fallback_size); - let _ = socket.set_recv_buffer_size(fallback_size); - } - if let Err(e) = socket.set_send_buffer_size(desired_size) { - debug!("failed to set udp send buffer to {} bytes: {}, trying {}", desired_size, e, fallback_size); - let _ = socket.set_send_buffer_size(fallback_size); - } - - socket.set_nonblocking(true)?; - socket.bind(&addr.into())?; - Ok(socket.into()) -} - -/// 高性能 UDP worker:直接在接收循环中处理请求,避免 spawn 开销 / High-performance UDP worker: process requests directly in receive loop, avoiding spawn overhead -async fn run_udp_worker( - worker_id: usize, - socket: Arc, - engine: Engine, -) -> anyhow::Result<()> { - // 预分配缓冲区 / Pre-allocate buffer - // 使用 BytesMut 避免 Bytes::copy_from_slice 的内存分配 / Use BytesMut to avoid memory allocation in Bytes::copy_from_slice - use bytes::BytesMut; - let mut buf = BytesMut::with_capacity(4096); - // 复用发送缓冲区:用于缓存命中时 patch TXID,避免每包堆分配 / Reuse send buffer to patch TXID on cache hits, avoiding per-packet heap allocation - let mut send_buf = BytesMut::with_capacity(512); - - // 自适应流控:每 100 个请求检查一次是否需要调整 permits - // Adaptive flow control: check if adjustment needed every 100 requests - let mut request_count = 0u32; - - info!(worker_id, "UDP worker started"); - - loop { - // 确保有足够的空间 / Ensure sufficient space - if buf.capacity() < 4096 { - buf.reserve(4096 - buf.len()); - } - - // ✅ 使用 tokio 的 recv_buf_from 配合 BytesMut,实现零拷贝的高性能接收 - // ✅ Use tokio's recv_buf_from with BytesMut for zero-copy high-performance reception - // 由于使用双 socket 方案(IPv4 + IPv6 分离),不会出现混合地址族的 sockaddr 问题 - // Since we use dual-socket approach (IPv4 + IPv6 separated), no mixed address family sockaddr issues - match socket.recv_buf_from(&mut buf).await { - Ok((_len, peer)) => { - // 零拷贝获取 Bytes / Zero-copy obtain Bytes - let packet_bytes = buf.split().freeze(); - - // 每 100 个请求检查一次流控调整 / Check flow control adjustment every 100 requests - request_count += 1; - if request_count >= 100 { - request_count = 0; - engine.adjust_flow_control(); // Now synchronous with atomic CAS - } - - // ✅ 优化:使用 handle_packet_fast 避免重复解析 - // ✅ Optimization: Use handle_packet_fast to avoid re-parsing - // 如果缓存命中,直接返回;如果缓存未命中,返回预解析的数据 - // If cache hit, return directly; if cache miss, return pre-parsed data - match engine.handle_packet_fast(&packet_bytes, peer) { - Ok(Some(FastPathResponse::Direct(bytes))) => { - // 已包含正确 TXID,可直接发送 / Already contains correct TXID - let _ = socket.send_to(&bytes, peer).await; - } - Ok(Some(FastPathResponse::CacheHit { cached, tx_id, inserted_at })) => { - // 复用 send_buf:copy + patch TXID / Reuse send_buf: copy + patch TXID - send_buf.clear(); - if send_buf.capacity() < cached.len() { - send_buf.reserve(cached.len() - send_buf.capacity()); - } - send_buf.extend_from_slice(&cached); - - // RFC 1035 §5.2: Patch TTL based on residence time / 根据停留时间修正 TTL - let elapsed = inserted_at.elapsed().as_secs() as u32; - if elapsed > 0 { - kixdns::proto_utils::patch_all_ttls(&mut send_buf, elapsed); - } - - if send_buf.len() >= 2 { - let id_bytes = tx_id.to_be_bytes(); - send_buf[0] = id_bytes[0]; - send_buf[1] = id_bytes[1]; - } - let _ = socket.send_to(&send_buf, peer).await; - } - Ok(Some(FastPathResponse::AsyncNeeded { qname, qtype, qclass, tx_id, edns_present, pipeline_id })) => { - // 缓存未命中,使用预解析的数据避免重复解析 - // Cache miss, use pre-parsed data to avoid re-parsing - let permit_mgr = Arc::clone(&engine.permit_manager); - let timeout_ms = engine.get_request_timeout_ms(); - let timeout_dur = Duration::from_millis(timeout_ms); - - // 非阻塞式 try_acquire,避免在接收循环中 await / Non-blocking try_acquire to avoid await in receive loop - if let Some(permit) = permit_mgr.try_acquire() { - let engine = engine.clone(); - let socket = Arc::clone(&socket); - let packet_bytes = packet_bytes.clone(); - tokio::spawn(async move { - let _permit = permit; // 自动释放 / Auto-release on drop - - // ✅ 传递预解析数据给 handle_packet_internal,避免重复解析 - // ✅ Pass pre-parsed data to handle_packet_internal to avoid re-parsing - match tokio::time::timeout( - timeout_dur, - engine.handle_packet_internal_with_pre_parsed( - &packet_bytes, - peer, - false, - qname, - qtype, - qclass, - tx_id, - edns_present, - pipeline_id, - ) - ).await { - Ok(Ok(resp)) => { - let _ = socket.send_to(&resp, peer).await; - } - Ok(Err(e)) => { - debug!(error = %e, "handle_packet error"); - } - Err(_) => { - warn!( - timeout_ms, - upstream_timeout_ms = engine.get_upstream_timeout_ms(), - "request timeout after hedge and fallback exhausted" - ); - } - } - }); - } - } - Ok(None) => { - // 快速解析失败,回退到完整处理 - // Fast parse failed, fallback to full processing - let permit_mgr = Arc::clone(&engine.permit_manager); - let timeout_ms = engine.get_request_timeout_ms(); - let timeout_dur = Duration::from_millis(timeout_ms); - - // 非阻塞式 try_acquire,避免在接收循环中 await / Non-blocking try_acquire to avoid await in receive loop - if let Some(permit) = permit_mgr.try_acquire() { - let engine = engine.clone(); - let socket = Arc::clone(&socket); - let packet_bytes = packet_bytes.clone(); - tokio::spawn(async move { - let _permit = permit; // 自动释放 / Auto-release on drop - match tokio::time::timeout(timeout_dur, engine.handle_packet(&packet_bytes, peer)).await { - Ok(Ok(resp)) => { - let _ = socket.send_to(&resp, peer).await; - } - Ok(Err(e)) => { - debug!(error = %e, "handle_packet error"); - } - Err(_) => { - warn!( - timeout_ms, - upstream_timeout_ms = engine.get_upstream_timeout_ms(), - "request timeout" - ); - } - } - }); - } - } - Err(_) => { - // 解析错误,忽略 / Parse error, ignore - } - } - } - Err(e) => { - // 接收错误,清除缓冲区并继续 / Receive error, clear buffer and continue - debug!(error = %e, "UDP recv error"); - buf.clear(); - } - } - } -} - -async fn run_tcp(listener: TcpListener, engine: Engine) -> anyhow::Result<()> { - loop { - let (stream, peer) = listener.accept().await?; - let engine = engine.clone(); - tokio::spawn(async move { - let _ = handle_tcp_conn(stream, peer, engine).await; - }); - } -} - -async fn handle_tcp_conn( - mut stream: TcpStream, - peer: SocketAddr, - engine: Engine, -) -> anyhow::Result<()> { - const MAX_TCP_FRAME: usize = 64 * 1024; - let mut len_buf = [0u8; 2]; - - // ✅ 获取整体请求超时(包含 hedge + TCP fallback) - // ✅ Get overall request timeout (including hedge + TCP fallback) - let timeout_ms = engine.get_request_timeout_ms(); - - // Reusable buffer to avoid per-frame heap allocation / 可复用缓冲区,避免每帧堆分配 - // 使用 BytesMut 以支持零拷贝操作 / Use BytesMut for zero-copy operations - let mut buf = bytes::BytesMut::with_capacity(MAX_TCP_FRAME); - - loop { - if let Err(err) = stream.read_exact(&mut len_buf).await { - if err.kind() != std::io::ErrorKind::UnexpectedEof { - return Err(err.into()); - } - return Ok(()); - } - let frame_len = u16::from_be_bytes(len_buf) as usize; - if frame_len == 0 || frame_len > MAX_TCP_FRAME { - return Ok(()); - } - - // Reuse buffer: resize to exact frame length / 复用缓冲区:调整到精确帧长度 - // resize() is safe and efficient - it only initializes new bytes if growing - buf.clear(); - buf.resize(frame_len, 0); - if stream.read_exact(&mut buf).await.is_err() { - return Ok(()); - } - - // ✅ 优化:使用 handle_packet_fast 进行快速路径检查 - // ✅ Optimization: Use handle_packet_fast for fast path check - // 统一 UDP 和 TCP 的行为,避免重复解析 - // Unify UDP and TCP behavior to avoid re-parsing - let packet_bytes = buf.split().freeze(); - let timeout_dur = Duration::from_millis(timeout_ms); - - let resp = match engine.handle_packet_fast(&packet_bytes, peer) { - Ok(Some(FastPathResponse::Direct(bytes))) => { - // 快速路径命中:直接返回 / Fast path hit: return directly - bytes - } - Ok(Some(FastPathResponse::CacheHit { cached, tx_id, inserted_at })) => { - // 缓存命中:patch TXID / Cache hit: patch TXID - let mut resp_buf = bytes::BytesMut::with_capacity(cached.len()); - resp_buf.extend_from_slice(&cached); - - // RFC 1035 §5.2: Patch TTL based on residence time / 根据停留时间修正 TTL - let elapsed = inserted_at.elapsed().as_secs() as u32; - if elapsed > 0 { - kixdns::proto_utils::patch_all_ttls(&mut resp_buf, elapsed); - } - - if resp_buf.len() >= 2 { - let id_bytes = tx_id.to_be_bytes(); - resp_buf[0] = id_bytes[0]; - resp_buf[1] = id_bytes[1]; - } - resp_buf.freeze() - } - Ok(Some(FastPathResponse::AsyncNeeded { qname, qtype, qclass, tx_id, edns_present, pipeline_id })) => { - // 缓存未命中:使用预解析数据避免重复解析 - // Cache miss: use pre-parsed data to avoid re-parsing - match tokio::time::timeout( - timeout_dur, - engine.handle_packet_internal_with_pre_parsed( - &packet_bytes, - peer, - false, - qname, - qtype, - qclass, - tx_id, - edns_present, - pipeline_id, - ) - ).await { - Ok(Ok(r)) => r, - Ok(Err(_)) => return Ok(()), - Err(_) => { - warn!( - timeout_ms, - upstream_timeout_ms = engine.get_upstream_timeout_ms(), - "TCP request timeout after hedge and fallback exhausted" - ); - return Ok(()); // 关闭连接 / Close connection - } - } - } - Ok(None) => { - // 快速解析失败,回退到完整处理 - // Fast parse failed, fallback to full processing - match tokio::time::timeout(timeout_dur, engine.handle_packet(&packet_bytes, peer)).await { - Ok(Ok(r)) => r, - Ok(Err(_)) => return Ok(()), - Err(_) => { - warn!( - timeout_ms, - upstream_timeout_ms = engine.get_upstream_timeout_ms(), - "TCP request timeout" - ); - return Ok(()); // 关闭连接 / Close connection - } - } - } - Err(_) => { - // 解析错误,关闭连接 / Parse error, close connection - return Ok(()); - } - }; - - if resp.len() <= u16::MAX as usize { - let len_bytes = (resp.len() as u16).to_be_bytes(); - if stream.write_all(&len_bytes).await.is_err() { - return Ok(()); - } - if stream.write_all(&resp).await.is_err() { - return Ok(()); - } + Err(e) => { + error!("Conversion failed: {}", e); + Err(e) } } } diff --git a/src/service/mod.rs b/src/service/mod.rs new file mode 100644 index 0000000..2d17b0e --- /dev/null +++ b/src/service/mod.rs @@ -0,0 +1,151 @@ +//! Cross-platform service management for KixDNS +//! +//! Provides the ability to install, uninstall, and run KixDNS as a system service +//! on Windows (SCM), Linux (systemd, OpenRC, Procd), and FreeBSD (BSD rc.d). +//! +//! 跨平台服务管理:支持在 Windows (SCM)、Linux (systemd/OpenRC/Procd) +//! 和 FreeBSD (BSD rc.d) 上安装、卸载和运行服务。 + +#[cfg(target_os = "windows")] +pub mod windows; + +#[cfg(not(target_os = "windows"))] +pub mod unix; + +// ============================================================================ +// Common types +// ============================================================================ + +/// Detected init system on the current platform. +/// 检测到的当前平台的 init 系统。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InitSystem { + /// Windows Service Control Manager / Windows 服务控制管理器 + #[cfg(target_os = "windows")] + Scm, + /// systemd (most modern Linux distributions) + Systemd, + /// OpenRC (Gentoo, Alpine, etc.) + OpenRc, + /// Procd (OpenWrt) + Procd, + /// BSD rc.d (FreeBSD, etc.) + BsdRc, + /// Unknown — cannot auto-detect + Unknown, +} + +impl InitSystem { + /// Human-readable description of this init system. + pub fn description(&self) -> &'static str { + match self { + #[cfg(target_os = "windows")] + InitSystem::Scm => "Windows Service Control Manager", + InitSystem::Systemd => "systemd (Linux)", + InitSystem::OpenRc => "OpenRC (Gentoo/Alpine Linux)", + InitSystem::Procd => "Procd (OpenWrt)", + InitSystem::BsdRc => "BSD rc.d (FreeBSD)", + InitSystem::Unknown => "Unknown init system", + } + } +} + +/// Detect the init system on the current machine. +/// +/// Detection order: +/// 1. Windows → SCM +/// 2. procd (/sbin/procd) → OpenWrt +/// 3. systemd (/usr/lib/systemd/systemd) → systemd +/// 4. openrc-run (/sbin/openrc-run) → OpenRC +/// 5. target_os = "freebsd" → BSD rc.d +/// 6. Otherwise → Unknown +/// +/// 检测当前机器上的 init 系统。 +pub fn detect_init_system() -> InitSystem { + #[cfg(target_os = "windows")] + { + return InitSystem::Scm; + } + + #[cfg(not(target_os = "windows"))] + { + // Procd (OpenWrt) + if std::path::Path::new("/sbin/procd").exists() { + return InitSystem::Procd; + } + // systemd + if std::path::Path::new("/usr/lib/systemd/systemd").exists() + || std::path::Path::new("/lib/systemd/systemd").exists() + { + return InitSystem::Systemd; + } + // OpenRC + if std::path::Path::new("/sbin/openrc-run").exists() { + return InitSystem::OpenRc; + } + // FreeBSD + #[cfg(target_os = "freebsd")] + { + return InitSystem::BsdRc; + } + InitSystem::Unknown + } +} + +// ============================================================================ +// Top-level API (delegates to platform-specific modules) +// ============================================================================ + +/// Install KixDNS as a system service. +/// +/// On Windows: registers with SCM. +/// On Unix: auto-detects the init system and installs the appropriate service file. +/// +/// 安装 KixDNS 为系统服务。 +pub fn install_service(config: &std::path::PathBuf, listener_label: &str) -> anyhow::Result<()> { + #[cfg(target_os = "windows")] + { + windows::install_service(config, listener_label) + } + + #[cfg(not(target_os = "windows"))] + { + unix::install_service(config, listener_label) + } +} + +/// Uninstall KixDNS system service. +/// +/// On Windows: removes from SCM database. +/// On Unix: auto-detects the init system and removes the appropriate service file. +/// +/// 卸载 KixDNS 系统服务。 +pub fn uninstall_service() -> anyhow::Result<()> { + #[cfg(target_os = "windows")] + { + windows::uninstall_service() + } + + #[cfg(not(target_os = "windows"))] + { + unix::uninstall_service() + } +} + +/// Run KixDNS as a service (platform-specific). +/// +/// On Windows: dispatches via SCM with proper event handling. +/// On Unix: runs the DNS server with a signal-based shutdown handler. +/// +/// 以服务方式运行 KixDNS(平台相关)。 +pub fn run_service(config: std::path::PathBuf, listener_label: String) -> anyhow::Result<()> { + #[cfg(target_os = "windows")] + { + windows::run_service_dispatcher(config, listener_label) + } + + #[cfg(not(target_os = "windows"))] + { + unix::run_service(config, listener_label) + } +} diff --git a/src/service/unix.rs b/src/service/unix.rs new file mode 100644 index 0000000..8da272c --- /dev/null +++ b/src/service/unix.rs @@ -0,0 +1,485 @@ +//! Unix service management for KixDNS +//! +//! Supports: +//! - systemd (most modern Linux distributions) +//! - OpenRC (Gentoo, Alpine Linux) +//! - Procd (OpenWrt) +//! - BSD rc.d (FreeBSD) +//! +//! Unix 服务管理:支持 systemd、OpenRC、Procd 和 BSD rc.d。 + +use std::io::Write; +use std::path::{Path, PathBuf}; + +use anyhow::Context; +use tokio::sync::oneshot; +use tracing::info; + +use crate::service::InitSystem; +use crate::service::detect_init_system; + +// ============================================================================ +// Service file templates +// ============================================================================ + +/// Generate a systemd service unit. +/// +/// Output: `/etc/systemd/system/kixdns.service` +fn systemd_unit(exe: &Path, config: &Path, listener_label: &str) -> String { + format!( + r#"# KixDNS systemd service unit +# Auto-generated by kixdns service install — do not edit manually. +# +# Install: kixdns service install +# Uninstall: kixdns service uninstall +# Start: systemctl start kixdns +# Enable: systemctl enable kixdns + +[Unit] +Description=KixDNS DNS Server +Documentation=https://github.com/kixdns/kixdns +After=network.target +Wants=nss-lookup.target + +[Service] +Type=simple +ExecStart={exe} run \ + --config {config} \ + --listener-label {label} +Restart=on-failure +RestartSec=5 +User=nobody +Group=nogroup +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +LimitNOFILE=65536 +PrivateTmp=true +ProtectSystem=full +ProtectHome=true + +[Install] +WantedBy=multi-user.target +"#, + exe = exe.display(), + config = config.display(), + label = listener_label, + ) +} + +/// Generate an OpenRC init script. +/// +/// Output: `/etc/init.d/kixdns` +fn openrc_script(exe: &Path, config: &Path, listener_label: &str) -> String { + format!( + r#"#!/sbin/openrc-run +# KixDNS OpenRC init script +# Auto-generated by kixdns service install — do not edit manually. +# +# Install: kixdns service install +# Uninstall: kixdns service uninstall +# Start: rc-service kixdns start +# or: /etc/init.d/kixdns start + +name="KixDNS DNS Server" +description="High-performance async DNS server with hot-reloadable pipeline configuration" +command="{exe}" +command_args="run --config {config} --listener-label {label}" +command_user="nobody:nogroup" +pidfile="/run/${{RC_SVCNAME}}.pid" +command_background=false + +depend() {{ + need net + use dns logger +}} +"#, + exe = exe.display(), + config = config.display(), + label = listener_label, + ) +} + +/// Generate a Procd init script (OpenWrt). +/// +/// Output: `/etc/init.d/kixdns` +fn procd_script(exe: &Path, config: &Path, listener_label: &str) -> String { + format!( + r#"#!/bin/sh /etc/rc.common +# KixDNS Procd init script (OpenWrt) +# Auto-generated by kixdns service install — do not edit manually. +# +# Install: kixdns service install +# Uninstall: kixdns service uninstall +# Start: /etc/init.d/kixdns start +# Enable: /etc/init.d/kixdns enable + +USE_PROCD=1 + +start_service() {{ + procd_open_instance + procd_set_param command "{exe}" + procd_append_param command run --config {config} --listener-label {label} + procd_set_param user nobody + procd_set_param respawn 3600 5 0 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_close_instance +}} + +stop_service() {{ + # The service will be stopped automatically by procd + : +}} + +service_triggers() {{ + procd_add_reload_trigger "kixdns" +}} +"#, + exe = exe.display(), + config = config.display(), + label = listener_label, + ) +} + +/// Generate a FreeBSD rc.d script. +/// +/// Output: `/usr/local/etc/rc.d/kixdns` +fn bsd_rc_script(exe: &Path, config: &Path, listener_label: &str) -> String { + format!( + r#"#!/bin/sh +# +# PROVIDE: kixdns +# REQUIRE: NETWORKING SERVERS +# KEYWORD: shutdown +# +# Auto-generated by kixdns service install — do not edit manually. +# +# Install: kixdns service install +# Uninstall: kixdns service uninstall +# Start: service kixdns start +# Enable: echo 'kixdns_enable="YES"' >> /etc/rc.conf + +. /etc/rc.subr + +name="kixdns" +rcvar="kixdns_enable" + +load_rc_config $name + +: ${{kixdns_enable:="NO"}} +: ${{kixdns_config:="{config}"}} +: ${{kixdns_listener_label:="{label}"}} +: ${{kixdns_user:="nobody"}} + +command="{exe}" +command_args="run --config ${{kixdns_config}} --listener-label ${{kixdns_listener_label}}" +command_user="${{kixdns_user}}" +pidfile="/var/run/kixdns.pid" + +run_rc_command "$1" +"#, + exe = exe.display(), + config = config.display(), + label = listener_label, + ) +} + +// ============================================================================ +// Service file installation paths +// ============================================================================ + +fn systemd_path() -> PathBuf { + PathBuf::from("/etc/systemd/system/kixdns.service") +} + +fn openrc_path() -> PathBuf { + PathBuf::from("/etc/init.d/kixdns") +} + +fn procd_path() -> PathBuf { + PathBuf::from("/etc/init.d/kixdns") +} + +fn bsd_rc_path() -> PathBuf { + PathBuf::from("/usr/local/etc/rc.d/kixdns") +} + +// ============================================================================ +// Install logic +// ============================================================================ + +/// Install KixDNS as a system service on Unix. +/// +/// Auto-detects the init system and installs the appropriate service file. +pub fn install_service(config: &PathBuf, listener_label: &str) -> anyhow::Result<()> { + let init = detect_init_system(); + let exe = std::env::current_exe().context("failed to get current executable path")?; + + println!("→ Detected init system: {}", init.description()); + + match init { + InitSystem::Systemd => install_systemd(&exe, config, listener_label), + InitSystem::OpenRc => install_openrc(&exe, config, listener_label), + InitSystem::Procd => install_procd(&exe, config, listener_label), + InitSystem::BsdRc => install_bsd_rc(&exe, config, listener_label), + InitSystem::Unknown | _ => { + anyhow::bail!( + "Could not auto-detect init system.\n\ + ┌─ Please create a service script manually ──────────────────────┐\n\ + │ │\n\ + │ systemd: Write a .service unit to /etc/systemd/system/ │\n\ + │ OpenRC: Write an init script to /etc/init.d/ │\n\ + │ Procd: Write a procd script to /etc/init.d/ │\n\ + │ FreeBSD: Write a rc.d script to /usr/local/etc/rc.d/ │\n\ + │ │\n\ + │ The binary is at: {} │\n\ + │ Run it with: {} run --config {} │\n\ + │ │\n\ + └──────────────────────────────────────────────────────────────┘\n\ + ", + exe.display(), + exe.display(), + config.display(), + ) + } + } +} + +fn install_systemd(exe: &Path, config: &Path, listener_label: &str) -> anyhow::Result<()> { + let path = systemd_path(); + let content = systemd_unit(exe, config, listener_label); + + write_service_file(&path, &content, 0o644)?; + + // Reload systemd, enable and start + run_command("systemctl", &["daemon-reload"])?; + run_command("systemctl", &["enable", "kixdns"])?; + run_command("systemctl", &["start", "kixdns"])?; + + println!("✓ KixDNS installed as a systemd service."); + println!(" Unit file : {}", path.display()); + println!(" Status : systemctl status kixdns"); + println!(" Logs : journalctl -u kixdns -f"); + Ok(()) +} + +fn install_openrc(exe: &Path, config: &Path, listener_label: &str) -> anyhow::Result<()> { + let path = openrc_path(); + let content = openrc_script(exe, config, listener_label); + + write_service_file(&path, &content, 0o755)?; + + run_command("rc-update", &["add", "kixdns", "default"])?; + run_command("rc-service", &["kixdns", "start"])?; + + println!("✓ KixDNS installed as an OpenRC service."); + println!(" Init script : {}", path.display()); + println!(" Status : rc-service kixdns status"); + Ok(()) +} + +fn install_procd(exe: &Path, config: &Path, listener_label: &str) -> anyhow::Result<()> { + let path = procd_path(); + let content = procd_script(exe, config, listener_label); + + write_service_file(&path, &content, 0o755)?; + + run_command("/etc/init.d/kixdns", &["enable"])?; + run_command("/etc/init.d/kixdns", &["start"])?; + + println!("✓ KixDNS installed as a Procd service (OpenWrt)."); + println!(" Init script : {}", path.display()); + println!(" Status : /etc/init.d/kixdns status"); + Ok(()) +} + +fn install_bsd_rc(exe: &Path, config: &Path, listener_label: &str) -> anyhow::Result<()> { + let path = bsd_rc_path(); + let content = bsd_rc_script(exe, config, listener_label); + + write_service_file(&path, &content, 0o755)?; + + println!("✓ KixDNS installed as a FreeBSD rc.d service."); + println!(" Script : {}", path.display()); + println!(" Enable : echo 'kixdns_enable=\"YES\"' >> /etc/rc.conf"); + println!(" Start : service kixdns start"); + println!( + " Config : Add 'kixdns_config=\"{}\"' to /etc/rc.conf for custom path", + config.display() + ); + Ok(()) +} + +// ============================================================================ +// Uninstall logic +// ============================================================================ + +/// Uninstall KixDNS system service on Unix. +pub fn uninstall_service() -> anyhow::Result<()> { + let init = detect_init_system(); + + println!("→ Detected init system: {}", init.description()); + + match init { + InitSystem::Systemd => uninstall_systemd(), + InitSystem::OpenRc => uninstall_openrc(), + InitSystem::Procd => uninstall_procd(), + InitSystem::BsdRc => uninstall_bsd_rc(), + InitSystem::Unknown | _ => { + anyhow::bail!( + "Could not auto-detect init system.\n\ + Please manually remove the service file you created earlier." + ); + } + } +} + +fn uninstall_systemd() -> anyhow::Result<()> { + let path = systemd_path(); + + // Stop and disable + let _ = run_command("systemctl", &["stop", "kixdns"]); + let _ = run_command("systemctl", &["disable", "kixdns"]); + + // Remove unit file + if path.exists() { + std::fs::remove_file(&path).context("failed to remove systemd unit file")?; + } + let _ = run_command("systemctl", &["daemon-reload"]); + + println!("✓ KixDNS systemd service uninstalled."); + Ok(()) +} + +fn uninstall_openrc() -> anyhow::Result<()> { + let path = openrc_path(); + + let _ = run_command("rc-service", &["kixdns", "stop"]); + let _ = run_command("rc-update", &["delete", "kixdns"]); + + if path.exists() { + std::fs::remove_file(&path).context("failed to remove OpenRC init script")?; + } + + println!("✓ KixDNS OpenRC service uninstalled."); + Ok(()) +} + +fn uninstall_procd() -> anyhow::Result<()> { + let path = procd_path(); + + if path.exists() { + let _ = run_command("/etc/init.d/kixdns", &["stop"]); + let _ = run_command("/etc/init.d/kixdns", &["disable"]); + std::fs::remove_file(&path).context("failed to remove Procd init script")?; + } + + println!("✓ KixDNS Procd service uninstalled."); + Ok(()) +} + +fn uninstall_bsd_rc() -> anyhow::Result<()> { + let path = bsd_rc_path(); + + let _ = run_command("service", &["kixdns", "stop"]); + + if path.exists() { + std::fs::remove_file(&path).context("failed to remove BSD rc.d script")?; + } + + println!("✓ KixDNS BSD rc.d service uninstalled."); + println!(" Note: You may want to remove 'kixdns_enable=\"YES\"' from /etc/rc.conf manually."); + Ok(()) +} + +// ============================================================================ +// Run as service (Unix) +// ============================================================================ + +/// Run the DNS server with signal-based graceful shutdown. +/// +/// Sets up handlers for SIGTERM, SIGINT, and SIGQUIT to trigger +/// a graceful shutdown of the server. +pub fn run_service(config: PathBuf, listener_label: String) -> anyhow::Result<()> { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime for service")?; + + rt.block_on(async move { + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + + // Set up signal handlers inside the tokio runtime + // 在 tokio 运行时内设置信号处理 + tokio::spawn(async move { + wait_for_signal_async().await; + let _ = shutdown_tx.send(()); + }); + + crate::run_dns_server_with_shutdown(config, listener_label, shutdown_rx).await + })?; + + info!("Service stopped gracefully"); + Ok(()) +} + +/// Wait for a termination signal using tokio's signal module. +/// Listens for SIGTERM, SIGINT, and SIGQUIT. +pub async fn wait_for_signal_async() { + use tokio::signal::unix::{SignalKind, signal}; + + let mut term = signal(SignalKind::terminate()).expect("failed to create SIGTERM listener"); + let mut int = signal(SignalKind::interrupt()).expect("failed to create SIGINT listener"); + let mut quit = signal(SignalKind::quit()).expect("failed to create SIGQUIT listener"); + + tokio::select! { + _ = term.recv() => info!("Received SIGTERM"), + _ = int.recv() => info!("Received SIGINT"), + _ = quit.recv() => info!("Received SIGQUIT"), + } +} + +// ============================================================================ +// Helpers +// ============================================================================ + +/// Write a service file to disk, creating parent directories if needed. +fn write_service_file(path: &Path, content: &str, mode: u32) -> anyhow::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create directory: {}", parent.display()))?; + } + + let mut file = std::fs::File::create(path) + .with_context(|| format!("failed to create service file: {}", path.display()))?; + + file.write_all(content.as_bytes()) + .with_context(|| format!("failed to write service file: {}", path.display()))?; + + // Set file permissions + use std::os::unix::fs::PermissionsExt; + file.set_permissions(std::fs::Permissions::from_mode(mode)) + .with_context(|| format!("failed to set permissions on: {}", path.display()))?; + + Ok(()) +} + +/// Run a shell command and return its exit status. +fn run_command(program: &str, args: &[&str]) -> anyhow::Result<()> { + let output = std::process::Command::new(program) + .args(args) + .output() + .with_context(|| format!("failed to execute: {} {}", program, args.join(" ")))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!( + "Command failed ({} {}): {}", + program, + args.join(" "), + stderr.trim() + ); + } + + Ok(()) +} diff --git a/src/service/windows.rs b/src/service/windows.rs new file mode 100644 index 0000000..98247b6 --- /dev/null +++ b/src/service/windows.rs @@ -0,0 +1,246 @@ +//! Windows Service Control Manager (SCM) integration for KixDNS +//! +//! Windows SCM 集成:安装、卸载、运行 Windows 服务。 + +use std::ffi::OsString; +use std::path::PathBuf; +use std::sync::OnceLock; +use std::time::Duration; + +use anyhow::Context; +use tokio::sync::oneshot; +use tracing::info; + +use windows_service::{ + define_windows_service, + service::{ + ServiceAccess, ServiceControl, ServiceControlAccept, ServiceDependency, + ServiceErrorControl, ServiceExitCode, ServiceInfo, ServiceStartType, ServiceState, + ServiceStatus, ServiceType, + }, + service_control_handler::{self, ServiceControlHandlerResult}, + service_dispatcher, + service_manager::{ServiceManager, ServiceManagerAccess}, +}; + +/// Service name registered with the SCM / 在 SCM 中注册的服务名称 +pub const SERVICE_NAME: &str = "KixDNS"; + +/// Display name shown in Services.msc / 在服务管理器中显示的名称 +pub const SERVICE_DISPLAY_NAME: &str = "KixDNS DNS Server"; + +/// Service description / 服务描述 +pub const SERVICE_DESCRIPTION: &str = + "High-performance async DNS server with hot-reloadable pipeline configuration."; + +/// Global configuration: (config_path, listener_label) +/// Set before dispatching the service, read inside the service main function. +static SERVICE_CONFIG: OnceLock<(PathBuf, String)> = OnceLock::new(); + +// --------------------------------------------------------------------------- +// FFI service entry point — generated by the macro +// --------------------------------------------------------------------------- +define_windows_service!(ffi_service_main, my_service_main); + +/// The actual service main function called by the SCM on a background thread. +fn my_service_main(_arguments: Vec) { + // Init tracing inside the service context + let _ = tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .try_init(); + + let (config, listener_label) = match SERVICE_CONFIG.get() { + Some(cfg) => cfg.clone(), + None => { + tracing::error!("SERVICE_CONFIG not set; using defaults"); + (PathBuf::from("config/pipeline.json"), "default".to_string()) + } + }; + + if let Err(e) = run_service(config, listener_label) { + tracing::error!("Service exited with error: {:#}", e); + } +} + +/// Run the DNS server as a Windows service with proper SCM integration. +fn run_service(config: PathBuf, listener_label: String) -> anyhow::Result<()> { + // 1. Install the ring crypto provider (same as normal startup) + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install rustls crypto provider"); + + // 2. Register service control event handler + let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); + let shutdown_tx = std::sync::Mutex::new(Some(shutdown_tx)); + + let status_handle = service_control_handler::register(SERVICE_NAME, move |control_event| { + match control_event { + ServiceControl::Stop | ServiceControl::Shutdown => { + info!("Service received stop/shutdown signal"); + if let Some(tx) = shutdown_tx.lock().unwrap().take() { + let _ = tx.send(()); + } + ServiceControlHandlerResult::NoError + } + // Interrogate is mandatory — must respond NoError + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + })?; + + // 3. Report SERVICE_RUNNING to SCM + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::NO_ERROR, + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + info!( + "Service is now running (config={:?}, listener_label={})", + config, listener_label + ); + + // 4. Create tokio runtime and run the DNS server, passing the shutdown signal + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("failed to build tokio runtime for service")?; + + rt.block_on(async { + crate::run_dns_server_with_shutdown(config, listener_label, shutdown_rx).await + })?; + + // 5. Report SERVICE_STOPPED + status_handle.set_service_status(ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::NO_ERROR, + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + info!("Service stopped gracefully"); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Public API — called from mod.rs +// --------------------------------------------------------------------------- + +/// Install KixDNS as a Windows service. +/// +/// Registers the service with the SCM so that it can be started automatically +/// on boot or manually via `sc start` / Services.msc. +pub fn install_service(config: &PathBuf, listener_label: &str) -> anyhow::Result<()> { + let manager = ServiceManager::local_computer( + None::<&str>, // default database (active) + ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, + ) + .context("failed to open Service Control Manager (try running as Administrator)")?; + + let exe_path = std::env::current_exe().context("failed to get current executable path")?; + + // Build launch arguments: "service run --config --listener-label