Policy Routing

Ze supports policy-based routing (PBR) to steer traffic through alternate routing tables or next-hops based on L3/L4 match criteria. This is used for content filtering (Surfprotect), split tunneling, and traffic engineering scenarios where destination-based routing is insufficient.

Under the hood, ze uses nftables packet marking with kernel ip rules. Each matching packet gets an fwmark; a kernel ip rule maps that mark to the target routing table.

Configuration

Set references (@DstBypass, @SrcBypass) must be defined in the firewall config section. See Firewall Guide for set syntax.

policy {
    route surfprotect {
        interface "l2tp*";

        rule bypass-dst {
            from {
                destination-address @DstBypass;
                destination-port 80,443;
                protocol tcp;
            }
            then {
                accept;
            }
        }
        rule bypass-src {
            from {
                destination-port 80,443;
                protocol tcp;
                source-address @SrcBypass;
            }
            then {
                accept;
            }
        }
        rule block-quic {
            order 20;
            from {
                destination-address 0.0.0.0/0;
                destination-port 80,443;
                protocol udp;
            }
            then {
                drop;
            }
        }
        rule surfprotect-syn {
            order 30;
            from {
                destination-address 0.0.0.0/0;
                destination-port 80,443;
                protocol tcp;
                tcp-flags syn;
            }
            then {
                table 100;
                tcp-mss 1436;
            }
        }
        rule surfprotect-tcp {
            order 40;
            from {
                destination-address 0.0.0.0/0;
                destination-port 80,443;
                protocol tcp;
            }
            then {
                table 100;
            }
        }
    }
}

Table 100 must be populated separately via static routes. Static routes reference tables by name, so define a named routing table mapping to kernel table ID 100 and add the route under it:

routing-table {
    table pbr {
        id 100
    }
}

static {
    table pbr {
        route 0.0.0.0/0 {
            next { interface tun100 { } }
        }
    }
}

Interface binding

Each policy route binds to one or more ingress interfaces. A trailing * enables wildcard matching (e.g., l2tp* matches all L2TP interfaces). The interface match is prepended to every rule in the policy.

policy {
    route example {
        interface "l2tp*";
        interface eth1;
        ...
    }
}

Match criteria (from block)

Keyword Description Example
source-address Source IP prefix or @set reference 10.0.0.0/8, @AllowedSrc
destination-address Destination IP prefix or @set reference 0.0.0.0/0, @DstBypass
source-port Source port or port range 1024-65535
destination-port Destination port, range, or list 80,443
protocol L4 protocol tcp, udp, icmp
tcp-flags Comma-separated TCP flags syn, syn,ack

Set references (@name) resolve against firewall sets defined in the firewall config section.

TCP flag names: fin, syn, rst, psh, ack, urg.

Actions (then block)

Keyword Description
accept Skip this policy, packet routes normally
drop Drop the packet
table N Route via kernel table N (user must populate the table)
next-hop IP Route via the specified next-hop (table auto-managed)
tcp-mss N Clamp TCP MSS to N bytes (combinable with table or next-hop)

Only one terminal action (accept, drop, table, or next-hop) is allowed per rule. tcp-mss is a modifier and can be combined with a terminal action.

table action

then { table 100; } internally allocates an fwmark from the reserved range 0x50000-0x5FFFF, adds a SetMark action to the nftables rule, and creates a kernel ip rule mapping that fwmark to table 100. The user must populate table 100 (e.g., via static routes).

next-hop action

then { next-hop 10.0.0.1; } works like table but also auto-manages the kernel routing table. Ze allocates a table from range 2000-2999, adds a default route via the specified next-hop, and creates the fwmark and ip rule. Multiple rules targeting the same next-hop share a single auto-allocated table. The table and routes are cleaned up on config reload or shutdown.

Rule ordering

Rules are evaluated in order value order (lower first). Rules with equal order are sorted alphabetically by name. If order is omitted, it defaults to 0.

rule first  { order 10; ... }
rule second { order 20; ... }
rule third  { order 30; ... }

Reserved ranges

Ze reserves these ranges to prevent collisions:

Range Owner Purpose
Tables 1-999 user Explicit table IDs in config
Tables 253-255 kernel default (253), main (254), local (255)
Tables 1000-1999 ze VRF Auto-allocated VRF tables
Tables 2000-2999 ze policy-routing Auto-allocated next-hop tables
fwmarks 0x50000-0x5FFFF ze policy-routing Packet marks for table steering

User-specified table IDs in the 1000-2999 range are rejected at config validation time.

CLI

ze> show policy-routes

Returns JSON with all configured policy routes, their interface bindings, rules, and actions.

nftables internals

All policy routes are merged into a single nftables table ze_pr with family inet, chain type route, hook prerouting, and priority -150. Term names follow the pattern policyname-rulename. The table is managed through the firewall backend, sharing the same Apply reconciliation used by the firewall component.

Dependencies

The policy-routes plugin depends on the firewall plugin. Both must be available for policy routing to function. The firewall backend (nft) is loaded by the firewall plugin; policy routing registers its tables with the shared firewall table registry.

Auto-managed routes use protocol ze-policy-route (RTPROT 252).