Skip to main content

External and System Integrations

These executors project DNS context into HTTP, scripts, Linux sets, or RouterOS. Isolate side-effect failures from the primary resolution path.

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. RouterOS firewall, mangle, or routing rules can consume the resulting address set.

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"
# Plaintext API is the default (usually 8728); configure tls to enable API-SSL (usually 8729)
# 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
# With async=false, stop waiting after this duration; work continues in the background
wait_timeout: 8s
# Distinct-key limit applied independently to ingress and retry queues
queue_capacity: 16384
# 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: "oxi"
# 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 during normal shutdown or application reload
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"

To enable API-SSL, change the endpoint to the actual TLS port (usually 8729) and add tls explicitly:

args:
address: "router.example:8729"
tls:
# Optional: override the certificate server name inferred from address
server_name: "router.example"
# Optional: PEM file containing a self-signed certificate or private CA
ca: "/etc/oxidns/routeros-ca.pem"
# Optional, default false: skip certificate verification; cannot be used with ca
insecure: false

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.

tls

  • Type: object; Required: no; Default: none (plaintext API)
  • Purpose: Enables RouterOS API-SSL.
  • tls.server_name: optional string overriding the certificate server name inferred from address.
  • tls.ca: optional string containing the path to a PEM file for a self-signed certificate or private CA.
  • tls.insecure: optional boolean, default false; true disables certificate verification and cannot be combined with tls.ca.

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.

wait_timeout

  • Type: duration; Required: no; Default: 8s
  • Purpose: Limits how long async: false waits for one manager attempt. A timeout preserves the DNS response and does not cancel accepted work or background retries. It is independent from the RouterOS API receive_timeout.

queue_capacity

  • Type: usize; Required: no; Default: 16384
  • Purpose: Limits distinct IPs in the ingress queue and retry backlog independently. Existing keys still coalesce; a new key is dropped at capacity without changing the DNS response. Must be greater than 0.

address_list4

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

address_list6

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

comment_prefix

  • Type: string; Required: no; Default: oxi
  • 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 managed entries are removed during normal shutdown and application-level reload. All cleanup shares a 30-second budget; timeout is reported and shutdown continues.
  • Operational note: Set this to false when production restarts, reloads, or rolling deployments require policy continuity.

Behavior

  • The plugin itself does not modify DNS responses.
  • Startup does not wait for RouterOS address-list scans to finish. Persistent recovery runs in the background manager and retries with backoff without blocking DNS startup or dynamic observations.
  • It only passes through during the forward phase.
  • During the return phase:
    • For A/AAAA queries only, extracts every enabled-family A/AAAA record from the Answer section of NOERROR responses without rebuilding CNAME chains.
    • Deduplicates them and keeps the largest TTL.
    • Submits them to the background manager according to async or sync mode.
  • The manager is responsible for:
    • Dynamic entry refresh
    • Persistent entry consistency maintenance
    • Cleanup on shutdown
  • Startup always performs one persistent recovery scan, even with an empty desired set, so stale persistent rows from an older configuration can be removed. Failures retry until one scan succeeds.
  • The 180-second periodic reconcile runs only when persistent is configured and uses the in-memory set loaded during initialization; it never rereads files.
  • Dynamic entries are DNS observations rather than desired state, so they are excluded from startup and periodic full-table reconcile. Operator edits are not proactively corrected; only a later DNS observation that triggers a write may overwrite them.
  • Finite dynamic entries become eligible for refresh by a later DNS observation at 75% of their effective TTL. The plugin does not run a separate dynamic-refresh timer, and timeless entries are not refreshed automatically.
  • Before deleting an address-list row, the plugin rechecks its internal ID, target list/address key, and complete ownership comment. RouterOS timeout is lease data rather than a deletion-authorization condition. RouterOS still exposes final removal by internal ID and has no atomic compare-and-delete primitive.

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.
  • Dynamic entries with fixed_ttl: 0 do not expire naturally and have no record-count limit. Operators must plan RouterOS capacity and explicit cleanup.
  • An application-level reload gives this plugin direct shutdown/restart semantics: the old instance fully shuts down before the new instance is initialized and started, and pending observations from the old instance are not transferred. With cleanup_on_shutdown: true, owned entries are removed and a policy gap may exist while they are rebuilt; set it to false to retain RouterOS entries across reload. Different processes must not write the same plugin tag, comment prefix, and target list concurrently; rolling deployments need distinct ownership namespaces or an old writer that has already stopped.

ros_route

Purpose

Synchronizes A/AAAA addresses from successful DNS answers as static host routes in a selected RouterOS routing table. IPv4 addresses become /32 routes and IPv6 addresses become /128 routes, using the configured gateway for each family and one route distance.

This plugin is a side-effect-only continuation executor: it does not modify DNS requests, responses, or control flow. Place it before the executor that produces the final answer so it can observe the completed response on the return path.

