Guide

Configuration

Ze uses a JUNOS-like hierarchical configuration format.

Syntax Rules

Element Syntax Example
Blocks name { ... } bgp { ... }
Values key value or key value; router-id 1.2.3.4
Comments # to end of line # this is a comment
Lists [ item1 item2 ] receive [ update state ]
Strings Unquoted or "double quoted" run "/usr/bin/my-plugin"
Terminators ;, or automatic after a value token at a newline, EOF, or before } Both edit { default-action deny } and edit { default-action deny;} work
Inline blocks name { key value; key value; } remote { ip 10.0.0.1; as 65001; }

Indentation is not significant. Unknown keys are rejected with a suggestion for the closest valid key.

File Format

# Global BGP settings
bgp {
    router-id 1.2.3.4;
    local { as 65000; }

    # Peer group with shared defaults
    group upstream {
        timer {
            receive-hold-time 180;
        }
        remote { connect false; }   # passive: don't initiate outbound

        capability {
            asn4;
            route-refresh;
        }

        family {
            ipv4/unicast;
            ipv6/unicast;
        }

        peer transit-a {
            remote { ip 10.0.0.1; as 65001; }
        }

        peer transit-b {
            remote { ip 10.0.0.2; as 65002; }
            timer {
                receive-hold-time 90;    # overrides group value
            }
        }
    }

    # Standalone peer (no group)
    peer internal {
        remote { ip 192.168.1.1; as 65000; }
        local { ip 192.168.1.2; as 65000; }
        router-id 192.168.1.2

        family {
            ipv4/unicast
        }
    }
}

By default Ze enforces AS-wide BGP Identifier uniqueness: the first peer of an AS to present a router-id holds it, and a second peer of the same AS presenting the same router-id is rejected with a Bad BGP Identifier NOTIFICATION (RFC 6286 Section 2.1 makes this only a SHOULD). The identifier is claimed while the peer's OPEN is validated and released when its session ends, so which peer wins does not depend on which one establishes first. Set bgp { session { allow-shared-router-id true; } } to accept the duplication when it is intentional, for example one anycast speaker (AS112) peering over both IPv4 and IPv6 with the same router-id. When enabled, Ze performs no AS-wide router-id uniqueness check at all.

allow-shared-router-id does not relax RFC 6286 Section 2.2, which Ze always enforces on reception: an OPEN whose BGP Identifier is 0.0.0.0, or whose BGP Identifier is Ze's own router-id when the peer is in the same AS (an internal peer), is answered with OPEN Message Error / Bad BGP Identifier and the connection is closed. The same identifier from an external peer is accepted, as the RFC requires; if that leads to a connection collision, Section 2.3 preserves the connection initiated by the speaker with the larger AS number. Ze also refuses to start with its own router-id 0.0.0.0, since Section 2.1 defines the BGP Identifier as a non-zero integer and every conformant peer would reject such an OPEN.

OSPF

The top-level ospf block enables Ze's native OSPF engine. The root container configures OSPFv2 for IPv4; ospf { address-family ipv6 { ... } } enables the OSPFv3 IPv6 instance on the same engine. Interface and neighbor state, LSDB flooding, SPF, inter-area logic, NSSA policy, redistribution, and route installation are shared. The wire codec, raw transport, and prefix strategy are address-family specific.

For IPv4, the engine opens active non-passive broadcast and point-to-point interfaces through the raw IPv4 transport. For IPv6, it uses the OSPFv3 raw IPv6 transport (ff02::5 / ff02::6) and the OSPFv3 prefix model (Intra-Area-Prefix, Link, AS-External, and NSSA LSAs). Both families install selected routes through the shared Loc-RIB -> sysrib -> fibkernel path.

ospf {
    router-id 10.0.0.1
    reference-bandwidth 100000
    maximum-paths 8

    areas {
        area 0.0.0.0 {
            area-type normal
        }
    }

    interfaces {
        interface eth0 {
            area 0.0.0.0
            network-type point-to-point
            hello-interval 10
            dead-interval 40
            priority 1
        }
        interface lo0 {
            area 0.0.0.0
            network-type loopback
        }
    }
}

Area binding is per interface: interfaces/interface/area must reference a declared areas/area. network-type accepts broadcast, point-to-point, or loopback; loopback records send no Hellos and open no raw socket. The NSM consumes interface mtu-ignore, retransmit, transmit-delay, and dead-interval settings during DD exchange and inactivity tracking. router-id must be dotted quad when configured; if it is omitted, Ze derives one from the highest loopback IPv4 address, then the highest interface IPv4 address. Numeric leaves are range-checked by YANG, including maximum-paths 1..32, interface cost 1..65535, priority 0..255, and OSPF metrics 0..16777215.

The IPv6 address family mirrors the area and interface model under address-family ipv6. Presence of the ipv6 container enables the OSPFv3 instance; instance-id defaults to 0 and is used for RFC 5340 per-link demultiplexing.

The router-information container advertises this router's optional capabilities in the RFC 7770 Router Information LSA. enabled true originates it; the scope leaf-list (link / area / as, multiple allowed) selects the flooding scope and defaults to area + AS when enabled with no scope listed. It applies to both address families (a top-level container drives the OSPFv3 family unless that family sets its own); OSPFv2 additionally requires opaque true, since the OSPFv2 RI LSA is an opaque LSA. See the OSPF guide's Router Information section for the advertised capability bits.

The extended-prefix and extended-link boolean leaves gate origination of the RFC 7684 Extended Prefix (Opaque type 7) and Extended Link (Opaque type 8) Opaque LSAs. Both default false and require opaque true; they are the prefix/link attribute containers a Segment Routing (or other prefix/link-attribute) application fills, so they stay off until such a producer needs them. Reception and decode of a peer's Extended Prefix/Link LSAs is always on once the plugin is built, regardless of these leaves. See the OSPF guide's Extended Prefix/Link Attributes section.

The segment-routing container enables OSPF Segment Routing over the MPLS data plane (RFC 8665 for the IPv4 family, RFC 8666 for the IPv6 family under address-family ipv6). It carries an enable toggle, an srgb and srlb range (each lower-bound/upper-bound MPLS labels), a prefix-sid list (keyed by prefix, with an index into the SRGB plus the node-sid, no-php, and explicit-null flags), and an optional srms-preference. The SRGB and SRLB must be disjoint contiguous ranges within 16..1048575 and must not overlap the LDP or RSVP-TE label space; the ze doctor check doctor-ospf-segment-routing-overlap reports an unsound range. When enabled the RI LSA advertises SR-Algorithm 0 plus the SRGB/SRLB, the Extended Prefix LSA carries the node Prefix-SIDs, and the Extended Link LSA carries the Adjacency-SIDs. See the OSPF guide's Segment Routing section.

ospf {
    opaque true;
    segment-routing {
        enable true;
        srgb { lower-bound 16000; upper-bound 23999; }
        srlb { lower-bound 40000; upper-bound 40999; }
        prefix-sid 10.0.0.1/32 { index 1; node-sid true; }
    }
}

The fast-reroute container enables RFC 5286 LFA / TI-LFA IP fast reroute: SPF pre-computes a loop-free backup next-hop per primary and programs it into the FIB as a link-down backup. enable (default false) turns it on; mode selects lfa (default, base loop-free alternates only) or ti-lfa (adds a Segment-Routing repair-list fallback, which requires segment-routing and carries repair labels on IPv4 only); node-protection (default true) prefers node-protecting alternates. It is a router-wide policy that also drives the OSPFv3 family (base-LFA selection).

ospf {
    fast-reroute {
        enable true
        mode ti-lfa
        node-protection true
    }
}

At the root IPv4 level, area-type selects normal, stub, or nssa. A stub or NSSA area sets default-cost (the injected default metric) and may set no-summary true for a totally-stubby/totally-NSSA area. An NSSA area takes an nssa { ... } block with translate-role (candidate electing by highest Router ID, always, or never), stability-interval (translator hysteresis in seconds, 0..65535, default 40), and default-originate (originate a Type 7 default into the NSSA). In address-family ipv6, the area entry currently selects only area-type.

ospf {
    router-id 10.0.0.1
    areas {
        area 0.0.0.0 {
            area-type normal
        }
        area 0.0.0.9 {
            area-type nssa
            no-summary true
            default-cost 20
            nssa {
                translate-role candidate
                stability-interval 40
                default-originate true
            }
        }
    }
    interfaces {
        interface eth0 {
            area 0.0.0.0
        }
        interface eth1 {
            area 0.0.0.9
        }
    }
}
ospf {
    router-id 172.30.0.2
    address-family ipv6 {
        instance-id 0
        areas {
            area 0.0.0.0 {
                area-type normal
            }
            area 0.0.0.9 {
                area-type nssa
            }
        }
        interfaces {
            interface eth0 {
                area 0.0.0.0
                network-type point-to-point
            }
            interface eth1 {
                area 0.0.0.9
                network-type point-to-point
            }
        }
    }
}

Interface authentication uses named key chains. A key-chains <name> block holds one or more keys (each with an algorithm, a $9$-encoded secret, and optional send/ accept lifetimes); extended-sequence true selects RFC 7474 AuType 3. Bind a chain to an interface directly, or set the interface authentication { mode inherit } and the chain bound to its area (area { authentication { key-chain } }) is used. algorithm is one of simple, md5, or hmac-sha-1/256/384/512; secrets are masked and $9$-encoded at rest, never shown in plaintext.

ospf {
    router-id 10.0.0.1
    key-chains core {
        extended-sequence true
        key 1 {
            algorithm hmac-sha-256
            secret $9$encoded
        }
    }
    areas {
        area 0.0.0.0 {
            authentication {
                key-chain core
            }
        }
    }
    interfaces {
        interface eth0 {
            area 0.0.0.0
            authentication {
                mode inherit
            }
        }
    }
}

Redistribution and default-route origination make the router an ASBR. The ospf container sets the per-source external metric, metric-type (type-1/type-2), and route tag, and configures default-information originate. The shared top-level redistribute block enrols the actual route flow: destination ospf imports routes as OSPFv2 Type 5 LSAs for IPv4, OSPFv3 AS-External-LSAs for normal IPv6 areas, or OSPFv3 NSSA-LSAs for attached IPv6 NSSA areas. destination bgp { import ospf } exports OSPF routes. import ospf into OSPF itself is a runtime no-op (loop prevention).

ospf {
    router-id 10.0.0.1

    default-information {
        originate true
        always false
        metric 10
        metric-type type-2
    }

    redistribute {
        connected {
            metric 20
            metric-type type-2
            tag 100
        }
        static {
        }
    }

    areas {
        area 0.0.0.0 {
        }
    }
    interfaces {
        interface eth0 {
            area 0.0.0.0
        }
    }
}

redistribute {
    destination ospf {
        import connected
        import static
        import bgp
    }
    destination bgp {
        import ospf
    }
}

With default-information originate and always false, the Type 5 default (0.0.0.0/0) is advertised only while a non-OSPF default route exists in the RIB; always true advertises it unconditionally. The redistribute source list accepts connected, static, kernel, bgp, and isis; the default-information and per-source metric are bound to 0..16777215.

Each import is scoped to the destination protocol that encloses it: an import connected under destination bgp feeds only BGP, and does NOT also leak into OSPF or IS-IS. To feed the same source into two protocols, list it under both destinations. Routes injected by a redistribute source (connected, static, l2tp, ...) into destination bgp also replay to a BGP peer that establishes AFTER the injection (e.g. a dynamic/inbound peer), matching how received/best-path routes are delivered to a new peer.

The as112 DNS plugin is also a redistribute source: redistribute { destination bgp { import as112 } } originates its four fixed covering prefixes (192.175.48.0/24, 192.31.196.0/24, 2620:4f:8000::/48, 2001:4:112::/48) into BGP. Three leaves under service { as112 { ... } } tune the announcement: asn (origin AS on the wire, default 112), community (leaf-list of well-known names or AA:NN values attached to the routes), and watchdog (boolean, default true: announce only while the DNS node is serving, RFC 7534 Section 3.3).

Inheritance

Configuration uses 3-level inheritance: BGP globals, group defaults, peer overrides.

Level Scope Example
BGP All peers bgp { router-id 1.2.3.4; }
Group Peers in group group upstream { timer { receive-hold-time 180; } }
Peer Single peer peer transit-a { timer { receive-hold-time 90; } }

Containers (like capability, family, timer) are deep-merged across levels. Leaf values (like receive-hold-time inside timer) override.

Peer Settings

Peers are keyed by name (peer <name> { }) where the name must start with a letter.

