Skip to main content

Executor Plugins

Executors are the core action layer in OxiDNS. They can read or write requests, set responses, query upstreams, cache results, perform fallback logic, emit logs, or trigger system integrations.

When reading this chapter, keep two questions in mind:

  1. Does this plugin act only in the forward stage, or can it also rewrite results on the return path?
  2. Is it part of the main resolution path, or an observability and side-effect plugin?

sequence

Purpose

Orchestrates matchers and executors into a pipeline. This is the most common entry executor.

Example Configuration

- tag: seq_main
type: sequence
args:
# Try cache first
- exec: "$cache_main"
# Stop immediately on cache hit
- matches: "has_resp"
exec: "accept"
# Arrays of matches are AND-ed together
# Examples prefer quick-setup matcher expressions directly
- matches:
- "client_ip $lan_ip_set"
- "qname $local_domains"
exec: "$hosts_main"
# Rules may also execute unconditionally
- exec: "$metrics_main"
# Only forward when no response exists yet
- matches: "!has_resp"
exec: "$forward_main"
# Normalize TTL after a response is available
- matches: "has_resp"
exec: "$ttl_main"

Configuration Details

args

  • Type: array; Required: yes; Default: none
  • Purpose: Defines the rule chain.
  • Runtime impact:
    • Rules execute in order.
    • Initialization fails when the array is empty.

args[].matches

  • Type: string or array
  • Required: no
  • Purpose: Match condition for the current rule.
  • Runtime impact:
    • Multiple conditions are combined with logical AND.
    • Omitted means the rule has no precondition.

args[].exec

  • Type: string; Required: no; Default: none
  • Purpose: Action to run when the rule matches.
  • Supports:
    • plugin references
    • quick setup expressions
    • built-in control flow

Behavior

  • Rules run sequentially.
  • A rule with multiple matches requires all of them to be true.
  • Other sequence instances can be called with jump or goto.

Built-In Control Flow

Besides plugin calls, sequence.args[].exec can also use built-in control flow:

accept

  • Ends the current sequence immediately.
  • This is an explicit early stop, so outer callers do not continue with later rules.
  • Does not build a response by itself; it is usually used after an earlier stage has already produced one.

return

  • Ends the current sequence immediately and gives control back to the caller.
  • Does not build a response.
  • If the current sequence was entered by jump, the caller continues with the next rule.

reject [rcode]

  • Builds a response immediately and ends the current sequence.
  • The default rcode is REFUSED.
  • Decimal numeric rcodes and case-insensitive English RCODE names are accepted, for example reject 2, reject SERVFAIL, reject servfail, or reject NXDOMAIN; common mappings and meanings are listed in the DNS Code Reference.
  • Only base DNS RCODEs 0..15 are supported; extended RCODEs require an EDNS OPT and are not generated by this built-in action.
  • reject 0 returns a plain NOERROR response and does not add an SOA automatically.
  • Stops later rules from running.

mark ...

  • Inserts one or more integer marks, then continues to the next rule in the current sequence.
  • Supports mark 1, mark 1 2 3, and mark 1,2,3.

jump seq_tag

  • Calls another sequence; conceptually this is a subroutine call.
  • The parameter must be the target sequence tag without $.
  • If the target sequence reaches its tail or executes return, the current sequence resumes with the next rule.
  • If the target sequence executes accept, reject, or another Stop, the current sequence stops too.

goto seq_tag

  • Transfers control one-way to another sequence.
  • The parameter must be the target sequence tag without $.
  • Once goto runs, the current sequence never resumes at later rules.
  • If the target sequence executes return, that return is propagated outward.

Typical Uses

  • One readable top-level entry.
  • Split cache, local answers, forwarding, and integrations into understandable policy layers.
  • Build complex branches with marks and matchers.
Notes
  • Referenced plugins must already exist.
  • A sequence needs at least one rule.

forward

Purpose

Sends DNS queries to upstreams.

Example Configuration

- tag: forward_main
type: forward
args:
# Effective fan-out in multi-upstream mode
concurrent: 3
# Concurrent upstream response selection: fastest / balanced / prefer_positive / consensus
response_selection: balanced
upstreams:
# Simplest UDP upstream
- tag: "cf_udp"
addr: "udp://1.1.1.1:53"
timeout: 3s

# Domain-based DoH upstream showing bootstrap, pooling, HTTP/3,
# and Linux socket options
- tag: "doh_main"
addr: "https://resolver.example/dns-query"
bootstrap: "8.8.8.8:53"
bootstrap_version: 4
port: 443
idle_timeout: 30
min_conns: 2
max_conns: 256
timeout: 5s
enable_pipeline: false
enable_http3: true
so_mark: 100
bind_to_device: "eth0"

# DoT upstream showing dial_addr, SOCKS5, TLS verification, and pipelining
- tag: "dot_backup"
addr: "tls://dns.example:853"
dial_addr: "203.0.113.53"
socks5: "user:pass@127.0.0.1:1080"
idle_timeout: 60
min_conns: 1
max_conns: 128
insecure_skip_verify: false
timeout: 4s
enable_pipeline: true

Configuration Details

concurrent

  • Type: integer; Required: no; Default: 1
  • Runtime range is clamped to 1..=32 and never exceeds the upstream count.
  • Purpose: Number of concurrent upstream fan-out requests.
  • Recommendation: use 2..=8 for routine racing; higher values are intended for short-term observation or explicit multi-protocol racing.

response_selection

  • Type: string; Required: no; Default: balanced
  • Values: fastest, balanced, prefer_positive, consensus
  • Purpose: Selects the winning response when concurrent upstreams return different results.
  • Modes:
    • fastest: the first successful DNS response wins; transport errors never win.
    • balanced: default mode. NOERROR with answers wins immediately; NXDOMAIN or NODATA waits briefly for a positive answer.
    • prefer_positive: positive answers win immediately; negative answers wait until all started concurrent upstreams finish.
    • consensus: positive answers win immediately; negative answers require two upstream confirmations, falling back to prefer_positive when fan-out is too small.

upstreams

  • Type: array; Required: yes; Default: none
  • Purpose: Defines one or more upstream targets.
  • Runtime impact:
    • One upstream means normal forwarding.
    • More than one enables racing behavior.

short_circuit

  • Type: boolean; Required: no; Default: false
  • Purpose: Stop the executor chain after a successful upstream response.
  • Notes:
    • When disabled, forward still populates response, but later executors can continue processing it.
    • When enabled, a successful upstream result immediately ends the remaining executor chain.

upstreams[].addr

  • Type: string; Required: yes
  • Purpose: Upstream address, protocol, and target.
  • Supports:
    • udp://
    • tcp://
    • tcp+pipeline://
    • tls://
    • tls+pipeline://
    • quic:// / doq://
    • https:// / doh://
    • h3://
  • Notes:
    • No scheme means UDP.
    • DoH addresses should include the full request path.
    • Startup and config validation do not resolve domain-based upstreams. Without bootstrap or dial_addr, the hostname is resolved through the OS resolver when the first connection is created.
    • For domain-based upstreams, choose either bootstrap or dial_addr to avoid runtime bootstrap dependency on the local DNS setup.
    • bootstrap and dial_addr are mutually exclusive at runtime. If both are configured, only dial_addr is effective and bootstrap is ignored.

upstreams[].tag

  • Type: string; Required: no
  • Purpose: Per-upstream log label.

upstreams[].dial_addr

  • Type: ip; Required: no
  • Purpose: Actual connection IP while preserving the hostname from addr for SNI, Host, and certificate validation.
  • Notes: Takes precedence over bootstrap when both are configured.

upstreams[].outbound

  • Type: string; Required: no
  • Purpose: References a profile from network.outbound.profiles to inject resolver and proxy defaults into this upstream.
  • Default: When omitted, network.outbound.default is used. Without a default, the upstream keeps system resolution/direct dialing.
  • Override rules: local dial_addr takes precedence over resolver use; local bootstrap takes precedence over the outbound resolver; local socks5 takes precedence over the profile proxy.
  • Note: Profile proxying applies only to TCP, DoT, and DoH2. UDP, DoQ, and DoH3 upstreams ignore SOCKS5 proxy settings.

upstreams[].port

  • Type: integer; Required: no
  • Purpose: Override the protocol default port.

upstreams[].bootstrap

  • Type: string; Required: no
  • Purpose: Bootstrap resolver for domain-based upstreams.
  • Notes: Must use an IP:port address. With bootstrap enabled, OxiDNS resolves the upstream hostname through that resolver and caches it according to DNS TTL. Ignored when dial_addr is also configured.

upstreams[].bootstrap_version

  • Type: integer; Required: no
  • Allowed values: 4, 6
  • Purpose: Force bootstrap resolution toward IPv4 or IPv6.

upstreams[].socks5

  • Type: string; Required: no
  • Purpose: SOCKS5 proxy for upstream connections.
  • Supports:
    • host:port
    • username:password@host:port
  • Notes: SOCKS5 applies only to TCP, DoT, and DoH2. UDP, DoQ, and DoH3 upstreams ignore this setting.

upstreams[].idle_timeout

  • Type: integer; Required: no; Default: 10
  • Unit: seconds
  • Purpose: Idle pooled connection lifetime.

upstreams[].max_conns

  • Type: integer; Required: no; Default: 64
  • Purpose: Maximum pooled connections.
  • Range: 1..4096

upstreams[].min_conns

  • Type: integer; Required: no; Default: 0
  • Purpose: Minimum warmed connections kept by the pool.
  • Range: 0..4096, and it must not exceed the upstream's effective max_conns.
  • Notes: When omitted, connections remain lazy and are not pre-created when the pool is created.

upstreams[].insecure_skip_verify

  • Type: boolean; Required: no; Default: false
  • Purpose: Skip TLS certificate validation.