Example Configuration

- tag: ros_route_policy
type: ros_route
args:
# RouterOS API endpoint
address: "192.168.88.1:8728"
# API credentials
username: "api-user"
password: "secret"
# API connect, command-send, and response-receive timeouts in seconds
connect_timeout: 5
send_timeout: 5
receive_timeout: 5
# Queue observations without blocking the DNS hot path
async: true
# Maximum wait for one manager attempt when async=false
wait_timeout: 8s
# Distinct-route limit applied independently to ingress and retry queues
queue_capacity: 16384
# The RouterOS table and matching routing rule must already exist
routing_table: "via_proxy"
# Configure at least one family gateway
gateway4: "192.168.88.2@main"
gateway6: "fe80::2%ether1"
# Managed route attributes
distance: 100
comment_prefix: "oxi"
# Dynamic-route TTL clamp
min_ttl: 60
max_ttl: 3600
# Check RouterOS connection tracking before deleting expired host routes
conntrack_guard: false
# Remove routes owned by this instance during shutdown or application reload
cleanup_on_shutdown: true
# DNS-independent routes that should remain present
persistent:
ips:
- "198.51.100.0/24"
- "2001:db8:100::/64"
files:
- "/etc/oxidns/persistent_routes.txt"

To enable API-SSL, change the endpoint to the configured TLS port (usually 8729) and add tls explicitly:

args:
address: "router.example:8729"
tls:
# Optional: override the certificate server name inferred from address
server_name: "router.example"
# Optional: PEM file containing a self-signed certificate or private CA
ca: "/etc/oxidns/routeros-ca.pem"
# Optional, default false: skip certificate verification; cannot be used with ca
insecure: false

Configuration Details

address

  • Type: string; Required: yes; Default: none
  • Purpose: RouterOS API endpoint in host:port form. Plaintext API commonly uses 8728, while API-SSL commonly uses 8729; use the port configured on the device.

username

  • Type: string; Required: yes; Default: none
  • Purpose: RouterOS API username. The account needs permission to inspect and manage the target routing table and, when conntrack_guard is enabled, read connection tracking.

password

  • Type: string; Required: yes; Default: none
  • Purpose: RouterOS API password. Do not expose real credentials in public repositories or shared examples.

tls

  • Type: object; Required: no; Default: none (plaintext API)
  • Purpose: Enables RouterOS API-SSL.
  • tls.server_name: optional string overriding the certificate server name inferred from address.
  • tls.ca: optional path to a PEM file containing a self-signed certificate or private CA.
  • tls.insecure: optional boolean, default false; disables certificate verification and cannot be combined with tls.ca.

connect_timeout

  • Type: u64; Required: no; Default: 5
  • Unit: seconds
  • Purpose: Maximum time allowed to establish a RouterOS API connection. Must be greater than 0.

send_timeout

  • Type: u64; Required: no; Default: 5
  • Unit: seconds
  • Purpose: Maximum time allowed to send one RouterOS API command. Must be greater than 0.

receive_timeout

  • Type: u64; Required: no; Default: 5
  • Unit: seconds
  • Purpose: Maximum time allowed to receive the next part of a RouterOS API response. Must be greater than 0; increase it only when the management plane is known to respond slowly.

async

  • Type: bool; Required: no; Default: true
  • Purpose: When enabled, the DNS return path only submits observations to the background manager. When disabled, it waits for one manager result, but RouterOS failures never modify the DNS response.

wait_timeout

  • Type: duration; Required: no; Default: 8s
  • Purpose: With async: false, limits how long a DNS request waits for the manager. On timeout, the DNS response returns normally and queued work and retries continue in the background. Must be greater than 0.

queue_capacity

  • Type: usize; Required: no; Default: 16384
  • Purpose: Distinct route-key limit applied independently to the deduplicating ingress queue and retry backlog. Repeated observations for the same key coalesce; a new key is dropped with a metric when full, without changing the DNS response. Must be greater than 0.

routing_table

  • Type: string; Required: yes; Default: none
  • Purpose: RouterOS routing table that receives managed routes. The plugin does not create the table, routing rules, or default routes; provision them on RouterOS first.

gateway4

  • Type: string; Conditionally required; Default: none
  • Purpose: RouterOS gateway expression used by IPv4 dynamic host routes and persistent IPv4 routes. Without it, IPv4 DNS addresses and IPv4 persistent entries are ignored.

gateway6

  • Type: string; Conditionally required; Default: none
  • Purpose: RouterOS gateway expression used by IPv6 dynamic host routes and persistent IPv6 routes. Without it, IPv6 DNS addresses and IPv6 persistent entries are ignored.
  • Constraint: Configure at least one of gateway4 or gateway6.

distance

  • Type: u8; Required: no; Default: 100
  • Purpose: RouterOS route distance applied to all managed routes.

