Skip to content

Latest commit

 

History

History
151 lines (115 loc) · 5.07 KB

File metadata and controls

151 lines (115 loc) · 5.07 KB

dnsink

A high-performance Rust DNS proxy that blocks malware, C2, and phishing domains at the DNS layer using live threat-intelligence feeds. Shannon-entropy tunneling detection with CDN whitelisting, DoH upstream, hot-reload, Prometheus metrics, terminal dashboard.

CI Docker Deploy with flyctl License: MIT

Contents

Quickstart

docker run -d --name dnsink \
  -p 53:5353/tcp -p 5353:5353/udp -p 9090:9090 \
  ghcr.io/kakarot-dev/dnsink:v0.2.0

dig @127.0.0.1 +tcp example.com         # resolves
dig @127.0.0.1 +tcp malware.example.com  # NXDOMAIN
curl http://127.0.0.1:9090/metrics       # Prometheus counters

Or launch the terminal dashboard:

cargo run --release -- --tui

Features

dnsink Pi-hole AdGuard Home crab-hole
Language Rust Shell/PHP Go Rust
Security feeds (URLhaus, OpenPhish, PhishTank) Yes No No No
DNS tunneling detection (Shannon entropy + CDN whitelist) Yes No No No
Bloom filter pre-screening Yes No No No
DNS-over-HTTPS upstream Yes Needs cloudflared Yes Yes
Hot-reload (lock-free via ArcSwap) Yes Restart-based Yes Yes
Prometheus /metrics Yes No No No
Two-stage lookup ~490 ns

Unlike Pi-hole and AdGuard Home (ad-blocking focused), dnsink targets active threat infrastructure — C2 servers, phishing pages, malware domains — using feeds that update hourly. The engine is feed-agnostic: ad/tracker blocking is a one-line opt-in via oisd = true in config (uses oisd.nl's ~32K-domain list). Point it at any domain list you want.

How it works

Client query (UDP/TCP :5353)
        |
        v
+---------------+
|   DnsProxy    |  receives raw DNS bytes, starts latency timer
+-------+-------+
        |
        v
+---------------+
| BloomFilter   |  stage 1: ~184 ns, 117 KB for 100K items, 1% FPR
|               |  definite miss -> skip trie, forward immediately
+-------+-------+
        | maybe blocked
        v
+---------------+
|  DomainTrie   |  stage 2: label-reversed radix trie
|               |  is_blocked at any ancestor = wildcard block
+-------+-------+
        |
   +----+----+
   |         |
blocked    allowed
   |         |
   v         v
NXDOMAIN  forward to upstream (UDP, TCP, or DoH)
   |         |
   +----+----+
        |
        v
   log + metrics

Two-stage lookup. The bloom filter eliminates the 99% of queries that are legitimate traffic in ~184 ns. Only probable matches reach the radix trie for authoritative confirmation. The trie stores domains in reverse-label order so wildcard blocks (malware.com blocks *.malware.com) fall out of the traversal naturally.

Hot-reload. Blocklists refresh on a configurable interval without dropping in-flight queries. ArcSwap gives lock-free reads; old data stays alive via Arc refcounts until outstanding queries drain.

Tunneling detection. Subdomain labels are scored by Shannon entropy; anything above the configured threshold with length above the minimum is flagged. A CDN whitelist (AWS / Akamai / Cloudflare) suppresses false positives on legitimate high-entropy providers using label-boundary-safe suffix matching.

Benchmarks

Criterion, 100K domains, release build:

Operation Time
Bloom lookup (miss) 184 ns
Bloom lookup (hit) 87 ns
Two-stage (miss) 288 ns
Two-stage (hit) 491 ns

Deploy

Docker (distroless/cc-debian12:nonroot, multi-arch amd64 + arm64):

docker run -d -p 53:5353/tcp -p 5353:5353/udp -p 9090:9090 \
  ghcr.io/kakarot-dev/dnsink:v0.2.0

Fly.iofly.toml ships with the repo. Requires a dedicated IPv4 for UDP ($2/mo):

flyctl apps create <your-app>
flyctl ips allocate-v4 --yes
flyctl deploy

fly.io requires UDP services to bind fly-global-services (wrong source IP on replies otherwise). The repo's config.docker.toml uses an asymmetric listen.tcp_address override to handle this. See config.docker.toml and fly.toml.

Configuration

Default config at config.toml. Minimal example:

[listen]
address = "127.0.0.1"
port = 5353

[upstream]
protocol = "doh"
doh_url = "https://1.1.1.1/dns-query"

[feeds]
urlhaus = true
openphish = true
refresh_secs = 3600

[tunneling_detection]
enabled = true
entropy_threshold = 3.5

[metrics]
bind_addr = "127.0.0.1:9090"

Full schema in src/config.rs.

License

MIT