Skip to content

Repository files navigation

KixDNS

English | 简体中文

Notice: This project is entirely built by AI (content, documentation, and the initial implementation were generated by AI).

KixDNS is an asynchronous, non-recursive DNS forwarding server written in Rust. It accepts DNS queries over UDP and TCP, optionally over inbound DoH, then applies ordered pipeline rules before forwarding or constructing a response.

This document describes the implementation currently on the main branch. Configuration fields that are accepted by deserialization but are not used by the running engine are called out explicitly.

Features

Performance

  • Zero-copy UDP processingBytesMut-based packet handling minimizes memory copies.
  • Lazy request parsing — forwarding can avoid full deserialization when no rule needs parsed fields.
  • Lightweight response scanning — extracts response metadata needed by cache and response rules.
  • Fast hashing — internal data structures use rustc-hash (FxHash) where applicable.
  • Async I/O — built on tokio with concurrent state managed by DashMap and moka.
  • Adaptive flow controlPermitManager adjusts concurrency using upstream latency.
  • Worker-aware UDP sockets — Unix workers use the platform's reuse-port support; non-Unix builds share one UDP socket.

Flexible Routing

  • Pipeline selection rules — route by listener label, client IP, domain, QCLASS, EDNS, GeoIP, GeoSite, and query type.
  • Logical matcher operatorsand, or, and_not, or_not, and not compose ordered matcher chains.
  • Two-phase processing — request matching can be followed by response matching and response actions.
  • Listener labels — select different pipelines from the same server instance.
  • Multiple upstream transports — UDP, TCP, TCP+UDP, DoH (RFC 8484), DoT, and DoQ (RFC 9250).
  • URL protocol prefixesudp://, tcp://, doh://, dot://, and doq:// select the transport.
  • EDNS Client Subnet (RFC 7871) — pipeline-level cache isolation plus Forward-action request rewriting.

Cache & Reliability

  • In-memory cache — configurable capacity, maximum lifetime, minimum TTL, and stale-serving behavior.
  • Concurrent miss deduplication — identical in-flight cache misses share one upstream operation.
  • Background refresh — near-expiry entries can be refreshed asynchronously.
  • Serve Stale (RFC 8767) — optionally return expired cache entries when upstream access fails.

GeoIP, GeoSite & DoQ

  • MaxMind GeoIP — MMDB lookup and country/private-IP matchers.
  • V2Ray GeoSite — load .dat or supported JSON files for domain-category matchers.
  • Database reload — configured GeoSite and GeoIP .dat files are watched and reloaded.
  • DoQ 0-RTT handling — global and per-upstream settings, with rejection/timeout fallback until restart.

Operations

  • Hot configuration reload — valid JSON changes are reloaded through the file watcher; invalid changes keep the previous configuration.
  • Structured tracing — text output by default, with filtering controlled by --debug and RUST_LOG.
  • GeoIP conversion — convert a V2Ray GeoIP .dat file to MMDB from the command line.

What Is Implemented

  • Inbound DNS over UDP and TCP.
  • Optional inbound DNS over HTTPS (DoH, RFC 8484) with a PEM certificate and private key.
  • Outbound UDP, TCP, TCP+UDP hedged requests, DoH, DoT, and DoQ.
  • Ordered pipeline selection and ordered request rules.
  • Request matchers for listener label, client address, domain, query type/class, EDNS, GeoIP, and GeoSite.
  • Response matchers and response actions for upstream fallback, filtering, response replacement, and response-based pipeline jumps.
  • An in-memory DNS cache with configurable capacity, maximum lifetime, minimum TTL floor, concurrent miss deduplication, background refresh, and optional RFC 8767 stale serving.
  • ECS request rewriting (RFC 7871) at Forward-action level, with optional pipeline-level ECS cache isolation.
  • MaxMind MMDB GeoIP lookup and V2Ray GeoIP/GeoSite data loading.
  • File watchers for the main JSON configuration, configured GeoSite files, and the configured GeoIP .dat/JSON file.
  • A command-line GeoIP .dat to MMDB converter.

Unix builds create separate UDP sockets for workers with the platform's reuse-port support. Non-Unix builds use one UDP socket shared by the workers. This is an implementation detail, not a throughput guarantee.

Quick Start

💡 New to KixDNS config? Try the Visual Config Editor — it is a browser-based GUI that generates pipeline.json for you.

Config Editor Preview

Build and run

The repository uses Rust edition 2024. The project does not declare an MSRV; use a toolchain that supports edition 2024.

cargo build --release

