Skip to main content
RunBook Academy

Proxmox VEIV · NetworkingSDN

BGP policy: route maps, prefix lists and filtering

Expert⏱ ~26 minpveshvtysh

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

Not yet marked complete on this device.

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:

FieldAPI nameMeaning
Sequence Nr.seq“Order in which the entry is evaluated”
Actionactionpermit or deny
Prefixprefix“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.

EntryMatches
permit 192.0.2.0/24Exactly 192.0.2.0/24
permit 192.0.2.0/24 le 32192.0.2.0/24 and everything inside it
permit 192.0.2.0/24 ge 25 le 32Everything inside it, but not the /24
permit 0.0.0.0/0 le 32Every IPv4 route
permit 0.0.0.0/0Only 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.

Configuration changebuild a prefix list describing your VM address space
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 yaml

Route-map entries

FieldAPI nameMeaning
Orderorder“Between 0 and 65535. Mandatory and must be unique”
Actionactionpermit or deny
MatchmatchThe conditions a route must satisfy
SetsetAttributes to modify on a matching route
Exit Actionexit-actionon-match-next, on-match-goto, or continue
Callcall“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.

Configuration changea route map that advertises only your VM space
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 yaml

Note 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”
Cluster-wide riskattach the outbound policy to a BGP controller
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 -40

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

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

Read-only / Safewhat are we actually advertising, and to whom
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'
Read-only / Safe
$ 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 2

Illustrative 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/0 when you mean “every route”. Without le 32 it matches only the default route.
  • A prefix list with no le on 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 pvesh instead of vtysh. pvesh shows intent; FRR holds the state.
  • Reusing an order value. It is “Mandatory and must be unique”.
  • Reaching for on-match-next and call early. 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.
  • ge and le control 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-action and call.
  • match route-type and match vni are the EVPN-aware conditions.
  • Policy attaches to a controller with route-map-in and route-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-map and show ip prefix-list with 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

  1. 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?

  2. 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?

  3. Q3. Which of these are accurate about how Proxmox SDN route maps and prefix lists behave? Select all that apply.

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