Skip to content

policy routing

Thomas Mangin edited this page Jul 25, 2026 · 5 revisions

Pre-Alpha. This page describes behavior that may change.

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, 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

policy {
    route surfprotect {
        interface "l2tp*";

        rule bypass-dst {
            from {
                destination-address @DstBypass;
                destination-port 80,443;
                protocol tcp;
            }
            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 {
    route 0.0.0.0/0 {
        table 100;
        next-hop 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

The from block specifies what traffic to match.

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

The then block specifies what to do with matched traffic. Only one terminal action (accept, drop, table, or next-hop) is allowed per rule. tcp-mss is a modifier that can be combined with a terminal action.

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

The table action 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 the table. The user must populate the target table (e.g., via static routes).

The next-hop action 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 base chain

The marking rules go in a filter chain, not an nftables route chain. nftables only accepts a route chain on output, where its job is re-routing locally generated packets after a mark change. Forwarded traffic is routed after prerouting, so a filter chain is the standard way to drive ip rule fwmark.

This is worth knowing because earlier builds asked the kernel for a route chain on prerouting, which the kernel rejects with EOPNOTSUPP, and that took the policy-routes plugin down at startup. Policy routing did not work at all on those builds.

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.

Reserved ranges

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.

Dependencies

The policy-routes plugin depends on the firewall plugin. Both must be available for policy routing to function. Auto-managed routes use protocol ze-policy-route (RTPROT 252).

See also

Adapted from main/docs/guide/policy-routing.md.

Home

About

First Steps

Configuration

Operation

Interfaces

Plugins

Plugin Development

Chaos Testing

Blueprints

Development

Reference

Clone this wiki locally