# No subcommand: uses config/pipeline.json and the default listener label
./target/release/kixdns

# Explicit run command
./target/release/kixdns run -c config/pipeline.json

The default listener addresses are UDP and TCP on 0.0.0.0:5353. The default upstream is 1.1.1.1:53. All paths are passed to the filesystem as written; relative paths are relative to the process working directory.

CLI

kixdns [COMMAND]

COMMANDS:
  run              Run the DNS server
  convert-geo-ip   Convert a GeoIP .dat file to MMDB
  help             Print help

Run options:

-c, --config <FILE>             Configuration path (default: config/pipeline.json)
    --listener-label <LABEL>    Listener label used by pipeline selectors (default: default)
    --debug                     Enable debug-level logging
    --udp-workers <NUM>         UDP worker count (0 selects available CPU parallelism)
-h, --help                      Show help
-V, --version                   Show version

GeoIP conversion options:

-i, --input <FILE>              Input V2Ray GeoIP .dat file
-o, --output <FILE>             Output MMDB file
-f, --filter <CODES>            Comma-separated country codes, for example CN,US,JP

The logging subscriber is text formatted by default, with RFC 3339 timestamps following the system's local timezone. The default filter is error unless --debug or the RUST_LOG environment variable changes it. The code does not configure JSON log output.

systemd

Create /etc/systemd/system/kixdns.service:

[Unit]
Description=KixDNS
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/kixdns run -c /etc/kixdns/pipeline.json
Restart=on-failure
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
sudo install -m 0755 target/release/kixdns /usr/local/bin/kixdns
sudo mkdir -p /etc/kixdns
sudo cp config/pipeline.json /etc/kixdns/
sudo systemctl daemon-reload
sudo systemctl enable --now kixdns

Network Protocols

Inbound

UDP and TCP listeners are always created from settings.bind_udp and settings.bind_tcp.

Inbound DoH is disabled unless settings.bind_doh is set. When it is set, settings.doh_tls_cert and settings.doh_tls_key are required and must point to PEM files. The path defaults to /dns-query and can be changed with settings.doh_path.

The inbound DoH handler:

  • accepts POST requests at the configured path and reads a DNS wire message body;
  • accepts GET requests at the configured path with a dns base64url-without-padding query parameter;
  • rejects DNS bodies larger than 64 KiB;
  • returns application/dns-message for a successful DNS response.

DoT and DoQ are implemented as outbound transports. There are no inbound DoT or DoQ listeners.

Outbound

The Forward action has an optional transport field:

Value Transport
udp DNS over UDP
tcp DNS over TCP
tcp_udp Sends TCP and UDP concurrently and uses the first accepted response
doh DNS over HTTPS
dot DNS over TLS
doq DNS over QUIC

The upstream field accepts a string, a comma-separated string, or a JSON string array. Multiple upstreams are queried concurrently and the first accepted response is returned. A response with SERVFAIL or REFUSED is not accepted when choosing among multiple upstreams.

The following prefixes override the transport field:

Prefix Transport Alias
udp:// UDP
tcp:// TCP
tcp+udp://, udp+tcp:// TCP+UDP
doh:// DoH https://
dot:// DoT tls://
doq:// DoQ quic://

Without a prefix, an omitted transport means UDP. DoH URLs without a path use /dns-query. DoH supports a host query parameter as an HTTP Host override; that parameter is removed before the request is sent.

DoT uses port 853 when no port is supplied. Its optional TLS name is supplied with sni or servername and it must not contain a DNS path.

DoQ uses port 853 when no port is supplied. Its optional query parameters are sni/servername and 0rtt/enable_0rtt. A DoQ upstream written with an IP literal requires an explicit SNI. 0-RTT is enabled globally by default, can be overridden per upstream, and is disabled for that upstream after a rejection or timeout until the process restarts.

When UDP forwarding fails, or when an upstream UDP response is truncated, TCP fallback is enabled by default and can be disabled with settings.enable_tcp_fallback. The fallback is applied by the forwarding path; it is not an additional listener.

Configuration

The configuration is JSON:

{
  "version": "1.0",
  "settings": {},
  "pipeline_select": [],
  "pipelines": []
}

version is optional. settings, pipeline_select, and pipelines default to an empty/default value when omitted. If no pipeline selector matches, the first pipeline is used. If there is no pipeline, the request uses the global default upstream.

Global settings