Setting Description Required
remote { ip; as; } Peer IP and AS number
local { ip; as; } Local bind address and AS
router-id BGP router ID
description Human-readable description
timer { } Timer container: receive-hold-time (seconds, 0 or 3-65535, default 90), send-hold-time (seconds, 0 or 480-65535, default 0), connect-retry (seconds, default 120)
remote { connect } Initiate outbound TCP connections: true or false (default: true)
local { accept } Accept inbound TCP connections: true or false (default: true)
port TCP port
md5-password TCP MD5 authentication
ttl-security Minimum TTL for incoming packets
outgoing-ttl TTL for outgoing packets
group-updates Enable/disable UPDATE grouping
rs-fast-path Enable reactor-native RS forwarding (bypasses plugin dispatch for UPDATE forwarding)
blackhole { } Honor RFC 7999's BLACKHOLE community from this peer. See Blackhole Honoring

Blackhole Honoring (RFC 7999)

A peer can ask Ze to discard the traffic for a prefix. It does this by tagging the announcement with the BLACKHOLE community, 65535:666. Ze installs a discard route for that prefix, and the traffic is dropped in the kernel FIB or in VPP.

Ze installs the discard route itself. There is no next-hop to allocate and no static route to pre-create: the community is read on the session and the prefix is programmed as a discard directly.

Honoring is off by default and is configured per session. Each leaf-list is a separate condition from RFC 7999 Section 3.3.

Leaf-list Meaning
communities The communities this session agreed to honor. blackhole and are the same well-known value; any ASN:VAL works. Unset means the well-known value when prefixes is set, and nothing otherwise. 65535:666
prefixes The prefixes this peer is authorized to advertise. A received blackhole is honored only when one of them covers it and is equal or shorter, which is RFC 7999 Section 3.3's first condition. An empty list authorizes nothing

Standard RTBH takes one line. Writing the prefixes a neighbour may blackhole within IS the explicit configuration directive RFC 7999 Section 4 asks for, so communities defaults to the well-known value:

bgp {
    peer transit-a {
        blackhole {
            prefixes [ 192.0.2.0/24 198.51.100.0/24 ];
        }
    }
}

Most operators run RTBH on their own community rather than on the well-known one. Name it, and name both when a peer sends either:

bgp {
    peer transit-a {
        blackhole {
            communities [ blackhole 65001:666 ];
            prefixes    [ 192.0.2.0/24 198.51.100.0/24 ];
        }
    }
}

A stated list is taken exactly: the well-known value is NOT added to it. An operator who names 65001:666 alone honors that community and not 65535:666.

The four combinations:

prefixes communities Behavior
set unset The well-known 65535:666 only
set set Exactly the stated set
unset set Nothing is honored: a community with no authorized prefix covers nothing
unset unset Nothing, which is RFC 7999 Section 4's default

With the second configuration above, a BLACKHOLE-tagged announcement of 192.0.2.1/32 installs a discard route. The same announcement for 203.0.113.1/32 installs an ordinary route, because no listed prefix covers it.

prefixes asks a different question from a prefix list. A prefix list bounds the LENGTH of an announcement, so a 192.0.2.0/24 le 24 entry rejects the /32 inside it. This leaf-list asks whether the peer is authorized to advertise a prefix that CONTAINS the announcement. The listed prefix must be equal to or shorter than the announced one. So 192.0.2.0/24 covers every host route inside 192.0.2.0/24, and it covers no part of 198.51.100.0/24.

The container is available at the bgp, group and peer levels. Both leaf-lists ACCUMULATE across the levels, so a peer that names one community keeps the ones its group named. A peer cannot drop a community its group agreed: where one session must be excluded, name the community on the peers rather than on the group.

A listen-range group states the agreement for every session it accepts. Such a session has no peer block, so the group is where an IXP route server writes one blackhole container for all of its members:

bgp {
    group ix {
        connection {
            remote {
                ip    dynamic;
                range 192.0.2.0/24;
            }
        }
        blackhole {
            prefixes 192.0.2.0/24;
        }
    }
}

Announcing a blackhole to a peer

The same communities list decides which peers Ze may ADVERTISE the well-known BLACKHOLE community to. RFC 7999 Section 3.1 requires the two networks to agree on use of the community before it is advertised, and naming it on the peer is Ze's half of that agreement.

announce blackhole <prefix> reaches only the sessions whose resolved list holds 65535:666, under either spelling, which includes a session configured with prefixes alone. A peer with no blackhole block, or one that named only its own value such as 65001:666, is left OUT of the announcement. It is not sent the prefix untagged: an ordinary announcement of a host route under attack attracts the traffic the operator asked to have discarded. When no selected peer has agreed, the command fails and names the peers that have not.

announce unicast <prefix> community 65535:666 meets the same gate, because the obligation is about the community rather than the verb. Any other community is untouched.

To advertise your own RTBH value to a peer, name that value on the announcement:

announce unicast 192.0.2.1/32 community 65001:666

A blackhole on a community Ze does not read

The knob above answers the standard case. For anything else, a policy modifier conditioned on a community rewrites whatever attribute the operator chooses, and a next-hop rewrite toward an address routed to a discard is the vendor two-step form of the same idea:

bgp {
    policy {
        modify RTBH {
            match { community 65001:666; }
            set { next-hop 192.0.2.1; }
        }
    }
    peer transit-a {
        filter import prefix-list:AUTHORIZED modify:RTBH;
    }
}

A route that meets no stated value passes through unchanged, so the rest of the session forwards normally. On Linux the rewritten next-hop MUST resolve through a device: route it to a dummy interface. A kernel blackhole route for that address makes the gateway unresolvable and the route is never installed.

Blackhole with origin validation

An operator who runs origin validation with rpki { action { invalid reject } } must also set blackhole-exempt, or no blackhole announcement is honored.

A blackhole prefix is as long as possible, usually a /32 or a /128. A ROA for the covering block carries its maxLength at the aggregate, often a /24. RFC 6811 then makes the /32 Invalid on length alone, and a session that rejects Invalid routes drops the announcement before the blackhole logic sees it. The symptom is a peer that reports the announcement as sent and a Ze that installs nothing.

bgp {
    peer transit-a {
        rpki {
            action { invalid reject; }
            blackhole-exempt true;
        }
        blackhole {
            communities blackhole;
            prefixes    192.0.2.0/24;
        }
    }
}

The exemption is narrow. It keeps the route only when a covering VRP names the route's own origin AS and disagrees on nothing but prefix length. A wrong origin AS stays Invalid, which is the hijack RFC 6811 exists to catch. A prefix with no covering VRP is NotFound rather than Invalid, so the exemption never reaches it.

Set blackhole-exempt on the same session that names a blackhole community. The exemption reads the communities THAT SESSION agreed to, so a peer running RTBH on 65001:666 gets it for 65001:666. On a session that names no community at all the exemption does nothing, and Ze logs that at configure time naming the peer.

Capabilities

Configured under capability { } at any inheritance level.

Capability Config Values
4-byte ASN asn4 presence (enabled by default)
Route Refresh route-refresh presence
Extended Message extended-message presence
Graceful Restart graceful-restart { restart-time 120; } See Graceful Restart guide
ADD-PATH add-path send/receive See ADD-PATH guide
Extended Next Hop nexthop { ipv4/unicast ipv6; } Per-family NH mapping
BGP Role role provider provider, customer, rs, rs-client, peer

Address Families

Configured under family { }. Each family requires a prefix { maximum N; } block.

family {
    ipv4/unicast { prefix { maximum 1000000; } }
    ipv6/unicast { prefix { maximum 200000; } }
    ipv4/mpls-vpn { prefix { maximum 500; } }
    l2vpn/evpn { prefix { maximum 10000; } }
}

Use ze --plugins to see available families from registered plugins.

Prefix Limits

Every family must have a prefix maximum. Ze refuses to start without one.

family {
    ipv4/unicast {
        prefix {
            maximum 1000000
            warning 900000
        }
    }
}

Warning defaults to 90% of maximum when not set. Enforcement behavior is set in the same per-family prefix block, so each family gets its own policy:

peer transit-a {
    family {
        ipv4/unicast {
            prefix {
                maximum 1000000
                teardown false
                idle-timeout 30
            }
        }
        ipv6/unicast {
            prefix {
                maximum 200000
                teardown true
            }
        }
    }
}

This peer warns on an IPv4 overflow and keeps the session, and stops the session on an IPv6 overflow. Each family reads its own leaves. A family that sets no value uses the default.

Setting Scope Default Description
teardown Per family true Send NOTIFICATION and close on exceed. false = warn only, and the UPDATE that crossed the maximum is dropped whole, not NLRI by NLRI.
idle-timeout Per family 0 Seconds to wait before reconnect after this family stopped the session. 0 keeps the peer down.
reconnect Per family derived never, backoff or timer. No value means timer when idle-timeout is above 0, and never when it is 0.
count Per family offered Which prefixes the count compared against maximum holds. See below.

What the count holds: offered or installed

The two values count different things. offered counts wire events, and installed counts prefixes. RFC 4271 Section 6.7 does not say whether a prefix limit governs what the peer offered or what the receiver kept, so this is an operator choice rather than a fixed answer.

peer transit-a {
    family {
        ipv4/unicast {
            prefix {
                maximum 1000000
                teardown false
                count installed
            }
        }
    }
}
Value What the count holds What a re-announced prefix does
offered Every announcement the peer sends, the announcements of a dropped UPDATE included. It rises on an announcement and falls on a withdrawal, whatever prefix each one names Raises the count again. The peer holds the same route, and the count says it holds two
installed The set of prefixes this family currently holds. The count is that set's size Nothing. The prefix is already in the set

The difference shows up on a peer with route churn, which is every transit session. BGP has no way to refresh one route: a peer changing an attribute sends the same prefix again, and that replaces the path it sent before (RFC 4271 Section 3.1). offered reads that second announcement as a second prefix. installed reads it as the prefix it already has.

Pick offered to bound how much a peer may SEND you, announcement by announcement, and accept that a peer which re-announces will reach the bound without ever holding that many routes. Pick installed to bound how many distinct prefixes a peer may advertise to you at once, which is the number that stops moving when the peer only churns attributes.

Neither value is the size of the RIB, and neither is the number show bgp summary reports. Both counts are taken before import policy runs. show bgp summary reports routes-received and routes-accepted, and both are the Adj-RIB-In size AFTER that policy, because Ze stores no route the policy rejected. A peer whose prefixes the policy rejects therefore reads lower there than in either count. Neither value changes enforcement, either: both drop the same UPDATE whole, and both send the same NOTIFICATION under teardown true.

installed keeps one entry per prefix for that family, so it costs memory in proportion to what the peer sends, bounded by maximum when one is configured. offered keeps a number.

offered can be driven below the routes Ze holds. A withdrawal for a prefix the peer never announced still lowers the count, so a peer sending N of them frees N slots that Ze is still using and the Adj-RIB-In can then pass the maximum. installed is immune: a withdrawal of a prefix that is not in the set removes nothing. This is recorded in plan/deferrals/fixit-bgp-per-family-prefix-enforcement.md and is not yet fixed.

After a prefix teardown, the peer stays down

A session stopped by a prefix limit does not come back on its own. The peer state reads idle-hold, ze show warnings names the family that stopped it, and the log says peer held down. An operator brings the peer back by recreating it: raise that family's maximum and commit, or delete and add the peer.

This is the default because a peer that reconnects into the same limit exceeds it again and flaps. To ask for the reconnect anyway:

peer transit-a {
    family {
        ipv4/unicast {
            prefix {
                maximum 1000000
                reconnect backoff
            }
        }
        ipv6/unicast {
            prefix {
                maximum 200000
                idle-timeout 30
            }
        }
    }
}

IPv4 comes back on the usual connect backoff, 5 to 60 seconds. IPv6 waits 30 seconds, and the wait doubles on each repeat teardown up to one hour. Ze refuses a reconnect value that contradicts idle-timeout in the same block, for example reconnect never beside idle-timeout 30.

Protocol Event Capture

Ze can write every message a peer sends to a file, together with the config operations applied while the capture runs. Replay the file on a developer machine with ze-test replay <file> to drive the same state machine over the same input. Capture is off by default. Turn it on for one peer when you need to reproduce a session bug, and turn it off again.

