Skip to content

Rule Engine Design: Aegis Packet Engine

1. Rule Types & Formats

Aegis matches packets against blocking rules in real-time. The rule engine must evaluate packets in nanoseconds.

Supported Matchers:

  1. IP Match: Match a single IPv4 (e.g. 192.168.1.100) or IPv6 address.
  2. Subnet / CIDR Match: Match an IP against a CIDR range (e.g. 10.0.0.0/8, 192.168.1.0/24).
  3. Application Match: Match an AppType (e.g. AppType::YOUTUBE).
  4. Domain Match: Substring or exact suffix matching on the SNI or HTTP Host (e.g. facebook.com).
  5. Port & Protocol Match: Match destination port (e.g. 443) and protocol (TCP/UDP).

2. IP & Subnet Matching (CIDR)

For matching IP addresses against subnets, a linear array scan is slow if there are many rules.

Matching Strategies:

  • Small Rule Sets (< 64 rules): A flat array of (IP, Mask) structs. Checked using SIMD or simple bitwise operations:
    struct IPFilter {
        uint32_t subnet;
        uint32_t mask;
        bool action_block;
    };
    // Check
    if ((ip & filter.mask) == filter.subnet) { return filter.action_block; }
    
  • Large Rule Sets: A binary Prefix Tree (Trie) where each bit of the IP decides the path. The leaves contain the block actions. This provides $O(1)$ lookup time relative to rule count (max 32 steps for IPv4).

3. Domain Matching Optimization

Domain rules are stored in a structure optimized for suffix matching.

Design:

  • Suffix Trie: Domains are reversed and inserted into a Trie.
  • Example: youtube.com is stored as m.o.c.e.b.u.t.u.o.y.
  • When a packet with SNI www.youtube.com arrives, we reverse the hostname (m.o.c.e.b.u.t.u.o.y.w.w.w) and scan the Trie from root.
  • This allows fast exact and suffix matches (*.youtube.com).

4. Concurrent Rule Hot-Reload (RCU Pattern)

Administrators must be able to reload rules without pausing packet processing or introducing lock contention on worker threads.

Double-Buffering Rule Swap:

  • Rules Container: Rules are encapsulated in a RuleTable class.
  • Atomic Pointer: The engine maintains an atomic pointer to the current rule set: std::atomic<const RuleTable*> active_rules_.
  • Worker Processing Loop:
    // Fast path thread grabs rule table pointer (lock-free)
    const RuleTable* rules = active_rules_.load(std::memory_order_consume);
    if (rules->isBlocked(ip, app, sni)) { ... }
    
  • Reload Thread:
  • Parses the new rules file.
  • Allocates a new RuleTable on the heap: RuleTable* new_rules = new RuleTable(...).
  • Swaps the atomic pointer: const RuleTable* old_rules = active_rules_.exchange(new_rules, std::memory_order_acq_rel).
  • Waits briefly (or schedules deletion) to ensure all worker threads have finished processing packets using the old_rules pointer (epoch-based reclamation or simple safe delay), then deletes old_rules.