comment_prefix

  • Type: string; Required: no; Default: oxi
  • Purpose: Combines with the plugin tag to form the ownership namespace stored in RouterOS comments for startup recovery, reconciliation, and safe cleanup.
  • Constraint: Neither comment_prefix nor the plugin tag may contain ; or =. Do not manually alter ownership comments on managed routes.

persistent

  • Type: object; Required: no; Default: none
  • Purpose: DNS-independent IP/CIDR routes that should remain present. They are synchronized at startup and reconciled every 180 seconds when the configured set is non-empty.

persistent.ips

  • Type: array<string>; Required: no; Default: empty
  • Purpose: Inline persistent IPv4/IPv6 addresses or CIDRs. Plain addresses normalize to /32 or /128, and CIDRs normalize to their network address.
  • Ignore rules: Entries whose family has no configured gateway and /0 default routes are ignored with a warning.

persistent.files

  • Type: array<string>; Required: no; Default: empty
  • Purpose: Loads persistent routes from text files, one IP/CIDR per line, with # comments supported.
  • Load timing: Files are read only during initialization or reload. Periodic reconciliation uses the in-memory set, so file changes require a reload.

min_ttl

  • Type: u32; Required: no; Default: 60
  • Unit: seconds
  • Purpose: Lower bound for dynamic host-route leases. Smaller DNS TTLs are raised to this value.

max_ttl

  • Type: u32; Required: no; Default: 3600
  • Unit: seconds
  • Purpose: Upper bound for dynamic host-route leases. Larger DNS TTLs are capped at this value.
  • Constraint: min_ttl must not exceed max_ttl.

fixed_ttl

  • Type: u32; Required: no; Default: none
  • Unit: seconds
  • Purpose: Overrides the DNS TTL for every dynamic host route, bypassing the min_ttl/max_ttl result. Set it to 0 for routes that do not expire by time.

cleanup_on_shutdown

  • Type: bool; Required: no; Default: true
  • Purpose: Removes dynamic and persistent routes in the current ownership namespace during normal shutdown and application reload. Shutdown and cleanup share one 30-second budget.
  • Recommendation: Set it to false when a restart or reload must not create a policy gap, allowing the next instance to recover route state from RouterOS comments.

conntrack_guard

  • Type: bool; Required: no; Default: false
  • Purpose: Queries RouterOS connection tracking before deleting an expired dynamic /32 or /128 host route. A connection to the destination defers deletion for 30 seconds.
  • Boundary: Query failures keep the route. Persistent configuration removal, shutdown cleanup, and CIDR routes bypass this guard.

Behavior

  • The forward stage only passes through. The return stage observes contexts whose first question is A/AAAA and whose RCODE is NOERROR. When the response has a question section, its first question must also match the request.
  • It extracts all enabled-family A/AAAA records from the Answer section without reconstructing CNAME chains. Duplicate IPs retain the largest TTL, and every address forms an independent dynamic host-route lease.
  • A later response that omits an old IP, or returns NODATA/NXDOMAIN, does not immediately withdraw an existing route; finite leases are removed by the expiry sweep.
  • RouterOS unavailability does not prevent DNS startup. The manager reconnects and retries in the background, and the DNS response remains unchanged.
  • Startup recovery converges persistent routes, restores unexpired dynamic leases from RouterOS comments, and schedules expired dynamic routes for cleanup. Failed scans retry with backoff.
  • Periodic reconciliation maintains persistent desired state only. Dynamic routes are DNS observations, so manual edits are not periodically overwritten; a later observation of the same IP may update the route.
  • A finite dynamic lease becomes eligible for another write after 75% of its TTL has elapsed or five minutes have passed since the previous successful write, whichever occurs first. The plugin never initiates DNS refreshes itself.

Typical Uses

  • Import addresses resolved for selected domains into a RouterOS policy-routing table.
  • Maintain dynamic destination host routes for proxy, VPN, or dedicated uplinks.
  • Maintain a small set of DNS-independent fixed networks through persistent.
Notes
  • ros_route has no quick setup form; use a full plugin definition.
  • The plugin manages static routes only. It does not create RouterOS routing tables, routing rules, or default routes. Invalid gateway or routing-table values generally surface on the first real write.
  • Dynamic leases have no entry-count limit, and routes with fixed_ttl: 0 do not expire naturally. Plan RouterOS routing-table and OxiDNS memory capacity accordingly.
  • Multiple OxiDNS instances must not concurrently manage the same comment_prefix, plugin tag, and routing_table combination. Use separate ownership namespaces during rolling deployment or ensure the old writer has stopped.
  • Application reload shuts down the old instance before starting the replacement, and pending observations are not transferred. With cleanup_on_shutdown: true, rebuilding routes may create a policy gap; use false when continuity is required.