peer transit-a {
    capture {
        enabled true
        directory /var/lib/ze/capture
        maximum-size 100
        on-limit rotate
    }
}
Setting Default Description
enabled false Capture this peer's inbound protocol events.
directory /var/lib/ze/capture Holds one file per peer, named bgp-<peer-address>.jsonl. Created if absent.
maximum-size 100 Hard cap on one file, in megabytes, range 1 to 1024. The writer refuses a record that would cross the cap, so a file never exceeds this value.
on-limit rotate rotate renames the full file to <file>.1 and starts a fresh one, so the newest events are kept and the peer uses at most twice maximum-size on disk. stop closes the file, so the oldest events are kept.

ze doctor reports a capture directory the daemon cannot write. When the writer queue fills, Ze sheds events rather than stopping the BGP read loop, counts them in ze_bgp_capture_dropped_events_total, and writes the gap into the file.

The file holds the peer's routing data: prefixes, AS paths, communities and next-hops. It holds no local secret. TCP-MD5 keys never travel on the wire, and captured config payloads are redacted. Handle the file like a routing-table dump.

PeeringDB Prefix Data

Ze can query PeeringDB to suggest prefix maximums. Configure the PeeringDB API URL and margin in the system block:

system {
    peeringdb {
        url "https://www.peeringdb.com"
        margin 10
    }
}
Setting Default Description
url https://www.peeringdb.com PeeringDB-compatible API base URL.
margin 10 Percentage added above PeeringDB prefix count (0-100).

Use resolve peeringdb max-prefix <asn> to look up prefix limits, then apply them via the config editor.

Hardware Tuning

Ze can apply hardware tuning at startup and on config commit. Tuning operations are Linux-only and require root or CAP_SYS_ADMIN.

system {
    tuning {
        cpu {
            governor performance;
        }
        irq-affinity eth0 {
            cpus 0,2,4-7;
        }
        ethtool eth0 {
            ring {
                rx 4096;
                tx 4096;
            }
        }
    }
}
Path Description
tuning/cpu/governor CPU scaling governor: performance, powersave, ondemand, conservative, schedutil
tuning/irq-affinity Per-interface IRQ CPU affinity (CPU list format: 0,2,4-7)
tuning/ethtool/ring/rx Receive ring buffer size (1-65535)
tuning/ethtool/ring/tx Transmit ring buffer size (1-65535)

Tuning is idempotent: only changed parameters are written. Write failures are reported but do not block the config commit. On non-Linux platforms the tuning block is accepted but no operations are applied.

Process Bindings

A peer attaches a program in an attach process block. The block names a plugin and states the relationship in two directions: receive is what the program is fed for this peer, and send is what it may originate toward it.

peer transit-a {
    ...
    attach process adj-rib-in {
        receive [ update-received state ];
    }
    attach process rpki {
        receive [ update-received ];
    }
}

A peer that attaches nothing feeds nothing. These blocks decide delivery. A running plugin that a peer does not attach receives no event from that peer, whatever the plugin asked for when it started, and it may send that peer nothing. Loading a plugin and attaching it to a peer are two separate acts.

Base event types for receive: update, open, notification, keepalive, refresh, state, negotiated, eor, rpki. Plugins may register more (for example update-rpki from bgp-rpki-decorator). A plain type is fed in both directions; update-received and update-sent name one. * names every registered type. Values are validated at config parse time.

Base message types for send: update to originate routes, refresh to ask the peer to re-advertise. * grants both, and every send type registered later. A send a peer's block does not grant is refused and reported.

Write the block on a group and every peer of that group carries it, dynamic members included. A member that restates the block replaces the group's list for itself alone. show event delivery prints the edges the running config produces, peer by peer.

Route Filters

Route filtering is configured via filter blocks at bgp, group, or peer level. Values are named filter instances from bgp policy or explicit <plugin>:<filter> references to plugin-declared filters.

bgp {
    filter {
        import [ rpki:validate ]
    }
    group customers {
        filter {
            import [ community:scrub ]
        }
        peer customer-a {
            remote { ip 10.0.0.1; as 65001; }
            filter {
                export [ aspath:prepend ]
            }
        }
    }
}

Chains are cumulative: bgp-level filters run first, then group, then peer. Mandatory filters (e.g., rfc:otc) always run before user filters and cannot be removed. Default filters (e.g., rfc:no-self-as) run unless overridden by a filter that declares overrides.

See Route Filters for details.

Cross-Protocol Redistribute

The redistribute block nests import rules under a destination protocol. Each destination protocol registers a RedistConsumer; the orchestrator dispatches accepted routes to the matching consumer.

Consumer Purpose
BGP IngressFilter Ingress ACL: when the source is ibgp or ebgp, gates received UPDATEs from intra-BGP sources.
redistribute-orchestrator plugin Dispatches non-BGP route-change events to registered consumers (BGP, IS-IS).
redistribute {
    destination bgp {
        import ebgp { family [ ipv4/unicast ipv4/vpn ]; }    # intra-BGP ACL
        import l2tp;                                          # egress, all families
        import connected { family [ ipv4/unicast ]; }         # egress, IPv4 unicast only
        import isis;                                          # IS-IS SPF routes into BGP
    }
    destination isis {
        import connected;                                    # connected/static/BGP into
        import static;                                       # IS-IS LSPs as TLV 135
        import bgp;                                          # Extended IP Reachability
    }
}

import isis pulls IS-IS SPF routes (both levels, single source isis) into BGP. destination isis injects connected/static/BGP prefixes into the IS-IS link-state database as Extended IP Reachability (TLV 135); destination isis { import isis } is accepted by the schema but is a runtime no-op (loop prevention rejects redistributing IS-IS into itself).

Both consumers read the same redistribute.Global() evaluator, so a single config block governs both behaviors. The producer side (per-protocol route- change events) lives in each protocol component; the orchestrator (redistribute-orchestrator) enumerates non-consumer producers at startup and subscribes.

The orchestrator auto-loads when redistribute {} is present in the config (it claims ConfigRoots: ["redistribute"]). No plugin { external redistribute-orchestrator { ... } } block is required. The intra-BGP IngressFilter rides the registry's filter chain at init time and needs no plugin spin-up.

Prefix-List Filter

Named prefix-list filters live under bgp { policy { prefix-list NAME { ... } } } and are invoked via peer filter chains. Each list is an ordered sequence of match entries, first-match-wins, with an implicit deny on no match.

bgp {
    policy {
        prefix-list CUSTOMERS {
            entry 10.0.0.0/8 {
                ge 16
                le 24
                action accept
            }
            entry 192.168.0.0/16 {
                ge 24
                le 24
                action reject
            }
        }
    }

    peer customer-a {
        connection { remote { ip 10.0.0.1; as 65001; } }
        filter {
            import [ CUSTOMERS ]
        }
    }
}

Each entry has:

Field Type Purpose
ge uint Minimum prefix length (inclusive). Defaults to the entry's own CIDR length.
le uint Maximum prefix length (inclusive). Defaults to 32 (IPv4) / 128 (IPv6).
action enum accept or reject.

A route matches an entry when its address is contained in the entry's CIDR AND its prefix length satisfies ge <= length <= le. Entries are evaluated in the order they appear; the first match determines the action. A route that matches no entry is implicitly rejected.

Multi-prefix UPDATEs. When an UPDATE carries several prefixes and the prefix-list accepts some but rejects others, the filter returns modify: the UPDATE is rewritten to carry only the accepted prefixes. The rejected ones are silently removed. In v1 this applies to the legacy IPv4 unicast NLRI only; IPv6 and labeled families still use whole-update accept/reject semantics.

Filter instance names are globally unique under bgp policy. Use the plain name as the primary reference form. Prefixed forms are accepted for disambiguation or advanced use:

Form Example
Plain filter name (preferred) CUSTOMERS
<filter-type>:<filter> prefix-list:CUSTOMERS
<plugin>:<filter> bgp-filter-prefix:CUSTOMERS

IRR Prefix-List Filter

The bgp-filter-irr plugin generates prefix-list filters from IRR (Internet Routing Registry) data. It queries a whois server for an AS-SET's prefixes and applies them as import filters, replacing the external bgpq4 workflow.

bgp {
    policy {
        irr {
            server whois.radb.net    // IRR whois server (default)
            peeringdb-url https://www.peeringdb.com  // PeeringDB API base URL (default)
            refresh-interval 3600   // seconds between re-queries (60-86400)
        }
    }

    peer customer-a {
        session {
            asn { local 65000; remote 65001; }
            irr {
                as-set AS-CUSTOMER    // explicit AS-SET (omit for PeeringDB auto-discovery)
            }
        }
        filter {
            import [ bgp-filter-irr:65001 ]
        }
    }
}

The suffix in bgp-filter-irr:65001 is the peer's remote ASN. Use the corresponding ASN in each peer's filter reference. The plugin queries IRR for the AS-SET's prefixes and builds a prefix-list keyed by ASN. Routes with prefixes in the list are accepted; all others are rejected (implicit deny).

When irr { as-set } is omitted, the plugin auto-discovers the AS-SET from PeeringDB using the peer's remote ASN. If PeeringDB has no AS-SET registered for the ASN, the plugin falls back to querying IRR directly using AS<number> (e.g. AS65001). If the IRR query also fails, the existing prefix-list is preserved and the error is reported via show bgp irr. Set irr { enable disable; } to opt a peer out of IRR filtering entirely.

For a complete configuration, verification workflow, troubleshooting table, and local recording, see Filter BGP Imports with IRR.

The plugin refreshes prefix-lists automatically at the configured interval and on demand via update bgp irr all. Resolved prefixes are stored as operational data in zefs (not in the config tree) and survive restarts.

Command Purpose
show bgp irr Per-ASN status: AS-SET, prefix count, last/next refresh
show bgp irr prefix <peer> List all prefixes for a peer
show bgp irr check <peer> <prefix> Test if a prefix would be accepted
update bgp irr all Refresh all peers immediately
update bgp irr asn <asn> Refresh a specific ASN
update bgp irr as-set <as-set> Refresh peers using a specific AS-SET

AS-Path Filter

