Configuration Overview
Before Starting
OxiDNS uses YAML configuration. For day-to-day editing, it is easiest to understand the file as six top-level parts:
runtime:
worker_threads: 4
api:
http: "127.0.0.1:9088"
log:
level: info
file: ./oxidns.log
network:
outbound:
default: direct
profiles:
direct:
resolver: system
proxy: none
include: []
plugins:
- tag: seq_main
type: sequence
args:
- exec: "forward 1.1.1.1"
Where:
runtime- Runtime parameters.
api- Management API settings.
log- Log output settings.
network- Shared outbound networking settings, such as resolver and proxy choices for HTTP downloads, upgrade checks, and webhook requests.
include- Load plugin definitions from other configuration files.
plugins- All plugin instance definitions. OxiDNS composes the full DNS pipeline from plugins.
After editing a config, validate it before starting:
oxidns check -c config.yaml
If the config uses relative paths and the runtime working directory is not the config directory, pass the working directory explicitly. -d is the single base for all runtime relative paths, including logs, SQLite files, rule files, and api.http.webui.root; paths do not become relative to /etc/oxidns just because the config file lives there:
oxidns check -c /etc/oxidns/config.yaml -d /var/lib/oxidns
In the Debian default layout, the config file lives at /etc/oxidns/config.yaml, while runtime-relative resources live under /var/lib/oxidns.
When the plugin composition is still undecided, start from Common Scenarios, then return to this page for field details.
Environment Variable Substitution
During startup, oxidns check, management API validation, and validation before saving a config, OxiDNS first parses the YAML into a data structure and then expands ${VAR} placeholders inside string scalars. The config.yaml file itself is not rewritten, so the WebUI still reads and saves the original placeholders.
Supported syntax:
| Syntax | Behavior |
|---|---|
${VAR} | Use the value of process environment variable VAR; fail if it is undefined |
${VAR:-default} | Use default when VAR is undefined or an empty string |
${env:VAR} | Explicitly read process environment variable VAR; useful when the name conflicts with a runtime placeholder |
${env:VAR:-default} | Explicitly read process environment variable VAR; use default when it is undefined or empty |
$${...} | Emit a literal ${...} |
Runtime placeholders used by executors such as script and http_request are preserved until request execution, so values like ${qname}, ${client_ip}, and ${resp_ip} are not treated as process environment variables during config loading. Use the explicit form, such as ${env:qname}, if you really need to read an environment variable with the same name.
Undefined variables fail fast, and the error includes the variable name and the YAML path of the offending scalar (for example plugins[0].args.password) so empty passwords or certificate paths do not silently pass validation.
Example:
api:
http:
listen: ${API_LISTEN:-0.0.0.0:8080}
ssl:
cert: ${API_TLS_CERT}
key: ${API_TLS_KEY}
auth:
type: basic
username: ${ADMIN_USER}
password: ${ADMIN_PASS}
Because substitution happens after YAML parsing, an environment value may contain any characters — *, &, :, #, ', ", \, newlines, even binary bytes — without breaking the config syntax. You do not need to manually quote values that contain special characters. When the entire scalar is exactly one placeholder (e.g. timeout: ${CACHE_TTL}), the expanded value is re-parsed once against the YAML 1.2 scalar rules, so number / boolean / null-shaped environment values still match numeric / boolean / null fields; everywhere else the value lands as a plain string. include paths support placeholders too:
include:
- ${OXIDNS_CONF_DIR}/plugins/common.yaml
Top-Level Fields
include
# []string, load plugin settings from other configuration files.
include:
- ./plugins/common.yaml
- ./plugins/server.yaml
Field notes:
include- Loads only
pluginsfrom included files. It does not merge includedruntime,api, orlogsettings. - Merge order is include-first: recursively load each
includein array order, then append the current file'splugins. - Relative paths are resolved from the directory of the configuration file that declares the
include. - Includes may recurse up to 8 levels.
- All merged plugin
tagvalues must still be globally unique.
- Loads only
runtime
runtime:
worker_threads: 4
Field notes:
worker_threads- Meaning: Number of Tokio multi-thread runtime workers.
- Default: Uses system available parallelism when omitted.
- Constraint: Must not be
0.
log
log:
level: info
file: ./oxidns.log
rotation:
type: daily
max_files: 7
Field notes:
level- Allowed values:
offtracedebuginfowarnerror - Default:
info
- Allowed values:
file- Meaning: Optional log file path.
- If omitted, logs go only to stdout.
- When configured, OxiDNS writes to both stdout and the log file.
- Log files are written as UTF-8 plain text without terminal ANSI color escape codes.
rotation- Meaning: Log file rotation policy.
- Default:
never
rotation supports the following forms:
type: nevertype: minutely- Rotate every minute.
type: hourly- Rotate every hour.
type: daily- Rotate every day.
type: weekly- Rotate every week.
- Optional
max_filescontrols how many rotated files are retained;0disables automatic cleanup.
network
network.outbound centralizes outbound policy for internal HTTP clients and upstreams. When omitted, behavior stays compatible: HTTP clients use system DNS with direct connections, and upstreams keep their own settings.
network:
outbound:
default: direct
profiles:
direct:
resolver: system
proxy: none
remote:
resolver:
nameservers:
- addr: "1.1.1.1:53"
- addr: "tls://dns.google:853"
dial_addr: 8.8.8.8
- addr: "https://cloudflare-dns.com/dns-query"
dial_addr: 1.1.1.1
ip_version: 4
timeout: 5s
proxy: none
proxy:
socks5: 127.0.0.1:1080
Field notes:
outbound.default- Meaning: Which profile HTTP clients and upstreams use when they do not set
outboundexplicitly. - Default: none; without a default profile, OxiDNS uses system DNS + direct connections.
- Constraint: If set, it must reference an existing entry in
profiles. - Note: The default profile proxy is applied strictly to upstreams. Startup fails if a default SOCKS5 proxy is applied to UDP, DoQ, or DoH3 upstreams, because those connection models do not support profile proxying.
- Meaning: Which profile HTTP clients and upstreams use when they do not set
outbound.profiles.<name>.resolversystem: Use system DNS. HTTP clients perform this lookup asynchronously so it does not block runtime worker threads.nameservers: Resolve target names through configured DNS nameservers. Supportsudp://,tcp://,tls://,https://,doh://,h3://,quic://, anddoq://; no scheme defaults to UDP.- Protocol features: UDP/TCP are always available. DoT requires
resolver-dot, DoH requiresresolver-doh, DoQ requiresresolver-doq, and DoH3 requiresresolver-doh3. Legacyupstream-*features still enable the shared DNS client dependencies for existing build scripts, but newnetwork.outbound.resolver.nameserversconfigs should enableresolver-*explicitly. ip_version: Optional,4queries A records and6queries AAAA records. When omitted, IPv4 is used.timeout: Optional resolver query timeout. Defaults to5s.proxy: Optional.noneconnects nameservers directly;profilelets TCP/DoT/DoH nameservers reuse this profile's SOCKS5 proxy. UDP/DoQ/DoH3 nameservers do not support SOCKS5.- Domain-based nameservers must set
dial_addr; the hostname inaddris kept for SNI/certificate validation anddial_addris used for the actual connection.
outbound.profiles.<name>.proxynoneordirect: Connect directly.socks5: Connect through a SOCKS5 proxy. The format is the same as upstreamsocks5.
download, upgrade, and http_request can reference a profile with args.outbound: remote. The legacy socks5 field remains supported. When both outbound and socks5 are set on the same plugin, socks5 overrides the profile proxy while the resolver still comes from the outbound profile. forward upstreams use network.outbound.default when outbound is omitted; they can also set outbound: remote to select another profile. Local upstream dial_addr, bootstrap, and socks5 fields override profile-injected values.
api
api.http supports two forms.
Shorthand:
api:
http: "127.0.0.1:9088"
Expanded form:
api:
http:
listen: "127.0.0.1:9443"
ssl:
cert: "/etc/oxidns/api.crt"
key: "/etc/oxidns/api.key"
client_ca: "/etc/oxidns/client-ca.crt"
require_client_cert: true
auth:
type: basic
username: "admin"
password: "secret"
webui:
root: "/etc/oxidns/webui"
index: "index.html"
Field notes:
http.listen- API listen address. Supports
ip:port,[ipv6]:port, and:port. :portbinds as dual-stack[::]:port; use0.0.0.0:portfor IPv4-only.
- API listen address. Supports
http.ssl.cert- API certificate file.
http.ssl.key- API private key file.
http.ssl.client_ca- Optional client certificate CA.
http.ssl.require_client_cert- Whether mutual TLS is required.
http.auth- Currently supports
basic. - See the Management API chapter for the Basic Auth header encoding rules.
- Currently supports
http.cors.allowed_origins- Optional WebUI/API cross-origin allowlist; when omitted, it is inferred from
http.listen. 0.0.0.0and[::]automatically allow any origin; a specific IP automatically allows any WebUI port on the same host.- When configured explicitly, entries are matched exactly against the browser's
Origin. - Use
"*"to allow any origin, but not for credentialed browser requests.
- Optional WebUI/API cross-origin allowlist; when omitted, it is inferred from
http.webui.root- Optional WebUI static file directory. When enabled, the WebUI is mounted at
/and the management API is available under/api/*. - Relative paths resolve against
-d/--working-dir; with the Debian service default-d /var/lib/oxidns,root: "./webui"means/var/lib/oxidns/webui. - See WebUI Deployment for build steps, publish directories, and standalone nginx deployment.
- Optional WebUI static file directory. When enabled, the WebUI is mounted at
http.webui.index- Optional index file name. Defaults to
index.html.
- Optional index file name. Defaults to
Validation rules:
listenmust not be empty.certandkeymust be configured together.require_client_cert: truerequiresclient_ca.basic.usernameandbasic.passwordmust both be non-empty.webui.rootmust not be empty.webui.index, when configured, must not be empty.
plugins
Each plugin definition uses the same outer structure:
- tag: cache_main
type: cache
args:
size: 4096
General rules:
tag- Unique plugin instance identifier.
- Must not be empty.
- Must be unique across the whole config.
type- Plugin type name.
- Must match a registered plugin factory.
args- Plugin parameters.
- Different plugins accept different shapes: object, string, array, or null.
Responsibilities of the Four Plugin Categories
server
Purpose: Accept DNS requests and send them into an executor entry.
Traits:
- Does not implement complex policy logic.
- Usually configures a bind address, TLS parameters, and an entry executor.
executor
Purpose: Perform actions.
Typical actions include:
- Query upstreams
- Generate local answers
- Read and write cache
- Adjust TTL
- Handle ECS
- Run fallback and concurrent races
- Perform observability and system integrations
matcher
Purpose: Evaluate conditions for use in sequence rules.
Typical match dimensions include:
- Query name
- Query type
- Client IP
- Response IP
- Response code
- Environment variables
- Sampling outcome
- Rate-limit state
provider
Purpose: Provide reusable datasets for matchers or other plugins.
Current main provider types:
domain_setip_setgeoipgeositeadguard_rule
The sequence Orchestration Model
sequence is the policy hub of OxiDNS. Most non-trivial configs use it as the primary entry.
Example:
- tag: seq_main
type: sequence
args:
- matches:
- "$lan_clients"
- "qtype A,28"
exec: "$cache_main"
- matches: "!has_resp"
exec: "$forward_main"
- exec: "accept"
Each rule has two key fields:
matches- One matcher expression or an array of expressions.
- When it is an array, every condition must be true for the rule to match.
exec- The action to execute when the rule matches.
Referencing Plugins and Quick Setup
Reference Existing Plugins
Use $tag to reference a plugin that has already been defined:
- exec: "$forward_main"
- matches:
- "$is_internal"
- "!has_resp"
exec: "$cache_main"
Quick Setup
If a sequence rule uses type + arguments instead of $tag, OxiDNS creates a temporary plugin on the fly.
Example:
- exec: "forward 1.1.1.1 8.8.8.8"
- matches: "qname domain:example.com"
exec: "ttl 300"
Common quick setup forms today:
- matcher
_true_falseqname ...qtype ...qclass ...client_ip ...resp_ip ...ptr_ip ...cname ...mark ...env ...random ...rate_limiter ...rcode ...has_resphas_wanted_ansstring_exp ...
- executor
forward ...cache ...ttl ...prefer_ipv4prefer_ipv6sleep ...debug_print ...query_summary ...metrics_collector ...black_hole ...drop_respecs_handler ...forward_edns0opt ...ipset ...nftset ...upgrade ...download ...reload_provider ...reload
Built-In sequence Control Flow
Besides calling plugins, sequence.args[].exec can also use built-in control flow:
accept
- Ends the current
sequenceimmediately. - This is an explicit early stop, so callers do not continue with later rules.
- Does not build a response by itself.
- Typical use:
- Close out the pipeline after
cache,hosts, orarbitraryhas already written a response. - Stop later
forwardor side-effect stages once a branch has already made the decision.
- Close out the pipeline after
return
- Ends the current
sequenceimmediately and returns control to the caller. - Does not build a response.
- If the current
sequencewas entered viajump, the caller resumes at the rule afterjump. - If the current
sequenceis the top-level entry, this acts like an early exit from the current rule chain.
reject [rcode]
- Builds a DNS response from the current request immediately and ends the current
sequence. - The default
rcodeisREFUSED, so plainrejectmeans “reject this request”. - A decimal numeric code or English RCODE name can be provided explicitly; English names are case-insensitive. Common mappings and meanings are listed in the DNS Code Reference, for example:
reject 2=>SERVFAILreject SERVFAIL/reject servfail=>SERVFAILreject 3=>NXDOMAINreject NXDOMAIN=>NXDOMAIN
rejectonly supports base DNS RCODEs0..15; extended RCODEs require an EDNS OPT and are not generated by this built-in action.reject 0returns a plainNOERRORresponse and does not add an SOA automatically.- Callers do not continue with later rules.
- A typical use is returning a specific error code directly, for example:
- matches: "qtype HTTPS"
exec: "reject NXDOMAIN"
mark ...
- Inserts one or more unsigned integer marks into
DnsContext.marks. - Supported forms:
mark 1mark 1 2 3mark 1,2,3
- Continues to the next rule in the current
sequence. - Does not build a response and does not terminate the current
sequence.
jump seq_tag
- Calls another
sequence; conceptually this behaves like a subroutine call. - The parameter must be the target
sequencetag without a leading$. - If the called
sequence:- reaches its tail normally, the current
sequenceresumes at the rule afterjump. - executes
return, the currentsequencealso resumes at the rule afterjump. - executes
accept,reject, or another operation that returnsStop, the currentsequencestops as well.
- reaches its tail normally, the current
goto seq_tag
- Transfers control to another
sequence; conceptually this behaves like a one-way jump. - The parameter must be the target
sequencetag without a leading$. - The current
sequencenever resumes aftergoto:- If the target
sequencereaches its tail, control does not return to the rules aftergoto. - If the target
sequenceexecutesreturn, thatreturnis propagated outward and still does not return to the rules aftergoto. - If the target
sequenceexecutesaccept,reject, or anotherStop, that result propagates outward directly.
- If the target
- This is useful when ownership of the request should be handed off permanently to another policy branch.
Example:
- matches: "$rate_ok"
exec: "mark 100"
- matches: "!$rate_ok"
exec: "reject 2"
Example showing the difference between jump and goto:
- tag: child_seq
type: sequence
args:
- exec: "mark 2"
- exec: "return"
- tag: parent_jump
type: sequence
args:
- exec: "mark 1"
- exec: "jump child_seq"
- exec: "mark 3"
- tag: parent_goto
type: sequence
args:
- exec: "mark 1"
- exec: "goto child_seq"
- exec: "mark 3"
parent_jumpends with marks1,2,3because execution resumes afterjump.parent_gotoends with marks1,2because execution never returns aftergoto.
Common Rule Syntax
Domain Rules
These forms appear in plugins such as qname, cname, domain_set, hosts, and redirect:
full:example.com- Exact match.
domain:example.com- Suffix match.
keyword:cdn- Substring match.
regexp:^api[0-9]+\\.example\\.com$- Regular-expression match.
example.com- Without a prefix, common domain-rule users such as
qname,cname, anddomain_setusually treat it asdomain:example.com;hostsandredirecttreat it as an exactfull:example.commatch.
- Without a prefix, common domain-rule users such as
IP Rules
These forms appear in client_ip, resp_ip, ptr_ip, ip_set, and related plugins:
- Single IP:
1.1.1.1 - CIDR:
192.168.0.0/16 - IPv6 CIDR:
2400:3200::/32
Provider References
Matchers and providers can reference providers through:
$tag- References a defined provider with the required match capability.
- Domain-oriented references can target
domain_setorgeosite. - IP-oriented references can target
ip_setorgeoip.
&/path/to/file- Loads rules directly from a file.
Example:
args:
- "domain:example.com"
- "$core_domains"
- "&/etc/oxidns/domains.txt"