ZJDNS: A Recursive DNS Server That Packs Every Transport It Can Find
High performance recursive DNS server with DNS hijack protection, TLS/QUIC/HTTPS/HTTP3/DTLS, (PQ)DNSCrypt, TLCP/DTLCP support
At a glance
- What is it?
- ZJDNS is a Go-based recursive resolver with DNSSEC, anti-pollution guards, and support for TLS, QUIC, HTTPS, DTLS, DNSCrypt, and Chinese TLCP/DTLCP. The breadth is real, but the documentation is thin in places and the license is unstated, so adoption needs care.
- Who is it for?
- ZJDNS is for operators who need a single recursive resolver that speaks every modern encrypted DNS transport, including the Chinese TLCP/DTLCP standards, and who are comfortable with a project that has no declared license and sparse English documentation. Do not adopt it if you require a clear open-source license, a stable release cadence, or a well-trodden upgrade path.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Star history
- hezhijie0327/ZJDNS46 ★ · +0
| Repository | 2026-08-23 | 2026-10-22 |
|---|---|---|
| hezhijie0327/ZJDNS | 46 | 46 |
0 Stars in the last 60 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
What ZJDNS Actually Solves
ZJDNS is a recursive DNS server written in Go. It resolves names from the IANA root servers down to authoritative servers, with full DNSSEC validation. The core problem it targets is DNS pollution and interception, common in networks that tamper with UDP DNS traffic. It also aims to be a single daemon that supports every encrypted transport you might want: DNS over TLS, DNS over QUIC, DNS over HTTPS, HTTP/3, DTLS, DNSCrypt with post-quantum KEMs, and the Chinese TLCP and DTLCP protocols. That last set is rare. Most open-source resolvers stop at DoT and DoH. ZJDNS goes further, which makes it attractive for operators in China or those who must interoperate with Chinese standards. The intended audience is network engineers who run their own recursive resolver and need both anti-pollution features and encrypted client-facing endpoints.
The Architecture: Memory Cache, Delegation Cache, and Singleflight
The resolver's design is visible from the README and the docs/ARCHITECTURE.md reference. It uses a pure in-memory store with an LRU cache, atomic counters, and optional snapshot files compressed with zstd. Cache hits go through a pre-packed direct-send path, which the README claims is zero-allocation and nanosecond-level. There is no way to verify that claim from the material, but the design intent is clear: minimize per-query overhead. A separate delegation cache stores zone-cut records, meaning NS names and DS records, in an LRU with a snapshot file. Subsequent queries for subdomains skip known hierarchy levels. Concurrent misses for the same key are merged with singleflight, so a burst of identical queries triggers only one upstream lookup. A/AAAA records are sorted by latency probe. Lazy expiration and a cap on entry count handle eviction. This is a coherent architecture for a high-QPS resolver, but the reliance on snapshots means the default configuration, with an empty state_file, loses all cache on restart.
Anti-Pollution Guards: How They Differ
The README lists four guards: hopguard, spoofguard, poisonguard, and splitguard. Each targets a different attack vector. Hopguard fingerprints the IP TTL of incoming packets to detect spoofed responses. Spoofguard reads UDP multiple times to catch forged answers that arrive after the real one. Poisonguard checks for out-of-zone data, which is a classic poisoning sign. Splitguard handles TCP segmentation to prevent RST injection. These are not just names; each is a per-upstream boolean flag in the config. You can enable them selectively. For example, a recursive upstream might have poisonguard, spoofguard, splitguard, and hopguard all true, while a forwarder to 8.8.8.8 might only need spoofguard. This granularity is useful, but it also means you must understand each guard's trade-offs. The documentation does not explain false-positive risks, such as whether hopguard drops legitimate responses from path-MTU changes. That is a gap.
Protocol Coverage: More Than Just DoT and DoH
The protocol table in the README is the project's centerpiece. Plain DNS on UDP/TCP port 53, DoT on 853, DoQ on 853, DoH and DoH3 on 443, DTLS on 853, DNSCrypt on 443, and TLCP variants for both DoT and DoH, plus DTLCP on 853. Each maps to an RFC or a Chinese standard, and the docs folder includes the RFC texts. The DNSCrypt support includes post-quantum KEMs, a forward-looking choice. TLCP uses SM2/SM3/SM4 ciphers, which are Chinese national standards. That is a significant differentiator. However, the README does not state how mature each protocol implementation is. For instance, DTLS support is marked as DTLS 1.2+, but there is no mention of DTLS 1.3. The TLCP support is tied to GB/T 38636-2020, but no interoperability test results are provided. You are taking the implementation on faith.
Getting It Running: Commands and Config Keys
Building is straightforward. The README gives `go build -o zjdns ./cmd/zjdns`. Running with no arguments starts a recursive resolver on UDP and TCP port 53. You can specify a config file with `--config config.json`, or generate a sample with `--generate-config`. For DNSCrypt, there is `--generate-config --dnscrypt --provider example.com`. Client testing uses standard tools: `dig`, `kdig`, and for DNSCrypt, a TXT query to `2.dnscrypt-cert.example.com`. The config schema is JSON. Upstream entries have fields like `address`, `protocol`, `server_name`, `skip_tls_verify`, `privacy_profile`, `skip_cache`, `match`, `proxy`, `public_key`, `pqdnscrypt`, `ephemeral_keys`, and the four guard booleans. The server block configures listening ports per protocol, certificate settings, and features like ECS, DNS64, cache, and KTLS. One notable key is `state_file` under cache: it defaults to empty, meaning no persistence. You must set a path to enable snapshots. The README also mentions `./zjdns.cache` as an example, so you can split cache, latency, and delegation stores with separate state files.
The RFC 10029 MQTYPE Feature: Bundled Queries
A distinctive feature is MQTYPE, based on RFC 10029. It allows an upstream to bundle multiple QTYPEs into a single query using MQQUERY. The config takes a list of numeric QTYPEs, like `[1, 28]` for A and AAAA. The outbound query attaches an MQQUERY bundle, and the resolver merges the records into a warm cache, including DNSSEC signatures, then strips the bundled records from the client response. The server side also supports client MQQUERY merging, with eight FORMERR validation checks and a merge budget. If the authoritative server does not support it, ZJDNS falls back to independent queries. This is an advanced optimization that reduces round trips. But it is new, and the README does not specify which authoritative servers support it. In practice, you may rarely see the benefit unless your upstreams are also modern. The fallback is a safety net, but it means the feature only pays off in controlled environments.
Limitations and Gaps You Should Know
The most obvious limitation is the license. The repository metadata says NOASSERTION, and the README has a LICENSE link but no text is shown. That is a blocker for many organizations. You cannot legally redistribute or modify the code without knowing the terms. Second, the documentation is in Chinese. The README is entirely in Chinese, with no English translation. That limits the audience. Third, the project has no recent releases listed and no known last push date. The material does not show a version number or a changelog. You are building from the default branch, which could change under you. Fourth, KTLS requires Linux and `modprobe tls`, and it silently falls back on non-Linux. That is fine, but it means you cannot rely on KTLS on BSD or macOS. Fifth, the snapshot persistence is opt-in. If you do not set state_file, a restart clears everything, including the delegation cache, which means a cold start after every reboot. That is a real operational cost.
Alternatives: Where ZJDNS Fits and Where It Does Not
The obvious alternative is CoreDNS, which also runs in Go and supports DoT, DoH, and forward proxies. CoreDNS is plugin-based, has a large ecosystem, and is widely deployed in Kubernetes. The difference is that CoreDNS is primarily a forwarder and plugin host, not a full recursive resolver with DNSSEC validation out of the box. You would need to add plugins or use a separate recursor like Unbound. Unbound is the other alternative. It is a mature recursive resolver with DNSSEC, and it supports DoT and DoH in recent versions. But Unbound does not support QUIC, DTLS, DNSCrypt, or TLCP. ZJDNS covers those protocols natively. So the choice is: if you need the exotic transports, ZJDNS is the only open-source option that lists them. If you need a stable, well-documented, license-clear resolver, Unbound or CoreDNS with a recursor backend is safer. The trade-off is protocol breadth versus ecosystem maturity.
Maintenance and Upgrade Cost
The material gives little to assess maintenance. There are no release notes, no version tags, and no changelog. The project appears to be a single-maintainer effort, given the repository name. That means you are dependent on one person's schedule. The codebase is in Go, which compiles to a single binary, so deployment is simple. Upgrades would mean rebuilding from source and testing your config against the new binary. The config schema is JSON, and the README shows examples, but there is no migration guide. The snapshot format is zstd-compressed, and the README mentions a state file for DNSCrypt certificates as well. If you enable persistence, you must manage those files and ensure they are backed up. The lack of a declared license is the biggest maintenance risk: if the author abandons the project, you have no legal basis to fork it. Before relying on ZJDNS, you should contact the author or check the LICENSE file in the repository directly.
Editorial conclusion
ZJDNS is for operators who need a single recursive resolver that speaks every modern encrypted DNS transport, including the Chinese TLCP/DTLCP standards, and who are comfortable with a project that has no declared license and sparse English documentation. Do not adopt it if you require a clear open-source license, a stable release cadence, or a well-trodden upgrade path. Before deploying, verify the license with the author, confirm the DNSCrypt and TLCP configurations against your environment, and test the snapshot persistence behavior, since the default is no persistence at all.
Frequently asked questions
How do I build and start ZJDNS?
Build with `go build -o zjdns ./cmd/zjdns`, then run `./zjdns` for a default recursive resolver on UDP and TCP, or `./zjdns --config config.json` for your own file. `./zjdns --generate-config` writes an example configuration, and adding --dnscrypt --provider writes a DNSCrypt one.
Which ports does ZJDNS listen on?
53 for plain DNS over UDP and TCP, 853 for DoT, DoQ, TLCP DoT and DTLCP, and 443 for DoH, DoH3, DNSCrypt and TLCP DoH. The container image additionally exposes 8443, 8853, 9853, 9443 and 6060.
What does the fallback flag do for a ZJDNS upstream?
A fallback upstream is queried in parallel with the primary ones, but its answer is only used if the primary produces nothing within 500 ms or every primary fails early. An adopted result carries EDE 65280, is never cached, and the primary keeps filling the cache behind it.
Does KTLS acceleration work on every platform in ZJDNS?
No. The offload uses af_alg with setsockopt(TCP_ULP) and applies only to TLS and HTTPS over TCP on Linux. On other systems it silently falls back to userspace encryption instead of failing.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hezhijie0327-zjdns)Star history chart for your README
[](https://hysenlabs.com/projects/hezhijie0327-zjdns)