Named AS-path regex filters live under bgp { policy { as-path-list NAME { ... } } }. Each list is an ordered sequence of regex entries with accept/reject action. The AS-path is converted to a space-separated decimal string and matched using Go RE2 (linear time, inherently ReDoS-safe). Use [0-9] instead of \d in regex strings (ze's config parser interprets backslash as escape).

bgp {
    policy {
        as-path-list ALLOW-PEER {
            entry "^65001" { action accept }
        }
        as-path-list TRANSIT {
            entry "^[0-9]+( [0-9]+)+" { action accept }
        }
    }

    peer customer-a {
        filter { import [ as-path-list:ALLOW-PEER ] }
    }
}

Community Match Filter

Named community-match filters live under bgp { policy { community-match NAME { ... } } }. Each entry checks for presence of a specific community value in the route's standard, large, or extended community attributes.

bgp {
    policy {
        community-match BLOCK-NO-EXPORT {
            entry no-export { type standard; action reject }
        }
        community-match ALLOW-CUSTOMER {
            entry 65001:100 { type standard; action accept }
            entry 65001:100:200 { type large; action accept }
        }
    }

    peer transit-a {
        filter { import [ community-match:BLOCK-NO-EXPORT ] }
    }
}

Route Attribute Modifier

Named modifier definitions live under bgp { policy { modify NAME { ... } } }. Operations use set, del, increment, or decrement containers. Only present leaves are applied; undeclared attributes pass through unchanged. A definition applies to every route that reaches it. A definition that states a match container applies its operations only to the routes that meet the condition, and every other route leaves the filter unchanged. Use a match filter earlier in the chain when you want the rest dropped, because a rejected route leaves the pipe.

bgp {
    policy {
        modify PREFER-LOCAL {
            set {
                local-preference 200
            }
        }
        modify PREPEND-3 {
            set {
                as-path-prepend 3
            }
        }
    }

    peer transit-a {
        filter {
            import [ prefix-list:CUSTOMERS modify:PREFER-LOCAL ]
            export [ modify:PREPEND-3 ]
        }
    }
}
Leaf Type Range Purpose
local-preference uint32 0-4294967295 Set LOCAL_PREF
med uint32 0-4294967295 Set MED
del/med empty present Remove MULTI_EXIT_DISC from the route
origin enum igp, egp, incomplete Set ORIGIN
next-hop IP address IPv4 Set NEXT_HOP
as-path-prepend uint8 1-32 Prepend local AS N times

del { med; } is the mechanism RFC 4271 Section 5.1.4 requires a speaker to implement. It takes effect on an import chain only. That section requires the removal before Decision Process phases 1 and 2, and the import chain is the only chain that runs there. On an export chain, the directive is refused and logged. It is mutually exclusive with set med, increment med, and decrement med. The configuration is refused when removal and one of these operations are both stated.

A metric received from one neighboring AS is already kept off a session toward another with no configuration at all, which is the same section's propagation rule. del { med; } is for the operator who wants the metric gone from the route itself.

The match container holds three leaf-lists, community, large-community and extended-community. Values across all three are alternatives: any one present in the route satisfies the condition.

Static Routes

Routes can be configured directly per peer:

peer transit-a {
    ...
    update {
        attribute {
            origin igp
            next-hop 10.0.0.2
            local-preference 100
            community [ no-export ]
        }
        nlri {
            ipv4/unicast add 10.0.0.0/24
            ipv4/unicast add 10.0.1.0/24
        }
    }
}

MPLS

Enable MPLS label processing on an interface (Linux LSR). This sets net.mpls.conf.<iface>.input=1; the global label-table size is set via the net.mpls.platform_labels sysctl.

interface ethernet eth0 {
    unit 0 {
        mpls { enable true }
    }
}

BGP labeled-unicast routes are then programmed into the kernel MPLS FIB (label push). Inspect installed entries with show mpls forwarding.

RSVP-TE (traffic-engineered LSPs)

The rsvp-te {} container configures explicitly-routed MPLS tunnels with bandwidth admission control. RSVP runs directly on IP (protocol 46, requires CAP_NET_RAW).

rsvp-te {
    router-id 10.0.0.1
    interface eth0 {
        max-bandwidth 10e9
        max-reservable-bandwidth 8e9
    }
    tunnel to-egress {
        destination 10.0.0.9
        tunnel-id 1
        bandwidth 1e9
        explicit-route 1 {
            address 10.0.0.5/32
        }
        explicit-route 2 {
            address 10.0.0.9/32
        }
        fast-reroute {
            backup          facility
            node-protection false
        }
    }
    bypass around-eth0 {
        merge-point 10.0.0.5
        explicit-route 1 {
            address 10.1.0.5/32
        }
    }
}

fast-reroute on a tunnel requests RFC 4090 local protection (facility backup); bypass defines a facility-backup LSP from this Point of Local Repair to a merge-point, explicitly routed around the protected resource (ze has no CSPF).

Inspect LSP, interface, tunnel, and protection state with the component's show rsvp-te session, show rsvp-te interface, show rsvp-te tunnel, and show rsvp-te fast-reroute commands. See RSVP-TE for details and current status.

IS-IS

The isis {} top-level container configures the native IS-IS link-state IGP (ISO/IEC 10589, RFC 1195 / 5305 / 5301). IS-IS runs directly over Layer 2 and requires CAP_NET_RAW. At least one Network Entity Title (net) is required; the System ID is derived from the 6 octets before the NSEL of the first NET unless an explicit system-id is given.

isis {
    net 49.0001.0000.0000.0001.00
    level l1-l2
    lsp-lifetime 1200
    hostname r1
    interfaces {
        interface eth0 {
            circuit-type point-to-point
            metric 10
            hello-interval 10
            hold-multiplier 3
            priority 64
            address-family ipv4-unicast { }
        }
        interface eth1 {
            passive true
        }
    }
}

Per-interface level-1 {} / level-2 {} containers override metric, timers, and DIS priority per level.

Authentication is configured as named key-chains {}. Each key has a key-id, an algorithm (cleartext, hmac-md5, or hmac-sha-1/224/256/ 384/512), a secret (stored $9$-encoded, like PPPoE/WireGuard keys), and optional send-lifetime / accept-lifetime windows for hitless rotation. Chains are referenced by per-interface (IIH Hellos) and per-level (LSP/CSNP/PSNP) auth-key-chain leaves: the area key for Level 1 and the domain key for Level 2:

isis {
    net 49.0001.0000.0000.0001.00
    key-chains area-key {
        key 1 {
            algorithm hmac-sha-256
            secret $9$....               # entered plaintext, $9$-encoded on commit
        }
    }
    key-chains iih-key {
        key 1 { algorithm hmac-sha-256  secret ... }
    }
    level-1 { auth-key-chain area-key }       # L1 LSP/CSNP/PSNP
    interfaces {
        interface eth0 {
            level-1 { auth-key-chain iih-key }    # L1 Hellos on this circuit
        }
    }
}

Under configured auth, a PDU with no, wrong, or misordered Authentication TLV is rejected (no adjacency, not stored) and ze_isis_auth_failures_total increments. See docs/guide/isis.md for the full authentication behaviour. Adjacency, LSDB, SPF, and route install are delivered by the runtime IS-IS feature set; this release wires the component, config, and circuits.

PKI Certificate Store

The pki {} top-level container holds CA and device certificates for IPsec, TLS, and other certificate-based features.

pki {
    ca <name> {
        certificate <base64-DER>
    }
    certificate <name> {
        certificate <base64-DER>
        intermediate <base64-DER>        # optional intermediate CA
        private {
            key $9$...                   # auto-encoded via ze:sensitive
        }
    }
}

CA certificates are trusted roots for chain validation. Device certificates include the certificate itself and optionally a private key (PKCS8, SEC1/ECDSA, or PKCS1/RSA in base64-encoded DER). Private keys use $9$ sensitive encoding and are never shown in CLI output.

Chain validation runs at config load: device certificates must chain to a loaded CA. Expired certificates are rejected with a descriptive error.

IPsec VPN Configuration

The vpn { ipsec {} } container configures site-to-site IPsec VPN tunnels. The data model defines ESP groups, IKE groups, and peers. Ze validates the config at load time; strongSwan handles runtime negotiation.

vpn {
    ipsec {
        interface pppoe0

        esp-group ESP-RW {
            lifetime 86400
            pfs disable
            proposal 10 {
                encryption aes128gcm
                hash sha256
            }
        }

        ike-group IKE-RW {
            key-exchange ikev2
            lifetime 0
            close-action start
            dead-peer-detection {
                action restart
                interval 10
                timeout 30
            }
            proposal 10 {
                encryption aes128gcm
                hash sha256
                dh-group 14
            }
        }

        site-to-site {
            peer management-bridge {
                ike-group IKE-RW
                esp-group ESP-RW
                connection-type initiate
                remote-address mgmt.example.com
                authentication {
                    mode x509
                    local-id EXAFO000000400
                    remote-id management-bridge
                    x509 {
                        ca-certificate exa-vpn-ca
                        certificate EXAFO000000400
                    }
                }
                vti {
                    bind vti0
                }
            }
        }
    }
}

Encryption algorithms: aes128, aes256, aes128gcm, aes256gcm. The schema also names chacha20poly1305 and 3des, and no build carries a transform for either. A proposal that names one is refused at commit, and the refusal lists the implemented set. Hash algorithms: sha256, sha384, sha512. The schema also names sha1, and no build carries a transform for it, so the same refusal applies.

An IKE proposal requires hash beside every cipher, because it names the PRF that RFC 7296 Section 3.3.3 makes mandatory. An ESP proposal requires hash beside a non-AEAD cipher and refuses it beside an AEAD cipher, because an AEAD cipher carries its own integrity. DH groups: 1-31 (14 = MODP-2048 recommended minimum). Authentication modes: pre-shared-secret (with a $9$-encoded key), x509 (PKI store references), eap-tls, or eap-mschapv2.

Cross-reference validation runs at config load: peer IKE/ESP group references must name defined groups, ca-certificate and certificate names must exist in the PKI store, and local-id must match the certificate's subject CN.

A ca-certificate is mandatory for x509 and for every EAP mode. It is the trust anchor the remote certificate must chain to, and without one any self-signed certificate would authenticate. Every EAP mode also needs a certificate, which the responder signs its AUTH with (RFC 7296 Section 2.16). Set remote-id as well whenever the authority issues to more than one client: it is what binds the certificate to this peer. See IPsec VPN for the identity rules.

This validation runs at commit and at reload. It does not run under ze config validate, which reads the schema and does not run a plugin's config verifier.

Interface Configuration

Ze manages network interfaces with a descriptive-name model. Each interface type is a YANG list keyed by a user-chosen name. The MAC address serves as the binding between the config entry and the physical (or virtual) hardware.

Interface Types

Type Description MAC required
ethernet Physical ethernet interface
veth Virtual ethernet pair
bridge Bridge interface
dummy Virtual dummy interface
loopback Loopback (container, no key)

Logical Name and the os-name Selector

An interface name is a logical, human-readable handle chosen by the operator. By default the logical name is also used as the OS/kernel device name, so every interface whose name already matches its kernel device resolves unchanged. To alias a logical name to a different kernel device, set the os-name selector on an ethernet interface:

interface {
    ethernet uplink {
        os-name eth0;   # logical "uplink" binds to kernel device eth0
    }
}

Every subsystem that takes an interface (IS-IS, routing, and others) refers to the logical name. A shared resolver in the iface component maps the logical name to its kernel device via the os-name selector (defaulting to the name itself) and serves the ifindex, MAC, MTU, and addresses, so operator-facing names are decoupled from kernel device names. IS-IS, for example, resolves isis { interface uplink { } } to kernel device eth0 through this resolver. The device can also be selected by its hardware MAC instead of by name (see Binding by Hardware MAC).

The os-name selector applies to ethernet interfaces, the kind Ze matches against pre-existing kernel devices. Created kinds (dummy, veth, bridge, tunnels, wireguard, xfrm) are made by Ze under their logical name, so os-name is ignored on them and the logical name is always the kernel device name.

Binding by Hardware MAC (mac/match)

Names and OS device names can change between boots (NIC reordering, slot moves). To pin a logical interface to a physical NIC regardless of the name the kernel gives it, select the device by its hardware MAC with mac { match }:

interface {
    ethernet uplink {
        mac {
            match a0:36:9f:12:34:56;   # bind "uplink" to the NIC with this MAC
        }
    }
}

The resolver scans every interface and binds uplink to the one carrying that MAC. It matches the device's permanent (factory) address (IFLA_PERM_ADDRESS) when the NIC reports one, so the binding survives an operational MAC override (mac { address }) on the very same interface; for virtual devices that report no permanent address it matches the current address instead. When no device carries the MAC the binding defers until the device appears (the resolver fires a link event then). mac/match takes precedence over os-name and, like os-name, applies to ethernet only.

MAC Address Binding

For ethernet, veth, and bridge interfaces, the MAC container carries two independent leaves. mac { address } overrides the operational MAC (omit it to keep the hardware-assigned one); when set it must be unique within each type. mac { match } selects the kernel device by its hardware MAC (see above). The two are independent: a NIC can be matched by its permanent MAC and have its operational MAC overridden at the same time. Names are descriptive labels chosen by the operator; mac { address } ties the named config entry to a specific operational hardware address.

Discovery During Init

Running ze init discovers OS interfaces via netlink (Linux) or stdlib (other platforms) and writes initial config to ze.conf. Each discovered interface gets an entry named after its OS name, with mac { address } populated and the os-name selector recording the original OS device name (so renaming the config entry still maps back to the kernel device). Loopback appears as an empty loopback { } container.

The --seed flag skips this discovery entirely: an appliance-image seed database must not bake the build host's interfaces into the active config (they belong to the wrong machine and would shadow the appliance's first-boot template). The appliance discovers its own interfaces at first boot instead.

Example

interface {
    ethernet uplink {
        mac {
            address 00:1a:2b:3c:4d:5e;
        }
    }
    ethernet mgmt {
        mac {
            address 00:1a:2b:3c:4d:5f;
        }
        mtu 1500;
    }
    bridge fabric {
        mac {
            address 00:1a:2b:3c:4d:60;
        }
        stp true;
    }
    dummy blackhole {
    }
    loopback {
    }
}

The MAC address validator provides format checking and live autocomplete from currently discovered OS interfaces when editing config interactively.

Interface Offload and Steering

L2 interfaces (ethernet, dummy, veth, bridge) support an offload container with boolean leaves for hardware offload and software packet steering features. Each leaf uses three-state semantics: true enables, false disables, absent preserves the OS default (no kernel call is made).

Feature Mechanism Description
gro kernel ioctl Generic Receive Offload: aggregates incoming packets
gso kernel ioctl Generic Segmentation Offload: delays outgoing segmentation
sg kernel ioctl Scatter-Gather I/O: multi-buffer frame assembly
tso kernel ioctl TCP Segmentation Offload: hardware TCP segmentation
lro kernel ioctl Large Receive Offload: hardware RX coalescing (disable on routers)
hw-tc-offload kernel ioctl Hardware TC offload: NIC executes TC rules in hardware
rps sysfs Receive Packet Steering: distribute RX across CPUs
rfs sysfs Receive Flow Steering: steer RX to application CPU

