Skip to content

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:

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

PhaseRules run here?
ReceiveNo
ParseNo
Request rulesYes — request hook
RouteNo
ForwardNo
Wait for responseNo
Response rulesYes — response hook
SendNo

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).

TypeTypical hookTests
every_nth_globalbothProcess-wide query index % N == 0 (N >= 1)
every_nth_workerbothWorker-local transaction id % N == 0 (N >= 1)
sample_percentboth~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:

  1. Drop — if soft drop is set at the end of the rule, or the rule already stopped with drop_now
  2. Retry — otherwise, if soft retry is set (response hook only)
  3. 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

ActionEffect
clear_dropClears soft-drop intent from an earlier drop or Rhai txn.drop_query() on this rule
dropSoft 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_nowHard 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

ActionEffect
clear_retryClears soft-retry intent from an earlier retry or Rhai txn.request_retry() on this rule (response hook only)
clear_retry_poolClears retry_pool on the transaction
clear_poolClears selected_pool so Route uses the configured default pool
retrySoft 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_nowHard retry (response hook only) — stop immediately and re-enter Route; blocked by soft drop unless clear_drop ran earlier on this rule
set_retry_poolPool used on retry Route if retry occurs — first Route ignores it
set_retry_source_v4One-shot IPv4 egress for next retry forward if retry occurs; first forward ignores
set_retry_source_v6One-shot IPv6 egress for next retry forward if retry occurs; first forward ignores
clear_retry_source_v4Clears retry_source_override_v4
clear_retry_source_v6Clears 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.

ActionvalueEffect
clear_drop—Clear soft-drop intent on this rule
clear_pool—Clears selected_pool — Route uses the configured default pool
clear_tagTag 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
rhaiScript pathRuns the linked Rhai script at this position in the list
set_poolPool nameSets the target pool for the first Route
set_retry_poolPool namePool for retry Route if retry occurs; first Route ignores — see Retry actions
set_source_v4IPv4 addressPins upstream egress to this local IPv4 address for this query (every forward unless retry source wins)
set_source_v6IPv6 addressPins upstream egress to this local IPv6 address for this query (every forward unless retry source wins)
set_retry_source_v4IPv4 addressOne-shot IPv4 egress for next retry forward if retry occurs; first forward ignores
set_retry_source_v6IPv6 addressOne-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_tagkey=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.

ActionvalueEffect
clear_drop—Clear soft-drop intent on this rule
clear_pool—Clears selected_pool — next Route uses the configured default pool
clear_tagTag 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
rhaiScript pathRuns the linked Rhai script at this position in the list
set_rcodeRCODE name or RCODEN aliasSets response code metadata (for example before Send)
set_retry_poolPool namePool for retry Route if retry occurs; first Route ignores — see Retry actions
set_retry_source_v4IPv4 addressOne-shot IPv4 egress for next retry forward if retry occurs; first forward ignores
set_retry_source_v6IPv6 addressOne-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_tagkey=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.