Field Default Meaning
min_ttl 0 Minimum upstream/cache TTL in seconds.
bind_udp 0.0.0.0:5353 UDP listen address.
bind_tcp 0.0.0.0:5353 TCP listen address.
bind_doh null Inbound DoH listen address; null disables it.
doh_tls_cert null PEM certificate path; required with bind_doh.
doh_tls_key null PEM private-key path; required with bind_doh.
doh_path /dns-query Inbound DoH request path.
default_upstream 1.1.1.1:53 Default upstream. A comma-separated list is accepted.
upstream_timeout_ms 9000 Timeout for one upstream operation.
request_timeout_ms null Overall request timeout. When null, it is upstream_timeout_ms * 2.5. It must be at least upstream_timeout_ms.
response_jump_limit 10 Maximum response-phase pipeline jumps.
udp_pool_size 64 Outbound UDP socket-pool size.
tcp_pool_size 64 TCP connections per upstream pool.
doh_pool_size 8 Maximum idle DoH connections per upstream.
dot_pool_size 64 DoT connections per upstream pool.
doq_pool_size 16 DoQ connections per upstream pool.
tcp_health_check_error_threshold 3 Reset a TCP/DoT connection after this many consecutive errors; 0 disables this check.
tcp_connection_max_age_seconds 300 Maximum TCP/DoT connection age; 0 disables aging.
tcp_connection_idle_timeout_seconds 60 TCP/DoT idle timeout; 0 disables it.
doq_connection_idle_timeout_seconds 60 DoQ idle timeout; 0 disables it.
doq_keepalive_interval_ms 15000 DoQ keepalive interval; 0 disables keepalive.
doq_enable_0rtt true Global DoQ 0-RTT setting.
enable_tcp_fallback true Retry UDP failure or truncation with TCP.
flow_control_enabled false Enable adaptive permit-based flow control.
flow_control_initial_permits 500 Initial permits when flow control is enabled.
flow_control_min_permits 100 Minimum permits when flow control is enabled.
flow_control_max_permits 800 Maximum permits when flow control is enabled.
flow_control_latency_threshold_ms 100 Latency threshold used by adaptive flow control.
flow_control_adjustment_interval_secs 5 Permit adjustment interval.
cache_capacity 10000 Maximum entries in the DNS response cache; must be greater than zero.
cache_max_ttl 86400 Maximum lifetime of a DNS cache entry in seconds.
dashmap_shards 0 Internal shard setting; 0 uses the DashMap default, otherwise the value must be a power of two.
cache_background_refresh false Refresh entries before their TTL expires.
cache_refresh_threshold_percent 10 Refresh threshold based on remaining TTL percentage.
cache_refresh_min_ttl 5 Minimum refresh TTL considered for background refresh.
serve_stale false Keep and serve expired entries according to RFC 8767 behavior.
serve_stale_ttl 30 TTL written into a stale response.
serve_stale_expire_ttl 86400 Maximum stale age in seconds; 0 means no stale-age limit.
serve_stale_ttl_reset true Reset the stale-age window when stale data is served.
serve_stale_client_timeout_ms 0 0 serves stale immediately; a positive value tries the upstream for this many milliseconds first.
geoip_db_path null MaxMind MMDB path.
geoip_dat_path null V2Ray GeoIP .dat or supported V2Ray JSON path. The current range loaders use IPv4 ranges.
geosite_data_paths [] V2Ray GeoSite .dat or JSON paths; multiple files are accepted.

The following fields are deserialized by the current config type but are not read by the running engine: geoip_auto_convert and geoip_filter_countries. Use convert-geo-ip with --filter for conversion-time filtering. The top-level background_refresh_rule is also currently ignored by runtime configuration compilation.

Pipeline selection

Each item in pipeline_select has a pipeline id, an optional matcher list, and an optional matcher_operator. Items are evaluated in array order. The first matching item whose pipeline id exists selects that pipeline.

Pipelines and rules

Each pipeline has an id, an optional rules array, and an optional pipeline-level ecs object. Rules are evaluated in configuration order. An empty matcher list matches. Within a rule, matchers form a left-to-right chain; each matcher can carry its own operator, or the rule-level matcher_operator can set the operator for a list whose item operators are all omitted.

The same structure is used for response_matchers, response_matcher_operator, response_actions_on_match, and response_actions_on_miss. Response actions run after a Forward response, or on the miss path after upstream attempts fail when miss actions are configured.

Pipeline selector and request matchers

Type Fields
any none
listener_label value
client_ip cidr
domain_suffix value
domain_regex value
qclass value: IN, CH/CHAOS, or HS
edns_present expect: boolean
geosite value: GeoSite tag
geosite_not value: GeoSite tag
geoip_country country_codes: string array
geoip_private expect: boolean
qtype value: A, AAAA, CNAME, MX, TXT, NS, PTR, SOA, SRV, or OPT

