# Virtual workspace root. The binary lives in crates/ferrite; future subsystem # crates (core, dns, proxy, …) join as siblings under crates/. [workspace] resolver = "3" members = ["crates/*"] # `cargo build` / `cargo run` at the root build the binary, not every member — # release CI relies on this (`cargo build --release --target …`). default-members = ["crates/ferrite"] [workspace.package] version = "0.2.0" edition = "2024" rust-version = "1.97" license = "MIT" authors = ["Korob Labs "] repository = "https://github.com/koroblabs/ferrite-server" # Not on crates.io: ferrite ships as a binary. publish = false # One authoritative version/feature spec per dependency; member crates opt in # with `.workspace = true`. [workspace.dependencies] # ── Internal crates ─────────────────────────────────────────────────────────── ferrite-api = { path = "crates/api" } ferrite-app = { path = "crates/app" } ferrite-blocklist = { path = "crates/blocklist" } ferrite-clients = { path = "crates/clients" } ferrite-core = { path = "crates/core" } ferrite-dhcp = { path = "crates/dhcp" } ferrite-secure = { path = "crates/secure" } ferrite-edge = { path = "crates/edge" } ferrite-dns = { path = "crates/dns" } ferrite-proxy = { path = "crates/proxy" } ferrite-stats = { path = "crates/stats" } ferrite-storage = { path = "crates/storage" } ferrite-updater = { path = "crates/updater" } ferrite-upstream = { path = "crates/upstream" } # ── Async runtime ───────────────────────────────────────────────────────────── tokio = { version = "1", features = ["full"] } # ── DNS ─────────────────────────────────────────────────────────────────────── # Upstream resolution (plain + DoT/DoH/DoQ) and DNS wire-format encode/decode. hickory-resolver = { version = "0.26.1", features = ["tls-ring", "https-ring", "quic-ring"] } hickory-proto = "0.26.1" # Message/Query/PTR parsing (dns/, clients/resolver) # Tunneled upstream: ferrite speaks DNS-over-TLS / DNS-over-TCP itself, directly # over a userspace egress stream (WireGuard/SOCKS5), so its own upstream queries # can ride the tunnel. hickory/reqwest own their sockets and can't, hence raw TLS. # default-features off drops rustls's `aws_lc_rs` provider: ring is already in the # tree (hickory's *-ring features, plus `ring` directly), and pulling aws-lc too # would compile a second full crypto stack — dead weight in the binary and a C # toolchain the musl cross-build has to satisfy. See `install_default_crypto`. tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] } # TLS client over an arbitrary egress stream (tunneled DoT) webpki-roots = "1" # Mozilla root store for tunneled DoT certificate verification x509-parser = "0.18" # parse presented certs for the /tools/cert inspector # Let's Encrypt for encrypted DNS (DNS-01). ring, not the default aws-lc-rs, for the # same reason as tokio-rustls above; rcgen makes the key and the signing request. instant-acme = { version = "0.8.5", default-features = false, features = ["ring", "hyper-rustls", "rcgen"] } # ── HTTP: API server + outbound client ──────────────────────────────────────── axum = { version = "0.8", features = ["macros"] } # REST API + serves the web UI # hyper/hyper-util: serve the panel on a single peeked connection so the :80 # listener can demux panel-vs-proxy by Host (already in the tree via axum). hyper = { version = "1", features = ["server", "http1"] } http = "1" # DNS over HTTPS over HTTP/3 (QUIC) for devices; ring, like the rest of TLS. quinn = { version = "0.11", default-features = false, features = ["runtime-tokio", "rustls-ring"] } h3 = "0.0.8" h3-quinn = "0.0.10" # The relay: a box's own key and self-signed certificate, and its ID. rcgen = { version = "0.14", default-features = false, features = ["crypto", "ring", "pem", "x509-parser"] } data-encoding = "2" hyper-util = { version = "0.1", features = ["tokio", "service"] } tower-http = { version = "0.6", features = ["cors", "trace"] } # CORS + request tracing middleware # `rustls-no-provider` rather than `rustls`: the latter force-enables the aws-lc-rs # provider (a second crypto stack next to ring). With no provider chosen here, # reqwest uses the process-wide default that `install_default_crypto` installs. reqwest = { version = "0.13.4", features = ["rustls-no-provider", "json", "http2"], default-features = false } # blocklist & update downloads; http2 required for DoH endpoints that reject HTTP/1.1 (e.g. Quad9 → HTTP 505) # ── Blocklist engine ────────────────────────────────────────────────────────── fst = "0.4" # finite-state-transducer set — fast exact-domain blocklist lookups memmap2 = "0.9" # serve FSTs from mmap'd files (page cache) instead of anonymous RSS psl = "2" # public suffix list (eTLD+1 boundaries) regex = "1" # wildcard / adblock-style rule matching (blocklist/engine) # ── Storage (SQLite) ────────────────────────────────────────────────────────── rusqlite = { version = "0.37", features = ["bundled"] } # embedded SQLite — query log, rollups, settings tokio-rusqlite = "0.7" # async wrapper around rusqlite # ── Allocator ───────────────────────────────────────────────────────────────── # musl's malloc retains freed pages and fragments badly under threads (the old # "RSS grows to 500 MB and never comes back"); mimalloc returns memory to the OS. mimalloc = "0.1" # extended: mi_process_info — mimalloc's own committed-memory figure, exposed in # /api/stats/system so allocator retention is distinguishable from other RSS. libmimalloc-sys = { version = "0.1", features = ["extended"] } # ── Caching & concurrent state ──────────────────────────────────────────────── dashmap = "6" # concurrent maps — client registry, sessions, in-flight resolves lru = "0.18" # LRU caches — DNS response cache + blocklist decision cache arc-swap = "1" # lock-free hot-swap of the blocklist FST on reload parking_lot = "0.12" # faster Mutex / RwLock # ── Serialization ───────────────────────────────────────────────────────────── serde = { version = "1", features = ["derive"] } serde_json = "1" # JSON API payloads toml = "1" # config file parsing (config.toml) postcard = { version = "1", default-features = false, features = ["use-std"] } # compact binary state snapshots (snapshot/) chrono = { version = "0.4", features = ["serde"] } # timestamps bytes = { version = "1", features = ["serde"] } # zero-copy byte buffers for DNS wire data (serde: the state snapshot writes them straight out) # ── Logging ─────────────────────────────────────────────────────────────────── tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # ── Error handling ──────────────────────────────────────────────────────────── thiserror = "2" # typed library errors (FeriteError) anyhow = "1" # app-level error context # ── Crypto / auth ───────────────────────────────────────────────────────────── ring = "0.17" # secure RNG (DNS query IDs) + SHA checksums (updater) + auth tokens hex = "0.4" # lowercase hex for session tokens and checksums (already in the tree via boringtun) argon2 = "0.5" # web UI password hashing (Argon2id) subtle = "2" # constant-time comparison (API key check) # ── Networking & system ─────────────────────────────────────────────────────── # "all": SO_MARK (set_mark) for kernel-WG fwmark routing, Linux-only API. socket2 = { version = "0.6", features = ["all"] } # low-level socket options (mDNS multicast, SO_MARK) sysinfo = "0.39" # CPU / memory / system stats dirs = "6" # locate config & data directories # ── Self-update (download & unpack release archives) ────────────────────────── flate2 = "1" # gzip decompression yaml-rust2 = { version = "0.13", default-features = false } # reads AdGuardHome.yaml for import tar = "0.4" # tar extraction (server binary + web bundle) # ── WireGuard egress (userspace, in-process; no TUN / no root) ──────────────── boringtun = { version = "0.7.1", default-features = false } # WireGuard protocol (Noise handshake + transport crypto), no netstack smoltcp = { version = "0.13", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-udp"] } # userspace TCP/IP stack over the tunnel # ── Zero-copy relay (Linux-only) ────────────────────────────────────────────── libc = "0.2" # splice(2) + pipe2(2): move proxied bytes without a userspace copy # ── WireGuard egress (kernel backend; Linux-only, CAP_NET_ADMIN) ────────────── rtnetlink = "0.21" # wg netdev / address / route / rule via rtnetlink genetlink = "0.2.6" # generic-netlink family resolution + connection netlink-packet-generic = "0.4" # GenlMessage wrapper netlink-packet-wireguard = "0.4.1" # WG_CMD_SET/GET_DEVICE messages netlink-packet-core = "0.8" # NLM_F_* flags for genl requests futures-util = { version = "0.3", default-features = false } # drive netlink response streams # ── Misc ────────────────────────────────────────────────────────────────────── async-trait = "0.1" # async methods in the Storage trait base64 = "0.22" # dev-dependencies tower = "0.5" [profile.release] # Line-table debug info: keeps release optimisations but lets gdb show file:line # in backtraces. Negligible performance impact; small binary size increase. debug = 1 # The profile shipped artifacts are built with (`make linux-arm64`, release CI). # Same optimisations as `release`, but sized for distribution: on a static musl # build the line tables inherited from `release` dominate the file — 171 MB # unstripped vs 23 MB stripped for aarch64 — and nobody debugs a released binary # with gdb. `cargo build --release` keeps its debug info for local work. # # `panic` stays at unwind on purpose: with `abort` a panic in one tokio task # would take the whole DNS server down instead of just that task. [profile.dist] inherits = "release" strip = "symbols" # Cross-crate inlining shrinks the binary further and is measurably faster; # "thin" gets nearly all of it for a fraction of "fat"'s link time. lto = "thin" codegen-units = 1