Cargo.toml
Rama main · réplica de lectura del repositorio original.
# 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 <opensource@koroblabs.com>"]
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 `<name>.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