upstreams[].timeout

  • Type: duration; Required: no; Default: 5s
  • Purpose: Per-upstream query timeout.

upstreams[].enable_pipeline

  • Type: boolean; Required: no; Default: false
  • Purpose: Enable pipelining for TCP or DoT.
  • Notes: tcp+pipeline:// and tls+pipeline:// force this on.
  • Recommendation: Before enabling it, run oxidns probe upstream <addr> against the target upstream. For TCP/DoT, the probe forces concurrent queries through one connection so you can decide whether that upstream is safe for pipeline mode and catch timeouts, connection closes, response ID confusion, or crossed questions first.
  • Verdict: If the probe reports supported, enabling pipeline is reasonable. If it reports unsupported, unstable, or inconclusive, keep pipeline disabled or retest with lower concurrency / a longer timeout.

upstreams[].enable_http3

  • Type: boolean; Required: no; Default: false
  • Purpose: Use HTTP/3 for DoH.

upstreams[].so_mark

  • Type: integer; Required: no
  • Purpose: Linux SO_MARK.

upstreams[].bind_to_device

  • Type: string; Required: no
  • Purpose: Linux SO_BINDTODEVICE.

quick setup

- exec: "forward 1.1.1.1"
- exec: "forward 1.1.1.1 8.8.8.8"
- exec: "forward 1.1.1.1 short_circuit=true"

Quick setup supports the trailing flag forms short_circuit, short_circuit=true, and short_circuit=false. Use the full plugin form for bootstrap, proxy, HTTP/3, pool settings, and other advanced options.

Behavior

  • Single-upstream mode queries the configured upstream directly.
  • Multi-upstream mode races queries from a randomized starting point and keeps the first successful answer.
  • With short_circuit enabled, a successful upstream response stops the remaining executor chain immediately.

Metrics

Exported through the global GET /api/metrics endpoint:

  • forward_query_total
  • forward_success_total
  • forward_error_total
  • forward_timeout_total
  • forward_latency_count
  • forward_latency_sum_ms

Per-upstream series are also exported with an upstream label (the upstream tag, or its resolved address when no tag is configured):

  • forward_upstream_query_total
  • forward_upstream_success_total
  • forward_upstream_error_total
  • forward_upstream_timeout_total
  • forward_upstream_latency_count
  • forward_upstream_latency_sum_ms

Typical Uses

  • Standard forwarding
  • Multi-upstream resilience
  • Mixed-protocol upstream groups
Notes
  • More upstreams are not automatically better. Keep upstream groups semantically clear.

cache

Purpose

Provides TTL-aware response caching with negative cache support and persistence.

Example Configuration

- tag: cache_main
type: cache
args:
# Maximum number of cached entries
size: 8192
# Stop the chain immediately when cache returns a response
short_circuit: true
# Serve stale responses briefly after original TTL expiry and refresh lazily
lazy_cache_ttl: 120
# Cache NXDOMAIN / NODATA responses too
cache_negative: true
# Upper bound for negative-cache TTL
max_negative_ttl: 300
# Fallback TTL when a negative response has no SOA
negative_ttl_without_soa: 60
# Upper bound for positive TTL
max_positive_ttl: 600
# Minimum positive TTL required for cache admission
min_positive_ttl: 4
# Exclude ECS from the cache key for a better hit ratio
ecs_in_key: false
# Persist cache contents to disk
dump_file: "./dns_cache.dump"
# Periodic dump interval in seconds
dump_interval: 600

Configuration Details

size

  • Type: integer; Required: no; Default: 1024
  • Purpose: Cache capacity.

lazy_cache_ttl

  • Type: duration; Required: no
  • Purpose: Enable lazy cache for successful positive responses.
  • Behavior:
    • The original response TTL still defines the fresh-hit window.
    • lazy_cache_ttl defines the stale reply TTL and keeps entries briefly available after freshness expires.
    • Stale hits trigger an asynchronous background refresh.
    • This setting does not shorten the original fresh TTL.

dump_file

  • Type: string; Required: no
  • Purpose: Persistence dump file path.

dump_interval

  • Type: duration; Required: no; Default: 600s
  • Purpose: Periodic dump interval.

short_circuit

  • Type: boolean; Required: no; Default: false
  • Purpose: Stop the chain when the cache produces a response.
  • Notes:
    • When set to false, later executors still run even if cache has already populated response.
    • To skip later forward stages on cache hits, handle it explicitly in sequence, for example with has_resp or accept.

cache_negative

  • Type: boolean; Required: no; Default: true
  • Purpose: Cache negative responses.

max_negative_ttl

  • Type: duration; Required: no; Default: 300s
  • Purpose: Cap negative-cache TTL.

negative_ttl_without_soa

  • Type: duration; Required: no; Default: 60s
  • Purpose: Fallback TTL for negative answers without SOA.

max_positive_ttl

  • Type: duration; Required: no
  • Purpose: Cap positive-cache TTL.

min_positive_ttl

  • Type: duration; Required: no
  • Purpose: Require a minimum positive TTL before writing a response to cache.
  • Notes: Positive responses whose effective cache TTL is lower than this value are not cached. The check runs after max_positive_ttl capping.

ecs_in_key

  • Type: boolean; Required: no; Default: false
  • Purpose: Include ECS information in the cache key.

quick setup

- exec: "cache"
- exec: "cache short_circuit=true"
  • With no arguments, quick setup uses the default cache configuration.
  • It currently supports the trailing flag forms short_circuit, short_circuit=true, and short_circuit=false.
  • Use the full plugin form for other advanced settings.

Behavior

  • Reads from cache on the forward path and writes responses on the return path.
  • Respects DNS TTL semantics instead of using a fixed timeout.
  • Can persist cache contents through dump and load operations.

Plugin API

  • GET /plugins/<cache_tag>/entries
    • Reads cache entries with pagination. Supports limit, cursor, and qname; qname is a case-insensitive substring filter over the cache-key domain.
  • GET /plugins/<cache_tag>/flush
  • GET /plugins/<cache_tag>/dump
  • POST /plugins/<cache_tag>/load_dump

Metrics

Exported through the global GET /api/metrics endpoint. The cache plugin does not expose a cache-specific stats/metrics endpoint.

  • cache_lookup_total
  • cache_hit_total{kind="fresh|stale"}
  • cache_miss_total
  • cache_expired_total
  • cache_insert_total
  • cache_skip_total{reason="truncated|no_ttl|low_positive_ttl"}
  • cache_lazy_refresh_total{result="started|success|failed"}
  • cache_entry_count

Typical Uses

  • Lower upstream latency
  • Protect upstreams from repeated identical traffic
  • Preserve warm cache state across restarts
Notes
  • Decide carefully whether ECS should be part of the cache key. It improves correctness for ECS-aware policies but reduces hit ratio.

fallback

Purpose

Runs a primary executor first and falls back to a secondary executor when the primary is too slow or fails.

Example Configuration

- tag: fallback_main
type: fallback
args:
# Preferred path
primary: "forward_fast"
# Backup path
secondary: "forward_stable"
# Let the backup take over after 200 ms
threshold: 200
# Keep the backup running in parallel for lower tail latency
always_standby: true

Configuration Details

primary

  • Type: string; Required: yes
  • Purpose: Primary executor tag.

secondary

  • Type: string; Required: yes
  • Purpose: Secondary executor tag.

threshold

  • Type: integer; Required: no
  • Unit: milliseconds
  • Purpose: Delay before the secondary is allowed to take over.

always_standby

  • Type: boolean; Required: no; Default: false
  • Purpose: Keep the secondary in standby for all requests rather than only after the threshold condition.

short_circuit

  • Type: boolean; Required: no; Default: false
  • Purpose: Stop the executor chain after fallback selects the winning response.

Behavior

  • Provides controlled degradation instead of unconditional double-querying.
  • Useful when one path is usually faster but another path is more complete or stable.
  • With short_circuit enabled, the winning branch writes its response and then immediately stops the remaining executor chain.

Metrics

Exported through the global GET /api/metrics endpoint:

  • fallback_primary_total
  • fallback_primary_error_total
  • fallback_secondary_total

Typical Uses

  • Low-latency primary plus stable backup
  • Tail-latency protection
Notes
  • A too-aggressive threshold can turn the secondary into a routine dependency.

hosts

Purpose

Returns local static answers using host-style entries.

Example Configuration

- tag: hosts_main
type: hosts
args:
entries:
# Unprefixed rules default to full:
- "router.local 192.168.1.1"
# Exact-name rule
- "full:gateway.local 192.168.1.2"
# Suffix rule returning both IPv4 and IPv6
- "domain:svc.local 10.0.0.10 fd00::10"
# Keyword rule
- "keyword:nas 192.168.1.20"
# Regex rule
- "regexp:^api[0-9]+\\.corp\\.local$ 10.10.0.5"
files:
# Merge more hosts rules from files
- "/etc/oxidns/hosts.txt"
short_circuit: true

Configuration Details

entries

  • Type: array; Required: no; Default: empty array
  • Purpose: Defines inline hosts rules.
  • Rule format:
    • <domain_rule> <ip1> <ip2> ...

files

  • Type: array; Required: no; Default: empty array
  • Purpose: Specifies the list of external hosts rule files.

short_circuit

  • Type: bool; Required: no; Default: false
  • Purpose: Stops the remaining executor chain after a local answer is generated.

Rule format:

<domain_rule> <ip1> <ip2> ...

