Rules and actions
This page is the behavioral home for declarative policy in Conduit: how you declare rules under rules: in your config file, what they can change on each query, and when they run on the dataplane. For the query pipeline, see Architecture and packet path. For Rhai for rules (Rule Rhai), see Rhai (Rule Rhai overview).
Overview
A rule is a named piece of policy with:
- A hook —
requestorresponse(the Request rules or Response rules phase) - Selectors — conditions on the transaction (query name, type, response code, tags, …)
- Actions — built-in effects when every selector on that rule matches
Rules live under the top-level rules: key. match_mode: first_match is the only accepted mode: on each hook, Conduit walks the rule list from top to bottom and stops at the first rule whose selectors all match. Later rules on that hook are skipped for that query.
When no rule matches on the request hook, Conduit continues to Route with the default pool path (Pools and backends). When no rule matches on the response hook, Conduit continues to Send with the upstream answer or error already on the transaction.
Request and response hooks
Every query runs through a defined pipeline of phases. Rules (built-in actions and optional Rhai scripts) run only at Request rules and Response rules — the rule’s hook: must match that phase.
| Hook | Pipeline phase | When it runs | Runs again on retry? |
|---|---|---|---|
Request (hook: request) |
Request rules | After Parse, before Route | No — once per transaction |
Response (hook: response) |
Response rules | After Wait for response (answer or timeout), before Send or another Route | Yes — once per forward attempt |
Pipeline placement
| Phase | Rules run here? |
|---|---|
| Receive | No |
| Parse | No |
| Request rules | Yes — request hook |
| Route | No |
| Forward | No |
| Wait for response | No |
| Response rules | Yes — response hook |
| Send | No |
Outcomes after each hook
Both hooks can drop the query (drop action or Rhai txn.drop_query() — no DNS reply). Completed policy drops increment conduit_queries_dropped_total (reason = request_rules or response_rules). There is no separate accept action: when policy does not drop, Conduit continues the pipeline.
| Hook | Drop? | If not dropped, continue to… | Retry? |
|---|---|---|---|
| Request rules | Yes | Route → Forward → … | No |
| Response rules | Yes | Send | Yes → Route (Forward through Response rules again) |
On retry, the request hook does not re-run; tags and pool choice from the first request pass stay on the transaction unless response policy changes them. Global limits and examples: Retries and transactions.
For sequence diagrams and the full packet path, see Architecture and packet path.
Empty selectors (selectors: []) match every query on that hook. Use with care on the response hook: a catch-all response rule runs after every forward attempt unless a more specific rule above it matches first.
flowchart TD
Start[Hook runs] --> Walk[Next rule in list]
Walk --> Match{All selectors match?}
Match -->|yes| Next[Next action in list]
Next --> Kind{Action type?}
Kind -->|built-in| Builtin[Apply built-in effect]
Kind -->|rhai| Script[Run script at this step]
Builtin --> MoreActions{More actions?}
Script -->|error| Done[Stop rule / continue pipeline]
Script -->|drop_now| DropNode[Drop]
Script --> MoreActions
MoreActions -->|yes| Next
MoreActions -->|no| Resolve[Resolve soft drop / retry / continue]
Resolve --> Done
Match -->|no| MoreRules{More rules?}
MoreRules -->|yes| Walk
MoreRules -->|no| Done
Minimal example — route internal A queries to a pool and pin egress:
rules:
match_mode: first_match
rules:
- name: internal-a
hook: request
selectors:
- type: qtype
value: A
actions:
- type: set_pool
value: internal
- type: set_source_v4
value: "10.0.0.5"
Every field on rules:, selectors, and actions: Reference: rules.
When changes to rules take effect
When you reload or apply configuration (SIGHUP, conduitctl reload, or conduitctl apply), Conduit validates the file (including rules and script paths) and loads the result into the active configuration runtime snapshot for later queries.
Queries already in progress keep the rules they started with — they do not switch mid-query to a half-applied config. If validation fails, Conduit keeps the last-good snapshot and DNS keeps flowing. See Configuration model.
Selectors
Every selector on a rule must match (logical AND). Supported types, grouped by purpose:
Query identity
Conditions on the question being asked — use on the request hook.
| Type | Typical hook | Tests |
|---|---|---|
qname_exact |
request | Exact query name |
qname_suffix |
request | Query name suffix |
qtype |
request | Query type — IANA name (A, AAAA) or numeric alias (TYPE1, TYPE28) |
qclass |
request | Query class — IANA name (IN) or alias (CLASS1) |
opcode |
request | DNS opcode — name (QUERY) or alias (OPCODE0) |
edns_option |
request | EDNS option present in the query — name (COOKIE) or alias (CODE10) |
Wire-enum selector values use the same names and numeric aliases as Rule Rhai (RecordType::A, TYPE1, Rcode::SERVFAIL, RCODE2, …). Unknown values fail at config load. Matching uses wire numbers internally (not string labels).
Response outcome
Conditions on the upstream result — use on the response hook after Wait for response or timeout.
| Type | Typical hook | Tests |
|---|---|---|
rcode |
response | Response code — IANA name (SERVFAIL, NXDOMAIN) or alias (RCODE2, RCODE3) |
Transaction metadata
Tags set earlier on the same transaction (for example by a prior rule, Client ACLs tag, or Rhai for rules), and client IP membership in a named CIDR data source.
| Type | Typical hook | Tests |
|---|---|---|
tag |
both | Tag presence or value |
client_cidr |
both | Client socket IP is in the named type: cidr data source (value = source name). Rule-only — not valid on event sink filters. |
rules:
match_mode: first_match
rules:
- name: partners-servfail-retry
hook: request
selectors:
- type: client_cidr
value: partner_nets
actions:
- type: set_tag
value: partner
For host-managed ingress allow/deny without rules, prefer Client ACLs. Use client_cidr when you need rule-hook actions or Rhai after admission.
Sampling and cadence
Limit which queries the rule applies to. Decisions are deterministic per transaction (same transaction always gets the same pass/fail for a given selector).
| Type | Typical hook | Tests |
|---|---|---|
every_nth_global | both | Process-wide query index % N == 0 (N >= 1) |
every_nth_worker | both | Worker-local transaction id % N == 0 (N >= 1) |
sample_percent | both | ~value% of transactions (0..100); optional key / key_from — see below |
sample_percent accepts optional salt fields (mutually exclusive):
| Field | Where | Meaning |
|---|---|---|
| (omit both) | everywhere | Global bucket from transaction id only (legacy behavior) |
key: "…" |
rule selectors; tracing/event top-level sample_key |
Static salt — independent ~N% slice for this policy |
key_from: qname |
rule selectors; tracing sample_key_from; event selectors |
Per-query-name salt (canonical wire qname) |
key_from: rule_name |
rule selectors only | Salt is the rule’s name (resolved at compile time) |
key_from: sink_name |
event sink filters only (selectors or top-level) | Salt is the sink’s canonical name |
Use key for a shared slice across a zone (for example key: "internal.example" with qname_suffix). Use key_from: qname when each qname should get its own ~N% slice. Different keys at the same percentage are independent — a transaction can pass one rule’s sample and fail another’s.
Rhai: txn.sample_percent(percent) uses the global bucket; txn.sample_percent(percent, key) uses static key:; txn.sample_percent_for_qname(percent) and txn.sample_percent_for_rule(percent) match key_from: qname and key_from: rule_name; txn.every_nth_worker(n) and txn.every_nth_global(n) match the cadence selectors. See Transaction API — Sampling.
every_nth_worker uses the per-worker transaction counter that starts at 1, so N=4 matches ids 4, 8, 12, … on each worker thread.
every_nth_global uses a process-wide query index incremented once when each query transaction is created, before selector evaluation.
Examples
sample_percent only — tag roughly 10% of all queries on the request hook (deterministic per transaction id):
- name: sample-audit-tag
hook: request
selectors:
- type: sample_percent
value: "10"
actions:
- type: set_tag
value: audit=1
AND with query identity — only queries under this suffix and in the sample pass (~25% of suffix matches when using a zone key):
- name: sample-internal-zone
hook: request
selectors:
- type: qname_suffix
value: ".internal.example."
- type: sample_percent
value: "25"
key: "internal.example"
actions:
- type: set_tag
value: sampled_internal=1
Per-rule salt — key_from: rule_name binds the sample to this rule’s name (independent from other rules at the same percentage):
- name: audit-canary
hook: request
selectors:
- type: sample_percent
value: "10"
key_from: rule_name
actions:
- type: set_tag
value: audit_canary=1
Every Nth on worker vs process-wide — same N, different scope (only the selector type changes):
- name: canary-every-fourth-worker
hook: request
selectors:
- type: every_nth_worker
value: "4"
actions:
- type: set_pool
value: canary
- name: canary-every-fourth-global
hook: request
selectors:
- type: every_nth_global
value: "4"
actions:
- type: set_pool
value: canary
For sample_percent on tracing or event export (no rule required), see Tracing and Event export.
Action order on one rule
When every selector on a rule matches, Conduit runs that rule’s actions: list. Every action runs in list order (top to bottom) — built-in actions and type: rhai steps are interleaved exactly as written.
Order matters when multiple actions touch the same transaction fields. Put safety-critical or cheap built-in effects above a rhai step when they must run before script logic or when the script might fail (sandbox limits — further actions on that rule are skipped after a script error).
When you use set_pool and set_source_v4 / set_source_v6 on the same rule, list set_pool first, then the source action — so Forward checks the override against the allowed addresses for the pool you just selected.
Outcome at end of rule
Soft drop and soft retry are resolved after all actions on the rule have run. Conduit then applies one of these results:
- Drop — if soft drop is set at the end of the rule, or the rule already stopped with
drop_now - Retry — otherwise, if soft retry is set (response hook only)
- Continue — otherwise, proceed to the next pipeline phase
If both soft drop and soft retry are set, drop takes precedence over retry.
retry_now and Rhai txn.request_retry_now() do not clear soft drop. If an earlier action on the same rule set soft drop (drop or txn.drop_query()), retry_now still results in drop, not retry. Call clear_drop first only when you mean to cancel that soft drop on this rule — Conduit does not do that implicitly.
Drop actions
| Action | Effect |
|---|---|
clear_drop | Clears soft-drop intent from an earlier drop or Rhai txn.drop_query() on this rule |
drop | Soft drop — later actions on this rule still run; if drop is still set at the end of the rule, the query stops at this hook with no DNS reply |
drop_now | Hard drop — stop immediately; no further actions on this rule run |
Rhai equivalents: txn.drop_query() (soft), txn.drop_query_now() (hard), txn.clear_drop(). See Outcome at end of rule.
Retry actions
| Action | Effect |
|---|---|
clear_retry | Clears soft-retry intent from an earlier retry or Rhai txn.request_retry() on this rule (response hook only) |
clear_retry_pool | Clears retry_pool on the transaction |
clear_pool | Clears selected_pool so Route uses the configured default pool |
retry | Soft retry (response hook only) — later actions on this rule still run; if retry is still requested at the end of the rule, re-enter Route |
retry_now | Hard retry (response hook only) — stop immediately and re-enter Route; blocked by soft drop unless clear_drop ran earlier on this rule |
set_retry_pool | Pool used on retry Route if retry occurs — first Route ignores it |
set_retry_source_v4 | One-shot IPv4 egress for next retry forward if retry occurs; first forward ignores |
set_retry_source_v6 | One-shot IPv6 egress for next retry forward if retry occurs; first forward ignores |
clear_retry_source_v4 | Clears retry_source_override_v4 |
clear_retry_source_v6 | Clears retry_source_override_v6 |
Rhai equivalents: txn.set_pool(name), txn.clear_pool(), txn.set_retry_pool(name) (same as set_retry_pool above), txn.set_retry_source_v4(addr) / txn.set_retry_source_v6(addr), txn.clear_retry_source_v4() / txn.clear_retry_source_v6(), txn.request_retry() (soft), txn.request_retry_now() (hard), txn.clear_retry() (soft retry only), txn.clear_retry_pool(). See Outcome at end of rule.
The selected pool can also come from an earlier matching rule or from the default pool when no rule sets one. At Forward, Conduit still requires the override to be in the allowed set for that pool (global forward.sources_* ∪ that pool’s sources_*). If the override is not allowed, Conduit falls back to round-robin among configured sources — same behavior as Rhai set_source_v4 / set_source_v6. See Dual-stack forwarding.
Request-hook actions
Use on hook: request — after Parse, before Route.
| Action | value | Effect |
|---|---|---|
clear_drop | — | Clear soft-drop intent on this rule |
clear_pool | — | Clears selected_pool — Route uses the configured default pool |
clear_tag | Tag key (non-empty) | Removes a tag key from the transaction |
clear_retry_pool | — | Clears retry_pool — see Retry actions |
drop | — | Soft drop — see Drop actions |
drop_now | — | Hard drop — stop further actions on this rule |
rhai | Script path | Runs the linked Rhai script at this position in the list |
set_pool | Pool name | Sets the target pool for the first Route |
set_retry_pool | Pool name | Pool for retry Route if retry occurs; first Route ignores — see Retry actions |
set_source_v4 | IPv4 address | Pins upstream egress to this local IPv4 address for this query (every forward unless retry source wins) |
set_source_v6 | IPv6 address | Pins upstream egress to this local IPv6 address for this query (every forward unless retry source wins) |
set_retry_source_v4 | IPv4 address | One-shot IPv4 egress for next retry forward if retry occurs; first forward ignores |
set_retry_source_v6 | IPv6 address | One-shot IPv6 egress for next retry forward if retry occurs; first forward ignores |
clear_retry_source_v4 | — | Clears retry_source_override_v4 |
clear_retry_source_v6 | — | Clears retry_source_override_v6 |
set_tag | key=value or key (→ true) | Sets a tag on the transaction |
set_source_v4 / set_source_v6 — request hook only. The address must appear in forward.sources_v4 / forward.sources_v6 or a pool’s sources_v4 / sources_v6 (union checked when config is validated).
set_retry_source_v4 / set_retry_source_v6 — request or response hook. Stashes a one-shot egress override for the next retry forward; does not trigger retry. Address validation matches set_source_* (global union at validate/reload). See Source selection lifecycle.
clear_retry_source_v4 / clear_retry_source_v6 — request or response hook; clears the retry-source stash only (not standing set_source_* overrides).
set_pool / clear_pool — set or clear selected_pool for Route. When unset, Route uses the pool named default, or the first pool in config. set_retry_pool / clear_retry_pool — write or clear the one-shot retry_pool stash; see Retries and transactions — Pool selection lifecycle.
Response-hook actions
Use on hook: response — after an upstream answer or forward timeout, before Send or a retry.
| Action | value | Effect |
|---|---|---|
clear_drop | — | Clear soft-drop intent on this rule |
clear_pool | — | Clears selected_pool — next Route uses the configured default pool |
clear_tag | Tag key (non-empty) | Removes a tag key from the transaction |
clear_retry | — | Clear soft-retry intent on this rule — see Retry actions |
clear_retry_pool | — | Clears retry_pool — see Retry actions |
drop | — | Soft drop — see Drop actions |
drop_now | — | Hard drop — stop further actions on this rule |
retry | — | Soft retry in the current pool — see Retry actions |
retry_now | — | Hard retry — see Retry actions; blocked by soft drop unless clear_drop ran earlier on this rule |
rhai | Script path | Runs the linked Rhai script at this position in the list |
set_rcode | RCODE name or RCODEN alias | Sets response code metadata (for example before Send) |
set_retry_pool | Pool name | Pool for retry Route if retry occurs; first Route ignores — see Retry actions |
set_retry_source_v4 | IPv4 address | One-shot IPv4 egress for next retry forward if retry occurs; first forward ignores |
set_retry_source_v6 | IPv6 address | One-shot IPv6 egress for next retry forward if retry occurs; first forward ignores |
clear_retry_source_v4 | — | Clears retry_source_override_v4 |
clear_retry_source_v6 | — | Clears retry_source_override_v6 |
set_tag | key=value or key (→ true) | Sets a tag on the transaction |
retry, retry_now, clear_retry, clear_retry_pool, and set_retry_pool — see Retry actions and Retries and transactions. Use retry to stay in the current pool; pair set_retry_pool with retry or retry_now to target a different pool on the next attempt. Use clear_retry_pool when a pool set for retry Route should not apply (for example same-pool retry after request policy set set_retry_pool for another pool).
set_source_v4 / set_source_v6 are not supported on the response hook (standing egress is request-only; use set_retry_source_* on the response hook for outcome-driven retry egress). See Transaction API — Egress.
Scripted policy (Rhai for rules)
When built-in actions are not enough, add type: rhai anywhere in a matching rule’s actions: list. Conduit runs each rhai step at that position in the list — not after all built-ins.
Conduit still uses match_mode: first_match on each hook: only the first matching rule runs. List order example — built-in pool, then script refinement:
- name: geo-route
hook: request
selectors:
- type: qname_suffix
value: ".example."
actions:
- type: set_pool
value: default
- type: rhai
value: scripts/geo-pool.rhai
- type: set_tag
value: geo_routed=true
The script can override earlier YAML effects (for example txn.set_pool("vip") after set_pool: default). One .rhai file can be referenced from multiple rules; Conduit compiles it once per (rule_name, path) and runs it only when that rule wins first-match on the matching hook.
Request- and response-hook script examples (request hook — see Rhai policy for runnable labs):
- name: block-on-list
hook: request
selectors:
- type: qname_suffix
value: "bad.example."
actions:
- type: rhai
value: scripts/blocklist.rhai
- name: route-by-table
hook: request
selectors:
- type: qname_suffix
value: "customer.example."
actions:
- type: rhai
value: scripts/route-by-table.rhai
Simple SERVFAIL failover to a backup pool does not need Rhai — use set_retry_pool + retry on the response hook (Retries and transactions — Declarative examples). Reach for Rhai when policy depends on lookup tables, custom metrics, upstream latency, or other logic beyond built-in selectors.
The script receives a sandboxed txn object — policy fields only (pool, tags, egress, drop, retry). It does not edit DNS wire bytes. Phase-specific behavior, guards, and pairing request/response scripts: Hooks and phases. Method reference: Transaction API.
Overview and when to use scripts: Rhai for rules (Rhai).
Validation errors (common)
These messages appear when config is loaded or validated (reload, apply, or startup):
| Message (typical) | Cause |
|---|---|
retry … only valid on response hook |
retry, retry_now, or clear_retry on hook: request |
set_retry_pool requires a pool name in value |
Empty value on set_retry_pool |
set_source_v4 … only valid on request hook |
Standing source action on hook: response |
set_source_v4 … not in configured sources_v4 |
Address not in global or pool sources_v4 union |
set_source_v4 requires forward.sources_v4 or pool sources_v4 |
No v4 sources configured anywhere |
set_retry_source_v4 … not in configured sources_v4 |
Retry-source address not in global or pool sources_v4 union |
set_retry_source_v4 requires forward.sources_v4 or pool sources_v4 |
Retry-source action but no v4 sources configured |
unknown action type |
Typo in type: |
rule name must not be empty |
Missing or blank name on a rule |
duplicate rule name '…' |
Two rules share the same name |
Full validation rules: Reference: rules.
Related topics
- Architecture and packet path — Request rules and Response rules in the pipeline
- Pools and backends —
set_pool, default pool, backend weights - Retries and transactions — Pool selection lifecycle,
set_retry_pool,retry, attempt limits; full request/response rule examples - Rule action order — soft vs hard drop, list order, first-forward ignore of
set_retry_pool - Declarative failover — SERVFAIL / timeout failover without Rhai
- Event export — request
set_tagplus sink filters - Tracing —
activation.sample_percentand selectors without a matching rule - Dual-stack forwarding —
set_source_v4/set_source_v6 - Rhai — Rhai for rules; Rule Rhai overview; Hooks and phases for script-specific phase behavior