Pipeline selectors support all of the rows above. Request rules support all rows except listener_label.

Domain suffix matching and GeoSite matching are case-insensitive. domain_regex and request_domain_regex use Rust regular-expression syntax.

Response matchers

Type Fields
upstream_equals value; compares the runtime upstream label literally
request_domain_suffix value
request_domain_regex value
response_upstream_ip cidr; comma-separated CIDRs are accepted
response_answer_ip cidr; comma-separated CIDRs are accepted
response_type value; the first answer record type, or the query type if there is no answer
response_rcode value: NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED; an unknown value acts as the OTHER fallback
response_qclass value
response_edns_present expect: boolean
response_answer_ip_geoip_country country_codes: string array
response_answer_ip_geoip_private expect: boolean
response_request_domain_geosite value: GeoSite tag
response_request_domain_geosite_not value: GeoSite tag
response_txt_content mode: exact, prefix, or regex; value is the text/pattern

The successful upstream label currently includes the transport prefix, for example udp:1.1.1.1:53 or tcp:1.1.1.1:53. Therefore upstream_equals values must include that prefix. response_upstream_ip currently parses a raw IP or host:port value; it does not strip the transport prefix.

Logical operators

Operators are evaluated left to right. The first matcher seeds the result.

Operator Meaning
and The next matcher must be true. This is the default.
or The next matcher is used when the accumulated result is false.
and_not The next matcher must be false when the accumulated result is true.
or_not The next matcher must be false when the accumulated result is false.
not Accepted as a deserialization alias for and_not.

The aliases and-not, andnot, or-not, and ornot are also accepted.

Actions

Type Fields Behavior
log level (optional) Emits a tracing event for the matched rule. Supported levels are trace, debug, info, warn, and error.
static_response rcode Returns NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, or REFUSED.
static_ip_response ip Returns an A or AAAA response based on the IP address.
static_txt_response text, ttl (optional) Returns a TXT response. text accepts a string or string array; ttl defaults to 300.
jump_to_pipeline pipeline Starts processing the referenced pipeline.
allow none Request phase: forward with the global default UDP upstream. Response phase: keep the current upstream response.
deny none Returns REFUSED.
forward upstream (optional), transport (optional), ecs (optional) Forwards to the selected upstream. A missing upstream uses default_upstream.
continue none Continues with the next request rule, or the next pipeline decision in the response flow.
replace_txt_response text Response-phase action that replaces TXT records in the current response. In the request phase it does not produce a response.

ECS

ECS is configured on a Forward action:

{
  "type": "forward",
  "upstream": "8.8.8.8:53",
  "ecs": {
    "mode": "from_client_ip",
    "prefix_v4": 24,
    "prefix_v6": 56
  }
}

The supported modes are:

  • clear: remove the request ECS option;
  • from_client_ip: derive the subnet from the client address, defaulting to /24 for IPv4 and /56 for IPv6; private, loopback, and ULA addresses are not injected;
  • static: inject a configured IP and prefix.

A pipeline-level ecs object changes the cache key so responses can be isolated by client subnet. It does not replace the Forward action's ECS rewrite setting. The engine warns when an action uses ECS without a pipeline-level ecs setting.

Configuration examples

Basic routing and static response

{
  "version": "1.0",
  "settings": {
    "bind_udp": "0.0.0.0:5353",
    "bind_tcp": "0.0.0.0:5353",
    "default_upstream": "1.1.1.1:53"
  },
  "pipeline_select": [
    {
      "pipeline": "internal",
      "matchers": [
        { "type": "listener_label", "value": "edge-internal" }
      ]
    }
  ],
  "pipelines": [
    {
      "id": "internal",
      "rules": [
        {
          "name": "internal-dns",
          "matchers": [
            { "type": "domain_suffix", "value": ".internal" }
          ],
          "actions": [
            { "type": "forward", "upstream": "10.0.0.53:53", "transport": "tcp" }
          ]
        }
      ]
    },
    {
      "id": "default",
      "rules": [
        {
          "name": "block-example",
          "matchers": [
            { "type": "domain_suffix", "value": ".blocked.example" }
          ],
          "actions": [
            { "type": "static_response", "rcode": "NXDOMAIN" }
          ]
        },
        {
          "name": "default-forward",
          "matchers": [ { "type": "any" } ],
          "actions": [
            { "type": "forward", "upstream": null }
          ]
        }
      ]
    }
  ]
}