Behavior

  • Handles only IN class A / AAAA requests with exactly one question.
  • Unprefixed rules default to full: to match mosdns hosts.
  • Rule-family priority is fixed as full -> domain -> regexp -> keyword.
  • domain: uses the longest matching suffix.
  • Repeated patterns use last-write-wins semantics in load order: inline entries first, then each configured file line by line.
  • Positive local answers return same-family addresses with a fixed TTL of 10.
  • If the domain matches but the requested address family is missing, the plugin returns NoError + empty answer + fake SOA instead of passing through.
  • Non-matching queries pass through to subsequent execution.
  • By default it keeps running the remaining chain after a local response; enable short_circuit to stop immediately for both positive and empty local replies.

Metrics

Exported through the global GET /api/metrics endpoint:

  • hosts_hit_total
  • hosts_miss_total

Typical Uses

  • Local service discovery
  • Small fixed overrides

arbitrary

Purpose

Injects arbitrary DNS records from zone-style rule strings.

Example Configuration

- tag: arbitrary_main
type: arbitrary
args:
rules:
# TXT record
- "example.com. 60 IN TXT \"hello world\""
# MX record
- "mail.example.com. 300 IN MX 10 mx1.example.com."
# A / AAAA / CNAME / PTR records are also supported
- "www.example.com. 120 IN A 192.0.2.10"
- "www.example.com. 120 IN AAAA 2001:db8::10"
- "alias.example.com. 120 IN CNAME www.example.com."
- "10.2.0.192.in-addr.arpa. 300 IN PTR host.example.com."
files:
# Load more static records from files
- "/etc/oxidns/zone.txt"
short_circuit: false

Configuration Details

rules

  • Type: array; Required: no
  • Purpose: Inline record rules.
  • Syntax:
    • Each list item is parsed as an independent zone snippet.
    • Supports $ORIGIN, $TTL, $INCLUDE, $GENERATE, owner inheritance, TTL units, comments, quoted strings, and multiline ( ) syntax.
    • Common record types are parsed directly, including A, AAAA, CNAME, NS, PTR, DNAME, ANAME, MD, MF, MB, MG, MR, NSAPPTR, MX, RT, AFSDB, RP, MINFO, HINFO, TXT, SPF, AVC, RESINFO, SOA, SRV, NAPTR, and CAA.
    • Other record types can be loaded through RFC3597 generic syntax: TYPE#### \# <len> <hex>.
    • Defaults TTL to 3600 when omitted.

files

  • Type: array; Required: no
  • Purpose: External rule files.
  • Syntax: Uses the same zone parser as rules.

short_circuit

  • Type: bool; Required: no; Default: false
  • Purpose: Stop the remaining executor chain after setting a synthetic response.
  • Notes: By default arbitrary only sets the response and lets the chain continue.

Behavior

  • Produces fully synthetic answers.
  • Matches exactly on qname + qtype + qclass.
  • When a request carries multiple questions, all matched records are accumulated into one response.
  • By default the executor only sets the response and keeps the remaining chain running.
  • When short_circuit is enabled it returns Stop after a match.
  • Quick setup syntax is intentionally not supported.
  • Useful when hosts is too limited.

Typical Uses

  • TXT test records
  • Local authority-style data
Notes
  • Keep rule files readable. Arbitrary records become hard to audit faster than hosts entries.
  • This is still a static answer generator, not a full authoritative server with transfer or dynamic update support.
  • The parser is broader than the zone parser used by mosdns arbitrary, but matching remains an exact static lookup.

redirect

Purpose

Rewrites matching names toward different target names or answer destinations.

Example Configuration

- tag: redirect_main
type: redirect
args:
rules:
# Exact-name redirect
- "full:old.example.com new.example.net"
# Suffix redirect
- "domain:legacy.example.com modern.example.net"
# Keyword redirect
- "keyword:staging staging-gateway.example.net"
# Regex redirect
- "regexp:^api[0-9]+\\.legacy\\.example\\.com$ api-gateway.example.net"
# Unprefixed rules default to full:
- "old-static.example.com static.example.net"
files:
# Merge more redirect rules from files
- "/etc/oxidns/redirect.txt"

Configuration Details

rules

  • Type: array; Required: no; Default: empty array
  • Purpose: Defines inline redirect rules.
  • Rule format:
    • <domain_rule> <target_name>
  • <domain_rule> supports:
    • full:
    • domain:
    • keyword:
    • regexp:
    • bare domains without a prefix, treated as exact full: matches

files

  • Type: array; Required: no; Default: empty array
  • Purpose: Specifies the list of external redirect rule files.
  • File format is the same as rules, one rule per line. Empty lines and # comments are ignored.

Rule format:

full:old.example.com new.example.net
domain:legacy.example.com modern.example.net
keyword:staging staging-gateway.example.net
regexp:^api[0-9]+\.legacy\.example\.com$ api-gateway.example.net
old-static.example.com static.example.net

Behavior

  • Only handles IN queries. If there is no question or no matching rule, the executor passes through to the remaining chain.
  • If multiple rules match, the earliest loaded matching rule wins. Load order is inline rules first, then files in declaration order and line order.
  • redirect does not resolve the target name by itself. Use it with a later executor such as forward in a sequence so that executor can produce the real response for the rewritten target name.
  • Forward phase:
    • Rewrites the request QUESTION NAME.
  • Return phase:
    • Restores the target name in the response question back to the original name.
    • Prepends a CNAME original -> target record to the answers.

Common sequence usage:

- exec: "$redirect_main"
- exec: "$forward_main"

Typical Uses

  • Point a unified entry domain to another set of records.
  • Perform alias redirection for specific domains without changing client configuration.
Notes
  • Put redirect before forward in the usual case. If the remaining chain does not produce a response, redirect will not synthesize target records by itself.
  • It is better suited for simple queries such as A / AAAA / TXT.
  • Full semantic transparency is not guaranteed for complex records and some extension scenarios.

ecs_handler

Purpose

Controls EDNS Client Subnet forwarding or injection.

Example Configuration

- tag: ecs_main
type: ecs_handler
args:
# Strip client-supplied ECS first
forward: false
# Add ECS when the request has none
send: true
# IPv4 ECS prefix length
mask4: 24
# IPv6 ECS prefix length
mask6: 48

- tag: ecs_preset
type: ecs_handler
args:
# Preset ECS source
preset: "203.0.113.10"
# Fixed-source mode usually keeps these switches explicit
forward: false
send: true
mask4: 24
mask6: 48

Configuration Details

forward

  • Type: boolean; Required: no
  • Purpose: Preserve ECS from the client side.

send

  • Type: boolean; Required: no
  • Purpose: Send ECS to upstreams.

preset

  • Type: string; Required: no
  • Purpose: Use a preset ECS source.

mask4

  • Type: integer; Required: no
  • Purpose: IPv4 ECS mask.

mask6

  • Type: integer; Required: no
  • Purpose: IPv6 ECS mask.

quick setup

- exec: "ecs_handler 203.0.113.10/24"

Behavior

  • Can preserve, synthesize, or normalize ECS before forwarding.
  • Interacts with cache correctness if ECS is also part of the cache key.

Typical Uses

  • Geo-sensitive upstream policies
  • Client-network-aware answers
Notes
  • Keep ECS handling and cache-key policy aligned.

forward_edns0opt

Purpose

Forwards selected EDNS0 options to upstreams.

Example Configuration

- tag: edns_forward
type: forward_edns0opt
args:
# Preserve only the selected EDNS0 option codes
codes: [10, 12]

Configuration Details

codes

  • Type: array; Required: yes
  • Purpose: EDNS0 option codes to preserve and forward.

quick setup

- exec: "forward_edns0opt 10,12"

Behavior

  • Keeps only selected EDNS0 options instead of blindly forwarding everything.

Typical Uses

  • Preserve specific client-side EDNS signaling needed by upstreams.

ttl

Purpose

Rewrites response TTL values.

Example Configuration

Full object form:

- tag: ttl_main
type: ttl
args:
# First force TTL to 300
fix: 300
# Then keep a lower bound
min: 60
# And cap the upper bound
max: 600

Configuration Details

fix

  • Type: duration; Required: no
  • Purpose: Force all TTLs to one fixed value.

min

  • Type: duration; Required: no
  • Purpose: Lower bound for TTLs.

max

  • Type: duration; Required: no
  • Purpose: Upper bound for TTLs.

quick setup

- exec: "ttl 300"
- exec: "ttl 60-600"

Behavior

  • Adjusts TTLs on the response path.
  • Can fix, clamp, or normalize TTLs.

Typical Uses

  • Stabilize answer retention
  • Avoid extreme upstream TTL values

ip_selector

Experimental Plugin (Introduced in v1.2.0)

ip_selector was first introduced in v1.2.0 and is currently in an experimental stage. Configuration fields, default values, probing strategies, and cache semantics may be adjusted in future releases based on real-world usage feedback. It is recommended to validate the plugin in non-critical environments first and monitor the release notes for related changes.

Purpose

Response IP selector. It only shapes A/AAAA records already present in response, using cached probe scores and bounded active probing to produce a stable order or a smaller answer set.

ip_selector does not race upstream DNS responses, suppress dual-stack families, or carry its own domain-rule system. Use forward / fallback for upstream racing, prefer_ipv4 / prefer_ipv6 for dual-stack policy, and sequence with matchers/providers for domain-specific routing.

Example Configuration

- tag: ip_select
type: ip_selector
args:
selection_mode: first_success
outbound: remote
probe_methods: ["tcp:443", "tcp:80"]
probe_stagger: 200
probe_timeout: 600
max_wait: 1000
top_n: 1
dnssec_policy: reorder_only
max_parallel_probes: 256
cache:
enabled: true
size: 4096
ttl: 3600
failure_ttl: 60

Recommended sequence placement:

- exec: $ip_select
- exec: $cache_main
- matches: !$has_resp
exec: $forward_main
- exec: accept

This lets cache store the original upstream response while ip_selector applies final response shaping on the return path.

Configuration Details