Ze talks to the kernel directly via ethtool ioctls and sysfs writes. The ethtool(8) CLI program is not required.

interface {
    ethernet uplink {
        mac {
            address 00:1a:2b:3c:4d:5e;
        }
        offload {
            gro true;
            tso true;
            lro false;
            rps true;
        }
    }
}

Tunnel, wireguard, and PPPoE interfaces do not support offloads (the offload container is part of the interface-l2 YANG grouping, not interface-common).

Backend Capability Errors

The interface, traffic/control, and firewall components carry a backend leaf. The YANG schema annotates feature nodes with a list of supporting backends via the ze:backend extension. When the config selects a backend that does not implement a used feature, commit and ze config validate reject the config before any Apply call runs.

For traffic/control today the gate enforces only the "no backend configured" case (non-Linux startup, or an explicit empty backend leaf); per-feature annotations for tc-specific qdiscs and filter types land with spec-fw-7-traffic-vpp when a second backend exists to reject against.

Example: the bridge, tunnel, wireguard, veth, and mirror nodes are annotated ze:backend "netlink". Selecting backend vpp while using any of them produces a diagnostic naming the YANG path, the active backend, and the list of backends that DO implement the feature:

/interface/bridge: feature not supported by backend "vpp" (supported: netlink)
/interface/tunnel: feature not supported by backend "vpp" (supported: netlink)

Fix: change backend back to netlink (the default on Linux), or remove the unsupported entries. The same gate runs at daemon reload (SIGHUP) and on first-apply at startup, so the daemon cannot boot into an unworkable state.

Route Priority

The route-priority leaf on a unit sets the Linux route metric for default routes on that interface. Lower values are preferred by the kernel. When a link goes down, the metric is increased by 1024 to deprioritize the interface, allowing traffic to shift to an alternative uplink. When the link comes back up, the original metric is restored.

The leaf defaults to 254. A default route ze learns from the network (a DHCP lease, a PPPoE session) is installed at that metric, so it ranks below a static route and below every route a routing protocol produces. The number matches the order rib admin-distance uses for protocols: connected 0, static 10, ebgp 20, ospf 110, isis 115, ibgp 200. It is also the administrative distance a Cisco IOS DHCP client gives the default route it learns, which is the same ranking decision on another vendor. That distance is not a Linux metric, and ze does not read it: 254 is the metric ze writes to the kernel. The pppoe-client list carries its own route-priority leaf with the same default, which ranks the route a PPPoE session installs when IPCP completes.

The metric decides ownership, not only preference. ze installs a learned default route with RTM_NEWROUTE in replace mode, and the kernel matches such a route on destination, metric and table. It does not match on the protocol that installed it. Two default routes at different metrics are two kernel routes, and the lower metric forwards. Two default routes at the SAME metric are one kernel route, owned by whoever wrote it last: a learned route at metric 0 replaces an operator's static default at metric 0, gateway included.

For IPv4, ze installs the default route with that metric when a DHCP lease provides a gateway. For IPv6, ze leaves the router advertisement default routes to the kernel until the unit WRITES route-priority. Writing it above 0 makes ze suppress the kernel's automatic RA default route (accept_ra_defrtr=0) and install ::/0 routes with the configured metric when NDP neighbor events indicate a router (NTF_ROUTER flag). Multiple routers on the same link are each installed with the same metric. On clean shutdown or config removal, accept_ra_defrtr is restored to 1.

The written leaf is the whole test, and DHCP is no part of it. A unit that takes its IPv6 address from SLAAC or from a static address, with no DHCP of either family, hands ze the same ownership: ze suppresses the kernel's RA default route AND installs its own at the written metric. The two go together on every unit, because one map answers for both.

interface {
    ethernet uplink {
        mac {
            address 00:1a:2b:3c:4d:5e;
        }
        unit default {
            route-priority 1;
            ipv4 {
                dhcp {
                    enabled true;
                }
            }
        }
    }
    ethernet backup {
        mac {
            address 00:1a:2b:3c:4d:5f;
        }
        unit default {
            route-priority 5;
            ipv4 {
                dhcp {
                    enabled true;
                }
            }
        }
    }
}

With this config, uplink (metric 1) is preferred over backup (metric 5). If uplink goes down, its metric becomes 1025 (1 + 1024), so backup (metric 5) takes over. When uplink recovers, its metric returns to 1 and traffic shifts back. A unit that writes no route-priority installs its learned default routes at 254 and leaves the IPv6 RA default routes to the kernel.

Upgrading from a release that installed learned routes at metric 0

A DHCP, PPPoE or dhcp-auto default route now lands at metric 254. Before this release ze installed it at metric 0, where a plain static default route also lands. The kernel holds one route per destination and metric, so the learned route replaced the operator's static default, took its gateway, and re-stamped it proto 253. The operator saw one route where they configured two. Both routes now exist and the static one forwards.

What changes on the box. Read the routes with show route default. A learned default that read metric 0 yesterday reads metric 254 today. Any policy that matches on that metric (a ip rule, a firewall mark, a monitoring check) must name 254. A static default route that ze had silently taken over reappears with its own gateway after the next lease, because ze no longer writes over it.

To keep the old metric, write route-priority 0 on the unit. An explicit 0 is not the same as an absent leaf: it puts learned routes back on metric 0, with the takeover that metric carries. The pppoe-client list takes the same leaf with the same meaning.

A unit that writes no route-priority still leaves IPv6 to the kernel. The non-zero default did not hand ze the RA default routes of every interface. Writing the leaf above 0 is what sets accept_ra_defrtr=0 and installs ::/0, exactly as before.

VLAN 802.1p QoS Maps

VLAN units (units with vlan-id) accept two QoS maps that translate between the 3-bit 802.1p PCP field of the 802.1Q tag header and the kernel's internal packet priority. ingress-qos-map (keyed by PCP) classifies received tagged frames: a frame arriving with the listed PCP gets the mapped internal priority, which tc filters and nftables can then act on. egress-qos-map (keyed by priority) stamps outgoing frames: a packet leaving with the listed internal priority gets the mapped PCP on the wire, so downstream L2 switches honor the class of service without inspecting IP headers. Both sides of every entry are 0-7; unmapped values fall back to 0. The maps require vlan-id and are applied when the VLAN sub-interface is created.

interface {
    ethernet eth0 {
        unit v100 {
            vlan-id 100;
            ingress-qos-map 6 {
                priority 6;
            }
            egress-qos-map 6 {
                pcp 6;
            }
            ipv4 {
                address [10.0.0.1/30]
            }
        }
    }
}

This mirrors a BNG-style CoS setup: traffic classified to internal priority 6 (for example by a tc class matching DSCP CS6) leaves eth0.100 with PCP 6 in the 802.1Q header, and tagged frames arriving with PCP 6 are classified to internal priority 6 for upstream scheduling. The configured maps are visible in show interface output as ingress-qos-map / egress-qos-map.

VPP backend limitation: VPP's qos record copies the VLAN PCP value verbatim into internal QoS bits, so ingress maps must be identity (pcp == priority for every entry, e.g. pcp 6 { priority 6; }). Non-identity ingress maps (e.g. pcp 6 { priority 3; }) are rejected at config validation. Egress maps are fully supported via VPP's qos egress-map + qos mark pipeline. Use the netlink backend if remapped ingress is required.

Class-of-Service Profiles

Named 802.1p profiles define PCP-to-priority mappings that can be referenced from interfaces instead of repeating inline ingress-qos-map / egress-qos-map lists on every unit. The class-of-service section is provided by the cos plugin. Profiles are inherited by all VLAN units on the interface; individual units can override with a different profile or opt out with none.

class-of-service {
    ieee-802.1p residential {
        ingress {
            pcp 0 { priority 0; }
            pcp 5 { priority 5; }
            pcp 6 { priority 6; }
        }
        egress {
            priority 0 { pcp 0; }
            priority 5 { pcp 5; }
            priority 6 { pcp 6; }
        }
    }
}

interface {
    ethernet eth0 {
        class-of-service residential;
        unit v100 { vlan-id 100; }
        unit v200 { vlan-id 200; class-of-service none; }
    }
}

The profile residential is applied to unit v100 (inherited from the parent interface). Unit v200 opts out with class-of-service none and gets no QoS maps. A unit cannot have both a class-of-service reference and inline qos maps; the two are mutually exclusive.

Use show class-of-service to list the registered profiles and their ingress/egress map entries.

Reverse Path Filtering (rpf-check)

The rpf-check leaf in the ipv4 and ipv6 unit containers controls unicast Reverse Path Forwarding verification. Three modes are available:

Value Linux sysctl Behavior
disable rp_filter=0
strict rp_filter=1 Packet must arrive on the interface the kernel would use to reach the source
loose rp_filter=2 Source address must be reachable via any interface
interface {
    ethernet eth0 {
        mac {
            address 02:00:00:00:00:01;
        }
        unit default {
            ipv4 {
                rpf-check strict;
            }
        }
    }
}

IPv6 rpf-check is accepted in config but only enforced with the VPP data plane. On Linux without VPP, a warning is logged and the setting has no effect.

The legacy rp-filter 0|1|2 integer syntax is still accepted for backward compatibility but emits a deprecation warning. Use rpf-check in new configs.

IPv6 Router Advertisements

The router-advertisement container in a unit's ipv6 container makes Ze send IPv6 Router Advertisements on that unit (RFC 4861). Hosts on the link build addresses by stateless address autoconfiguration (SLAAC), learn a default router, and learn DNS resolvers. This is the job radvd does on other systems.

The container is Linux only and netlink only. A tree with backend vpp rejects it at config verify with feature not supported by backend "vpp".

interface {
    backend netlink;
    ethernet eth0 {
        unit 0 {
            ipv6 {
                address [ 2001:db8:1::1/64 ];
                forwarding true;
                router-advertisement {
                    enabled true;
                    maximum-interval 900;
                    minimum-interval 300;
                    router-lifetime 1800;
                    hop-limit 64;
                    managed false;
                    other-config false;
                    reachable-time 30000;
                    retransmit-timer 1000;
                    prefix 2001:db8:1::/64 {
                        on-link true;
                        autonomous true;
                        valid-lifetime 86400;
                        preferred-lifetime 43200;
                    }
                    rdnss {
                        server [ 2001:db8:1::53 2001:db8:1::54 ];
                        lifetime 3600;
                    }
                }
            }
        }
    }
}
Leaf Range Unit Default Meaning
enabled boolean false Send advertisements on this unit
maximum-interval 4..1800 seconds 600 Longest wait between unsolicited advertisements
minimum-interval 3..1350 seconds 200 Shortest wait between unsolicited advertisements
router-lifetime 0..9000 seconds 1800 How long hosts keep Ze in their default router list
hop-limit 0..255 64 Hop Limit hosts put in their outgoing packets. 0 states no value
managed boolean false The M flag: hosts get their addresses from DHCPv6
other-config boolean false The O flag: hosts get other configuration from DHCPv6
reachable-time 0..3600000 milliseconds 0 Neighbor reachable time for hosts. 0 states no value
retransmit-timer uint32 milliseconds 0 Neighbor Solicitation retransmit time. 0 states no value
prefix <cidr> list One Prefix Information option for each entry
prefix <cidr> on-link boolean true The L flag
prefix <cidr> autonomous boolean true The A flag, which turns SLAAC on for this prefix
prefix <cidr> valid-lifetime uint32 seconds 2592000 How long the prefix stays valid
prefix <cidr> preferred-lifetime uint32 seconds 604800 How long addresses from the prefix stay preferred
rdnss server up to 8 Resolver addresses advertised to hosts (RFC 8106)
rdnss lifetime uint32 seconds none How long hosts use these resolvers

Set ipv6 forwarding true on an advertising unit. Hosts send Ze their off-link traffic. The kernel drops that traffic while net.ipv6.conf.<device>.forwarding is 0. ze doctor reports doctor-iface-ra-forwarding for each advertising interface in that state. Leave accept-ra at 0 unless the same interface also learns from another router.

Two zero values are legal input and each one carries its own meaning. router-lifetime 0 says that Ze advertises its prefixes and its resolvers while it is not a default router (RFC 4861 Section 4.2). rdnss lifetime 0 tells hosts to stop using the resolvers (RFC 8106 Section 5.1). rdnss lifetime has no default: leave the leaf out and Ze sends 3 x maximum-interval instead, which keeps "retire the resolvers" apart from "say nothing about a lifetime".

Config verify applies four cross-leaf rules and rejects the commit on each one:

Rule Rejected input
minimum-interval is at most 0.75 x maximum-interval minimum-interval 500 with maximum-interval 600
router-lifetime is 0, or at least maximum-interval router-lifetime 300 with maximum-interval 600
preferred-lifetime is at most valid-lifetime valid-lifetime 3600 with the default preferred-lifetime 604800
A prefix carries no host bits, and is not link-local prefix 2001:db8:1::1/64, prefix fe80::/64

The third row is the common trap. preferred-lifetime defaults to 604800, so lowering only valid-lifetime below that number is rejected. Set both leaves together.

See Interface Management for the send loop, the counters, and the RFC references.

Storage SMART Management

Ze can monitor disk health via direct ATA/NVMe ioctls (no smartctl binary needed).

storage {
    smart {
        enabled true;
        check-interval 1800;
        temperature {
            informational 45;
            critical 55;
            difference 4;
        }
        self-test {
            short {
                interval 24h;
                time 02:00;
            }
            long {
                interval 7d;
                time 03:00;
                day sunday;
            }
        }
    }
}
Leaf Type Default Description
enabled boolean false Enable SMART monitoring
check-interval uint32 (60-86400) 1800 Health poll interval in seconds
temperature/informational uint8 (1-100) 45 Informational warning threshold (Celsius)
temperature/critical uint8 (1-100) 55 Critical error threshold (Celsius)
temperature/difference uint8 (1-50) 4 Rate-of-change warning threshold (Celsius per interval)
self-test/short/interval duration 24h Short self-test interval
self-test/short/time HH:MM 02:00 Preferred time of day for short tests
self-test/long/interval duration 7d Extended self-test interval
self-test/long/time HH:MM 03:00 Preferred time of day for extended tests
self-test/long/day weekday sunday Preferred day for extended tests

Temperature alerts are emitted to the report bus: temp-high (above informational), temp-rising (rate of change exceeds difference), temp-critical (above critical), smart-failing (SMART health status failed). Live status via show storage smart.

Authentication Users

Local operator users are declared under system.authentication.user. The list key is the username; the base user fields are a one-way bcrypt-hashed password and optional authorization profile names. Builds with SSH add the optional public-keys list to each user.

At boot, the daemon merges configured users over the zefs bootstrap users and publishes them with their authorization policy as one accepted generation. SSH, web, REST, and gRPC read that live generation. Reload publishes a new generation only after the candidate succeeds.

system {
    authentication {
        user alice {
            plaintext-password "secret"     # write-only; hashed at commit and at load
            profile [ admin ]
        }
        user bob {
            password "$2a$10$..."           # canonical form, paste from `ze passwd`
            profile [ read-only ]
        }
    }
}
Leaf Stored on disk Notes
password bcrypt hash ($2a$10$...) Canonical, displayed in show config. Hand-editing a literal plaintext here triggers a ze config validate warning.
plaintext-password only in a file you wrote Junos-style write-only input. Ze bcrypt-hashes the value into password and removes this leaf. Ze never writes this leaf itself.

Both leaves are inputs and only password reaches the running tree. Ze applies the same transform on two paths:

Path What happens to the file
Editor commit, ze config set, ze config import The file Ze writes carries the hash. It never carries the plaintext
A config file loaded at daemon start or at SIGHUP Ze hashes the leaf in memory and leaves your file as you wrote it. It warns once, naming the file, because the plaintext is still in it

For end-to-end usage (login, hashing, multi-user setup) see authentication.md.

Remote AAA backends authenticate operators against a central server, with local users as the fallback: system.authentication.tacacs (RFC 8907, see tacacs.md) and system.authentication.radius (RFC 2865 operator login, see radius.md). Both store their shared secret as a ze:sensitive leaf and fall through to local bcrypt when the server is unreachable.

Authorization and web/API effects

Authorization profiles apply to command dispatch and web config mutation. The web config editor checks the user's profile before set, add, delete, rename, commit, or discard. Read-only users can still load /show/ pages. Terminal-mode web config commands use the same checks before they mutate a draft.

REST and gRPC without a token or per-user authenticator use the api identity as read-only. Reads such as show version and show bgp summary work; write commands and config sessions return 403. Configure environment.api-server.token or per-user credentials when API clients need write access.

Config commits, discards, daemon reloads, and failed logins are recorded in the local audit log. See audit.md for storage and query details.

TACACS+ AAA

system.authentication.tacacs adds RFC 8907 TACACS+ as the first backend in the AAA chain; local bcrypt remains the fallback. Servers are tried in declaration order on connection failure. Explicit FAIL replies stop the chain (no fallthrough to local). See tacacs.md for the full flow and operational notes.

system {
    authentication {
        tacacs {
            server 10.0.0.1 { port 49; key "$9$encrypted-key"; }
            server 10.0.0.2 { port 49; key "$9$encrypted-key"; }
            timeout 5
            accounting true
        }
        tacacs-profile 15 { profile [ admin ]; }
        tacacs-profile 1  { profile [ read-only ]; }
    }
}
Leaf Type Default Notes
tacacs.server <ip> list, ordered-by-user - Failover order
tacacs.server <ip>.port uint16 49 TCP
tacacs.server <ip>.key string (ze:sensitive) required Stored as $9$ ciphertext
tacacs.timeout uint16 (1-300) 5 Per-server connect timeout (s)
tacacs.source-address ip-address none Local source IP for outbound TCP
tacacs.authorization boolean false Per-command TACACS+ authorization
tacacs.accounting boolean false START/STOP records on every CLI command
tacacs-profile <N>.profile leaf-list required Map priv-lvl 0-15 to local authz profile name(s)

Sysctl Configuration

Kernel tunables are managed by the sysctl plugin via a generic key/value list:

sysctl {
    setting net.ipv4.conf.all.forwarding {
        value 1
    }
    setting net.core.somaxconn {
        value 4096
    }
}

Keys use kernel-native names (e.g., the /proc/sys/ path with / replaced by . on Linux, or MIB names on Darwin). Known keys get type and range validation on commit; unknown keys are validated by attempting the write.

The sysctl plugin uses three-layer precedence: config values (above) are authoritative and override both transient values (set sysctl from CLI) and plugin defaults (e.g., fib-kernel declaring forwarding=1). See the command reference for CLI usage.

Sysctl Profiles

Named profiles group co-dependent kernel tunables applied per interface unit. Five built-in profiles cover common network operator use cases:

Profile Purpose Keys set
dsr Direct Server Return ARP tuning arp_announce=2, arp_ignore=1
router Enable IPv4/IPv6 forwarding forwarding=1 (both)
hardened Anti-spoofing rp_filter=1, log_martians=1, arp_filter=1
multihomed Prevent ARP flux arp_filter=1
proxy Proxy ARP proxy_arp=1, arp_accept=1

Apply profiles to an interface unit:

interface {
    ethernet eth0 {
        unit default {
            sysctl-profile [ dsr hardened ]
        }
    }
}

Profiles emit as defaults: explicit sysctl { setting ... } config overrides them. Multiple profiles per unit are composable; last wins on key overlap.

User-defined profiles are declared in the sysctl config block:

sysctl {
    profile my-edge {
        setting net.ipv4.conf.<iface>.forwarding {
            value 1
        }
        setting net.ipv4.conf.<iface>.rp_filter {
            value 2
        }
    }
}

The <iface> placeholder is substituted with the actual interface name at apply time.

Connection Tracking (Conntrack)

Declarative connection tracking management under system { conntrack {} }. Covers helper module loading, table sizing, protocol timeouts, and TCP behavior flags:

system {
    conntrack {
        module ftp
        module sip
        module tftp

        table-size 262144
        hash-size 65536
        expect-max 1024

        accounting
        timestamp
        log-invalid tcp

        timeout {
            generic 600
            tcp {
                established 432000
                close-wait 60
            }
            udp {
                timeout 30
                stream 120
            }
        }

        tcp {
            be-liberal false
            loose true
            max-retrans 3
        }
    }
}

Helper modules: Load kernel conntrack ALG helpers via modprobe. Valid modules: ftp, h323, sip, pptp, tftp, sane, irc, amanda, netbios-ns, snmp. Modules are load-only (removing from config stops loading on next boot but does not unload at runtime). On gokrazy, modules are built-in and loading is skipped.

Dual-setting prevention: Sysctl keys managed by conntrack (e.g., nf_conntrack_max) are rejected if also set in sysctl { setting { ... } }. The error message names the friendly conntrack config option to use instead.

Monitoring: Use show system conntrack to view current entry count, configured max, loaded modules, per-protocol timeouts, and TCP behavior flags.

Flow Export

Export interface counters and per-flow records to external collectors over UDP via sFlow v5, NetFlow v9 (RFC 3954), or IPFIX (RFC 7011). Configured under a single flow-export { } section. The component loads only when this section is present. See the Flow Export guide for the full reference.

flow-export {
    collector edge-sflow {
        address 192.0.2.10;
        port 6343;             // default 6343
        protocol sflow;        // sflow | netflow9 | ipfix
        polling-interval 20;   // counter polling seconds, default 20
        agent-address 198.51.100.1;
        source-address 198.51.100.1;  // optional: bind UDP datagrams to this local IP
    }
    collector ipfix {
        address 192.0.2.21;
        port 4739;
        protocol ipfix;
        template-refresh 600;  // NetFlow v9 / IPFIX, default 600
        observation-domain 1;
    }
    sampling {
        interface eth0 {
            rate 1024;         // 1-in-N packets
            trunc-size 128;    // header bytes, default 128
            group 1;           // psample group, default 1
        }
    }
    conntrack {
        enabled true;
        active-timeout 60;     // seconds between table dumps, default 60
    }
    enrichment {
        bgp true;              // attach BGP next-hop to flow records
    }
}

Operational: show flow export [<collector>] reports per-collector datagrams-sent, bytes-sent, errors, sequence, and last-export-time. Prometheus metrics use the ze_flowexport_* prefix.

Note: these protocols are unencrypted UDP. Run flow export over a dedicated management VLAN. Per-flow records are IPv4-only; sampling requires Linux with CAP_NET_ADMIN and the kernel psample module.

Traffic Usage

Account per-(port, protocol) and, optionally, per-IP byte totals on selected interfaces using eBPF programs attached via TCX. Configured under a single traffic { usage { } } section. The plugin loads only when this section is present and is Linux only (no-op elsewhere). See the Traffic Usage guide for the full reference.

traffic {
    usage {
        enabled true
        interfaces {             // keyed list, like ospf/isis; names are ze
            interface eth0 {     // interface names, resolved to the OS device
            }
            interface wan {
                track-ip false   // per-interface override of the global default
                max-entries 65536 // ... and a larger top-talker map for the WAN
            }
        }
        interval 1000            // map poll interval ms (global), default 1000, 100..3600000
        track-ip true            // global default, inherited per interface
        max-entries 10240        // global default per-map LRU capacity
    }
}

stale-timeout (default 300000 ms; 0 disables) removes metric series unseen for that long, bounding /metrics cardinality. track-ip, stale-timeout, and max-entries are global defaults; any interface may override them under its own interface { } block, inheriting the global value when unset. See the Traffic Usage guide for the full reference.

Operational: show traffic usage [name <interface>] reports per-interface ingress/egress port totals (and per-IP totals when track-ip is on) plus map fill levels. Prometheus metrics use the ze_traffic_usage_* prefix.

Note: accounting is IPv4 only and monitoring only (never drops or modifies traffic). TCX requires Linux >= 6.6 with CAP_BPF and CAP_NET_ADMIN; a ze doctor check (doctor-traffic-usage-ebpf) warns when enabled but eBPF/TCX is unavailable.

Environment Block

Global settings outside BGP:

environment {
    tcp {
        attempts 3;        # connection retry attempts
    }
    log {
        level warn;        # default log level
        bgp.routes debug;  # per-subsystem override
    }
    cli {
        format {
            default text;  # default output format: text, table, json, yaml, ndjson
        }
        transcript enabled;  # record session commands and output to local file
    }
}

The cli { format { default } } leaf controls the output format when no explicit pipe operator is specified. The default is text. Override per-session with set cli format <value> in operational mode; explicit pipe operators always win.

The cli { transcript } leaf enables local transcript recording. When set to enabled, ze cli and ze config edit sessions write all commands and their output to $XDG_DATA_HOME/ze/transcripts/ (defaults to ~/.local/share/ze/transcripts/). Transcripts include a header with timestamp, username, and remote host. Transcript writes are best-effort and never block CLI operation. Default is disabled.

TLS Certificates From the PKI Store

Three TLS listeners can serve a certificate held in the pki {} store instead of a self-signed one: the web/API HTTPS listener, and the DoT/DoH listeners of the as112 and geodns services. Each has a certificate leaf that names a pki { certificate <name> } entry.

