MistEye DNS Guard is a Rust-based local DNS relay with macOS support and a Linux MVP. It forwards DNS traffic to a configured upstream resolver, records observed domains and public IPs in a configurable observation window, checks them with the MistEye Threat Detection API, and sends malicious indicators to a configured Webhook. The window defaults to 120 seconds and can be set from 1 to 86400 seconds through window_seconds.
The relay is designed to keep DNS forwarding off the critical path of threat detection: MistEye, Webhook, SQLite, and observation processing run asynchronously.
The current release primarily validates macOS and provides a Linux MVP with:
- Local UDP and TCP DNS relay.
- UDP, TCP, DNS-over-TLS (DoT), and DNS-over-HTTPS (DoH) upstreams.
- Query-domain, CNAME/ANAME, and public A/AAAA observation.
- Optional macOS/Linux
lsofprocess-connection observation; Linux resolves executable paths through/proc/<pid>/exe. - Configurable deduplicated observation windows from 1 to 86400 seconds, defaulting to 120 seconds.
- MistEye
domainandipdetection. - Persistent SQLite queues for detection jobs and Webhook events.
- HMAC-SHA256 Webhook signatures and retry handling.
- System DNS configuration through macOS
networksetup, Linuxresolvectl, NetworkManager, or a staticresolv.conf. - macOS PF or Linux nftables plus
SO_MARKprotection for traditional DNS/DoT/DoQ bypass and relay self-traffic. - Recovery snapshots, lease heartbeats, and a recovery helper for abnormal exits.
- An embedded API and web console for starting/stopping the relay, system DNS takeover/recovery, and status.
Malicious results are reported but not blocked in this version.
The current release focuses on local DNS relay, threat observation, and system DNS integration on macOS, with a Linux MVP. Application-managed DoH/DoQ, VPN-internal DNS, hosts/mDNS/LLMNR, and HTTP payload inspection are outside the current scope and will continue to be developed and improved as the project evolves.
In the current implementation, this project does not collect user identity information, account data, files, message content, or other business content. Runtime observations are processed locally and are limited to DNS domains, public IP indicators, and optional process/connection metadata needed for threat observation and status display.
DNS queries are forwarded to the upstream resolver selected in the configuration as part of the relay's core function; this is separate from telemetry collection. Observed data is not sent to other external services by default. When MistEye detection is enabled, the relevant indicators are sent to the configured MistEye Threat Detection API for checking. If a Webhook is configured, malicious detection results are also sent to that user-configured endpoint. The project does not use the collected data for unrelated purposes.
GitHub Releases ship versioned prebuilt binaries, so you do not need Rust or a local build:
| Asset | Platform |
|---|---|
misteye-dns-macos-aarch64 |
macOS Apple Silicon (M-series) |
misteye-dns-linux-amd64 |
Linux x86_64 |
After download, rename the asset to misteye-dns and make it executable. You also need the config template misteye-dns.toml.example from this repository.
macOS Apple Silicon:
curl -L -o misteye-dns \
https://github.com/slowmist/MistEye-DNS-Guard/releases/latest/download/misteye-dns-macos-aarch64
curl -L -o misteye-dns.toml.example \
https://raw.githubusercontent.com/slowmist/MistEye-DNS-Guard/main/misteye-dns.toml.example
chmod +x misteye-dnsLinux x86_64:
curl -L -o misteye-dns \
https://github.com/slowmist/MistEye-DNS-Guard/releases/latest/download/misteye-dns-linux-amd64
curl -L -o misteye-dns.toml.example \
https://raw.githubusercontent.com/slowmist/MistEye-DNS-Guard/main/misteye-dns.toml.example
chmod +x misteye-dnsThe rest of the run instructions are the same; use ./misteye-dns directly, for example:
cp misteye-dns.toml.example misteye-dns.toml
chmod 600 misteye-dns.toml
./misteye-dns doctor --config ./misteye-dns.toml
sudo ./misteye-dns serve --config ./misteye-dns.tomlUse the next section only when you want to build from source.
Run the following commands from the project root. The project uses Rust 2024 Edition, pins the Rust toolchain to 1.95.0, and uses Cargo.lock to pin dependency versions.
| Platform | Build requirements | Runtime requirements for system DNS takeover |
|---|---|---|
| macOS | Xcode Command Line Tools, Rust/Cargo 1.95.0 | networksetup, PF (both included with macOS) |
| Linux | C/C++ toolchain, pkg-config, cmake, Rust/Cargo 1.95.0 |
lsof, nftables; systemd-resolved or NetworkManager is recommended |
On macOS, install the compiler tools if they are not already present:
xcode-select --installDebian/Ubuntu:
sudo apt-get update
sudo apt-get install -y build-essential pkg-config cmake lsof nftables dnsutilsFedora/RHEL:
sudo dnf install gcc gcc-c++ make pkgconf-pkg-config cmake lsof nftables bind-utilsIf Rust is not installed, install it with rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shReopen the terminal after installation. The Rust toolchain requirements are:
| Tool | Requirement | Purpose |
|---|---|---|
rustup |
Recommended | Installs and manages Rust toolchains |
rustc |
1.95.0 | Rust compiler |
cargo |
Same toolchain as rustc |
Builds the project, downloads dependencies, and runs binaries |
rustfmt |
Recommended | Formats Rust source code |
clippy |
Recommended | Performs Rust static checks |
If Rust is already installed but the formatting and static-checking components are missing, install them with:
rustup component add rustfmt clippyVerify the basic toolchain:
rustc --version # should report 1.95.0
cargo --version
rustfmt --version
clippy-driver --versioncargo build --release --locked --bin misteye-dnsThe main program is written to target/release/misteye-dns. The repository also contains the udp-bench binary, so plain cargo run fails because Cargo cannot choose between multiple binaries; specify the main binary during development:
cargo run --locked --bin misteye-dns -- --helpTo run without keeping a release binary, replace ./misteye-dns in the commands below with:
cargo run --release --locked --bin misteye-dns --Build a Linux binary on Linux with:
cargo build --release --locked --bin misteye-dnsBuild a Linux binary from macOS with Docker:
./scripts/build-linux.sh # linux/amd64
./scripts/build-linux.sh arm64 # linux/arm64The script requires Docker, Docker Buildx, and a running Docker daemon. It writes dist/misteye-dns-linux-amd64 or dist/misteye-dns-linux-arm64.
If you already downloaded misteye-dns-macos-aarch64 from Releases (or built target/release/misteye-dns locally), you do not need Rust or Cargo. The examples below use ./misteye-dns; replace that with ./target/release/misteye-dns when using a local build. The recommended macOS workflow is the local web console:
cp misteye-dns.toml.example misteye-dns.toml
chmod 600 misteye-dns.tomlOpen misteye-dns.toml and fill in [misteye].api_key. The Webhook URL is optional. The default template uses the macOS-friendly Relay address 127.0.0.1:15353 and Google DoH with 8.8.8.8:443 bootstrap.
Optionally validate the configuration before starting:
./misteye-dns doctor --config ./misteye-dns.tomlStart the web console with administrator authorization:
sudo ./misteye-dns serve --config ./misteye-dns.tomlOpen http://127.0.0.1:8080. In the web console:
- Click Start + system DNS takeover for complete monitoring of the host's system DNS. The program configures system DNS and the macOS PF redirect automatically.
- Use Start relay only when the DNS client is already configured to send requests to the Relay. For a non-53 listener such as
127.0.0.1:15353, Clash or another client must support specifying the port directly. - Click Stop to stop the Relay and restore the saved system DNS/PF state.
The page shows whether the Relay is running and whether system DNS takeover has been verified. For normal macOS users, this web workflow is preferred over the command-line workflow below.
If you already have a binary (./misteye-dns from Releases, or ./target/release/misteye-dns from a local build), start here. The examples below use ./misteye-dns; swap in the local build path when needed. For a new checkout, copy the configuration template first:
cp misteye-dns.toml.example misteye-dns.toml
chmod 600 misteye-dns.tomlEdit misteye-dns.toml and set:
[misteye].endpointand either[misteye].api_keyor an API-key environment variable.[webhook].urlonly if you want HTTP notifications; it is optional.- A single Google DoH upstream is sufficient for the macOS/Clash setup described below. Use
https://dns.google/dns-querywith8.8.8.8:443as its bootstrap address; add another upstream only when you have tested that resolver in your network.
Environment variables are optional. The program accepts either form:
[misteye]
endpoint = "https://app-api.misteye.io/functions/v1/detect"
api_key = "mist_xxx"or:
export MISTEYE_API_KEY="mist_xxx"When both are present, the configured environment variable takes precedence. The Webhook HMAC secret is also optional:
[webhook]
url = "https://your-webhook.example.com/misteye"
secret = "optional-secret"or:
export MISTEYE_WEBHOOK_SECRET="optional-secret"Without a Webhook secret, Webhook delivery still works but requests are not signed. Use chmod 600 and do not commit the TOML file if it contains secrets.
./misteye-dns doctor --config ./misteye-dns.toml
sudo ./misteye-dns run \
--config ./misteye-dns.toml \
--apply-system-dnsFor command-line use, --apply-system-dns is recommended because it configures the system DNS and enables complete monitoring. An interactive terminal opens the MISTEYE DNS GUARD MANAGEMENT CONSOLE by default. It reads the same SQLite data as the web console and shows the last 24 hours of DNS Relay domains, process connections, malicious detections, and queue status. Press r to refresh immediately, or q, Esc, or Ctrl-C for a normal shutdown and recovery. It also works over an allocated SSH TTY; systemd, nohup, and piped runs automatically keep regular log output.
Only use the following Relay-only command when DNS is configured manually or by another DNS client:
./misteye-dns run --config ./misteye-dns.tomlWith the terminal console disabled, the CLI prints INFO-level startup progress and runtime logs by default. Use -v for DEBUG or -vv for TRACE when troubleshooting:
INFO records each domain/IP submitted to MistEye together with its observation count, result, match count, and duration. High-frequency process-socket details are emitted only at DEBUG.
./misteye-dns -v run --config ./misteye-dns.toml
./misteye-dns -vv run --config ./misteye-dns.tomlConfigure the log file directly under [api]. While the terminal console is active, logs go only to this file so they cannot corrupt the full-screen UI. Without the console, the same logs are also printed to the terminal:
[api]
log_path = "misteye-dns.log"RUST_LOG can select module-specific filters. Since sudo may discard environment variables, pass it through sudo env:
sudo env RUST_LOG=misteye_dns=debug ./misteye-dns run \
--config ./misteye-dns.tomlImportant: starting the relay alone does not make the operating system use it. Relay-only mode observes only DNS requests sent directly to the Relay. If the Relay listens on port 53, you can manually set the active network service DNS to 127.0.0.1 (and ::1 when IPv6 loopback is enabled). If it listens on macOS's recommended non-53 port such as 15353, use a DNS client that supports a custom port or use system-DNS takeover mode. Enter IP addresses in system DNS settings, not 127.0.0.1:53.
For complete monitoring of the operating system DNS path, use system-DNS takeover mode. It configures the system resolver and, on macOS, installs the PF rules needed to redirect port 53 to a non-53 Relay port. If takeover mode is not used, manual DNS configuration is required for every network service that should be monitored.
After the local relay has been checked, start system-DNS takeover mode:
sudo ./misteye-dns run \
--config ./misteye-dns.toml \
--apply-system-dnsThis mode requires a loopback listener and administrator/root authorization. On macOS, the listener may use any non-zero local port. If 127.0.0.1:53 is already occupied by mDNSResponder, set listen = "127.0.0.1:15353" (or another free port); PF will redirect local IPv4/IPv6 UDP/TCP DNS traffic from port 53 to that configured relay port. On Linux and Windows, system takeover currently still requires port 53. A Webhook URL is optional; without it, malicious events are retained in SQLite. macOS changes system DNS and PF; Linux changes system DNS and nftables, so verify the recovery path first.
For macOS with Shadowrocket or another VPN/DNS proxy, this only observes queries that reach the system resolver at 127.0.0.1/::1. If the proxy's Network Extension resolves names itself through DoH, DoT, or a remote DNS server, those queries can bypass the local system DNS and PF. Configure the proxy to use the system DNS or disable its independent DNS interception when full system-DNS observation is required.
When Shadowrocket, a proxy client, or a VPN client is running on the host, configure its DNS output explicitly to the Misteye Relay when the client supports a custom port:
DNS server / DNS override: 127.0.0.1:15353
Fallback DNS: empty, or also 127.0.0.1:15353
With a custom-port setting, 127.0.0.1:15353 sends DNS requests to Misteye first, after which Misteye forwards them to the configured [[upstreams]]. Keep the local Relay entry first when the client also retains other resolver entries.
If a VPN client accepts only an IP address and not a port, use 127.0.0.1. When --apply-system-dns is enabled, PF redirects local port 53 to the Relay port configured by listen. For Network Extension/VPN clients that support a custom port, 127.0.0.1:15353 is preferred because it avoids relying on the port-53 redirect.
For Clash Verge or mihomo TUN mode, add the Relay address to both default-nameserver and nameserver. Keep the other entries generated by Clash if they are required by the current profile; the important change is adding the local Relay address first:
dns:
default-nameserver:
- 127.0.0.1:15353
# keep the existing Clash entries below if needed
nameserver:
- 127.0.0.1:15353
# keep the existing Clash entries below if neededdefault-nameserver is used to resolve configured DNS server addresses, while nameserver handles normal domain queries. Do not replace the entire Clash DNS block just for Misteye; add 127.0.0.1:15353 to the two existing lists and keep the TUN settings that already work in your installed Clash Verge version.
Keep the existing TUN stack, route, fake-IP, and DNS-hijack settings that already work in your Clash Verge version. Change one setting at a time when troubleshooting, because these options vary between clients and profiles.
The Clash “traffic tunnel management” feature is not required when dig @127.0.0.1 -p 15353 example.com succeeds. Use it only when the Relay's configured upstream DNS must itself be forwarded through a proxy; that requires a proxy node/group that supports UDP and a separate local tunnel port. Misconfigured tunnels can create a DNS loop.
Keep PF enabled for other system applications that do not have their own DNS setting. PF redirects system DNS to Misteye and blocks direct external UDP/TCP 53 bypasses. It cannot reliably intercept DoH/DoT/DoQ generated inside a VPN Network Extension or force DNS lookups performed internally by a remote proxy server; those cases require the VPN client to support a custom/local DNS server or a future macOS Network Extension/DNS Proxy integration.
Verify both the Relay endpoint and the PF redirect after configuration:
# Verify the configured Relay port
dig @127.0.0.1 -p 15353 example.com
# Verify PF redirection from system port 53 to the Relay port
dig @127.0.0.1 example.comStart the management service:
sudo ./misteye-dns serve --config ./misteye-dns.tomlOpen http://127.0.0.1:8080. By default the console is loopback-only. “Start relay” only starts forwarding; “Start + system DNS takeover” invokes the current platform's DNS and firewall backends and verifies the protected state. The console displays the current platform and connection-monitor backend, and can stop/recover, show queues and logs, and list observed domains.
Process-connection observation can be enabled or disabled in the console with a 1–60 second scan interval. These options apply when the Relay starts. The console shows the actual state for the current run, supports process-name and remote-IP filters, and can clear connection history. A malicious IP event shows the snapshotted source process, PID, executable path, protocol, port, and attribution confidence. Domain events explicitly remain DNS Relay observations with no reliable original-process identity. Clearing history does not stop the monitor or remove IPs already queued for detection, malicious events, or their saved source snapshots.
To expose it beyond the local machine, configure [api] and set token or token_env:
[api]
listen = "192.168.1.20:8080"
token = "change-this-long-random-token"
token_env = "MISTEYE_DNS_API_TOKEN"Loopback is recommended. A remote listener expands control over system DNS/PF and must be protected with a host firewall and a strong random token. The API never returns the API token through its configuration endpoint.
Use TOML for structured settings. Environment variables are optional and safer for secrets. The old single-upstream fields upstream and upstream_bootstrap remain supported for compatibility, but new configurations should use [[upstreams]]. Copy the checked-in template first:
cp misteye-dns.toml.example misteye-dns.toml
chmod 600 misteye-dns.tomlThe configuration supports these important fields:
| Section | Field | Purpose |
|---|---|---|
| root | listen |
Local relay address; system mode requires loopback. On macOS it may use a non-53 port and PF redirects system DNS to it; Linux/Windows currently require port 53. |
| root | [[upstreams]] |
Ordered resolver list; the next entry is tried after a timeout or transport failure. |
| root | window_seconds |
Domain/IP aggregation and deduplication window from 1 to 86400 seconds, defaulting to 120. Shorter windows submit detections sooner; longer windows merge more duplicate indicators. |
| root | database_url |
SQLite queue and state database. |
| root | udp_pool_size |
UDP/TCP/DoT upstream pool size. |
connection_monitor |
enabled |
Enable macOS/Linux process connection and direct-IP observation. |
connection_monitor |
scan_interval_seconds |
lsof snapshot interval from 1 to 60 seconds. |
terminal_ui |
mode |
auto enables interactive terminals, always forces it, and never disables it. |
terminal_ui |
refresh_interval_ms |
Automatic refresh interval from 250 to 5000 milliseconds. |
terminal_ui |
max_rows |
Maximum recent records loaded for each terminal panel, from 10 to 200. |
detection |
whitelist |
Exact domains, *.example.com subdomains, or exact IPs skipped by MistEye; service endpoint hosts are added automatically. |
[[upstreams]] |
url |
udp://, tcp://, tls://, or https:// resolver URL. |
[[upstreams]] |
bootstrap |
Bootstrap IPs required when that upstream host is a domain. |
misteye |
endpoint |
MistEye detection API endpoint. |
misteye |
api_key_env |
Environment variable name for the API key. |
misteye |
timeout_seconds |
Total detection request timeout; defaults to 15 seconds, range 1–300. |
misteye |
connect_timeout_seconds |
TCP/TLS connection timeout; defaults to 5 seconds, range 1–300 and no greater than the total timeout. |
webhook |
url |
Malicious-indicator and guard-state notification endpoint. |
webhook |
secret_env |
Environment variable name for the HMAC secret. |
system_dns |
snapshot_path |
DNS recovery snapshot path. |
api |
listen |
API and web-console bind address; defaults to 127.0.0.1:8080. |
api |
token / token_env |
API authentication token; required for non-loopback binds. |
api |
log_path |
CLI, management service, and Relay child-process log file; defaults to misteye-dns.log. |
Do not commit misteye-dns.toml, database files, snapshots, or PF files. They are ignored by .gitignore.
Run the doctor command before changing system networking:
./misteye-dns doctor \
--config ./misteye-dns.tomlExpected output:
configuration valid
The program binds the local relay first, prepares recovery state, applies PF protection, changes system DNS, and verifies both before reporting protected status. On macOS with a non-53 listener, the PF snapshot includes the 53→relay-port redirect. Press Ctrl-C for a normal shutdown; the program removes its PF anchor and restores the saved DNS state. If the process or machine stops unexpectedly, the recovery-helper performs the same cleanup when it detects the stale lease.
If a previous run left a recovery snapshot, restore it manually with:
sudo ./misteye-dns recover \
--snapshot ./misteye-dns.snapshot.json \
--firewall-snapshot ./misteye-dns.snapshot.firewall.jsonDo not run --apply-system-dns until the configuration and recovery path have been verified on the target Mac. The command changes host networking and requires administrator authorization.
MIT