selection_mode

  • Type: string; Required: no; Default: first_success
  • Values: first_success, best_within_budget, background
  • Purpose:
    • first_success: use the first candidate IP with a successful probe.
    • best_within_budget: choose the lowest-latency IP within max_wait.
    • background: return immediately using the original or cached order while refreshing probe scores in the background.

probe_methods

  • Type: array<string> | string; Required: no; Default: ["tcp:443", "tcp:80"]
  • Values: tcp:<port>, ping, none
  • Purpose: Probe methods used to score answer IPs. ping is best-effort and fails open when unavailable.

outbound

  • Type: string; Required: no; Default: network.outbound.default
  • Purpose: References a network.outbound.profiles entry so TCP probes can reuse the profile proxy.
  • Notes:
    • Only tcp:<port> probes use proxy settings; ping always runs locally.
    • This field only uses the profile proxy. Target IPs are already known and are not resolved again.

socks5

  • Type: string; Required: no; Default: none
  • Purpose: Local SOCKS5 proxy override for TCP probes.
  • Notes:
    • Uses the same format as upstreams[].socks5.
    • When both outbound and socks5 are configured, local socks5 overrides the profile proxy.
    • Only tcp:<port> probes use SOCKS5; ping always runs locally.

probe_stagger

  • Type: integer; Unit: milliseconds; Required: no; Default: 200
  • Purpose: Delay between launching different probe methods.

probe_timeout

  • Type: integer; Unit: milliseconds; Required: no; Default: 600
  • Purpose: Timeout for one probe attempt.

max_wait

  • Type: integer; Unit: milliseconds; Required: no; Default: 1000
  • Purpose: Maximum per-response wait budget for selection.

top_n

  • Type: integer; Required: no; Default: 1
  • Purpose: Keep the first N selected addresses. Use 0 to reorder only without deleting records.

dnssec_policy

  • Type: string; Required: no; Default: reorder_only
  • Values: reorder_only, skip
  • Purpose: When the request carries DO bit or the response contains RRSIGs covering A/AAAA, the default policy only reorders records. skip leaves the response unchanged.

max_parallel_probes

  • Type: integer; Required: no; Default: 256
  • Purpose: Plugin-wide active probe concurrency limit.

cache

  • Type: object; Required: no
  • Fields:
    • enabled: enable probe score cache, default true.
    • size: target cache capacity, default 4096.
    • ttl: successful score retention in seconds, default 3600.
    • failure_ttl: failed score retention in seconds, default 60.

quick setup

- exec: "ip_selector"
- exec: "ip_selector best_within_budget tcp:443,tcp:80,ping"

ip_selector accepts only the OxiDNS-native names listed above and does not provide compatibility aliases.

Metrics

  • ip_selector_probe_total{method,result}
  • ip_selector_probe_latency_count{method}
  • ip_selector_probe_latency_sum_ms{method}
  • ip_selector_selected_total{source="probe|cache|fallback"}
  • ip_selector_cache_entries
  • ip_selector_dropped_probe_total{reason="parallel_limit|inflight"}

prefer_ipv4 / prefer_ipv6

Purpose

Biases dual-stack results toward one address family.

Example Configuration

- tag: prefer_v4
type: prefer_ipv4
args:
# Cache whether the preferred family exists
cache: true
# Keep the preference cache for one hour
cache_ttl: 3600

Configuration Details

cache

  • Type: boolean; Required: no; Default: true
  • Purpose: Cache preference decisions.

cache_ttl

  • Type: integer; Required: no; Default: 3600
  • Unit: seconds
  • Purpose: Retention for the preference cache.

quick setup

- exec: "prefer_ipv4"
- exec: "prefer_ipv6"
Notes
  • Quick setup uses the default configuration: cache: true and cache_ttl: 3600.
  • Use the full plugin configuration when disabling the cache or changing its TTL.

Behavior

  • Helps make A and AAAA selection more stable when both families exist.
  • Preferred-family queries pass through normally and warm the preference cache when they return preferred-family answers.
  • Non-preferred-family queries are blocked immediately when the cache says the preferred family exists; otherwise prefer_ipv4 / prefer_ipv6 runs the downstream chain concurrently for the original query and a preferred-family probe.

Typical Uses

  • Prefer the family that works better on a given network
  • Reduce dual-stack instability
Notes
  • Preference is not a substitute for fixing broken transport paths.

black_hole

Purpose

Generates local interception responses for matched DNS queries, covering every qtype.

Example Configuration

- tag: sinkhole
type: black_hole
args:
mode: custom
ips:
# Returned for A queries
- "0.0.0.0"
# Returned for AAAA queries
- "::"
short_circuit: true

- tag: block_nxdomain
type: black_hole
args:
mode: nxdomain
short_circuit: true

Configuration Details

mode

  • Type: string; Required: no; Default: nxdomain when ips is empty, custom when ips is configured
  • Values:
    • nxdomain: returns NXDOMAIN and adds an SOA authority record for negative caching.
    • nodata: returns an empty NOERROR response and adds an SOA authority record for negative caching.
    • null: returns 0.0.0.0 for A, :: for AAAA, and NODATA for other qtypes.
    • custom: returns matching address-family values from ips for A / AAAA; missing families and other qtypes return NODATA.
    • refused: returns an empty REFUSED response.
  • Compatibility: legacy ips + short_circuit config does not need mode; it is treated as custom.

ips

  • Type: array; Required: no; Default: empty array
  • Purpose: Local synthetic addresses used by custom mode.
  • Operational impact:
    • IPv4 addresses are used only for A responses.
    • IPv6 addresses are used only for AAAA responses.
    • ips is allowed only for implicit or explicit custom mode.

short_circuit

  • Type: bool; Required: no; Default: false
  • Purpose: Stops the remaining executor chain after an interception response is generated.

quick setup

- exec: "black_hole"
- exec: "black_hole nxdomain short_circuit=true"
- exec: "black_hole nodata"
- exec: "black_hole null"
- exec: "black_hole custom 0.0.0.0 :: short_circuit=true"
# legacy form, equivalent to custom
- exec: "black_hole 0.0.0.0 ::"

Behavior

  • Plain black_hole returns NXDOMAIN.
  • nxdomain, nodata, and refused cover every qtype.
  • null and custom return addresses for A/AAAA and NODATA for other qtypes.
  • Requests without a question pass through unchanged.
  • By default it keeps running the remaining chain after a match; enable short_circuit to stop immediately.

Metrics

Exported through the global GET /api/metrics endpoint:

  • blackhole_block_total

Typical Uses

  • Blocking ads, trackers, and malicious domains
  • Returning NXDOMAIN / NODATA / REFUSED by policy
  • Returning null or custom placeholder addresses

drop_resp

Purpose

Drops the current response.

Example Configuration

- tag: clear_response
type: drop_resp
# No standalone args; execution simply clears the current response

Configuration Details

No standalone configuration fields.

quick setup

- exec: "drop_resp"

Behavior

  • Clears the existing response from context so later rules can continue.

Typical Uses

  • Discard unwanted intermediate results
  • Force a later branch to rebuild the answer

reverse_lookup

Purpose

Maintains a reverse IP-to-name cache and optionally handles PTR requests.

Example Configuration

- tag: reverse_lookup_main
type: reverse_lookup
args:
# Reverse-cache capacity
size: 65535
# Retention time for IP -> name mappings
ttl: 7200
# Answer PTR directly from the learned cache
handle_ptr: true

Configuration Details

size

  • Type: integer; Required: no
  • Purpose: Reverse cache capacity.

handle_ptr

  • Type: boolean; Required: no; Default: false
  • Purpose: Answer PTR requests from the reverse cache.

ttl

  • Type: duration; Required: no
  • Purpose: Reverse cache retention TTL.

Behavior

  • Learns from successful responses.
  • Can expose cached domain names for IP lookups and PTR handling.

Plugin API

  • GET /plugins/<tag>?ip=<ip_addr>

Typical Uses

  • Debugging resolved destinations
  • Supporting PTR-like introspection for learned answers
Notes
  • This is an auxiliary index, not a replacement for authoritative PTR data.

learn_domain

Experimental Plugin (Introduced in v1.2.0)

learn_domain was first introduced in v1.2.0 and is currently in an experimental stage. Filtering rules, write trigger timing, and the integration interface with dynamic_domain_set may change in future releases. Before deploying in production, please evaluate the impact of learning rates and disk writes on the persistence layer.

Purpose

Observes qnames in the DNS pipeline and writes matching request names into a target dynamic_domain_set. It is a side-effect executor and never modifies the DNS request or response.

Example Configuration

- tag: learned_allow
type: dynamic_domain_set
args:
path: "/etc/oxidns/learned-allow.txt"

- tag: learn_allow
type: learn_domain
args:
provider: learned_allow
phase: after
questions: first
qtypes: ["A", "AAAA"]
success_only: true
answer_required: true
rule_kind: full
async: true
error_mode: continue
timeout: 1s

Configuration Details

provider

  • Type: string; Required: yes
  • Purpose: Target dynamic_domain_set provider tag.
  • Constraint: must reference dynamic_domain_set, not a regular domain_set.

phase

  • Type: string; Required: no; Default: after
  • Values: before, after
  • Purpose:
    • before: learn from the request questions before downstream executors run.
    • after: run downstream first, then decide whether to learn based on the response.

questions

  • Type: string; Required: no; Default: first
  • Values: first, all
  • Purpose: Learns only the first question or all questions.

qtypes

  • Type: array; Required: no; Default: ["A", "AAAA"]
  • Purpose: DNS query types eligible for learning.

success_only

  • Type: bool; Required: no; Default: true
  • Purpose: Only applies to phase: after; requires NOERROR.

answer_required

  • Type: bool; Required: no; Default: true
  • Purpose: Only applies to phase: after; requires at least one answer record.

