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 Optional semicolons (;) Both router-id 1.2.3.4 and router-id 1.2.3.4; 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)

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. Peer-level settings control enforcement behavior:

peer transit-a {
    prefix {
        teardown false
        idle-timeout 30
    }
    family {
        ipv4/unicast { prefix { maximum 1000000; } }
    }
}
Setting Default Description
teardown true Send NOTIFICATION and close on exceed. false = warn only, drop excess NLRIs.
idle-timeout 0 Seconds before reconnect after prefix teardown. 0 = no reconnect.

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

Plugins are bound to peers via process blocks. Each process block names a plugin and configures what events it receives.

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

Base event types for receive: update, open, notification, keepalive, refresh, state, negotiated, eor, rpki. Plugins may register additional types (e.g., update-rpki from bgp-rpki-decorator). Validated at config parse time against registered event types.

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 { set { ... } } } }. Only present leaves are applied; undeclared attributes pass through unchanged. For conditional modification, compose with match filters earlier in the chain.

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

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, chacha20poly1305, 3des. Hash algorithms: sha1, sha256, sha384, sha512. DH groups: 1-31 (14 = MODP-2048 recommended minimum). Authentication modes: pre-shared-secret (with $9$-encoded key) or x509 (PKI store references).

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

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.

For IPv4, ze installs the default route with the configured metric when a DHCP lease provides a gateway. For IPv6, ze suppresses the kernel's automatic RA default route (accept_ra_defrtr=0) and installs ::/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.

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. The default value is 0 (kernel default), which preserves existing behavior when not configured.

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.

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 SSH login users are declared under system.authentication.user. The list key is the username; each entry has a one-way bcrypt-hashed password and an optional list of authorization profile names.

system {
    authentication {
        user alice {
            plaintext-password "secret"     # write-only; hashed on commit
            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 never persisted Junos-style write-only input. The commit hook bcrypt-hashes the value into password and removes this leaf.

Both leaves are mutually exchangeable inputs; only password survives a commit. 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 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.

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";
    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.
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.

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