# Routing Rules Engine

MintFlow chooses traffic outputs by DNS name and destination IP. On iOS, App Routing means matching the domains/IP ranges an app uses; it does not identify the originating app by bundle ID. Simple Mode selects one ordinary output and leaves saved custom rules inactive.

Rules Mode has two related decisions: **how a DNS query is answered** and **how packets reach the resulting address**. A single top-to-bottom ordering cannot describe both.

## DNS priority: highest to lowest

Only effective, enabled configuration participates; a Pro-only entry excluded during compilation has no priority.

| Order | Source | Matching and result |
|---:|---|---|
| 1 | User static DNS records | Exact hostname in `[dns-hosts]`. Answer locally; no DNS-owned traffic route is created. |
| 2 | Tailscale-derived DNS | Exact derived name, then most specific enabled derived subtree, then authoritative negative answers for the active tailnet zone. These names stay local. |
| 3 | User `DOMAIN-SET` routes | Most specific matching suffix wins, regardless of row position. Includes the domain itself and its subdomains. |
| 4 | User `DOMAIN-KEYWORD` routes | First literal substring match in saved keyword order wins. |
| 5 | Built-in ad filter | Consult the selected list only when none of the above matched. A block returns NODATA without an upstream lookup. |
| 6 | `DNS-FALLBACK` | Resolves remaining names through the selected action/output. It is not an ad-filter exemption. |

A forwarding domain-set or keyword match (`DIRECT`, `L3VPN`, `L4PROXY`) bypasses the ad filter; `DROP` or `REJECT` keeps its existing DNS denial behavior. A higher-priority static answer also wins over a domain Reject. Do not use DNS rules as a substitute for an IP firewall.

Unknown names within the active Tailscale zone receive a local negative answer rather than escaping to public DNS. Static records and Tailscale names are active only when their feature access and DNS mode permit them.

## Domain sets, keywords, and conflicts

A set containing `example.com` matches `example.com` and `a.example.com`, but not `badexample.com`. A narrower `help.example.com` suffix wins over `example.com`, even if the broad rule uses Reject and appears first.

Keywords match a literal substring of the canonical DNS query name, not a URL, path, SNI, or HTTP Host. `ads` can match `ads.example.com` and `downloads.example.com`; it is not a label-boundary rule. Matching normalizes case and a trailing dot; international names use their ASCII form. Keywords use ASCII letters, digits, dots, hyphens, and underscores; no wildcard or regex syntax. Up to 256 keyword routes are supported, with paid output restrictions unchanged.

Domain sets always beat keywords. Within keywords, saved order decides; neither longest keyword nor Reject automatically wins. Conflicting actions/targets for the same normalized suffix or keyword are compilation errors, not an ordering trick.

## Worked example

```ini
[domain-sets]
blocked=example.com
allowed=help.example.com

[routing]
DOMAIN-KEYWORD,help,REJECT
DOMAIN-SET,blocked,REJECT
DOMAIN-SET,allowed,DIRECT
DOMAIN-KEYWORD,ads,REJECT
CIDR,192.168.40.0/24,DIRECT
DNS-FALLBACK,DIRECT
DEFAULT,DIRECT
```

- `help.example.com` uses Direct DNS: the narrower set wins over both the parent Reject and the earlier keyword. It also bypasses the ad filter.
- `shop.example.com` matches the broader set and is rejected.
- `ads.other.test` reaches the keyword rule and is rejected.
- Other names still pass through the ad filter before Direct DNS fallback.
- An exact static record for any of these names would take priority over all those domain rules.

Broad forwarding sets or keywords can exempt many domains from filtering. For a false positive, prefer a narrow domain set. Use DNS Log's rule-type filter and the ad-filter checker to inspect the winning source.

## IP routes and packet forwarding

`CIDR` and `CIDR-SET` entries become IP routes. Among matching prefixes, the longest prefix wins: `192.168.40.10/32` is more specific than `192.168.40.0/24`, which is more specific than `DEFAULT`. Moving a CIDR row above a domain row does not change DNS priority.

DNS decisions can attach VPN/proxy or blocking policies to resolved IPv4 destinations. A Direct DNS result continues through ordinary IP routing, so an explicit CIDR route can still select a VPN/proxy for that address. User static DNS records also leave packet routing to the IP rules. `DNS-FALLBACK` selects unresolved-name policy; `DEFAULT` is the final IP route. Configure both deliberately.

Domain-based routing depends on queries reaching MintFlow DNS. Shared IPs, cached addresses, app-owned encrypted DNS, and literal-IP connections limit what a domain rule can distinguish. Review Flow Capture and DNS Log together when investigating an unexpected output.

## Reserved rules and Simple Mode

`VPN-DNS-SERVER-DOMAINS,L3VPN|L4PROXY,target` chooses the bootstrap path for hostname-based encrypted VPN DNS. It is a reserved DNS infrastructure rule, not a user domain whitelist.

In Simple Mode, saved static records, Tailscale-derived DNS, domain sets, keywords, and custom CIDR rules stay inactive. DNS ad filtering still runs before the selected output. A TCP-only proxy cannot carry arbitrary UDP; Simple Mode drops unsupported UDP rather than silently sending it Direct.

Save profile changes, inspect compilation diagnostics, and restart VPN when requested. Filter-only changes and explicit runtime controls may apply live. [Profile routing syntax](../profile/routing.md) · [Internal DNS](internal-dns.md)