rule_kind

  • Type: string; Required: no; Default: full
  • Values: full, domain
  • Purpose: Rule kind written into the target dynamic_domain_set.

async

  • Type: bool; Required: no; Default: true
  • Purpose: Enqueues learned rules and continues immediately. When false, waits for provider persistence.

error_mode

  • Type: string; Required: no; Default: continue
  • Values: continue, stop, fail
  • Purpose: Controls execution behavior if learning fails.

timeout

  • Type: duration; Required: no; Default: 1s
  • Purpose: Limits provider write waiting time when async: false.

Behavior

  • Learned domains are normalized to lowercase with trailing dots removed.
  • The default output is full:example.com to avoid broad accidental matches.
  • With the default phase: after, only successful A / AAAA responses with answers are learned.
  • phase: before does not inspect response conditions.
  • v1 only learns the request question qname and does not learn CNAME targets.

Typical Uses

  • Add successfully resolved domains to a dynamic allow list.
  • Persist domains seen on a policy branch into a local file for later qname $learned_allow or parent domain_set matching.

query_summary

Purpose

Records concise query summaries.

Example Configuration

- tag: summary_main
type: query_summary
args:
# Extra title so multiple summary points are easy to distinguish
msg: "main pipeline"

Configuration Details

msg

  • Type: string; Required: no
  • Purpose: Extra summary label.

quick setup

- exec: "query_summary main"

Behavior

  • Emits compact logs or summaries for operator visibility.

Typical Uses

  • Light observability on the main path
  • Distinguish different branches

query_recorder

Purpose

Persists the entry request, the post-next response, and sequence execution-path events into a recorder-owned SQLite database, then exposes history, aggregate stats, and an SSE stream.

Example Configuration

- tag: query_recorder_main
type: query_recorder
args:
# SQLite path for this recorder. Different recorders should use different paths.
path: "./data/query-recorder-main.sqlite"
# Hot-path enqueue buffer size
queue_size: 8192
# Batch size per SQLite flush
batch_size: 256
# Background flush interval in milliseconds
flush_interval_ms: 200
# Number of recent records kept in memory for SSE tail replay
memory_tail: 1024
# Retention window in days; minimum 1
retention_days: 7
# Cleanup interval in hours; minimum 1
cleanup_interval_hours: 1
# Maximum concurrent SQLite readers; minimum 1
reader_concurrency: 2

Configuration Details

path

  • Type: string; Required: yes
  • Purpose: SQLite path for this recorder.

queue_size

  • Type: integer; Required: no; Default: 8192
  • Purpose: Bounded queue size between the request path and the writer thread.

batch_size

  • Type: integer; Required: no; Default: 256
  • Purpose: Number of records flushed per SQLite batch.

flush_interval_ms

  • Type: integer; Required: no; Default: 200
  • Purpose: Maximum batch flush interval in milliseconds.

memory_tail

  • Type: integer; Required: no; Default: 1024
  • Purpose: Size of the in-memory tail used by stream?tail=n.

retention_days

  • Type: integer; Required: no; Default: 7; Minimum: 1
  • Purpose: Record retention window. Expired rows are deleted by the cleanup task.

cleanup_interval_hours

  • Type: integer; Required: no; Default: 1; Minimum: 1
  • Purpose: Cleanup task cadence.

reader_concurrency

  • Type: integer; Required: no; Default: 2; Minimum: 1
  • Purpose: Limits concurrent SQLite readers used by WebUI / API history and stats queries, so read bursts on large recorder databases do not occupy too many blocking threads or too much memory.

Behavior

  • This is a pure executor observer and does not change server finalization logic.
  • It captures a structured snapshot of the entry request, enables DnsContext.execution_path, runs next, and commits immediately after next returns.
  • Successful runs store the current response. Failed runs store error and an empty response shape.
  • Request and response payloads are not stored as wire blobs. Question, RR, and EDNS fields are extracted into JSON text columns.
  • The client_ip field comes from the transport source address seen by OxiDNS; if a local forwarding chain such as systemd-resolved, dnsmasq, AdGuardHome, dae, or clash sits in front, rows may show only 127.0.0.1.
  • Each recorder uses versioned tables under the same prefix:
    • qr_<safe_tag>_<fnv64hex>_v1_records
    • qr_<safe_tag>_<fnv64hex>_v1_steps
    • qr_<safe_tag>_<fnv64hex>_v1_questions
    • qr_<safe_tag>_<fnv64hex>_v1_meta
  • records contains only the fixed schema fields for structured snapshots. steps stores sequence path events for path analysis and hit-rate reporting. questions is a derived index table from questions_json for faster qname/qtype queries. meta records completed derived-table migrations.
  • Every recorder owns its own bounded queue, SQLite connection, writer thread, tail buffer, and SSE broadcaster.
  • v1 assumes different recorders use different path values. There is no cross-recorder writer sharing or path coordination.

Data Shape

  • questions_json is always a question array, for example:
[
{ "name": "www.example.com.", "qtype": "A", "qclass": "IN" }
]
  • answers_json, authorities_json, additionals_json, and signature_json are RR arrays, for example:
[
{
"name": "www.example.com.",
"class": "IN",
"ttl": 300,
"rr_type": "A",
"payload_kind": "A",
"payload_text": "192.0.2.1",
"payload": { "ip": "192.0.2.1" }
}
]
  • req_edns_json and resp_edns_json are EDNS objects or NULL.
  • The v1 suffix in the table name is the schema version. Future upgrades add new versioned tables rather than altering the existing ones in place.

API

  • GET /plugins/<tag>/records
    • Returns record rows ordered by created_at_ms descending.
    • Query parameters:
      • cursor=<created_at_ms>:<id>
      • limit=<n>, default 100, max 500
      • since_ms=<unix_ms>
      • until_ms=<unix_ms>
      • qname=<text>, substring match against request question names
      • client_ip=<text>, substring match against the client IP string
      • qtype=<type> / rcode=<rcode> / status=all|error|has_response|no_response
  • GET /plugins/<tag>/records/<id>
    • Returns one full record plus steps.
  • DELETE /plugins/<tag>/records
    • Clears all history rows and steps for the current recorder, and clears the in-memory tail.
    • The operation first flushes the background writer queue and returns cleared_records for the number of deleted main-table rows.
  • GET /plugins/<tag>/stats/plugins
    • Returns hit stats grouped by matcher / executor / builtin.
    • Supports since_ms, until_ms, kind=matcher|executor|builtin|all, and the record filters.
  • GET /plugins/<tag>/stats/top_clients / stats/top_qnames
    • Returns client IP or QNAME rankings.
    • Supports limit=<n>, default 20; the backend no longer enforces a 200 cap.
    • Supports the same time range and filter parameters as records.
  • GET /plugins/<tag>/stats/qtype / stats/rcode
    • Returns QTYPE or RCODE distribution.
    • Supports the same time range and filter parameters as records.
  • GET /plugins/<tag>/stats/latency
    • Returns latency summary values, histogram buckets, and slow-query rankings.
    • Supports slow_limit=<n> or limit=<n>, default 20; the backend no longer enforces a 200 cap.
  • GET /plugins/<tag>/stats/timeseries
    • Returns query trends aggregated by minute or hour.
    • Supports bucket=minute|hour and buckets=<n> (default 60, maximum 720).
  • GET /plugins/<tag>/stream
    • Streams newly written records over SSE.
    • Supports tail=<n> to replay the in-memory tail.
    • Clients should send Accept: text/event-stream and tolerate heartbeat frames, error events, empty payloads, and brief disconnects.

Typical Uses

  • Persistent audit and troubleshooting trails
  • sequence path analysis and plugin hit-rate reporting
  • Real-time query log feeds for dashboards or control planes
Notes
  • Place the recorder close to the entry point when the full main-path trace is required.
  • If an earlier branch short-circuits before the recorder, that request will not be recorded.
  • If next fails and the server later emits a fallback response, the database still reflects the plugin's point of view: error plus an empty response.
  • If the management API is disabled, the recorder still writes SQLite data but does not expose query or SSE routes.
  • To preserve real client IPs, point clients directly at OxiDNS or configure a trusted src_ip_header for HTTP/DoH reverse-proxy deployments.

metrics_collector

Purpose

Collects Prometheus metrics for query handling.

Example Configuration

- tag: metrics_main
type: metrics_collector
args:
# Collector label exported through /api/metrics
name: "main"

Configuration Details

name

  • Type: string; Required: no
  • Purpose: Metrics label namespace.

quick setup

- exec: "metrics_collector main"

Behavior

  • Exposes query counters, inflight counts, and latency metrics through the management API.

API

  • GET /api/metrics
    • Prometheus text format. This is the single global endpoint, and built-in metrics from other plugins are exported through the same route.

Typical Uses

  • Prometheus integration
  • Observe multiple policy entry points separately

debug_print

Purpose

Prints a debug message.

Example Configuration

- tag: debug_main
type: debug_print
args:
# Log title; defaults to "debug print" when omitted
msg: "before forward"

Configuration Details

msg

  • Type: string; Required: yes
  • Purpose: Message content.

quick setup

- exec: "debug_print cache branch"

Typical Uses

  • Temporary debugging
  • Reading sequence branches during development

sleep

Purpose

Sleeps for a bounded duration inside the chain.

Example Configuration

- tag: sleep_100ms
type: sleep
args:
# Add 100 ms of async delay
duration: 100

Configuration Details

duration

  • Type: duration; Required: yes
  • Purpose: Sleep duration.

quick setup

- exec: "sleep 100"

Typical Uses

  • Testing
  • Timing experiments

http_request

Purpose

Sends callback requests to external http/https services. It can trigger before the current DNS flow enters downstream executors or after downstream execution completes, which makes it suitable for webhooks, audit pipelines, alerts, and external integrations.

Example Configuration

