BGP policy: route maps, prefix lists and filtering
What you'll learn
- Write a prefix list that matches the address space you intend and nothing else
- Build a route map from match, set and exit-action clauses
- Attach policy to a controller with route-map-in and route-map-out
- Verify what the fabric is advertising, rather than what you configured it to advertise
Prerequisites
Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12
The EVPN lesson built a fabric. The fabric lesson gave it an underlay. Both stopped at the point where the Proxmox cluster starts talking BGP to something that is not a Proxmox node — a datacentre spine, a firewall, a provider edge — and at that point a question arrives that the earlier lessons did not need to answer:
What is this cluster telling the rest of the network about itself, and what is it willing to believe in return?
By default the answer to both is “everything it knows”. A BGP session without policy advertises every prefix in its table and accepts every prefix the peer sends. Inside a fabric of nodes you administer, that is fine. Across a boundary it is not, and the primitives Proxmox gives you for drawing that boundary are prefix lists and route maps.
The two primitives
They compose, and the composition is the thing to get straight before touching a command.
A prefix list is a named, ordered list of address-space tests. It answers “is this prefix one of the ones I mean?” and nothing else. It has no opinion about what to do with the route.
A route map is a named, ordered list of rules. Each rule matches on something — often a prefix list — and then permits or denies the route, optionally modifying its attributes on the way through.
Prefix lists are reusable. One list describing your VM address space can be referenced by the inbound map, the outbound map, and a map on a different controller. That reuse is the reason to build them separately rather than embedding CIDRs in every rule.
Prefix list entries
The fields, as the API and the documentation define them:
| Field | API name | Meaning |
|---|---|---|
| Sequence Nr. | seq | “Order in which the entry is evaluated” |
| Action | action | permit or deny |
| Prefix | prefix | “Destination network in CIDR notation” |
Prefix >= | ge | “Optional minimum prefix length” |
Prefix <= | le | “Optional maximum prefix length” |
ge and le are where most prefix-list bugs live, so they are worth
stating precisely. An entry with a prefix and neither modifier matches
only that exact prefix. 192.0.2.0/24 matches 192.0.2.0/24 and
does not match 192.0.2.0/25 or 192.0.2.128/25.
Adding le 32 makes it match that prefix and every more-specific route
inside it. Adding ge 25 le 32 makes it match the more-specifics and
not the /24 itself.
| Entry | Matches |
|---|---|
permit 192.0.2.0/24 | Exactly 192.0.2.0/24 |
permit 192.0.2.0/24 le 32 | 192.0.2.0/24 and everything inside it |
permit 192.0.2.0/24 ge 25 le 32 | Everything inside it, but not the /24 |
permit 0.0.0.0/0 le 32 | Every IPv4 route |
permit 0.0.0.0/0 | Only the default route |
That last pair is the one that produces a genuinely confusing incident.
An operator writes 0.0.0.0/0 intending “everything”, gets a filter
that permits the default route and nothing else, and watches the fabric
lose all its specific routes while the default keeps working — so
connectivity survives in a degraded, asymmetric form that is much harder
to diagnose than a clean outage.
pvesh create /cluster/sdn/prefix-lists --id pve-vm-space
pvesh create /cluster/sdn/prefix-lists/pve-vm-space/entries \
--seq 10 --action permit --prefix 192.0.2.0/24 --le 32
pvesh create /cluster/sdn/prefix-lists/pve-vm-space/entries \
--seq 20 --action permit --prefix 198.51.100.0/24 --le 32
pvesh get /cluster/sdn/prefix-lists/pve-vm-space/entries --output-format yamlRoute-map entries
| Field | API name | Meaning |
|---|---|---|
| Order | order | “Between 0 and 65535. Mandatory and must be unique” |
| Action | action | permit or deny |
| Match | match | The conditions a route must satisfy |
| Set | set | Attributes to modify on a matching route |
| Exit Action | exit-action | on-match-next, on-match-goto, or continue |
| Call | call | “Optionally call another route map” |
The match clause supports: route-type, vni,
ip-address-prefix-list, ip6-address-prefix-list,
ip-next-hop-prefix-list, ip6-next-hop-prefix-list,
ip-next-hop-address, ip6-next-hop-address, metric,
local-preference, peer, and tag.
The set clause supports: ip-next-hop, ip-next-hop-peer-address,
ip-next-hop-unchanged, ip6-next-hop, ip6-next-hop-peer-address,
ip6-next-hop-prefer-global, local-preference, tag, weight,
metric, and src.
vni and route-type are the EVPN-specific matches, and they are what
lets a policy distinguish a Type-2 MAC/IP advertisement from a Type-5
prefix route — which is the distinction that matters when you want a
fabric to export its subnets but not its per-host MAC bindings.
MAP=export-vm-space
pvesh create /cluster/sdn/route-maps/entries \
--route-map-id "$MAP" \
--order 10 \
--action permit \
--match key=ip-address-prefix-list,value=pve-vm-space
pvesh create /cluster/sdn/route-maps/entries \
--route-map-id "$MAP" \
--order 20 \
--action deny
pvesh get "/cluster/sdn/route-maps/entries/$MAP" --output-format yamlNote the shape of the --match argument. match and set are arrays
of property strings, each written as key=<name>,value=<value> — not
name=value, which is the form people reach for first and which the API
rejects. exit-action uses the same convention with an optional index,
as key=on-match-goto,value=40.
Exit actions, and the one that is easy to misread
exit-action controls what happens after an entry matches:
on-match-next— continue evaluating from the next entry.on-match-goto— jump to a specified later entry.continue— proceed rather than terminating on this match.
Without one, evaluation stops at the first matching entry and that
entry’s permit or deny is the verdict. This is the standard
first-match behaviour and it is what you want almost always.
The reason on-match-next exists is layered policy: a first entry that
sets local-preference on a class of routes and then continues, so a
later entry can decide whether to permit them. It is powerful and it
makes a map much harder to read, which matters because the person
reading it next is likely to be doing so during an incident.
call is the other composition tool — one map invoking another — and
the same caution applies with more force. A two-level policy is
correspondingly harder to reason about, and FRR’s own documentation is
the place to go before building one.
Attaching policy to a controller
Policy that is not attached does nothing. The BGP and EVPN controllers take two properties:
route-map-in— “Route Map that should be applied for incoming routes”route-map-out— “Route Map that should be applied for outgoing routes”
CONTROLLER=bgp-spine
pvesh set "/cluster/sdn/controllers/$CONTROLLER" \
--route-map-out export-vm-space
pvesh get /cluster/sdn/dry-run --output-format yaml | head -40The controller also carries the properties that decide what kind of BGP session this is, and they are worth reading together with the policy because a policy written for the wrong session type is a policy that does nothing:
| Property | Meaning |
|---|---|
asn | “autonomous system number” |
peers | “peers address list” |
ebgp | “Enable eBGP (remote-as external)” |
bgp-mode | “Whether to use eBGP or iBGP. Auto mode chooses depending on BGP controller or falls back to iBGP” |
ebgp-multihop | “Set maximum amount of hops for eBGP peers” |
loopback | “Name of the loopback/dummy interface that provides the Router-IP” |
bgp-multipath-as-path-relax | “Consider different AS paths of equal length for multipath computation” |
peer-group-name | “Name of the peer group for this EVPN controller” |
Verification that can fail
The SDN configuration being valid tells you FRR was given a configuration. It does not tell you what BGP is doing with it. That answer only comes from FRR.
PEER=192.0.2.1
vtysh -c 'show ip bgp summary'
vtysh -c "show ip bgp neighbors $PEER advertised-routes"
vtysh -c "show ip bgp neighbors $PEER received-routes"
vtysh -c 'show route-map'
vtysh -c 'show ip prefix-list'$ vtysh -c 'show ip bgp neighbors 192.0.2.1 advertised-routes'BGP table version is 14, local router ID is 10.255.0.1
Network Next Hop Metric LocPrf Weight Path
*> 192.0.2.0/24 10.255.0.1 0 100 32768 i
*> 198.51.100.0/24 10.255.0.1 0 100 32768 i
Total number of prefixes 2Illustrative output
show route-map reports a hit counter per entry, which is the fastest
way to tell “my rule is wrong” from “my rule never ran”. An entry with
zero hits after the session has been up for a while did not match
anything, which usually means the prefix list, not the map.
When you need this, and when you do not
Route maps are the most complex thing in this part, and a good number of Proxmox estates never need them.
You do not need policy if the cluster’s BGP sessions are all internal to a fabric you administer end to end. Inside a fabric, advertising everything is the point.
You need policy at a boundary: a session to a spine that belongs to the network team, a session to a firewall, an eBGP session to a provider, or an EVPN fabric where two tenants must not learn each other’s routes.
You need it urgently in one case: a Proxmox cluster with two BGP sessions to different parts of the network can, without an outbound filter, advertise routes learned from one into the other and become a transit path. The traffic between two datacentre segments starts flowing through a hypervisor’s control-plane CPU. Everything works, slowly, and nothing points at the cause.
An outbound map that permits only your own address space and denies the rest prevents that, and it is the single most valuable policy on this page.
Common mistakes
- Writing
0.0.0.0/0when you mean “every route”. Withoutle 32it matches only the default route. - A prefix list with no
leon an aggregate. It matches the exact prefix and none of the more-specifics inside it. - Forgetting the implicit deny. A map that matches nothing rejects everything, and the outbound version of that is asymmetric reachability rather than a clean outage.
- Attaching a policy without a console session available. An inbound filter that denies everything can remove your route to the node.
- Verifying with
pveshinstead ofvtysh.pveshshows intent; FRR holds the state. - Reusing an
ordervalue. It is “Mandatory and must be unique”. - Reaching for
on-match-nextandcallearly. They make a policy much harder to read, and readability is what you need at 3 AM. - No outbound filter on a cluster with two external sessions. The hypervisor becomes a transit router and nothing says so.
Key takeaways
- Prefix lists test address space; route maps decide what to do. Build lists separately so they can be reused.
geandlecontrol prefix-length matching. A bare prefix matches exactly that prefix and nothing more specific.- Route-map entries take
order(unique, 0–65535),action,match,set,exit-actionandcall. match route-typeandmatch vniare the EVPN-aware conditions.- Policy attaches to a controller with
route-map-inandroute-map-out, and does nothing until it is attached and applied. - Both prefix lists and route maps carry an implicit deny, so a broken policy drops everything rather than leaking a little.
- Verify with
vtysh:advertised-routes,received-routes,show route-mapandshow ip prefix-listwith their hit counters. - If the cluster has two external BGP sessions, an outbound filter is what stops it becoming a transit path.
Knowledge check
Knowledge check · 4 questions
Q1. A prefix list entry reads: permit 192.0.2.0/24. The fabric is failing to advertise 192.0.2.0/25 and 192.0.2.128/25, which are the actual VNet subnets. Why?
Q2. After attaching a route-map-out to a BGP controller, guests remain reachable from within the cluster but stop being reachable from the rest of the network. Traffic initiated by the guests still works. What is the most likely cause?
Q3. Which of these are accurate about how Proxmox SDN route maps and prefix lists behave? Select all that apply.
Q4. A Proxmox cluster with BGP sessions to two separate parts of the network and no outbound route map can begin forwarding traffic between them, becoming an unintended transit path.
Passing score: 75%. Answers are checked in this browser.