Module 17 — Two-tier Policy Lookup

Requires Hyperscan (libhs). No DPDK needed — fully runnable.

What you learn

The complete policy decision engine: how rte_hash exact match (Tier 1) and Hyperscan fallback (Tier 2) combine into the single function called for every DNS packet in the DP application. Includes filter_details application, malicious domain pre-check, multi-group evaluation, and a performance benchmark showing why the two-tier design is essential at 2M packets/sec.


Why two tiers are necessary

Naive approach: scan every domain with Hyperscan
  2M DNS/sec × 5 µs/scan = 10,000 ms/sec of CPU time → bottleneck

Two-tier approach:
  90% of domains hit the hash table (exact match, ~50 ns)
  10% fall through to Hyperscan (~5 µs)

  2M × 0.90 × 50 ns   =  90 ms/sec  (Tier 1)
  2M × 0.10 × 5,000 ns = 1000 ms/sec (Tier 2)
  Total = 1090 ms/sec across 4 cores = 272 ms/core
  → comfortable headroom at 2M DNS/sec

Where this fits in the real application

Worker lcore receives DNS packet
  │
  ├─► dns_parse_message()      → domain = "blocked.example.com"
  │
  ├─► rte_hash_lookup(subscriber_table, src_ip) → subscriber_t *sub
  │     sub->group_id → which enterprise group
  │
  └─► url_policy_for_dns(domain, qtype, groups, n)
        │
        ├─► check_malicious_domain(domain)
        │     rte_hash_lookup(malicious_domain_table, domain)
        │     HIT → PROCESS_WORKFLOW (sinkhole) immediately
        │     MISS ↓
        │
        └─► fetch_url_policy_for_domain(domain, group)
              │
              ├─── Tier 1: rte_hash_lookup_data(domain_details_table, domain, &fd)
              │      HIT  → apply_filter_details(fd, group, port)
              │      MISS ↓
              │
              └─── Tier 2: hs_scan_domain_group(domain, group->database, scratch)
                     HIT  → apply_filter_details(synthesised_fd, group, port)
                     MISS → group->default_policy

Build and run

make
./policy_lookup

Expected output (abbreviated):

=== Module 17: Two-tier Policy Lookup ===

Test 1: google.com
  Tier 1 HIT → is_whitelisted=1 → ALLOW
  Result: ALLOW      Expected: ALLOW      PASS ✓

Test 7: ads.tracker.io
  Tier 1 MISS → falling through to Hyperscan
  Tier 2 HIT (Hyperscan match, pattern_id=30)
  is_blacklisted=1 → SINKHOLE
  Result: SINKHOLE   Expected: SINKHOLE   PASS ✓

Results: 9 passed, 0 failed

Performance:
  Tier 1 (hash): ~45 ns/lookup
  Tier 2 (hash miss + Hyperscan): ~3200 ns/lookup
  Tier 1 is ~71x faster than Tier 2

Key concepts

1. Decision hierarchy (most restrictive wins)

apply_filter_details(fd, group, domain, port):
  1. is_whitelisted   ALLOW  (highest priority)
  2. is_blacklisted   SINKHOLE
  3. category check   DROP
  4. port check       DROP
  5. default_policy   group-level fallback (lowest priority)

A whitelisted domain is always allowed even if its category is blocked. A blacklisted domain is always blocked even if the group’s default is ALLOW.

2. Multi-group evaluation

/* Subscriber belongs to groups A and B */
for (int g = 0; g < n_groups; g++) {
    int d = fetch_url_policy_for_domain(domain, groups[g], port);
    if (d != ALLOW_PACKET)
        final_decision = d;   /* most restrictive wins */
}

3. Malicious domain check — runs before group policy

if (check_malicious(domain, &ctx)) {
    /* Blocked regardless of group policy */
    return PROCESS_WORKFLOW;
}

The malicious table (malicious_domain_table) is populated from threat intelligence feeds (URLHaus, IDPS feeds) via Kafka. It runs before group policy and cannot be overridden by a whitelist.

4. filter_details fields and their meaning

Field Value Effect
is_whitelisted 1 Always ALLOW, skip all other checks
is_blacklisted 1 Always SINKHOLE (DNS) or RST (TLS)
category_bitmask 0x00000002 Social media category
category_bitmask 0x00000004 Entertainment category
is_port_based 1 Also check port_mask for this domain

5. PROCESS_WORKFLOW (2) vs DROP_PACKET (1)

PROCESS_WORKFLOW (2): For DNS → build sinkhole response (Module 18)
                        For TLS → inject TCP RST
                        The packet is "processed" before dropping

DROP_PACKET (1):       Silently discard the packet
                        Used for category-blocked domains where
                        silent drop is preferred over sinkhole

The DNS sinkhole redirects the client to a walled garden IP. Pure drops look like a network timeout to the client — appropriate for malware/phishing where you don’t want to tip off the attacker.


Next module

Module 18 — DNS Sinkhole: When url_policy_for_dns() returns PROCESS_WORKFLOW, the DNS query packet is rewritten in-place into a sinkhole response.


Source files

File Download
policy_lookup.c policy_lookup.c
Makefile Makefile