- tag: webhook_notify_after
type: http_request
args:
method: POST
url: "https://hooks.example.com/dns"
phase: after
async: true
timeout: 5s
headers:
X-Client-IP: "${client_ip}"
X-Qname: "${qname}"
query_params:
source: "oxidns"
qname: "${qname}"
json:
qname: "${qname}"
client_ip: "${client_ip}"
rcode: "${rcode_name}"
resp_ip: "${resp_ip}"

Config Fields

args.method

  • Type: string; Required: yes
  • Purpose: Selects the HTTP method such as GET, POST, PUT, PATCH, or DELETE.

args.url

  • Type: string; Required: yes
  • Purpose: The target URL.
  • Notes: Supports ${key} placeholder interpolation. The rendered URL must use either http or https.

args.phase

  • Type: string; Required: no; Default: after
  • Allowed values: before, after
  • Purpose: Controls whether the request is sent before or after downstream executors run.

args.async

  • Type: boolean; Required: no; Default: true
  • Purpose: Chooses bounded background dispatch or inline synchronous dispatch.

args.timeout

  • Type: string; Required: no; Default: 5s
  • Purpose: Caps the total time budget for one HTTP call.
  • Supported units: ms, s, m, h, d

args.error_mode

  • Type: string; Required: no; Default: continue
  • Allowed values:
    • continue: only log the failure and keep running
    • stop: return Stop on failure
    • fail: return an executor error immediately

args.headers

  • Type: map<string,string>; Required: no; Default: empty
  • Purpose: Adds HTTP request headers.
  • Notes: Header values support ${key} placeholder interpolation.

args.query_params

  • Type: map<string,string>; Required: no; Default: empty
  • Purpose: Appends additional query parameters to the rendered URL.
  • Notes: Values support ${key} placeholder interpolation and are combined with any query already present in args.url.

args.body

  • Type: string; Required: no
  • Purpose: Sends a raw string body.
  • Notes: Supports ${key} placeholder interpolation and can be paired with args.content_type.

args.json

  • Type: object | array; Required: no
  • Purpose: Sends a JSON body.
  • Notes: Automatically sets Content-Type: application/json. Every string leaf supports ${key} interpolation while non-string values are preserved as-is.

args.form

  • Type: map<string,string>; Required: no
  • Purpose: Sends an application/x-www-form-urlencoded body.
  • Notes: Values support ${key} interpolation and the plugin automatically sets the matching Content-Type.

args.content_type

  • Type: string; Required: no
  • Purpose: Sets Content-Type for raw args.body.
  • Notes: This helper can only be used with args.body, not with args.json or args.form.

args.outbound

  • Type: string; Required: no
  • Purpose: References a profile from network.outbound.profiles to control the resolver and proxy used by this HTTP request.
  • Notes: When omitted, the request uses network.outbound.default. If args.socks5 is also set, socks5 only overrides the proxy while the resolver still comes from the outbound profile.

args.socks5

  • Type: string; Required: no
  • Purpose: Routes requests through a SOCKS5 proxy.
  • Notes: Uses the same format as upstream[].socks5, including host:port, username:password@host:port, and bracketed IPv6.

args.insecure_skip_verify

  • Type: boolean; Required: no; Default: false
  • Purpose: Skips HTTPS certificate validation.

args.max_redirects

  • Type: integer; Required: no; Default: 5
  • Purpose: Limits how many redirects are followed.

args.queue_size

  • Type: integer; Required: no; Default: 256
  • Purpose: Sets the bounded queue capacity used by async mode.

Available Placeholders

  • Same as script: qname, qtype, qtype_name, qclass, qclass_name
  • Source fields: client_ip, client_port, server_name, url_path
  • Runtime fields: marks, has_resp
  • Response fields: rcode, rcode_name, resp_ip
  • Cron metadata: cron_plugin_tag, cron_job_name, cron_trigger_kind, cron_scheduled_at_unix_ms

Behavior

  • With phase: before, the HTTP request is dispatched first and the downstream executor chain runs afterward.
  • With phase: after, the downstream executor chain runs first and the HTTP request is dispatched against the resulting context.
  • async: true uses a bounded background queue. Queue insertion failures are handled according to error_mode.
  • async: false waits for the HTTP call on the current request path.
  • Only terminal 2xx responses are treated as success. 3xx responses are followed up to max_redirects.
  • The plugin drains and discards the HTTP response body so connections remain reusable, but it does not write that body back into DnsContext.
  • If Content-Type is already set explicitly in args.headers, the plugin does not overwrite it.
Notes
  • args.body, args.json, and args.form are mutually exclusive.
  • This is a side-effect executor. In v1 it cannot rewrite DNS requests, responses, marks, or attrs based on the HTTP result.
  • v1 does not support multipart uploads or quick setup syntax.
  • Configure two separate http_request plugin instances when both trigger moments are required.

script

Purpose

Runs an explicitly configured external command and injects a stable subset of the current DnsContext into command arguments or environment variables.

Example

- tag: script_notify
type: script
args:
command: "bash"
args:
- "/etc/oxidns/notify.sh"
- "${qname}"
- "${client_ip}"
env:
FDNS_QNAME: "${qname}"
FDNS_CLIENT_IP: "${client_ip}"
FDNS_MARKS: "${marks}"
timeout: "5s"
error_mode: continue
max_output_bytes: 4096

Config Fields

args.command

  • Type: string; Required: yes
  • Purpose: Command path or program name to execute.
  • Notes: This field is never templated.

args.args

  • Type: array<string>; Required: no; Default: empty
  • Purpose: Positional command arguments.
  • Notes: Each item supports ${key} interpolation.

args.env

  • Type: map<string,string>; Required: no; Default: empty
  • Purpose: Extra child-process environment variables.
  • Notes: Values support ${key} interpolation and overlay the inherited process environment.

args.cwd

  • Type: string; Required: no; Default: none
  • Purpose: Working directory for the child process.

args.timeout

  • Type: string; Required: no; Default: 5s
  • Purpose: Maximum execution time for one script run.
  • Supported units: ms, s, m, h, d

args.error_mode

  • Type: string; Required: no; Default: continue
  • Allowed values:
    • continue: log failure or timeout, then return Next
    • stop: log failure or timeout, then return Stop
    • fail: return an executor error immediately

args.max_output_bytes

  • Type: usize; Required: no; Default: 4096
  • Purpose: Maximum captured stdout/stderr length before truncation.

Available Placeholders

  • Request fields: qname, qtype, qtype_name, qclass, qclass_name
  • Source fields: client_ip, client_port, server_name, url_path
  • Runtime fields: marks, has_resp
  • Response fields: rcode, rcode_name, resp_ip
  • Cron metadata: cron_plugin_tag, cron_job_name, cron_trigger_kind, cron_scheduled_at_unix_ms

Behavior

  • The plugin does not mutate DNS requests or responses.
  • It runs only the explicit configured command and does not wrap it with sh -c, cmd /c, or similar shell shortcuts.
  • Arguments and environment variables are rendered from the current DnsContext on each execution.
  • On timeout the child process is terminated, then error_mode decides how the sequence continues.
Notes
  • v1 does not support quick setup syntax.
  • command must not be empty.
  • Only the documented built-in placeholders are accepted; unknown placeholders fail plugin initialization.
  • This is a side-effect executor. It does not support writing attrs, marks, or DNS responses back through stdout.

ipset

Purpose

Writes response IPs into Linux ipset through the embedded Rust netlink backend, without requiring the runtime ipset command.

Example Configuration

- tag: ipset_main
type: ipset
args:
# ipset used for A answers
set_name4: "oxidns_v4"
# ipset used for AAAA answers
set_name6: "oxidns_v6"
# Aggregate IPv4 writes to /24 prefixes
mask4: 24
# Aggregate IPv6 writes to /64 prefixes
mask6: 64

Configuration Details

set_name4

  • Type: string; Required: no; Default: none
  • Purpose: Specifies the ipset name used to write IPv4 addresses.

set_name6

  • Type: string; Required: no; Default: none
  • Purpose: Specifies the ipset name used to write IPv6 addresses.

mask4

  • Type: integer; Required: no; Default: 24
  • Purpose: Specifies the prefix length used when writing IPv4 addresses into ipset.

mask6

  • Type: integer; Required: no; Default: 32
  • Purpose: Specifies the prefix length used when writing IPv6 addresses into ipset.

quick setup

- exec: "ipset oxidns_v4,inet,24 oxidns_v6,inet6,64"

Format:

<set_name>,<family>,<mask>

Here, family is inet or inet6.

Behavior

  • Extracts unique A/AAAA addresses from the answer section.
  • Writes them into the corresponding set according to the address family.
  • Delivers them to the background writer through a non-blocking queue.

Typical Uses

  • Policy routing
  • Firewall integration
Notes
  • On non-Linux platforms it degrades to a no-op.
  • When the queue is full, the side effect is dropped and does not block the DNS hot path.

nftset

Purpose

Writes response IPs into nftables sets through the embedded Rust netlink backend, without requiring the runtime nft command.

Example Configuration

Structured form:

- tag: nftset_main
type: nftset
args:
ipv4:
# IPv4 target uses the ip family
table_family: "ip"
table_name: "mangle"
set_name: "dns_v4"
mask: 24
ipv6:
# IPv6 target uses the ip6 family
table_family: "ip6"
table_name: "mangle"
set_name: "dns_v6"
mask: 64

Compatibility form:

- tag: nftset_legacy
type: nftset
args:
# Compatibility fields, useful when migrating old configs
table_family4: "ip"
table_name4: "mangle"
set_name4: "dns_v4"
mask4: 24
table_family6: "ip6"
table_name6: "mangle"
set_name6: "dns_v6"
mask6: 64

Configuration Details