pki {
    ca corp-ca {
        certificate <base64-DER>;
    }
    certificate lan {
        certificate <base64-DER>;
        intermediate <base64-DER>;
        private {
            key <base64-DER>;
        }
    }
}

environment {
    web {
        enabled true;
        certificate lan;
    }
}

service {
    as112 {
        enabled true;
        tls {
            enabled true;
            certificate lan;
        }
    }
}

The listener sends the leaf certificate and every intermediate the store entry holds, so a client can build the chain to the trust anchor. A store entry with no intermediate serves the leaf alone.

Rule Detail
Default
Fail closed A certificate that names no store entry, or names one with no private { key }, stops the listener. ze never falls back to a self-signed certificate for a name the operator configured. Web: the daemon refuses to start, and a reload naming a bad certificate is rejected. DoT/DoH: the secure listeners do not start, the error is logged, and cleartext DNS is unaffected.
Mutually exclusive In a tls {} container, certificate and cert-file/key-file are two sources of the same material. Setting both is rejected at commit.
Name 1 to 255 characters, A-Z a-z 0-9 . _ -. It is a store key, never a file path.
Rotation Changing the referenced certificate's material and reloading rotates it live. The web listener serves the new chain from the next handshake without rebinding, so open SSE streams survive. DoT/DoH rebind, because their listener signature folds in the certificate fingerprint.
One commit A single commit can add a certificate AND reference it. The reload installs the store before any consumer applies its config.
Env override ze.web.certificate sets the web certificate and takes precedence over the config file.
Pre-flight ze doctor reports a reference that is missing, keyless, expired, or whose intermediate does not reach a configured CA, as doctor-tls-reference or doctor-tls-expired. Run it before deploying.

An external geodns plugin process cannot read the in-process store: a certificate reference there fails loudly and the secure listeners stay down.

Named Listeners

Every service that accepts inbound connections models its listen endpoints as a named YANG list:

environment {
    web {
        enabled true;
        server primary {
            ip 0.0.0.0;
            port 3443;
        }
        server admin {
            ip 127.0.0.1;
            port 13443;
        }
    }
}

Each server <name> { ... } block becomes a bound TCP listener on the same service. The same pattern applies to environment.ssh, environment.mcp, environment.looking-glass, telemetry.prometheus, environment.api-server.rest, environment.api-server.grpc, and plugin.hub.

Binder lifetime rules are uniform across every service:

Rule Detail
Minimum At least one entry is required when a service is enabled. An enabled block with no server {} uses the YANG refine default (e.g. for web). 0.0.0.0:3443
Bind order Entries are bound in config declaration order for live trees; in map[string]any roundtrips (after ToMap()) the ordering falls back to alphabetical key order.
Failure mode Bind is all-or-nothing: if any listener fails to bind, the already-bound listeners are closed and the service fails to start. Partial binding is never accepted.
Shutdown Every listener closes when the service is stopped; there is no "primary plus extras" asymmetry.
Insecure web insecure true forces every entry's ip to 127.0.0.1 with a warning logged per rewrite.
MCP MCP is localhost-only. Non-loopback entries are rewritten to 127.0.0.1 with a warning.
Prometheus The implicit telemetry listener defaults to. Configure server <name> { ip 0.0.0.0. } explicitly when a remote scraper must connect. 127.0.0.1:9273

Port Conflict Detection

ze config validate runs CollectListeners over every enabled service and rejects the config if two listeners would bind overlapping ip:port pairs. Wildcard addresses (0.0.0.0, ::) conflict with any address in the same family; cross-family entries (0.0.0.0 vs ::1) never conflict. The check covers web, ssh, mcp, looking-glass, prometheus, plugin.hub, api-server.rest, and api-server.grpc.

Environment Variable Override

Each service exposes a ze.<svc>.listen variable that accepts one or more comma-separated ip:port pairs. IPv6 entries use bracket notation:

Variable Example Notes
ze.web.listen 0.0.0.0:3443,[::1]:3443 Overrides web server list entirely
ze.looking-glass.listen 0.0.0.0:8443 Overrides LG server list
ze.mcp.listen 127.0.0.1:8080,127.0.0.1:18080 MCP enforces 127.0.0.1 on every entry
ze.api-server.rest.listen 0.0.0.0:8081 Overrides REST server list
ze.api-server.grpc.listen 0.0.0.0:50051 Overrides gRPC server list

Env vars replace the config-file list when set; partial merging is not supported so the precedence is predictable. Precedence per service is: env var > CLI flag > config file > YANG default.

DNS Name Servers

Configure static DNS name servers and resolver tuning under system {}:

system {
    name-server [8.8.8.8 1.1.1.1]
    dns {
        resolv-conf-path /tmp/resolv.conf
        timeout 5
        cache-size 10000
        cache-ttl 86400
    }
}
Setting Type Default Description
name-server leaf-list ip-address (none) Static DNS servers. First server used by ze internal resolver. All written to resolv.conf at startup and after every config commit/reload.
resolv-conf-path string /tmp/resolv.conf Path for resolv.conf output. Default suits gokrazy (read-only rootfs). Empty disables.
timeout uint16 5 Query timeout in seconds. Range: 1-60.
cache-size uint32 10000 Maximum cached entries. 0 disables caching entirely.
cache-ttl uint32 86400 Maximum cache entry TTL in seconds. 0 means use only the response TTL.

The cache respects DNS response TTLs: entries expire at min(response TTL, cache-ttl). Records with TTL=0 from the server are not cached (per RFC 1035).

When static name servers are configured, DHCP-discovered DNS servers do not overwrite resolv.conf. When no static servers are configured, DHCP writes DNS to resolv-conf-path as before.

The internal resolver never falls back to a public recursive resolver. If no static name server exists and the configured resolv.conf path is missing or empty, DNS queries fail with no DNS server configured until DHCP or config provides a server.

Firmware Update Check and Self-Update

Configure periodic version checks and optional automated self-update under system { update-check {} }:

Check-only (default, existing behavior):

system {
    update-check {
        url https://update.example.com/version.json
    }
}

Daily check, report only. No download, no staging, no restart.

Fully automated (fleet):

system {
    update-check {
        url https://update.example.com/version.json
        interval 3600
        auto-apply true
        spread 1800
        maintenance-window {
            start 02:00
            end 06:00
        }
        restart {
            time 03:00
        }
    }
}

Check every hour, auto-download with up to 30 minutes random spread, replace binary only between 2 AM and 6 AM, restart at 3 AM.

Immediate restart (lab/single device):

system {
    update-check {
        url https://update.example.com/version.json
        interval 300
        auto-apply true
        spread 0
        restart {
            immediate
        }
    }
}

Check every 5 minutes, no spread, no maintenance window, restart as soon as staged.

Setting Type Default Description
url string (none) HTTPS URL serving version manifest JSON. HTTP allowed only for localhost.
interval uint32 86400 Check interval in seconds. Range: 60-604800.
auto-apply boolean false When true, automatically download, verify, and stage updates. When false, only check and report.
spread uint32 3600 Maximum random delay in seconds before downloading after a new version is detected. Prevents thundering herd. 0 disables. Range: 0-86400.
maintenance-window start string (none) Start time HH:MM in local time. Binary replacement only occurs during the window. Download and verification proceed at any time.
maintenance-window end string (none) End time HH:MM in local time. Windows crossing midnight are valid (e.g., 22:00-06:00).
restart immediate empty (none) Restart automatically after staging (5s drain delay). Mutually exclusive with restart time.
restart time string (none) Daily restart time HH:MM in local time. Mutually exclusive with restart immediate.

When auto-apply is false (default), ze only checks for new versions and reports via show system update and show warnings. No binary is downloaded.

When auto-apply is true, the server manifest must include a sha256 field. Ze refuses to auto-download binaries without a checksum. Manual commands (update system firmware apply) warn but proceed without verification.

See the Self-Update Guide for server setup and fleet deployment details.

NTP Client

Configure NTP under environment { ntp { } }:

environment {
    ntp {
        enabled true
        interval 300
        max-step 3600
        server pool0 {
            address 0.pool.ntp.org
        }
    }
}
Setting Type Default Description
enabled boolean false Enable NTP time synchronization.
interval uint32 3600 Sync interval in seconds. Range: 60-86400.
max-step uint32 3600 Maximum accepted NTP clock step in seconds. 0 explicitly allows unlimited steps.
persist-path string /perm/ze/timefile Path used to persist recovered time across restarts.
server <name>.address string (none) NTP server hostname or IP address. Configured servers take priority over DHCP option 42 servers.

NTP responses are validated and timestamps outside years 2020-2100 are rejected. max-step is checked before settimeofday; responses whose clock offset exceeds the cap are rejected and logged.

DHCP Server

Ze includes a built-in DHCP server plugin (RFC 2131/2132) for CPE deployments.

service {
    dhcp-server {
        enabled true;
        listen-interface br0;
        shared-network LAN {
            subnet 192.168.1.0/24 {
                range pool1 {
                    start 192.168.1.100;
                    stop  192.168.1.150;
                }
                range pool2 {
                    start 192.168.1.200;
                    stop  192.168.1.250;
                }
                lease-time 3600;
                default-router 192.168.1.1;
                dns-server 8.8.8.8;
                dns-server 8.8.4.4;
                domain-name home.lan;
                static-mapping printer {
                    mac-address aa:bb:cc:dd:ee:ff;
                    ip-address 192.168.1.10;
                }
            }
        }
    }
}
Setting Type Default Description
enabled boolean false Enable DHCP server.
listen-interface leaf-list (none) Interfaces to serve DHCP on.
shared-network <name> list (none) Named grouping of subnets.
subnet <prefix> list (none) Subnet with address pool and options.
range <name> list (none) Named dynamic address pool. Multiple ranges per subnet for disjoint pools.
range <name>.start string (none) First allocatable address.
range <name>.stop string (none) Last allocatable address.
lease-time uint32 86400 Lease duration in seconds (60-604800).
default-router string (none) Default gateway (option 3).
dns-server leaf-list (none) DNS servers (option 6).
domain-name string (none) Domain name for clients (option 15).
static-mapping <name> list (none) Static MAC-to-IP binding (excluded from dynamic allocation).

Multiple named ranges allow disjoint address pools within a single subnet. Ranges must not overlap and are allocated in order (first range fills before the second is used). Up to 10 ranges per subnet are supported.

PXE Boot Support

The DHCP server supports PXE boot provisioning (RFC 4578). When enabled, the server detects PXE clients via option 60 (PXEClient: prefix), reads client architecture from option 93, and responds with the appropriate bootfile.

service {
    dhcp-server {
        enabled true;
        listen-interface eth0;
        pxe {
            enabled true;
            tftp-server 192.168.1.1;
            bootfile-bios ipxe.pxe;
            bootfile-uefi ipxe.efi;
        }
        shared-network install {
            subnet 192.168.1.0/24 {
                range pool1 {
                    start 192.168.1.100;
                    stop 192.168.1.200;
                }
                default-router 192.168.1.1;
            }
        }
    }
}
Setting Type Default Description
pxe.enabled boolean false Enable PXE boot option injection.
pxe.tftp-server string (none) TFTP server IP for PXE boot (option 66, siaddr). Required when enabled.
pxe.bootfile-bios string (none) Boot file path for BIOS clients (option 67). Required when enabled.
pxe.bootfile-uefi string (none) Boot file path for UEFI clients (option 67). Required when enabled.

Non-PXE clients receive standard DHCP responses regardless of PXE configuration. When a PXE client has no option 93 (architecture), the server defaults to the BIOS bootfile.

Reactor Settings

Configure reactor behavior under environment { reactor { } }:

environment {
    reactor {
        update-groups true;           # cross-peer UPDATE grouping (default: true)
        forward-queue-size 256;       # per-destination forward channel capacity
        forward-batch-limit 1024;     # max items per drain batch (0=unlimited)
        forward-pool-max-bytes 0;     # buffer pool byte budget (0=unlimited/auto)
        forward-pool-headroom 0;      # extra bytes beyond auto-sized baseline
        forward-teardown-grace 5s;    # congestion teardown grace period
        read-buffer-size 65536;       # TCP read buffer (bytes)
        write-buffer-size 16384;      # TCP write buffer (bytes)
    }
}
Setting Type Default Description
update-groups boolean true Enable cross-peer UPDATE grouping. When enabled, peers with identical encoding contexts share a single UPDATE build.
forward-queue-size uint32 256 Per-destination forward channel capacity (range: 1-1000000).
forward-batch-limit uint32 1024 Max items per drain batch, bounds write lock hold time (range: 0-1000000, 0=unlimited).
forward-pool-max-bytes uint32 0 Combined byte budget for 4K+64K buffer pools (0=unlimited, auto-sized from peer prefix maximums).
forward-pool-headroom uint32 0 Extra bytes beyond auto-sized pool baseline. Ignored when forward-pool-max-bytes is set.
forward-teardown-grace string 5s Grace period before forced teardown on congestion (duration, e.g. 5s, 1m).
read-buffer-size uint32 65536 Per-session TCP read buffer size in bytes (range: 4096-16777216).
write-buffer-size uint32 16384 Per-session TCP write buffer size in bytes (range: 4096-16777216).

When update-groups is true (the default), the reactor groups established peers by their outbound encoding context (ContextID + policy). UPDATEs are built once per group and the wire bytes are fanned out to all group members. This reduces CPU usage proportionally to group size -- for a route server with 100 peers sharing the same capabilities, UPDATE building work is reduced by approximately 100x.

Disable update groups when:

When disabled (or when all peers have unique encoding contexts), behavior is identical to per-peer building with negligible overhead.

Environment Variable Override

Variable Type Description
ze.bgp.reactor.update-groups bool Cross-peer UPDATE grouping (default: true)

L2TP Tunnels

L2TPv2 (RFC 2661) tunnel support uses two config blocks. Protocol settings live under the root-level l2tp {} block. Listener endpoints follow the standard named-server pattern under environment { l2tp { } }.

l2tp {
    enabled true;
    shared-secret "my-tunnel-secret";
    auth-method chap-md5;
    allow-no-auth false;
    hello-interval 60;
    max-tunnels 1000;
    max-sessions 100;
}

environment {
    l2tp {
        server main {
            ip 0.0.0.0;
            port 1701;
        }
    }
}

Protocol settings (root l2tp {}):

Setting Type Default Description
enabled boolean true Presence of l2tp {} implies enabled. Use enabled false to disable. Use enabled true as a filler when no other settings are needed.
shared-secret string (unset) CHAP-MD5 challenge/response secret (RFC 2661 S4.2). If unset and a peer sends a Challenge AVP, the tunnel is rejected with StopCCN Result Code 4.
auth-method enum chap-md5 PPP Auth-Protocol first advertised in LCP. Values: chap-md5, ms-chap-v2, pap, none. none requires allow-no-auth true.
allow-no-auth boolean false Allows PPP LCP to proceed without an Auth-Protocol after explicit operator opt-in. Default false disconnects peers that reject all configured authentication methods.
hello-interval uint16 (unset) Seconds of peer silence before sending HELLO (1-3600). RFC 2661 recommends 60.
max-tunnels uint16 1024 Maximum concurrent tunnels. 0 explicitly requests unbounded admission by this knob. New SCCRQs beyond a positive limit receive StopCCN Result Code 2.
max-sessions uint16 1024 Maximum concurrent sessions per tunnel. 0 explicitly requests unbounded admission on each tunnel. New ICRQs/OCRQs beyond a positive limit receive CDN Result Code 4 (no resources).

Listener endpoints (environment { l2tp { } }):

Setting Type Default Description
server <name> list 0.0.0.0:1701 Named UDP listen endpoints, same pattern as other services.

PPP authentication (env var, YANG pending in spec-l2tp-7-subsystem):

Variable Type Default Description
ze.l2tp.auth.timeout duration 30s Upper bound on the PPP authentication phase per session. If no auth handler responds within this window, the session fails closed (LCP Terminate + session down). Malformed values fall back to 30s with a WARN log.

PPPoE Access

PPPoE (RFC 2516) access concentrator configuration. PPPoE uses raw Ethernet sockets on configured access interfaces, with no UDP listener needed. Sessions use the same PPP Driver, auth, pool, and shaper plugins as L2TP.

pppoe {
    enabled true;
    ac-name "ze";
    service-name "internet";
    auth-method chap-md5;
    cookie-timeout 5;
    max-sessions 65535;
    padi-rate-limit 100;
    interface eth0 {
    }
    interface eth0.100 {
        service-name "vlan100-service";
        max-sessions 1000;
    }
}
Setting Type Default Description
enabled boolean true Presence of pppoe {} implies enabled. Use enabled false to disable.
ac-name string ze Access Concentrator Name advertised in PADO (RFC 2516 S5.2).
service-name leaf-list (empty) Accepted Service-Name values. Empty list means accept any Service-Name.
auth-method enumeration chap-md5 PPP Auth-Protocol the AC advertises in its own LCP Configure-Request: none, pap, chap-md5, ms-chap-v2. The credential is verified by l2tp-auth-local or l2tp-auth-radius.
allow-no-auth boolean false Accept a subscriber whose LCP ends with no Auth-Protocol. Required beside auth-method none, which is otherwise refused at startup.
cookie-timeout uint16 5 AC-Cookie validity in seconds (1-300). Older cookies rejected in PADR.
max-sessions uint16 65535 Maximum concurrent PPPoE sessions per interface.
padi-rate-limit uint16 100 Maximum PADI packets per second per source MAC (1-10000).
interface <name> list (none) Access interfaces for PPPoE discovery. Each gets independent SID space.
interface / service-name leaf-list (global) Per-interface Service-Name filter, overrides global list.
interface / max-sessions uint16 (global) Per-interface session limit, defaults to global max-sessions.

L2TP Address Pool

The l2tp-pool plugin provides IPv4 address allocation for PPP sessions. A default pool and optional named pools are configured under l2tp { pool { } }. Named pools are selected per-session via the RADIUS Framed-Pool attribute; when Framed-IP-Address is present in Access-Accept, the pool is bypassed and the RADIUS-assigned address is used directly.

l2tp {
    pool {
        ipv4 {
            gateway 10.255.0.1
            start 10.255.0.10
            end 10.255.0.254
            dns-primary 8.8.8.8
            dns-secondary 8.8.4.4
        }
        named-pool gold {
            gateway 10.255.1.1
            start 10.255.1.10
            end 10.255.1.254
            dns-primary 1.1.1.1
            dns-secondary 1.0.0.1
        }
    }
}
Setting Type Description
gateway IPv4 address NAS-side IP sent to the subscriber as the PPP peer address
start IPv4 address First allocatable address in the pool range
end IPv4 address Last allocatable address in the pool range
dns-primary IPv4 address Primary DNS server sent via IPCP
dns-secondary IPv4 address Secondary DNS server sent via IPCP

Named pools use the same settings nested under named-pool <name> { }. The gateway must not overlap the pool range.

Hub Configuration

The plugin hub provides TLS transport for plugin communication and fleet management. Named server blocks declare listeners; hub-level client blocks declare outbound connections to a remote hub (managed mode).

plugin {
    hub {
        server local {
            ip 127.0.0.1;
            port 1790;
            secret "local-secret-minimum-32-characters";
        }
        server central {
            ip 0.0.0.0;
            port 1791;
            secret "central-secret-min-32-characters!";
            client edge-01 { secret "per-client-token-min-32-chars!!!"; }
        }
        client edge-01 {
            host 10.0.0.1;
            port 1791;
            secret "per-client-token-min-32-chars!!!";
            source-address 198.51.100.1;  // optional: bind outbound hub TLS to this local IP
        }
    }
}

Every ze instance has at least one server block (for local plugins and SSH). Secrets must be at least 32 characters. See Fleet Configuration for details.

Outbound Source Address

Operators who deploy Ze behind loopback or management interfaces can pin the local source address of a service's outbound connections. When source-address is set, the socket binds to that local IP before connecting; when it is omitted, the OS selects the source per its routing table (unchanged default behavior). The address is validated as an ip-address at config time, and must be assigned to a local interface or the connection fails with cannot assign requested address.

Service Config location Leaf Transport
BMP bgp bmp sender collector <name> source-address TCP
RPKI/RTR bgp rpki cache-server <addr> source-address TCP
Flow Export flow-export collector <name> source-address UDP
IRR (filter) bgp policy irr source-address TCP
Managed hub plugin hub client <name> source-address TLS
LDP ldp transport-address TCP

LDP reuses its existing transport-address leaf: RFC 5036 defines the transport address as the endpoint of the LDP TCP session, so Ze binds the outbound session to it (rather than adding a separate source-address). When transport-address is unset, the OS selects the source.

TACACS+ (system authentication tacacs source-address) and L2TP RADIUS already bind an outbound source; BGP peers bind their mandatory per-peer local ip. These predate this feature and keep their existing leaf names.

Validation

Validate a configuration file without starting ze:

ze config validate myconfig.conf

Unknown keys are rejected with a suggestion for the closest valid key.

Upgrading from a release that validated only on demand

The daemon now applies the same value rules at startup that ze config validate applies, so a config that started a daemon yesterday can stop one today. Read this before you upgrade.

A value a registered validator refuses now stops the daemon. Until this release the rules ran in ze config validate, in the API commit hook and in the web editor, and nowhere else. A file edited by hand and loaded with ze start or ze - reached the protocol encoders unchecked. It no longer does: ze exits with status 1, prints one line for each refused value, and binds nothing, so the daemon does not half-start. The line names the section, the leaf and the rule:

error: load config: config validation failed: isis: isis/hostname: "café.example" is not a valid IS-IS hostname for isis/hostname: octet 4 is 0xc3. RFC 5301 section 3 encodes the value in 7-bit ASCII, so each character must be a printable ASCII character from space (0x20) to tilde (0x7e)

The rules cover interface, sysctl, fib, plugin, telemetry, vpp, vpn, pki, l2tp, isis and ospf. A BGP value is unaffected here because BGP has always had its own deeper check at startup. A missing mandatory field stays a warning and does not stop the daemon.

A reload refuses the same way, and the daemon keeps running. Send SIGHUP with a config a validator refuses and the reload fails, the staged candidate is cleared, and the daemon keeps serving the config it already runs. The listeners do not rebind and open sessions are not disturbed. The refusal is on stderr:

reload error: reload: parse config: config validation failed: isis: isis/net: "49.0001.00" is not a valid IS-IS NET for isis/net: length 4 octets, want 8..20

There is no override and no force flag. Fix the value and send SIGHUP again.

Check before you upgrade. Run ze config validate over the config first. It runs the same walk over the same sections, so what it refuses is what the daemon will refuse. It reports every refusal in one run, one line each, and it starts nothing:

ze config validate /etc/ze/ze.conf

ze doctor loads the config the same way the daemon does, so a config a validator refuses now stops doctor at doctor-config-parse with that same message, and doctor's other checks do not run. That tells you the daemon will not start. It does not tell you what else is wrong with the machine, so run ze config validate first and ze doctor after the config is clean.

Healthcheck

Service healthcheck probes monitor availability and control BGP route announcement via watchdog groups:

bgp {
    healthcheck {
        probe dns {
            command "dig @127.0.0.1 example.com +short"
            group hc-dns
            interval 5
            rise 3
            fall 3
            withdraw-on-down false
            up-metric 100
            down-metric 1000
        }
    }
}

Each probe runs a shell command periodically. When the service is UP, routes in the watchdog group are announced with up-metric as MED. When DOWN, routes are either withdrawn (withdraw-on-down true) or re-announced with down-metric as MED (default). See Healthcheck Guide for full configuration reference.

Static Routes

Static routes are programmed directly to the kernel or VPP. The plugin auto-loads when the config contains a static { } section. Routes are grouped under named tables for policy-based routing support.

static {
    table default {
        route 0.0.0.0/0 {
            next {
                hop 10.0.0.1 { weight 3; }
                hop 10.0.0.2 { weight 1; }
            }
        }
        route 192.0.2.0/24 {
            blackhole { }
        }
    }
}

Named tables require a routing-table section mapping names to kernel table IDs. Interface-only next-hops (no gateway) are supported for point-to-point links. Multiple next-hops create an ECMP multipath route. Weight controls traffic distribution (higher = more). BFD failover removes a next-hop from the ECMP group on session DOWN and re-adds on UP.

See Static Routes Guide for named tables, interface-only next-hops, mixed ECMP, BFD failover, blackhole/reject, IPv6, and redistribute examples.

Policy Routing

Policy-based routing steers traffic to alternate routing tables or next-hops based on L3/L4 match criteria. Configured under policy { route <name> { ... } }.

See Policy Routing Guide for configuration syntax, match criteria, actions, reserved ranges, and CLI usage.

ExaBGP Migration

Ze auto-detects and migrates ExaBGP configuration files:

ze config migrate exabgp.conf > ze.conf