MikroTik Policy Routing
This chapter explains how the OxiDNS ros_address_list executor works with RouterOS address-list, mangle, and policy-routing mechanisms to build a system where DNS resolution results drive later egress selection.
The core idea is not to match domains directly inside RouterOS. Instead, the workflow is:
- OxiDNS resolves the domain first.
- It extracts the target IPs from the DNS response.
- Matching target IPs are synced into RouterOS
address-list. - During connection setup, RouterOS decides whether to apply policy marks based on whether the target IP matches that
address-list. - The rest of the connection keeps using the derived connection mark and routing mark, so the selected egress stays stable.
This design matters because:
- RouterOS does not need to repeat domain-resolution logic.
- The routing decision happens at the IP layer, which maps naturally to RouterOS primitives.
- OxiDNS maintains the dynamic mapping from domain to target IP.
- RouterOS remains responsible for mapping target IPs to egress policy.
Overall Workflow
OxiDNS-Side Flow
flowchart TD
A["User accesses a domain"] --> B["Request enters OxiDNS"]
B --> C["OxiDNS resolves the domain"]
C --> D["Gets target IPs"]
D --> E["Checks whether the IPs belong to policy_set"]
E -->|Yes| F["Calls RouterOS API"]
F --> G["Writes target IPs into policy_set"]
G --> H["Returns DNS answer to the client"]
E -->|No| I["Does not write policy_set"]
I --> H
RouterOS-Side Flow
flowchart TD
A["Client starts a network connection"] --> B["Packet enters RouterOS prerouting"]
B --> C{"Is there already conntrack state?"}
C -->|Yes| D["Reads existing connection-mark"]
D --> E["Keeps existing routing-mark"]
E --> F["Forwards through the already selected egress path"]
C -->|No| G["Checks whether target IP is in policy_set"]
G -->|Matched| H["Sets connection-mark = policy"]
H --> I["Sets routing-mark = policy"]
I --> J["Forwards through the policy egress path"]
G -->|Not matched| K["Does not set policy marks"]
K --> L["Uses the default main routing table"]
L --> M["Forwards through the default egress path"]
Division of Responsibilities
What OxiDNS Does
- Receives DNS requests.
- Resolves names according to policy.
- Extracts
AandAAAArecords from the final DNS response. - Syncs target IPs that should follow policy routing into RouterOS
address-list. - Refreshes the lifetime of those IPs based on DNS TTL.
What RouterOS Does
- Checks whether the destination address hits the policy set when a connection is first created.
- Applies
connection-markandrouting-markwhen it matches. - Keeps later packets in the same connection on the same path without re-running policy logic.
- Uses
routing-markto select the proper routing table or egress.
Good-Fit Scenarios
This pattern is especially useful when:
- The resolution results of a domain set must use a specific egress path.
- A service class should keep a stable egress instead of being randomly split across routes.
- RouterOS address-lists should be driven by domain policy sets instead of manually curated IP inventories.
- DNS-layer policy and network-layer policy should form a closed loop.
Configuration Example and Parameter Notes
Minimal Integration Example
This example shows:
qnameidentifies policy domains first.- Matching requests resolve normally.
- Once valid answers exist,
ros_address_listwrites the results into RouterOSpolicy_set.
plugins:
- tag: policy_domains
type: domain_set
args:
exps:
- "domain:stream.example"
- "domain:media.example"
- tag: match_policy_domain
type: qname
args:
- "$policy_domains"
- tag: forward_main
type: forward
args:
upstreams:
- addr: "udp://1.1.1.1:53"
- tag: ros_address_list_policy
type: ros_address_list
args:
address: "172.16.1.1:8728"
username: "api-user"
password: "secret"
async: true
address_list4: "policy_set_v4"
address_list6: "policy_set_v6"
comment_prefix: "oxidns"
min_ttl: 60
max_ttl: 1800
- tag: seq_main
type: sequence
args:
- matches: "$match_policy_domain"
exec: "$ros_address_list_policy"
- exec: "$forward_main"
Key ros_address_list Parameters in Policy-Routing Scenarios
address
address: "172.16.1.1:8728"
Meaning:
- RouterOS API address.
- OxiDNS uses it to connect to RouterOS and manage address-list entries.
connect_timeout / send_timeout / receive_timeout
connect_timeout: 5
send_timeout: 5
receive_timeout: 30
Meaning:
- These values control RouterOS API connection, command send, and response receive waits, in seconds.
- All three values must be greater than
0. receive_timeoutis the wait for the next RouterOS response chunk, not a total cap for the whole scan.
Recommended practice:
- Use dedicated, size-controlled
address-listtargets for OxiDNS. Avoid connecting the plugin directly to existing large shared lists. - If a legacy deployment cannot split the list yet, or the RouterOS management plane is slow enough that startup reconcile scans often exceed the default 5 seconds, increase
receive_timeoutfirst, for example to30or60. connect_timeoutandsend_timeoutcan usually keep their defaults unless the management network is slow or the RouterOS API is occasionally busy.
address_list4 / address_list6
address_list4: "policy_set_v4"
address_list6: "policy_set_v6"
Meaning:
- Defines which RouterOS
address-listreceives IPv4 and IPv6 targets.
Recommended practice:
- Keep IPv4 and IPv6 separate.
- Use names that reflect policy intent, such as:
policy_set_v4policy_media_v4policy_route_alt_v6
async
async: true
Meaning:
- Whether RouterOS writes happen asynchronously.
Recommended practice:
- In policy-routing scenarios,
trueis usually the better default.
Why:
- DNS responses should not be noticeably delayed by RouterOS API latency.
ros_address_listis primarily a side-effect and integration plugin, not the main resolution action.- Startup-time RouterOS address-list scans run in the background, so slow legacy list queries or a slow management plane should not block DNS service startup.
- Backgrounding reduces the impact on DNS startup and the request path, but it does not make large address lists a recommended target.
min_ttl / max_ttl
min_ttl: 60
max_ttl: 1800
Meaning:
- Constrains the lifetime of dynamically written address-list entries.
Why it matters:
- Prevents very small TTLs from causing excessive RouterOS refresh churn.
- Prevents very large TTLs from keeping stale IPs around too long.
Tuning principles:
min_ttl- Do not set it too low, or RouterOS refresh pressure rises.
max_ttl- Do not set it too high, or old IPs may linger too long after target changes.
fixed_ttl
fixed_ttl: 300
Meaning:
- Ignores the original DNS TTL and always writes dynamic entries with a fixed lifetime.
- If set to
0, dynamic entries are written without a RouterOStimeout.
Good fits:
- A predictable refresh interval is required.
- Upstream TTL differences should not change policy-routing behavior.
persistent
persistent:
ips:
- "1.1.1.1"
- "203.0.113.0/24"
files:
- "/etc/oxidns/persistent_policy_ips.txt"
Meaning:
- Adds permanent entries in addition to dynamically learned DNS results.
Good fits:
- Some destinations must always remain in the policy set.
- Dynamic learning and static policy should coexist in one managed set.
RouterOS Policy-Routing Model
OxiDNS only writes target IPs into address-list. The actual policy routing still happens on the RouterOS side.
The standard pattern has three steps:
- Match
dst-address-list=policy_setinprerouting. - Mark the first packet with
connection-mark. - Derive
routing-markfromconnection-mark, then let the corresponding routing table choose the egress.
Logic Breakdown
Step 1: Identify Whether the Destination Hits the Policy Set
RouterOS reads the destination IP and checks whether it belongs to the address-list maintained by OxiDNS.
This corresponds to:
Check whether target IP is in policy_set
Step 2: Mark the Connection
Once it matches, write connection-mark=policy immediately.
Why:
- Later packets in the same connection do not need to re-check the
address-list. - It prevents mid-connection route drift.
Step 3: Map the Connection Mark to a Routing Mark
Then derive routing-mark=policy from the connection-mark, and let the corresponding routing table forward traffic through the selected egress.
Why Use Both connection-mark and routing-mark
Per-packet matching on destination IP alone is not enough for two reasons:
- Later packets in the same connection should inherit the same routing decision.
- The
address-listmay refresh dynamically, but an established connection should not change route midstream.
So the more stable model is:
- The first packet is evaluated against
address-list. - If it matches, write
connection-mark. - Later packets reuse connection state and therefore keep a stable
routing-mark.
This matches the earlier RouterOS flow chart:
- Existing connections reuse state directly.
- Only new connections consult
policy_set.
Timing Between DNS and Connection Setup
This design relies on one practical assumption:
- The client usually sends a DNS query first.
- It then starts the network connection shortly afterward using the returned address.
So as long as OxiDNS writes the target IP to RouterOS quickly after answering DNS, the following connection will usually hit the expected address-list entry.
There are still boundary conditions:
- With
async: true, writes are asynchronous. - In theory there is a short window where the client has already started connecting, but RouterOS has not finished updating the
address-list.
How to Reduce the Impact of This Window
The impact can be reduced from three angles:
- Keep the OxiDNS-to-RouterOS API path stable and low-latency.
- Do not set
min_ttltoo low, which reduces RouterOS churn. - Use
persistentfor critical targets so the first dynamic write is not the only protection.
For environments where "the very first packet must already hit policy routing" is a strict requirement:
- Put the most critical targets into
persistent, or - Use
async: falseand accept the added latency on the DNS path.
Common Composition Patterns
Pattern 1: Only Write policy_set for Specific Domain Sets
Characteristics:
- Only DNS results for policy domains are synced into RouterOS.
- Default traffic still uses the main routing path.
Good fit:
- Only a small target set needs policy routing.
Pattern 2: Write All Successful Resolutions Into Different Address-Lists
Characteristics:
- Different
sequencebranches write different result classes into different lists.
For example:
policy_media_v4policy_backup_v4policy_low_latency_v4
Good fit:
- Multiple egress paths and multiple policy classes at once.
Pattern 3: Merge Dynamic Learning with Persistent Policy
Characteristics:
- Dynamic DNS results are written into the address-list.
- Fixed critical prefixes or IPs remain pinned through
persistent.
Good fit:
- Both dynamic policy and static fallback policy are required.
Example: Multiple Policy Egress Paths
This example writes two domain groups into two different RouterOS address-list targets.
plugins:
- tag: media_domains
type: domain_set
args:
exps:
- "domain:media.example"
- tag: backup_domains
type: domain_set
args:
exps:
- "domain:backup.example"
- tag: media_match
type: qname
args:
- "$media_domains"
- tag: backup_match
type: qname
args:
- "$backup_domains"
- tag: forward_main
type: forward
args:
upstreams:
- addr: "udp://1.1.1.1:53"
- tag: ros_address_list_media
type: ros_address_list
args:
address: "172.16.1.1:8728"
username: "api-user"
password: "secret"
async: true
address_list4: "policy_media_v4"
- tag: ros_address_list_backup
type: ros_address_list
args:
address: "172.16.1.1:8728"
username: "api-user"
password: "secret"
async: true
address_list4: "policy_backup_v4"
- tag: seq_main
type: sequence
args:
- exec: "$forward_main"
- matches:
- "$media_match"
exec: "$ros_address_list_media"
- matches:
- "$backup_match"
exec: "$ros_address_list_backup"
On RouterOS, map:
policy_media_v4to egress Apolicy_backup_v4to egress B
Debugging and Troubleshooting
Confirm Three Things on the OxiDNS Side
- The domain really matches the intended policy branch.
- The DNS response really contains
AorAAAA. ros_address_listsuccessfully connects to RouterOS and submits the observed results.
Useful tools:
debug_printquery_summary- RouterOS API logs
Confirm Three Things on the RouterOS Side
- The target IP actually appears in the
address-list. - New connections match that
address-listas expected. connection-markandrouting-markare written as intended.
Risks and Boundaries
1. DNS and the Real Connection Target Are Not Always the Same
If a client:
- Caches DNS for a long time
- Does not use OxiDNS
- Uses some other resolver result
then the RouterOS-side policy set may no longer cover the actual connection target.
2. One Domain May Return Many Changing IPs
Some services change addresses frequently. In that case, tune these more carefully:
max_ttlfixed_ttlpersistent
Otherwise, possible symptoms include:
- Old IPs lingering too long
- Excessively frequent updates
3. Asynchronous Writes Create a Small Window
async: true is usually the recommended default, but it does not guarantee that RouterOS has already finished writing the address-list by the exact moment the DNS response is returned.
Stronger consistency requires trading DNS-path latency against write-path certainty.
Implementation Recommendations
Recommendation 1: Start with Small, Well-Bounded Policy Sets
Do not write everything into RouterOS on day one.
Start with:
- A few domain sets that clearly need policy routing
- One separate
address-list - One separate
routing-mark
and validate the full loop first.
Recommendation 2: Keep DNS Decisions and Routing Decisions Layered
OxiDNS is responsible for:
- Which domains belong to which policy class
- Which resolution results should be synced
RouterOS is responsible for:
- Which egress policy each synced IP belongs to
This separation clarifies ownership and reduces troubleshooting complexity.
Recommendation 3: Prefer Connection Stability Over Re-Deciding Every Packet
The primary goal of policy routing is usually stable connections, not repeated policy evaluation on every packet.
So:
connection-markshould be the core state.routing-markshould be derived fromconnection-mark.address-listshould participate only in the first decision for new connections.
Summary
The OxiDNS ros_address_list plugin is essentially a DNS-result synchronizer:
- It converts domain resolution results into target IP sets that RouterOS can consume.
- RouterOS then performs the actual policy routing based on those target IP sets.
The full loop looks like this:
- OxiDNS decides how a domain should be resolved.
- OxiDNS writes target IPs that match policy into
policy_set. - RouterOS marks new connections based on
policy_set. - RouterOS derives routing marks from connection marks.
- Traffic leaves through the selected egress.
For the goal of "make later connections to specific domains consistently use a specific egress path", this is one of the most representative and practical ways to use the current OxiDNS ros_address_list plugin.