ipv4

  • Type: object; Required: no; Default: none
  • Purpose: Defines the target IPv4 nftables set.
  • Child fields:
    • table_family
    • table_name
    • set_name
    • mask

ipv6

  • Type: object; Required: no; Default: none
  • Purpose: Defines the target IPv6 nftables set.
  • Child fields:
    • table_family
    • table_name
    • set_name
    • mask

table_family4 / table_family6

  • Type: string; Required: no; Default: none
  • Purpose: In the compatibility form, defines the nftables table family for IPv4 / IPv6 respectively.

table_name4 / table_name6

  • Type: string; Required: no; Default: none
  • Purpose: In the compatibility form, defines the nftables table name for IPv4 / IPv6 respectively.

set_name4 / set_name6

  • Type: string; Required: no; Default: none
  • Purpose: In the compatibility form, defines the set name for IPv4 / IPv6 respectively.

mask4 / mask6

  • Type: integer; Required: no; Default: 24 for mask4, 48 for mask6
  • Purpose: In the compatibility form, defines the prefix length for IPv4 / IPv6 respectively.

quick setup

- exec: "nftset ip,mangle,dns_v4,ipv4_addr,24 ip6,mangle,dns_v6,ipv6_addr,64"

Format:

<family>,<table>,<set>,<type>,<mask>

Behavior

  • Extracts A/AAAA addresses.
  • Writes nftables interval elements according to the prefix.
  • Also uses the background writer so that the hot path remains non-blocking.

Typical Uses

  • nftables-driven routing or firewall policies
Notes
  • On non-Linux platforms it degrades to a no-op.

ros_address_list

Purpose

Writes response IPs into MikroTik RouterOS address lists, with dynamic entries, persistent entries, startup-time file loading, and shutdown cleanup.

Example Configuration

- tag: ros_address_list_main
type: ros_address_list
args:
# RouterOS API endpoint
address: "172.16.1.1:8728"
# API username
username: "api-user"
# API password
password: "secret"
# RouterOS API connection timeout, in seconds
connect_timeout: 5
# RouterOS API command send timeout, in seconds
send_timeout: 5
# RouterOS API response receive timeout, in seconds; increase only for unavoidable slow legacy scans
receive_timeout: 30
# Use asynchronous writes to avoid blocking the DNS hot path
async: true
# Address list used for A records
address_list4: "oxidns_ipv4"
# Address list used for AAAA records
address_list6: "oxidns_ipv6"
# Prefix for comments on OxiDNS-managed entries
comment_prefix: "oxidns"
# Lower bound for dynamic-entry TTL
min_ttl: 60
# Upper bound for dynamic-entry TTL
max_ttl: 3600
# Force dynamic entries to 300 seconds; use 0 to omit RouterOS timeout
fixed_ttl: 300
# Remove owned entries when the plugin shuts down
cleanup_on_shutdown: true
persistent:
ips:
# Persistent single IP
- "1.1.1.1"
# Persistent IPv4 CIDR
- "100.64.1.0/24"
# Persistent IPv6 CIDR
- "2001:db8::/64"
files:
# Load more persistent items from files
- "/etc/oxidns/persistent_ips.txt"

Configuration Details

address

  • Type: string; Required: yes
  • Purpose: RouterOS API endpoint.

username

  • Type: string; Required: yes
  • Purpose: RouterOS username.

password

  • Type: string; Required: yes
  • Purpose: RouterOS password.

connect_timeout

  • Type: u64; Required: no; Default: 5
  • Purpose: Maximum wait time, in seconds, for establishing a RouterOS API connection.
  • Notes: Must be greater than 0. Increase it if the management network or RouterOS API occasionally responds slowly.

send_timeout

  • Type: u64; Required: no; Default: 5
  • Purpose: Maximum wait time, in seconds, for sending one RouterOS API command.
  • Notes: Must be greater than 0. The default is usually sufficient.

receive_timeout

  • Type: u64; Required: no; Default: 5
  • Purpose: Maximum wait time, in seconds, for the next chunk of RouterOS API response data.
  • Tuning: Prefer a dedicated, size-controlled address-list for OxiDNS. Avoid connecting the plugin to an existing large shared list. Increase this value, for example to 30 or 60, only when slow legacy list queries or a slow RouterOS management plane cannot be avoided.

async

  • Type: bool; Required: no; Default: true
  • Purpose: Controls whether address writes use asynchronous mode. When enabled, the DNS response path only submits tasks, and a background manager completes the RouterOS interaction.

address_list4

  • Type: string; Required: no
  • Purpose: IPv4 address-list name.

address_list6

  • Type: string; Required: no
  • Purpose: IPv6 address-list name.

comment_prefix

  • Type: string; Required: no
  • Purpose: Prefix for generated RouterOS comments.

persistent

  • Type: object; Required: no; Default: none
  • Purpose: Defines the static address set that should be kept for the long term. This part does not depend on DNS responses to trigger. After plugin startup it can be synchronized to RouterOS directly and then kept consistent by the reconcile loop.

persistent.ips

  • Type: array<string>; Required: no; Default: empty
  • Purpose: Declares persistent IPs or CIDR ranges inline.

persistent.files

  • Type: array<string>; Required: no; Default: empty
  • Purpose: Loads the persistent address set from external files at plugin startup.
  • Notes: These files are read once during initialization. To apply later file changes, reload the plugin or the application.

min_ttl

  • Type: u64; Required: no; Default: 60
  • Purpose: Defines the minimum TTL allowed for dynamic address entries.

max_ttl

  • Type: u64; Required: no; Default: 3600
  • Purpose: Defines the maximum TTL allowed for dynamic address entries.

fixed_ttl

  • Type: u64; Required: no; Default: none
  • Purpose: Specifies one fixed TTL for all dynamically written entries. If it is set to 0, dynamic entries will not set a RouterOS timeout.

cleanup_on_shutdown

  • Type: bool; Required: no; Default: true
  • Purpose: Controls whether entries managed by the plugin are removed when the plugin exits.

Behavior

  • The plugin itself does not modify DNS responses.
  • Startup does not wait for RouterOS address-list scans to finish. Initial connectivity checks and persistent-entry reconciliation run in the background manager; failures are logged without blocking DNS startup or dynamic observations.
  • It only passes through during the forward phase.
  • During the return phase:
    • Extracts A/AAAA records from NOERROR responses.
    • Deduplicates them and keeps the largest TTL.
    • Submits them to the background manager according to async or sync mode.
  • The manager is responsible for:
    • Background initial connectivity verification
    • Dynamic entry refresh
    • Persistent entry consistency maintenance
    • Cleanup on shutdown

Typical Uses

  • DNS-driven policy routing on RouterOS
  • Maintaining dynamic destination groups from DNS answers
Notes
  • At least one of address_list4 or address_list6 is required.
  • Prefer dedicated OxiDNS address-list targets and avoid large shared lists to reduce RouterOS management-plane scan cost.
  • comment_prefix and the plugin tag must not contain ; or =.
  • Synchronous mode does not change the DNS response itself. Even if the RouterOS write fails, the DNS result is still preserved.

upgrade

Purpose

Runs the OxiDNS upgrade flow from the executor pipeline. It is suitable for maintenance tasks triggered by cron, sequence, or another executor.

Example Configuration

- tag: upgrade_auto
type: upgrade
args:
repository: svenshi/oxidns
asset: auto
bundle: auto
github_token: ghp_xxx
cache_dir: ./upgrade/cache
backup_dir: ./upgrade/backups
webui_dir: ./webui
skip_webui: false
no_restart: false
force: false
cleanup: true
timeout: 30s
outbound: remote
socks5: 127.0.0.1:1080
insecure_skip_verify: false

Options

  • force
    • Boolean. Default: false.
    • Continue downloading, verifying, and replacing even when the selected release is not newer than the current version.
  • cleanup
    • Boolean. Default: true.
    • Cleans cache_dir and backup_dir after a successful upgrade.
  • repository
    • GitHub repository. Default: svenshi/oxidns.
  • asset
    • Release asset name. auto selects the archive for the current platform and build bundle.
    • An explicit asset has the highest priority and skips bundle inference.
  • bundle
    • Type: auto | full | standard | minimal.
    • Default: auto.
    • Selects the release build bundle when asset: auto is used. full uses the legacy asset name; standard / minimal use slim asset names with a bundle prefix.
  • github_token
    • GitHub personal access token for API requests, used to raise the rate limit or access private repositories.
    • The value is sent as a Bearer token on GitHub API requests.
  • cache_dir / backup_dir
    • Download cache and pre-replacement backup directories.
  • webui_dir
    • Path. Default: ./webui.
    • Directory where the WebUI static assets are installed during an upgrade; keep it aligned with api.http.webui.root.
  • skip_webui
    • Boolean. Default: false.
    • When true, only the binary is replaced and the WebUI directory upgrade is skipped.
  • no_restart
    • Boolean. Default: false.
    • When true, a successful upgrade does not trigger an automatic restart.
    • The default false restarts automatically after a successful upgrade: CLI apply restarts the installed service through the system service manager, while the executor requests a graceful in-process restart through the application control channel so the new binary is loaded.
  • timeout, outbound, socks5, insecure_skip_verify
    • Same meaning as the CLI upgrade flags.
    • outbound references a profile from network.outbound.profiles. The legacy socks5 field remains supported and overrides the profile proxy.

Behavior

  • The executor always returns ExecStep::Next.
  • The plugin only runs the apply action. It does not provide check or download modes.
  • By default it updates only when a newer version is available. force: true forces the update.
  • By default it cleans cache and backup files after a successful upgrade. Set cleanup: false to keep rollback files.
  • The upgrade downloads the archive and verifies SHA256 with the GitHub release asset digest field.
  • asset: auto selects the archive from bundle; bundle: auto follows the current binary's build bundle, and custom builds must set bundle or asset explicitly.
  • On Unix it unpacks .tar.gz, backs up the current binary, and replaces it. Windows currently does not support plugin upgrades.
  • By default, after replacing the binary it backs up and installs the archive's webui/ directory into webui_dir; set skip_webui: true to skip it. If the archive has no webui/ (older releases), the WebUI upgrade is skipped without affecting the binary upgrade result.