Response-based fallback

{
  "settings": {
    "default_upstream": "223.5.5.5:53"
  },
  "pipelines": [
    {
      "id": "fallback",
      "rules": [
        {
          "name": "reject-polluted-answer",
          "matchers": [ { "type": "any" } ],
          "actions": [
            { "type": "forward", "upstream": "223.5.5.5:53", "transport": "udp" }
          ],
          "response_matchers": [
            { "type": "response_answer_ip", "cidr": "127.0.0.0/8,0.0.0.0/8" }
          ],
          "response_actions_on_match": [
            { "type": "continue" }
          ],
          "response_actions_on_miss": [
            { "type": "allow" }
          ]
        },
        {
          "name": "backup",
          "matchers": [ { "type": "any" } ],
          "actions": [
            { "type": "forward", "upstream": "8.8.4.4:53", "transport": "tcp" }
          ]
        }
      ]
    }
  ]
}

Inbound DoH

{
  "settings": {
    "bind_doh": "0.0.0.0:8443",
    "doh_tls_cert": "/etc/kixdns/cert.pem",
    "doh_tls_key": "/etc/kixdns/key.pem",
    "doh_path": "/dns-query"
  },
  "pipelines": [
    {
      "id": "default",
      "rules": [
        {
          "name": "forward-all",
          "matchers": [ { "type": "any" } ],
          "actions": [ { "type": "forward", "upstream": "1.1.1.1:53" } ]
        }
      ]
    }
  ]
}

Cache and reload behavior

The DNS response cache uses the upstream response's minimum TTL, raised to min_ttl when configured, and is capped by cache_max_ttl and cache_capacity. Negative responses use the SOA negative-cache TTL when an SOA is present. Identical in-flight cache misses share one upstream operation.

When cache_background_refresh is enabled, entries near expiry can trigger an asynchronous refresh. A failed refresh leaves the existing entry in place. When serve_stale is enabled, an expired entry can be returned with serve_stale_ttl, subject to serve_stale_expire_ttl and serve_stale_client_timeout_ms.

The main configuration watcher reloads a valid JSON file and clears the rule cache. Invalid reloads leave the previous configuration active. The listener addresses, UDP worker count, TLS DoH listener, connection-pool construction, cache construction, and other Engine initialization settings are created at startup; changing those settings should be followed by a restart.

GeoSite files in geosite_data_paths are watched and reloaded. GeoIP files supplied through geoip_dat_path are watched and reloaded. A GeoIP MMDB supplied through geoip_db_path is loaded at startup; the current watcher is for geoip_dat_path, not the MMDB path.

Tech Stack

  • tokio — asynchronous runtime
  • hickory-proto — DNS protocol and wire messages
  • serde / serde_json — configuration serialization
  • moka / dashmap — concurrent caches and maps
  • rustc-hash — fast hashing
  • quinn — QUIC/DoQ transport
  • reqwest / hyper — HTTP and DoH clients/servers
  • tokio-rustls / rustls — TLS for DoT, DoQ, and inbound DoH
  • maxminddb / maxminddb-writer — GeoIP MMDB lookup and conversion
  • arc-swap / notify — configuration state and file watching
  • clap / tracing — CLI parsing and logging

Tools

Config Editor

tools/config_editor.html is a browser-based editor for settings, pipeline selectors, pipelines, rules, matchers, actions, ECS fields, TXT actions, and response rules. It provides JSON import, JSON preview, JSON download, and a Mermaid flowchart view.

The file loads Vue, Bootstrap, and Mermaid from public CDNs, so the browser needs access to those URLs. Open the file directly, import or edit a configuration, download the JSON, then validate it by starting KixDNS:

tools/config_editor.html

See tools/README.md for the tool-specific notes.

tools/diagnose.html is a small browser smoke-check page for the editor's Vue/TXT/matcher code. It is not a DNS query service and does not provide a WebSocket API.

tools/check_geosite_tags.rs is a standalone source file in the repository. Cargo.toml does not declare it as a bin or example target.

Build and test

cargo build --release
cargo test
cargo clippy --all-targets --all-features -- -D warnings

The release profile sets opt-level = 3, lto = fat, codegen-units = 1, panic = abort, and strip = true.

The main dependencies include tokio, hickory-proto, serde, moka, dashmap, reqwest, tokio-rustls, quinn, maxminddb, notify, clap, and tracing. The exact versions are defined by Cargo.toml and Cargo.lock.

License

GPL-3.0

About

一个使用 RUST 构建的 DNS 转发器

Resources

Stars

73 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages