rustbgpd is configured via a single TOML file, passed as the first argument to the daemon:
rustbgpd /etc/rustbgpd/config.toml
The config file defines the initial boot state. At runtime, the gRPC API is the
source of truth -- peers can be added, removed, enabled, and disabled dynamically
without restarting the daemon. Neighbor add/delete mutations made via gRPC are
persisted back to the config file. Sending SIGHUP to the daemon triggers a
config reload with per-peer reconciliation. Starting with zero [[neighbors]] is valid when
all peers are managed via gRPC.
Required. Defines the local BGP speaker identity.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
asn |
u32 | yes | -- | Local autonomous system number |
router_id |
string | yes | -- | BGP router ID (must be valid IPv4) |
listen_port |
u16 | yes | -- | TCP port to listen on (typically 179) |
dynamic_neighbor_limit |
u32 | no | 100 |
Maximum number of auto-accepted dynamic peers (1--5000) |
runtime_state_dir |
string | no | "/var/lib/rustbgpd" |
Directory for daemon-owned runtime state (GR restart marker today) |
cluster_id |
string | no | -- | Route reflector cluster ID (must be valid IPv4; enables RR mode) |
[global]
asn = 65001
router_id = "10.0.0.1"
listen_port = 179
runtime_state_dir = "/var/lib/rustbgpd"runtime_state_dir must be writable by the rustbgpd process. In containers or
non-root deployments, override the default to a mounted writable path (for
example /var/lib/rustbgpd on a volume, or /data/rustbgpd).
dynamic_neighbor_limit caps the number of active peers auto-created from
[[dynamic_neighbors]] ranges. When omitted, rustbgpd allows up to 100 dynamic
peers at a time.
Required. Configures observability and management endpoints.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
prometheus_addr |
string | no | -- | host:port for Prometheus metrics (omit to disable) |
log_format |
string | yes | -- | Log output format ("json") |
prometheus_addr, when present, must be a valid ip:port socket address.
Optional birdwatcher-compatible HTTP server for looking glass frontends (Alice-LG, etc.).
| Field | Type | Required | Description |
|---|---|---|---|
addr |
string | yes | host:port for the looking glass server |
When configured, rustbgpd starts an HTTP server exposing birdwatcher-compatible
endpoints (/status, /protocols/bgp, /routes/protocol/{id},
/routes/peer/{peer}). Omit the section entirely to disable.
[global.telemetry.looking_glass]
addr = "0.0.0.0:8080"gRPC listeners are configured with optional subtables:
Preferred local-only gRPC transport. If neither grpc_uds nor grpc_tcp is
configured, rustbgpd enables this listener by default at
<runtime_state_dir>/grpc.sock.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled |
bool | no | true |
Enable this listener when the table is present |
path |
string | no | <runtime_state_dir>/grpc.sock |
Absolute Unix socket path |
mode |
u32 | no | 0o600 |
Filesystem mode applied to the socket after bind |
access_mode |
string | no | "read_write" |
Listener authorization mode: "read_write" or "read_only" |
token_file |
string | no | -- | Optional bearer token file for listener auth |
Optional TCP gRPC listener. Use this only when you need remote access or container/network exposure.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled |
bool | no | true |
Enable this listener when the table is present |
address |
string | yes* | -- | host:port bind address (required when enabled = true) |
access_mode |
string | no | "read_write" |
Listener authorization mode: "read_write" or "read_only" |
token_file |
string | no | -- | Optional bearer token file for listener auth |
If either listener subtable is present, at least one gRPC listener must remain
enabled after applying enabled = false.
access_mode = "read_only" permits query and watch RPCs but rejects mutating
RPCs such as neighbor add/delete, route injection, policy changes, peer-group
changes, shutdown, and MRT trigger requests with PERMISSION_DENIED. This is
intended for monitoring or dashboard listeners that should not expose control
plane writes.
Token file lifecycle: When token_file is configured, the file must exist
and contain a non-empty token at daemon startup. The token is read once during
config validation and kept in memory for the daemon's lifetime. Token rotation
requires a daemon restart. In orchestrated environments where secrets are
mounted after config files, ensure the token file is available before starting
the daemon.
[global.telemetry]
prometheus_addr = "0.0.0.0:9179"
log_format = "json"
[global.telemetry.grpc_uds]
path = "/var/lib/rustbgpd/grpc.sock"
mode = 0o660
access_mode = "read_write"
[global.telemetry.grpc_tcp]
address = "127.0.0.1:50051"
access_mode = "read_only"
# token_file = "/etc/rustbgpd/grpc.token"Optional, repeatable. Each entry defines one BGP peer. Omit entirely for a dynamic-only deployment where peers are added at runtime via gRPC.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | -- | Peer IP address (IPv4 or IPv6) |
remote_asn |
u32 | yes | -- | Peer's autonomous system number |
description |
string | no | -- | Human-readable label (used in logs; defaults to address if absent) |
peer_group |
string | no | -- | Named peer-group to inherit transport and policy defaults from |
hold_time |
u16 | no | 90 | BGP hold timer in seconds (0 or >= 3) |
max_prefixes |
u32 | no | -- | Maximum prefixes accepted before session teardown |
md5_password |
string | no | -- | TCP MD5 authentication password (RFC 2385, Linux only) |
ttl_security |
bool | no | false | Enable GTSM / TTL security (RFC 5082, Linux only) |
families |
[string] | no | (auto) | Address families to negotiate (see below) |
graceful_restart |
bool | no | true | Enable Graceful Restart receiving speaker (RFC 4724) |
gr_restart_time |
u16 | no | 120 | Restart time advertised in GR capability (seconds, 1--4095) |
gr_stale_routes_time |
u64 | no | 360 | Time to retain stale routes after peer reconnects (seconds, 1--3600) |
route_server_client |
bool | no | false | Transparent route-server mode for eBGP peers (see below) |
remove_private_as |
string | no | -- | Remove private ASNs from AS_PATH: "remove", "all", or "replace" (eBGP only) |
route_reflector_client |
bool | no | false | Mark this iBGP peer as a route reflector client (RFC 4456) |
local_ipv6_nexthop |
string | no | -- | Override IPv6 next-hop for eBGP exports (must be valid non-link-local IPv6) |
import_policy_chain |
[string] | no | -- | Named policy chain for import (mutually exclusive with inline import_policy) |
export_policy_chain |
[string] | no | -- | Named policy chain for export (mutually exclusive with inline export_policy) |
llgr_stale_time |
u32 | no | 0 | LLGR stale time in seconds (0 = disabled, max 16777215; RFC 9494) |
add_path |
table | no | -- | Add-Path (RFC 7911) config table (see below) |
log_level |
string | no | -- | Override log level for this peer: "error", "warn", "info", "debug", or "trace" |
The families field controls which AFI/SAFI combinations are negotiated with
the peer via MP-BGP capabilities. Supported values:
"ipv4_unicast"— IPv4 Unicast (AFI 1, SAFI 1)"ipv6_unicast"— IPv6 Unicast (AFI 2, SAFI 1)"ipv4_flowspec"— IPv4 FlowSpec (AFI 1, SAFI 133, RFC 8955)"ipv6_flowspec"— IPv6 FlowSpec (AFI 2, SAFI 133, RFC 8956)
Defaults: If families is omitted, the default depends on the neighbor
address type:
- IPv4 neighbor address →
["ipv4_unicast"] - IPv6 neighbor address →
["ipv4_unicast", "ipv6_unicast"]
Peer groups are reusable neighbor templates defined at the top level under
[peer_groups.<name>]. A neighbor can reference one with peer_group = "...".
Explicit neighbor settings win over peer-group settings. Peer-group definitions
can also be managed at runtime through the gRPC PeerGroupService; successful
mutations persist back to TOML.
[peer_groups.rs-clients]
hold_time = 90
families = ["ipv4_unicast", "ipv6_unicast"]
route_server_client = true
export_policy_chain = ["tag-ixp"]
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
peer_group = "rs-clients"
[[neighbors]]
address = "10.0.0.3"
remote_asn = 65003
peer_group = "rs-clients"
hold_time = 45 # neighbor override beats peer-group defaultPeer-group fields mirror inheritable neighbor settings: timers, families,
GR/LLGR, Add-Path, route-server / RR flags, private-AS handling, MD5/GTSM,
local_ipv6_nexthop, log_level, and import/export inline policy or named
chains.
# IPv4 peer with dual-stack
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
description = "upstream-provider"
hold_time = 90
max_prefixes = 10000
md5_password = "s3cret"
ttl_security = true
families = ["ipv4_unicast", "ipv6_unicast"]
# IPv6 peer (defaults to dual-stack)
[[neighbors]]
address = "fd00::2"
remote_asn = 65003
description = "ipv6-peer"Extended Next Hop (RFC 8950): When both "ipv4_unicast" and
"ipv6_unicast" are configured for a neighbor, rustbgpd automatically
advertises the Extended Next Hop capability. If negotiated, IPv4 unicast
routes may be exchanged via MP_REACH_NLRI / MP_UNREACH_NLRI using an
IPv6 next hop. For eBGP exports, local_ipv6_nexthop (if configured) is
used as the IPv6 self next-hop; otherwise the local IPv6 socket address is
used when available.
Optional, repeatable. Defines prefix ranges for auto-accepting inbound BGP connections. When an inbound TCP connection arrives from an address inside the configured prefix, rustbgpd creates an ephemeral peer using the referenced peer group.
Dynamic peers:
- inherit transport and policy defaults from the referenced peer group
- never initiate outbound TCP connections
- are not persisted back to the config file
- are removed automatically when the session returns to Idle
- count against
global.dynamic_neighbor_limit
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
prefix |
string | yes | -- | IPv4 or IPv6 prefix range in CIDR notation |
peer_group |
string | yes | -- | Peer group whose settings dynamic peers inherit |
remote_asn |
u32 | no | 0 |
Expected remote ASN. 0 means accept any ASN from the peer's OPEN |
description |
string | no | -- | Optional description applied to accepted dynamic peers |
[global]
asn = 65001
router_id = "10.0.0.1"
listen_port = 179
dynamic_neighbor_limit = 500
[global.telemetry]
prometheus_addr = "0.0.0.0:9179"
log_format = "json"
[peer_groups.ix-members]
hold_time = 90
families = ["ipv4_unicast", "ipv6_unicast"]
route_server_client = true
[[dynamic_neighbors]]
prefix = "10.0.0.0/24"
peer_group = "ix-members"
remote_asn = 0
description = "IXP auto-accept"
[[dynamic_neighbors]]
prefix = "2001:db8::/32"
peer_group = "ix-members"Validation rules:
peer_groupmust reference an existing[peer_groups.<name>]prefixmust be valid CIDR with a family-appropriate prefix length- static
[[neighbors]]cannot useremote_asn = 0; that sentinel is reserved for[[dynamic_neighbors]]
Operational note:
- disabling a dynamic peer keeps the peer entry in memory but prevents reconnect
- runtime gRPC CRUD for dynamic ranges is not implemented yet; TOML is the source of truth
Graceful Restart is enabled by default. rustbgpd implements:
- Helper mode (receiving speaker): when a peer with GR capability restarts, its routes are preserved as stale during the restart window instead of being immediately withdrawn. End-of-RIB markers from the peer clear stale flags per address family; if the timer expires before all End-of-RIB markers arrive, remaining stale routes are swept.
- Minimal restarting-speaker mode: after a coordinated daemon restart,
rustbgpd can temporarily advertise
restart_state = trueto static peers restored from config, using a marker file underruntime_state_dir. This helps peers retain our routes while we reconnect, butforwarding_preservedremains false because rustbgpd does not yet own or verify the FIB.
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
graceful_restart = true # default: true
gr_restart_time = 120 # seconds, advertised in GR capability (max 4095)
gr_stale_routes_time = 360 # seconds, how long to wait for EoR after reconnect (max 3600)To disable GR for a specific peer:
[[neighbors]]
address = "10.0.0.3"
remote_asn = 65003
graceful_restart = falseImplementation note: restarting-speaker mode is deliberately minimal and
honest. The daemon may advertise R=1 after a planned restart, but it does
not claim forwarding-state preservation (forwarding_preserved = false) and
does not persist route state across restarts.
See ADR-0024.
LLGR extends Graceful Restart with a second stale-timer phase. When the GR
timer expires, routes for LLGR-negotiated families are promoted to LLGR-stale
(with the LLGR_STALE well-known community added) instead of being purged.
Routes carrying NO_LLGR are purged at the GR-to-LLGR transition.
The effective LLGR stale time is min(local llgr_stale_time, peer's per-family minimum).
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
graceful_restart = true
llgr_stale_time = 3600 # seconds (0 = disabled, max 16777215)To disable LLGR for a specific peer, set llgr_stale_time = 0 (the default).
Best-path selection uses three-tier stale ranking: fresh > GR-stale > LLGR-stale, applied at step 0 (before LOCAL_PREF). LLGR-stale routes are least preferred but still participate in best-path selection until the LLGR timer expires.
See ADR-0024 for the two-phase timer design.
Add-Path allows accepting and advertising multiple paths per prefix.
Configure it per-neighbor with the [neighbors.add_path] table:
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
[neighbors.add_path]
receive = true # accept multiple paths per prefix from this peer
send = true # advertise multiple paths per prefix to this peer
send_max = 4 # limit to top 4 candidates (omit for unlimited)| Field | Type | Required | Default | Description |
|---|---|---|---|---|
receive |
bool | no | false | Accept multiple paths per prefix from peer |
send |
bool | no | false | Advertise multiple paths per prefix to peer |
send_max |
integer | no | — | Max paths per prefix (omit for unlimited) |
When receive is true, the Add-Path capability (code 69) is advertised in
OPEN with Receive mode. When send is true, Send mode is advertised.
If both are enabled, Both is advertised.
Multi-path send (route server mode): When send = true, the RIB
distributes multiple candidate paths per prefix to this peer, sorted by
best-path preference. Paths are assigned rank-based path IDs (best=1,
second=2, etc.). Split horizon, iBGP suppression, and per-candidate export
policy are evaluated for each path.
Both IPv4 and IPv6 unicast are supported. See ADR-0033.
For IX route-server clients, you can make eBGP export transparent by setting
route_server_client = true on the neighbor:
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
families = ["ipv4_unicast", "ipv6_unicast"]
route_server_client = trueWhen enabled:
- outbound unicast advertisements to that peer preserve the original next hop by default
- outbound unicast advertisements skip the automatic local-AS prepend normally applied on eBGP export
- outbound FlowSpec advertisements skip the automatic local-AS prepend
- explicit export-policy next-hop rewrites (
set_next_hop) still win for unicast LOCAL_PREFis still stripped, because the peer is still eBGP
This applies to:
- classic IPv4 unicast (
NEXT_HOP) - IPv4 unicast over IPv6 next hop (RFC 8950)
- IPv6 unicast (
MP_REACH_NLRI) - IPv4 and IPv6 FlowSpec export (
AS_PATHtransparency only; FlowSpec has no wire-levelNEXT_HOP)
route_server_client is only valid for eBGP neighbors. Config validation
rejects it on iBGP peers.
Strip private ASNs (64512–65534, 4200000000–4294967294) from AS_PATH before eBGP advertisement:
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
remove_private_as = "all"Three modes are available:
"remove"— remove private ASNs only if every ASN in the path is private (safe default)"all"— unconditionally remove all private ASNs from every segment; drop empty segments"replace"— replace each private ASN with the local ASN
remove_private_as is only valid for eBGP neighbors. Config validation
rejects it on iBGP peers. Route server client peers skip private AS
removal (they already skip AS_PATH manipulation).
See ADR-0045.
FlowSpec distributes traffic filtering rules via BGP. Enable it by adding
FlowSpec families to the families list:
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
families = ["ipv4_unicast", "ipv6_unicast", "ipv4_flowspec", "ipv6_flowspec"]FlowSpec rules have no next-hop (NH length = 0 in MP_REACH_NLRI). Traffic actions (rate-limit, redirect, DSCP mark) are encoded as extended communities per RFC 8955 section 7.
FlowSpec routes are injected and queried via the gRPC API:
InjectionService/AddFlowSpec— inject a FlowSpec rule with match components and actionsInjectionService/DeleteFlowSpec— withdraw a FlowSpec ruleRibService/ListFlowSpecRoutes— query the FlowSpec Loc-RIB
FlowSpec routes pass through the same policy engine as unicast routes: import/export policy, iBGP split-horizon, and route reflector rules all apply. See ADR-0035.
Each neighbor can carry its own import and export policy. These are
defined as nested arrays of tables within the [[neighbors]] entry.
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
[[neighbors.import_policy]]
prefix = "10.0.0.0/8"
ge = 24
le = 32
action = "deny"
[[neighbors.import_policy]]
prefix = "0.0.0.0/0"
le = 24
action = "permit"
set_local_pref = 200
[[neighbors.export_policy]]
prefix = "192.168.0.0/16"
action = "permit"
set_as_path_prepend = { asn = 65001, count = 2 }See the Policy entries section below for field details.
rustbgpd can act as a route reflector, relaxing the iBGP full-mesh requirement.
When cluster_id is set and at least one neighbor has route_reflector_client = true,
iBGP-learned routes from clients are reflected to all iBGP peers, while routes
from non-clients go to clients only.
[global]
asn = 65001
router_id = "10.0.0.1"
listen_port = 179
cluster_id = "10.0.0.1" # enables route reflector mode
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65001
route_reflector_client = true # this peer is a RR client
[[neighbors]]
address = "10.0.0.3"
remote_asn = 65001
# non-client -- receives reflected client routes onlySee ADR-0029 for reflection rules and ORIGINATOR_ID/CLUSTER_LIST handling.
Optional. Configures RPKI origin validation via a persistent RTR client (RFC 8210).
rustbgpd connects to one or more RPKI cache validators and uses their VRP
(Validated ROA Payload) data to classify routes as Valid, Invalid, or NotFound.
The RTR session stays connected after EndOfData, uses SerialNotify for
immediate refreshes when the cache sends them, falls back to periodic serial
polling at refresh_interval, and expires cached VRPs if no fresh EndOfData
arrives before the effective expiry timer.
You need a running RPKI validator that speaks RTR:
| Validator | Default RTR Port | Notes |
|---|---|---|
| Routinator | 3323 | Rust, recommended |
| rpki-client | 8282 | OpenBSD origin |
| FORT | 8323 | C, lightweight |
| OctoRPKI | 8282 | Go, Cloudflare |
[rpki]
[[rpki.cache_servers]]
address = "127.0.0.1:3323"For production, connect to 2+ caches. VRPs are merged (union) across all connected caches:
[rpki]
[[rpki.cache_servers]]
address = "rpki1.example.com:3323"
[[rpki.cache_servers]]
address = "rpki2.example.com:3323"| Field | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | -- | Cache server host:port |
refresh_interval |
u64 | no | 3600 | Seconds between Serial Queries |
retry_interval |
u64 | no | 600 | Seconds before reconnect on failure |
expire_interval |
u64 | no | 7200 | Seconds before discarding stale VRPs |
Every route receives a validation state based on RPKI data:
| State | Meaning | Best-path effect |
|---|---|---|
| Valid | Origin AS matches a VRP covering the prefix | Preferred |
| NotFound | No VRP covers the prefix | Neutral (default) |
| Invalid | VRP covers the prefix but origin AS doesn't match | Deprioritized |
Use match_rpki_validation in import or export policy statements to filter
routes by RPKI state. Import validation evaluates against the current VRP
snapshot at ingress time — see KNOWN_ISSUES.md for best-effort semantics.
Drop RPKI-invalid routes (recommended):
[[policy.export]]
match_rpki_validation = "invalid"
action = "deny"Prefer valid routes with higher LOCAL_PREF:
[[policy.export]]
match_rpki_validation = "valid"
action = "permit"
set_local_pref = 200
[[policy.export]]
match_rpki_validation = "not_found"
action = "permit"
set_local_pref = 100Prometheus metrics exposed at the configured metrics endpoint:
| Metric | Description |
|---|---|
bgp_rpki_vrp_count{af="ipv4|ipv6"} |
Current VRP entries by address family |
See ADR-0034 for design details.
Optional. Defines global import and export policy that applies to all neighbors that do not declare their own per-neighbor policy.
[[policy.import]]
prefix = "10.0.0.0/8"
ge = 8
le = 24
action = "permit"
set_local_pref = 150
[[policy.import]]
prefix = "0.0.0.0/0"
action = "deny"
[[policy.export]]
prefix = "172.16.0.0/12"
action = "deny"Named policies are reusable policy blocks defined under [policy.definitions].
Each has a name, optional default_action (default: "permit"), and a list of
statements. The same named definitions and chain attachments can also be
managed at runtime through the gRPC PolicyService; successful mutations are
persisted back to TOML.
[policy.definitions.reject-bogons]
default_action = "deny"
[[policy.definitions.reject-bogons.statements]]
action = "permit"
prefix = "0.0.0.0/0"
ge = 8
le = 24
[policy.definitions.set-lp-customer]
[[policy.definitions.set-lp-customer.statements]]
action = "permit"
set_local_pref = 150
[policy.definitions.tag-ixp]
[[policy.definitions.tag-ixp.statements]]
action = "permit"
set_community_add = ["LC:65001:1:100"]
set_next_hop = "self"| Field | Type | Required | Default | Description |
|---|---|---|---|---|
default_action |
string | no | "permit" |
Action when no statement matches ("permit" or "deny") |
statements |
array | no | [] |
Policy statements (same schema as inline entries) |
Neighbor sets are reusable peer identity groups for policy matching. They live
under [policy.neighbor_sets.<name>] and can match by exact neighbor address,
remote ASN, and/or peer-group name. A policy statement references one with
match_neighbor_set = "...". Neighbor sets are also manageable at runtime via
the gRPC PolicyService.
[policy.neighbor_sets.ixp-clients]
addresses = ["10.0.0.2", "10.0.0.3"]
remote_asns = [65002, 65003]
peer_groups = ["rs-clients"]Policy chains reference named definitions by name, evaluated in order with GoBGP-style semantics:
- Permit — accumulate route modifications, continue to next policy
- Deny — reject immediately, stop the chain
- After all policies — implicit permit with all accumulated modifications
Global chains:
[policy]
import_chain = ["reject-bogons", "set-lp-customer"]
export_chain = ["tag-ixp"]Per-neighbor chains (override global):
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
import_policy_chain = ["reject-bogons", "set-lp-customer"]
export_policy_chain = ["tag-ixp"]When multiple policies in a chain both set a scalar value (e.g. set_local_pref),
the later policy wins. List values (community add/remove) accumulate across the
chain.
Mutual exclusion: Inline policy and policy chain cannot both be set for the same direction on the same neighbor. This is a config validation error.
Both global ([[policy.import]] / [[policy.export]]) and per-neighbor
([[neighbors.import_policy]] / [[neighbors.export_policy]]) entries share
the same schema.
Each entry must have at least one match condition. Multiple conditions on the same entry are ANDed.
| Field | Type | Required | Description |
|---|---|---|---|
prefix |
string | no* | Network prefix in CIDR notation (IPv4 or IPv6) |
ge |
u8 | no | Minimum prefix length to match (inclusive) |
le |
u8 | no | Maximum prefix length to match (inclusive) |
match_community |
[string] | no* | Community match criteria (see below). OR within list. |
match_as_path |
string | no* | AS_PATH regex (Cisco/Quagga style, _ = boundary) |
match_neighbor_set |
string | no* | Named neighbor set matched against the evaluation peer |
match_route_type |
string | no* | Route source type: "local", "internal", "external" |
match_as_path_length_ge |
u32 | no* | Minimum AS_PATH length to match (inclusive) |
match_as_path_length_le |
u32 | no* | Maximum AS_PATH length to match (inclusive) |
match_local_pref_ge |
u32 | no* | Minimum LOCAL_PREF to match (inclusive) |
match_local_pref_le |
u32 | no* | Maximum LOCAL_PREF to match (inclusive) |
match_med_ge |
u32 | no* | Minimum MED to match (inclusive) |
match_med_le |
u32 | no* | Maximum MED to match (inclusive) |
match_next_hop |
string | no* | Exact next-hop IP address to match (unicast only) |
match_rpki_validation |
string | no* | RPKI state: "valid", "invalid", or "not_found" |
match_aspa_validation |
string | no* | ASPA state: "valid", "invalid", or "unknown" |
action |
string | yes | "permit" or "deny" |
*At least one of prefix, match_community, match_as_path,
match_neighbor_set, match_route_type, match_as_path_length_ge,
match_as_path_length_le, match_local_pref_ge, match_local_pref_le,
match_med_ge, match_med_le, match_next_hop, or
match_rpki_validation / match_aspa_validation is required.
These fields modify matching routes. Only valid with action = "permit".
| Field | Type | Description |
|---|---|---|
set_local_pref |
u32 | Set LOCAL_PREF on matching routes |
set_med |
u32 | Set MED on matching routes |
set_next_hop |
string | "self" or an IP address |
set_community_add |
[string] | Communities to add (standard, EC, or LC format) |
set_community_remove |
[string] | Communities to remove |
set_as_path_prepend |
table | { asn = 65001, count = 3 } (count 1-10) |
The match_community, set_community_add, and set_community_remove fields
accept these formats:
| Format | Example | Type |
|---|---|---|
ASN:VALUE |
"65001:100" |
Standard community |
| Well-known name | "NO_EXPORT", "NO_ADVERTISE", "NO_EXPORT_SUBCONFED" |
Standard community |
RT:ASN:VALUE |
"RT:65001:100" |
Extended community (route target) |
RO:ASN:VALUE |
"RO:65001:200" |
Extended community (route origin) |
LC:G:L1:L2 |
"LC:65001:100:200" |
Large community (RFC 8092) |
The match_as_path field accepts regular expressions with the Cisco/Quagga _
boundary convention. _ expands to (?:^| |$|[{}]) before compilation, matching
the start of the string, a space between ASNs, the end of the string, or
AS_SET delimiters ({/}).
| Pattern | Matches |
|---|---|
^65100_ |
AS_PATH starting with 65100 |
_65200$ |
AS_PATH ending with 65200 |
_65300_ |
AS_PATH containing 65300 |
^65100$ |
AS_PATH that is exactly 65100 |
Entries are evaluated in order. The first matching entry wins. If no entry matches, the default action is permit.
Use match_as_path_length_ge / match_as_path_length_le to match routes by
inclusive AS_PATH length. Either field may be used independently or together
as a range. AS_SET counts as 1 per RFC 4271.
[[policy.import]]
match_as_path_length_ge = 3
match_as_path_length_le = 8
action = "deny"match_neighbor_set evaluates against the peer currently being evaluated by
policy:
- import policy: the source peer that sent the route
- export policy: the destination peer receiving the route
match_route_type distinguishes:
"external"— learned from an eBGP peer"internal"— learned from an iBGP peer"local"— locally injected or originated
match_local_pref_* and match_med_* are inclusive comparisons. If the route
does not carry the relevant attribute, the comparison does not match.
match_next_hop is exact IP equality against the route's resolved next hop.
It applies to unicast routes. FlowSpec routes do not expose a policy-matchable
next hop because FlowSpec MP_REACH_NLRI carries NH length 0.
[[policy.export]]
match_neighbor_set = "ixp-clients"
match_route_type = "external"
match_next_hop = "2001:db8::1"
match_local_pref_ge = 200
match_med_le = 50
action = "permit"
set_community_add = ["65001:100"]Without ge/le, only exact prefix-length matches count. With them, a route
matches if its prefix falls within the given network and its mask length is
within [ge, le].
Example -- deny all specifics of 10.0.0.0/8 longer than /24:
[[policy.import]]
prefix = "10.0.0.0/8"
ge = 25
le = 32
action = "deny"For each neighbor, import and export policies are resolved independently:
- If the neighbor has a per-neighbor policy chain (
import_policy_chain/export_policy_chain), that chain is used. - If the neighbor has per-neighbor inline policy (
[[neighbors.import_policy]]or[[neighbors.export_policy]]), those are wrapped in a single-element chain. - Otherwise, the global chain (
import_chain/export_chain) is used. - Otherwise, the global inline policy (
[[policy.import]]/[[policy.export]]) is wrapped in a single-element chain. - If none of the above exist, all routes are permitted (no filtering).
Per-neighbor policy completely replaces the global policy for that direction -- the two are never merged. Inline and chain on the same neighbor/direction is a config error.
A realistic configuration with three peers, policy actions, and community matching:
[global]
asn = 65001
router_id = "10.0.0.1"
listen_port = 179
[global.telemetry]
prometheus_addr = "0.0.0.0:9179"
log_format = "json"
# gRPC defaults to a UDS at <runtime_state_dir>/grpc.sock when no listener
# is configured. Uncomment below to add a TCP listener (UDS stays active
# unless explicitly disabled with [global.telemetry.grpc_uds] enabled = false).
# [global.telemetry.grpc_tcp]
# address = "127.0.0.1:50051"
# token_file = "/etc/rustbgpd/grpc.token"
# Global import policy: deny default route and RFC 1918, permit up to /24
[[policy.import]]
prefix = "0.0.0.0/0"
action = "deny"
[[policy.import]]
prefix = "10.0.0.0/8"
le = 32
action = "deny"
[[policy.import]]
prefix = "172.16.0.0/12"
le = 32
action = "deny"
[[policy.import]]
prefix = "192.168.0.0/16"
le = 32
action = "deny"
# Prefer routes from AS 65100
[[policy.import]]
match_as_path = "^65100_"
action = "permit"
set_local_pref = 200
[[policy.import]]
prefix = "0.0.0.0/0"
le = 24
action = "permit"
# Upstream provider -- uses global import policy, custom export with prepend
[[neighbors]]
address = "10.0.0.2"
remote_asn = 65002
description = "upstream-provider"
hold_time = 90
max_prefixes = 50000
[[neighbors.export_policy]]
prefix = "192.168.1.0/24"
action = "permit"
set_as_path_prepend = { asn = 65001, count = 2 }
[[neighbors.export_policy]]
prefix = "192.168.2.0/24"
action = "permit"
[[neighbors.export_policy]]
prefix = "0.0.0.0/0"
le = 32
action = "deny"
# IXP route server -- tag routes with large community, next-hop self
[[neighbors]]
address = "10.0.1.2"
remote_asn = 65100
description = "ixp-rs1"
hold_time = 90
[[neighbors.export_policy]]
action = "permit"
prefix = "0.0.0.0/0"
le = 24
set_next_hop = "self"
set_community_add = ["LC:65001:1:100"]
# eBGP peer with MD5 auth -- per-peer import to reject specifics
[[neighbors]]
address = "10.0.2.2"
remote_asn = 65200
description = "peer-secure"
hold_time = 180
md5_password = "s3cret"
ttl_security = true
max_prefixes = 10000
[[neighbors.import_policy]]
prefix = "10.0.0.0/8"
ge = 25
le = 32
action = "deny"
[[neighbors.import_policy]]
prefix = "0.0.0.0/0"
le = 24
action = "permit"
set_med = 50Optional. Configures BMP (BGP Monitoring Protocol, RFC 7854) export to external collectors. rustbgpd acts as a BMP client, initiating TCP connections to each configured collector and streaming BGP state changes (peer up/down, route monitoring) as BMP messages.
[bmp]
sys_name = "rustbgpd" # optional, default "rustbgpd"
sys_descr = "my bgp speaker" # optional, default "rustbgpd <version>"
[[bmp.collectors]]
address = "10.0.0.100:11019"
reconnect_interval = 30 # seconds, default 30
[[bmp.collectors]]
address = "10.0.0.101:11019"| Field | Type | Required | Default | Description |
|---|---|---|---|---|
sys_name |
string | no | "rustbgpd" |
System name in BMP Initiation message |
sys_descr |
string | no | version string | System description in BMP Initiation message |
collectors |
array | no | [] |
List of BMP collector endpoints |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
address |
string | yes | -- | Collector host:port socket address |
reconnect_interval |
u64 | no | 30 | Seconds between reconnect attempts |
BMP messages sent to collectors:
| Message | When |
|---|---|
| Initiation (Type 4) | On TCP connect to collector |
| Peer Up (Type 3) | BGP session reaches Established (includes raw OPEN PDUs) |
| Peer Down (Type 2) | BGP session leaves Established |
| Route Monitoring (Type 0) | Inbound UPDATE received (pre-policy, raw PDU) |
| Stats Report (Type 1) | Periodic per-peer export every 60s (Adj-RIB-In route count, type 7) |
| Termination (Type 5) | On coordinated daemon shutdown (and on client channel shutdown) |
Route Monitoring messages carry the original raw BGP UPDATE PDU bytes (including the 19-byte BGP header), enabling collectors to decode the full UPDATE without loss.
When BMP is not configured, overhead remains minimal: raw frame capture uses
Bytes refcount clones (no message-data copy).
Optional. Configures periodic MRT TABLE_DUMP_V2 (RFC 6396) RIB snapshots for
offline analysis and archival. Dumps can also be triggered on demand via the
gRPC TriggerMrtDump RPC or the rustbgpctl mrt-dump CLI command.
[mrt]
output_dir = "/var/lib/rustbgpd/mrt"
dump_interval = 7200 # seconds between periodic dumps (default 7200)
compress = true # gzip output files (default false)
file_prefix = "rib" # filename prefix (default "rib")| Field | Type | Required | Default | Description |
|---|---|---|---|---|
output_dir |
string | yes | -- | Directory for MRT dump files (must exist and be writable) |
dump_interval |
u64 | no | 7200 | Seconds between periodic dumps (must be > 0) |
compress |
bool | no | false | Compress output files with gzip |
file_prefix |
string | no | "rib" |
Filename prefix for dump files |
Dump files are written atomically (temp file + rename) with collision-resistant names:
{file_prefix}.{YYYYMMDD.HHMMSS}.{nanoseconds}.mrt[.gz]
For example: rib.20260305.143022.123456789.mrt.gz
Each dump contains a complete TABLE_DUMP_V2 snapshot:
| Record | Contents |
|---|---|
PEER_INDEX_TABLE (subtype 1) |
All known peers with ASN and BGP ID |
RIB_IPV4_UNICAST (subtype 2) |
IPv4 routes from Adj-RIB-In per peer |
RIB_IPV6_UNICAST (subtype 4) |
IPv6 routes from Adj-RIB-In per peer |
RIB_IPV4_UNICAST_ADDPATH (subtype 8) |
IPv4 routes with path IDs (RFC 8050) |
RIB_IPV6_UNICAST_ADDPATH (subtype 9) |
IPv6 routes with path IDs (RFC 8050) |
Routes are sourced from Adj-RIB-In (not Loc-RIB) to avoid duplicate entries
for the best-path winner. Next-hop attributes are synthesized per the MP-BGP
architecture (IPv4 NEXT_HOP, IPv6 MP_REACH_NLRI, RFC 8950
IPv4-with-IPv6-NH MP_REACH_NLRI).
Peer metadata is retained during Graceful Restart and LLGR transitions, so dumps taken during a peer restart window still include correct peer entries.
When MRT is not configured, no timer or manager task is spawned — zero overhead.
See ADR-0044 for design details.
Neighbor mutations made through the gRPC API (AddNeighbor, DeleteNeighbor)
are automatically persisted back to the config file via atomic write (temp file
- rename). This ensures the on-disk config stays in sync with the running state.
Sending SIGHUP to the rustbgpd process triggers a config reload:
- The daemon re-reads the TOML config file
diff_neighbors()computes the delta between running and file stateReconcilePeersapplies per-peer add/delete operations- Global config changes (ASN, router_id, etc.) are logged as warnings but require a restart to take effect
Reload failures are reported per-peer with structured logging. The previous in-memory config snapshot is preserved when reconciliation is incomplete.
The following checks run at startup. Any failure prevents the daemon from starting:
| Rule | Error |
|---|---|
router_id must be a valid IPv4 address |
invalid router_id |
Each address in [[neighbors]] must be a valid IP address (IPv4 or IPv6) |
invalid neighbor address |
prometheus_addr must be a valid ip:port |
invalid prometheus_addr |
grpc_tcp.address must be a valid ip:port when grpc_tcp is enabled |
invalid gRPC config |
grpc_uds.path must be absolute when configured |
invalid gRPC config |
grpc_uds.mode must be <= 0o777 |
invalid gRPC config |
grpc_*.access_mode must be read_only or read_write |
invalid gRPC config |
grpc_*.token_file must exist, be readable, and contain a non-empty token when configured |
invalid gRPC config |
If grpc_tcp/grpc_uds tables are present, at least one listener must be enabled |
invalid gRPC config |
hold_time must be 0 (disabled) or >= 3 seconds |
invalid hold_time |
families entries must be "ipv4_unicast", "ipv6_unicast", "ipv4_flowspec", or "ipv6_flowspec" |
unsupported address family |
gr_restart_time must be <= 4095 |
gr_restart_time exceeds 4095 |
gr_restart_time must be > 0 when graceful_restart is enabled |
gr_restart_time must be > 0 |
gr_stale_routes_time must be > 0 and <= 3600 |
invalid gr_stale_routes_time |
| Policy prefix length must not exceed AFI max (32 for IPv4, 128 for IPv6) | invalid prefix length |
Policy entry must have at least one match condition (prefix, match_community, match_as_path, match_as_path_length_ge, match_as_path_length_le, match_rpki_validation, or match_aspa_validation) |
must have at least one match condition |
Import match_rpki_validation/match_aspa_validation evaluates against the current snapshot — routes arriving before the first VRP/ASPA table loads see not_found/unknown; later cache updates do not retroactively re-filter admitted routes (use best-path demotion for convergent behavior) |
(informational — no error) |
match_as_path_length_ge must not exceed match_as_path_length_le |
match_as_path_length_ge (...) exceeds match_as_path_length_le (...) |
set_* fields cannot be used with action = "deny" |
set_* fields cannot be used with action = "deny" |
set_as_path_prepend.count must be 1--10 |
count must be 1-10 |
match_as_path must be a valid regex |
invalid regex |
| RT/RO extended community ASN must be <= 65535 (2-octet AS sub-type) | ASN exceeds 65535 |
RPKI refresh_interval, retry_interval, expire_interval must be > 0 |
must be > 0 |
RPKI expire_interval must be >= refresh_interval |
expire_interval must be >= refresh_interval |
Named policy referenced in chain must exist in [policy.definitions] |
undefined policy |
| Inline policy and policy chain cannot both be set for the same neighbor/direction | mutually exclusive |
route_server_client is only valid on eBGP neighbors |
invalid route_server_client |
remove_private_as must be "remove", "all", or "replace" (eBGP only) |
invalid remove_private_as |
MRT output_dir must not be empty |
output_dir must not be empty |
MRT dump_interval must be > 0 |
dump_interval must be > 0 |
BMP collector address must be a valid ip:port |
invalid BMP collector address |
BMP collector reconnect_interval must be > 0 |
reconnect_interval must be > 0 |
cluster_id must be a valid IPv4 address |
invalid cluster_id |
runtime_state_dir must not be empty |
runtime_state_dir must not be empty |
llgr_stale_time must be <= 16777215 (24-bit) |
llgr_stale_time exceeds maximum |
route_reflector_client requires iBGP (local ASN == remote ASN) |
route_reflector_client requires iBGP |
local_ipv6_nexthop must be a valid non-link-local, non-loopback, non-multicast IPv6 address |
invalid local_ipv6_nexthop |
ge must be >= prefix length and <= AFI max (32 for IPv4, 128 for IPv6) |
invalid ge |
le must be <= AFI max |
invalid le |
ge must be <= le when both are set |
ge must be <= le |
| Config file must be valid TOML | failed to parse TOML |
| Field | Default value |
|---|---|
hold_time |
90 seconds |
connect_retry_secs |
5 seconds (not configurable) |
| gRPC listener | UDS at <runtime_state_dir>/grpc.sock with mode 0o600 |
ttl_security |
false |
families |
["ipv4_unicast"] for IPv4 peers; ["ipv4_unicast", "ipv6_unicast"] for IPv6 peers |
graceful_restart |
true |
gr_restart_time |
120 seconds |
gr_stale_routes_time |
360 seconds |
llgr_stale_time |
0 (disabled) |
description |
peer address used as label |
route_server_client |
false |
remove_private_as |
disabled (absent) |
| Policy default action | permit (when no entry matches) |