quick setup

- exec: upgrade
- exec: upgrade force
- exec: upgrade force=false
- exec: upgrade bundle=standard
  • Empty arguments run apply with the default configuration.
  • Only force and force=true|false are supported.
  • Other settings use defaults. Use full args configuration to override the repository, directories, restart mode, or proxy.
  • mode is not supported; the plugin always applies upgrades.

download

Purpose

Downloads one or more http/https files into a local directory and overwrites the target files only after the new content is fully written.

Example Configuration

- tag: rules_download
type: download
args:
timeout: 30s
outbound: remote
socks5: "127.0.0.1:1080"
downloads:
- url: "https://example.com/geosite.dat"
dir: "/etc/oxidns"
- url: "https://example.com/geoip.dat"
dir: "/etc/oxidns"
filename: "geoip.dat"

Quick Setup

- exec: "download https://example.com/rules.txt /etc/oxidns"

Behavior

  • downloads run sequentially in declaration order.
  • A failed item only emits a warning log and does not stop later items.
  • Missing target directories are created automatically.
  • Files are written to a temporary path first and then moved into place.
  • When outbound is set, downloads use the resolver and proxy defined by the referenced network.outbound.profiles entry.
  • When socks5 is set, all download connections are routed through that SOCKS5 proxy using the same format as upstream[].socks5.
  • By default, OxiDNS checks target files during startup and downloads any missing ones before other plugins initialize. A bootstrap failure aborts startup.
  • Set startup_if_missing: false to disable that bootstrap behavior.
Notes
  • Only http and https are supported.
  • When outbound is omitted, network.outbound.default is used. If both outbound and socks5 are set, socks5 overrides the profile proxy while preserving the profile resolver.
  • socks5 accepts host:port and username:password@host:port; bracket IPv6 addresses such as "[::1]:1080" are supported too.
  • startup_if_missing only fills missing files; it does not overwrite existing targets on every startup.
  • When used inside a normal sequence, the download time is paid directly by that request.
  • Overwriting a local file does not apply automatically. For file-backed providers that only need to pick up new data, prefer chaining reload_provider; if config.yaml, dependency topology, or the plugin list changed too, use reload.
- tag: rules_refresh
type: sequence
args:
- exec: "$rules_download"
- exec: "$reload_rules"

- tag: rules_download
type: download
args:
downloads:
- url: "https://example.com/geosite.dat"
dir: "/etc/oxidns"

- tag: provider_geosite
type: geosite
args:
file: "/etc/oxidns/geosite.dat"

- tag: reload_rules
type: reload_provider
args:
- "$provider_geosite"

Subscription Refresh Example

This example fits the common flow of “remote subscription -> scheduled download -> targeted provider refresh”:

plugins:
# 1. Run the subscription refresh flow periodically
- tag: subscription_cron
type: cron
args:
timezone: "Asia/Shanghai"
jobs:
- name: refresh_rule_subscriptions
interval: 6h
executors:
- "$subscription_refresh"

# 2. Chain download and targeted provider reload with a sequence
- tag: subscription_refresh
type: sequence
args:
- exec: "$subscription_download"
- exec: "$reload_rule_providers"

# 3. Download remote subscription files
- tag: subscription_download
type: download
args:
timeout: 60s
startup_if_missing: true
downloads:
- url: "https://example.com/geosite.dat"
dir: "/etc/oxidns/rules"
filename: "geosite.dat"
- url: "https://example.com/geoip.dat"
dir: "/etc/oxidns/rules"
filename: "geoip.dat"

# 4. Reload only the affected providers after download completes
- tag: reload_rule_providers
type: reload_provider
args:
- "$provider_geosite"
- "$provider_geoip"

# 5. These providers re-read the local files after reload
- tag: provider_geosite
type: geosite
args:
file: "/etc/oxidns/rules/geosite.dat"

- tag: provider_geoip
type: geoip
args:
file: "/etc/oxidns/rules/geoip.dat"
Notes
  • download writes the subscription content to local files.
  • reload_provider refreshes only the affected provider snapshots without rebuilding unrelated plugins.
  • startup_if_missing: true is useful for first-time deployment when files may not exist yet.
  • If the subscription source requires a proxy, set a SOCKS5 proxy on subscription_download.args.socks5.
  • To avoid overwriting existing files at startup, keep the default behavior and only bootstrap missing files.
  • If the update also changes config.yaml, provider topology, or the plugin list, use a full reload instead.

Full Reload Still Fits Config Changes

- tag: config_refresh
type: sequence
args:
- exec: "$subscription_download"
- exec: "$reload_all"

- tag: reload_all
type: reload

reload_provider

Purpose

Reloads one or more providers in place by tag, rebuilding their internal snapshots with the same startup configuration without triggering a full application reload.

Example Configuration

- tag: reload_rule_providers
type: reload_provider
args:
- "$geosite_cn"
- "$geoip_cn"

Quick Setup

- exec: "reload_provider $geosite_cn"

Behavior

  • Providers are reloaded sequentially in the order declared in args.
  • The semantics are the same as calling POST /plugins/<provider_tag>/reload for each referenced provider.
  • Once every provider reload succeeds, the executor returns Next.
  • Only provider-local data is refreshed; tags, dependencies, and other plugin configuration are unchanged.

Typical Uses

  • Refreshing only the affected domain_set, ip_set, geosite, geoip, or adguard_rule providers after download.
  • Reducing the blast radius and cost of a full application reload in background maintenance flows.
Notes
  • args only accepts provider references such as "$geoip_cn"; inline rules and file references are rejected.
  • If the update changes config.yaml, provider topology, the plugin list, or other non-provider structures, reload is still required.
  • Running this on a live request path may trigger file reads and recompilation, so it is usually a better fit for background cron or maintenance sequence flows.

reload

Purpose

Triggers the same application-level full reload as the management API POST /reload, reloading the active configuration and rebuilding all plugins.

Example Configuration

- tag: reload_all
type: reload

Quick Setup

- exec: "reload"

Behavior

  • Execution submits a reload request to the application control layer.
  • The semantics are the same as the management API POST /reload.
  • Once the reload request is accepted, the executor returns Next.
  • This is a full application reload. Reloading selected plugin tags is not supported.

Typical Uses

  • Pairing with download in a cron job so refreshed rule files take effect immediately.
  • Triggering a full configuration reload from a dedicated background sequence.
Notes
  • It must run inside a normal OxiDNS process with application control context attached.
  • Execution fails when another reload is already pending or in_progress.
  • Using it in a live request sequence triggers a full application reload and is usually not appropriate for latency-sensitive request paths.

cron

Purpose

Schedules a list of executors in the background. It does not participate in the live DNS request path and starts running only after plugin initialization.

Example Configuration

- tag: cron_jobs
type: cron
args:
timezone: "Asia/Shanghai"
jobs:
- name: refresh_sets
interval: 5m
executors:
- "$seq_refresh"
- "debug_print cron refresh"

- name: nightly_cleanup
schedule: "15 3 * * *"
executors:
- "sleep 2s"
- "$seq_cleanup"

Configuration Details

args.jobs

  • Type: array; Required: yes
  • Purpose: Defines one or more background jobs.
  • Runtime impact:
    • The array cannot be empty.
    • Each job maintains its own trigger state and overlap protection.

args.timezone

  • Type: string; Required: no
  • Default: system local time zone
  • Purpose: Overrides the time zone used by all schedule jobs in this cron plugin.
  • Notes:
    • Only affects schedule.
    • When omitted, OxiDNS uses the system local time zone and falls back to UTC if unavailable.
    • Use IANA names such as Asia/Shanghai, UTC, or America/Los_Angeles.

args.jobs[].name

  • Type: string; Required: yes
  • Purpose: Job name used in logs and runtime metadata.
  • Runtime impact:
    • Must be unique within the same cron plugin.

args.jobs[].schedule

  • Type: string; Required: exactly one of schedule or interval
  • Purpose: Schedule a job with a standard 5-field cron expression.
  • Notes:
    • Only minute hour day month day-of-week is supported.
    • Second-level cron expressions are not supported.
    • Next runs are computed in args.timezone or the system local time zone.

args.jobs[].interval

  • Type: string; Required: exactly one of schedule or interval
  • Purpose: Schedule a job with a fixed interval.
  • Supports:
    • 5m
    • 1h
    • 1d
  • Runtime impact:
    • Minimum interval is 1m.
    • The first run happens after one full interval elapses.

args.jobs[].executors

  • Type: array; Required: yes
  • Purpose: Ordered list of executors to run for the job.
  • Supports:
    • $tag explicit executor references
    • bare tag references
    • quick-setup expressions such as debug_print cron refresh
  • Runtime impact:
    • The array cannot be empty.
    • Later executors still run even if an earlier executor returns Stop, produces a response, or fails.

Behavior

  • schedule and interval are mutually exclusive.
  • If a job is still running when the next trigger arrives, that trigger is skipped and not replayed later.
  • Jobs run with an empty DnsContext, so this plugin is best suited for side-effect executors or dedicated background sequence chains.
  • cron itself cannot be executed inside a normal request sequence.

Typical Uses

  • Periodic side-effect tasks.
  • Scheduling a dedicated background sequence.
  • Providing a common trigger surface for future executors such as reload.
Notes
  • A cron job cannot reference another cron executor.
  • Executors that require a real DNS request usually do not make sense in an empty background context.