VyOSXXVIII · BGP Prefix FilteringPrefix filters
Distribute-list — applying an ACL or prefix-list to a BGP neighbour in/out
What you'll learn
- Explain what a distribute-list does at the BGP neighbour level
- Apply a numbered access-list as a distribute-list under a neighbour's address-family
- Explain why a prefix-list cannot be passed to distribute-list, and use the prefix-list node instead
- Distinguish the import and export directions and the evidence each one leaves
- Recognise the production failure modes around distribute-list configuration
Prerequisites
Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling) · 2026-08-19
A distribute-list is the BGP neighbour’s filter mechanism. It applies an access-list or prefix-list to the routes that flow in or out of the BGP neighbour, deciding which NLRI (Network Layer Reachability Information — the prefix and prefix-length) the local router accepts from the peer or sends to the peer.
The distribute-list is the legacy Cisco-style configuration. It
survives on VyOS 1.5, but with three properties that a reader
coming from older material or from IOS will get wrong, and each
one is a failed commit rather than a subtle mistake:
- It lives under the neighbour’s address-family, not directly under the neighbour:
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast distribute-list .... - Its directions are
importandexport, notinandout. - Its argument is a number, not a name. The VyOS documentation types it
<number>and describes it as applying “the access list filters named in<number>”. VyOS IPv4 access-lists are numbered 1-2699; there is no named IPv4 access-list to pass.
That third point is the one worth internalising, because it
disposes of a common piece of folklore: you cannot hand a
prefix-list to distribute-list on VyOS. If you want
prefix-length matching — and for BGP you almost always do — the
node you want is the sibling prefix-list import|export <name>,
which is a different command, not a different spelling of the
same one.
The local AS is also no longer part of the path. VyOS 1.4
replaced set protocols bgp <asn> ... with
set protocols bgp system-as <asn>, and peers moved to
set protocols bgp neighbor .... Every block below is written
in that form.
What a distribute-list filters
The distribute-list operates on the NLRI. It does not modify the AS_PATH, MED, Local Preference or any other attribute; it only accepts or rejects the route.
Its matching primitive is an access-list, and that is the
whole story on VyOS 1.5 — the node takes a number and resolves
it against policy access-list. An access-list matches the
network part of an address and has no field for a prefix-length,
so a distribute-list on a BGP session cannot distinguish
198.51.100.0/24 from 198.51.100.0/28.
The sibling node prefix-list import|export <name> filters the
same NLRI at the same two points in the pipeline, but with a
matcher that does see the length. It is the same job done by a
different command, and it is the one to reach for.
flowchart LR
subgraph "BGP peer"
P["Peer at 192.0.2.2"]
end
subgraph "Local router"
IN["import filter<br/>NLRI from peer"]
RIB["Adj-RIBs-In"]
LOC["Locally originated routes<br/>(network, aggregate)"]
OUT["export filter<br/>NLRI to peer"]
end
P -->|"UPDATE message"| IN
IN -->|"accepted routes only"| RIB
RIB -->|"best-path selection"| FWD["Forwarding table"]
LOC --> OUT
RIB --> OUT
OUT -->|"UPDATE message"| P
The diagram shows the two filter points. The import filter
rejects routes the peer sends that the list does not permit; the
export filter rejects routes this router would otherwise
advertise. Both distribute-list and prefix-list attach at
these same two points — the difference between them is what they
can match, not where they sit.
Applying an access-list as a distribute-list
The access-list must exist first, and it must be numbered. VyOS allows 1-2699 for IPv4; the classic convention of 1-99 for standard lists and 100-199 for extended lists still reads well to anyone who has touched IOS, and it costs nothing to follow.
configure
set policy access-list 10 description 'customer prefixes from AS65001'
set policy access-list 10 rule 10 action 'permit'
set policy access-list 10 rule 10 source network '198.51.100.0'
set policy access-list 10 rule 10 source inverse-mask '0.0.0.255'
set policy access-list 10 rule 10 description 'customer prefix'
set policy access-list 10 rule 20 action 'permit'
set policy access-list 10 rule 20 source network '203.0.113.0'
set policy access-list 10 rule 20 source inverse-mask '0.0.0.255'
set policy access-list 10 rule 20 description 'customer prefix'
set protocols bgp system-as 64512
set protocols bgp neighbor 192.0.2.2 remote-as '65001'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast distribute-list import '10'
commit
save
The filter is applied on receive from 192.0.2.2. The local router accepts only routes matching one of the two rules; everything else meets the implicit deny at the end of the list.
Two things about source are worth pausing on, because they are
where the access-list’s whole character shows.
It is not a bare address. It takes a selector: any,
host <address>, or the network / inverse-mask pair. Writing
source '198.51.100.0' on its own will not commit.
And network is paired with inverse-mask — the VyOS
documentation says each “requires” the other. That is the IOS
wildcard-mask model, not CIDR: 0.0.0.255 is the wildcard for a
24-bit network, 0.0.0.0 matches a single address, 0.255.255.255
matches a /8. The mask says which bits are ignored, which is
the inverse of a netmask and the source of a great many
transcription errors.
Which leads directly to the limitation that has kept this
command in the “legacy” column for twenty years: a wildcard
mask selects bits of an address, and a prefix-length is not part
of an address. 198.51.100.0/24, 198.51.100.0/26 and
198.51.100.0/28 all present 198.51.100.0 to the matcher, so
one rule admits all three. On a customer session that is the
difference between accepting an aggregate and accepting a few
hundred deaggregated more-specifics.
Filtering by prefix-length: use the prefix-list node
Here is the correction that matters most in this lesson. On
VyOS 1.5 you cannot pass a prefix-list to distribute-list.
The node’s argument is typed <number> and resolves to an
access-list; handing it ALLOW-CUSTOMER is not a stylistic
variant, it is a value the CLI will reject.
Prefix-length filtering is the sibling node, prefix-list:
configure
set policy prefix-list ALLOW-CUSTOMER description 'accepted customer aggregates'
set policy prefix-list ALLOW-CUSTOMER rule 10 action 'permit'
set policy prefix-list ALLOW-CUSTOMER rule 10 prefix '198.51.100.0/24'
set policy prefix-list ALLOW-CUSTOMER rule 10 description 'customer prefix'
set policy prefix-list ALLOW-CUSTOMER rule 20 action 'permit'
set policy prefix-list ALLOW-CUSTOMER rule 20 prefix '203.0.113.0/24'
set policy prefix-list ALLOW-CUSTOMER rule 20 description 'customer prefix'
set protocols bgp system-as 64512
set protocols bgp neighbor 192.0.2.2 remote-as '65001'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast prefix-list import 'ALLOW-CUSTOMER'
commit
save
Without ge or le, a prefix-list rule matches that prefix at
that exact length and nothing else — so this accepts
198.51.100.0/24 and rejects 198.51.100.0/26, which is
precisely what the access-list could not express. To admit a
range of lengths, add them: rule 10 ge 24 and rule 10 le 26
accept /24 through /26 inside 198.51.100.0/24.
Prefix-lists are named, so this is also the only one of the two whose configuration reads back like documentation.
The import and export directions
A filter is attached to a neighbour’s address-family in one of
two directions, and the direction decides which routes it ever
sees. The VyOS keywords are import and export; there is no
in or out in this CLI.
import — the filter is applied to routes the peer advertises to the local router. It decides which routes the local router accepts.
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast distribute-list import '10'
The peer advertises routes. The local router receives the
UPDATE. FRR applies the import filter. Matching routes are
accepted; the rest are dropped without any log line. The
post-filter view is show bgp ipv4 neighbors 192.0.2.2 routes;
the pre-filter view is
show bgp ipv4 neighbors 192.0.2.2 received-routes, which needs
one extra piece of configuration covered in the next section.
export — the filter is applied to routes the local router advertises to the peer. It decides what the peer is told.
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast distribute-list export '20'
The local router selects the routes it would advertise, FRR
applies the export filter, and only what matches goes into the
UPDATE. The evidence here is
show bgp ipv4 neighbors 192.0.2.2 advertised-routes, which
needs nothing extra because the router already knows what it
sent.
The import filter is the canonical protection against the peer sending bad routes. The export filter is the canonical protection against this router sending bad routes — which, on a transit-capable session, is the one that turns a local mistake into somebody else’s outage. Both are required for production BGP.
The interaction with route-maps
The distribute-list and the route-map are sibling filters — both can be applied to a BGP neighbour, in the same direction, and both reject routes. The difference:
- Distribute-list — match on the NLRI (prefix and prefix-length). Accept or reject.
- Route-map — match on any combination of NLRI, AS-path, community, MED, etc. Accept or reject; can also set attributes.
The canonical pattern: use the distribute-list for the simple prefix filtering, and use the route-map for the more complex policy (match on AS-path, set MED, etc.). The two can be combined on the same neighbour.
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast distribute-list import '10'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast route-map import 'RM-LP-CUSTOMER'
Both sit under the same address-family ipv4-unicast node and
both take import/export. The distribute-list applies first
(the NLRI filter); the route-map applies second (the policy
filter). A route is accepted only if both permit it — which is
also the trap: a route-map that looks correct in isolation
cannot set a Local Preference on a route the distribute-list
already discarded, and nothing tells you which of the two ate
it. show bgp ipv4 neighbors 192.0.2.2 received-routes is what
separates the two cases.
Proving the filter worked: received-routes vs routes
The comparison every filter change should end with is pre-filter against post-filter. On VyOS 1.5 the commands are address-family qualified:
show bgp ipv4 neighbors 192.0.2.2 received-routes
show bgp ipv4 neighbors 192.0.2.2 routes
show bgp ipv4 neighbors 192.0.2.2 advertised-routes
With soft-reconfiguration enabled, the two views answer two
different questions. received-routes is what the peer sent.
routes is what survived the import policy. The set difference
between them is exactly what your filter removed:
- Peer sends
198.51.100.0/24,203.0.113.0/24,198.51.100.0/26,198.51.100.64/26. - With the prefix-list above (two exact
/24entries, noge/le),routesshows the two/24s. - The two
/26s appear inreceived-routesand not inroutes. That difference is the proof, and it is the only proof — a route absent from both views was never sent, which is a peer problem, not a filter problem.
If instead the two views are identical, your filter is not running: either it is on the wrong neighbour, the wrong direction, or the wrong address-family. Check the FRR render before you touch the list itself.
The difference between distribute-list and prefix-list
The two are functionally equivalent but configured differently:
distribute-list | prefix-list | |
|---|---|---|
| What it references | A numbered access-list (1-2699) | A named prefix-list |
| VyOS 1.5 syntax | ... address-family ipv4-unicast distribute-list import|export <number> | ... address-family ipv4-unicast prefix-list import|export <name> |
| Matching granularity | Network part only | Network part and prefix-length, with ge / le ranges |
| Reads back as | A number you have to go look up | The name you gave the policy |
| Use it for | Migrating an existing IOS configuration that already uses numbered ACLs | Everything else |
These are not two spellings of one feature. They reference different policy objects and they match differently, and only one of them can express “accept this aggregate but not its more-specifics” — which is the requirement on nearly every real BGP session.
The canonical import and export filter patterns
Pattern 1: import filter — accept only customer routes from the upstream:
configure
set policy prefix-list FROM-UPSTREAM rule 10 action 'permit'
set policy prefix-list FROM-UPSTREAM rule 10 prefix '198.51.100.0/24'
set policy prefix-list FROM-UPSTREAM rule 10 description 'customer routes'
set policy prefix-list FROM-UPSTREAM rule 20 action 'permit'
set policy prefix-list FROM-UPSTREAM rule 20 prefix '203.0.113.0/24'
set policy prefix-list FROM-UPSTREAM rule 20 description 'customer routes'
set protocols bgp system-as 64512
set protocols bgp neighbor 192.0.2.2 remote-as '65001'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast prefix-list import 'FROM-UPSTREAM'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast soft-reconfiguration inbound
commit
save
The upstream sends all its routes. The local router accepts only 198.51.100.0/24 and 203.0.113.0/24. The implicit deny catches everything else.
Pattern 2: export filter — send only local routes to the upstream:
configure
set policy prefix-list TO-UPSTREAM rule 10 action 'permit'
set policy prefix-list TO-UPSTREAM rule 10 prefix '198.51.100.0/24'
set policy prefix-list TO-UPSTREAM rule 10 description 'local customer routes'
set policy prefix-list TO-UPSTREAM rule 20 action 'permit'
set policy prefix-list TO-UPSTREAM rule 20 prefix '203.0.113.0/24'
set policy prefix-list TO-UPSTREAM rule 20 description 'local customer routes'
set protocols bgp system-as 64512
set protocols bgp neighbor 192.0.2.2 remote-as '65001'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast prefix-list export 'TO-UPSTREAM'
commit
save
The local router has more routes than the customer prefixes. The local router advertises only 198.51.100.0/24 and 203.0.113.0/24 to the upstream. The implicit deny catches everything else.
Pattern 3: both directions:
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast prefix-list import 'FROM-UPSTREAM'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast prefix-list export 'TO-UPSTREAM'
The local router accepts only the customer’s routes from the upstream and sends only the customer’s routes to the upstream. Two filters, two directions, one neighbour — and note that they are two separate lists. Reusing one list for both directions is a common shortcut and a bad one: the set you are willing to accept and the set you are willing to originate are different sets, and the day they diverge you will change one and silently change the other.
The set policy access-list tree in detail
Two properties of the VyOS access-list catch people out, and both are visible in the syntax rather than in the behaviour.
It is numbered, not named. The node is
set policy access-list <1-2699>. There is no named IPv4
access-list; the named form exists only for IPv6, as
set policy access-list6 <name>. This asymmetry is worth
remembering, because it means a dual-stack policy written the
obvious way — one name for both families — cannot be expressed.
Its match fields are selectors, and the network selector is a
wildcard-mask pair. Each of source and destination takes
any, host <address>, or network together with
inverse-mask — the documentation states that each of the last
two requires the other:
configure
set policy access-list 10 description 'customer prefixes from AS65001'
set policy access-list 10 rule 10 action 'permit'
set policy access-list 10 rule 10 source network '198.51.100.0'
set policy access-list 10 rule 10 source inverse-mask '0.0.0.255'
set policy access-list 10 rule 10 destination any
set policy access-list 10 rule 10 description 'customer prefix'
commit
save
Read the mask the IOS way: a 1 bit is a bit the matcher
ignores. 0.0.0.255 matches a 24-bit network, 0.0.0.0 matches
exactly one address, and 0.255.255.255 matches a /8. Getting
this backwards — writing 255.255.255.0 where 0.0.0.255
belongs — produces a rule that commits cleanly and matches
almost nothing.
For BGP filtering the source is the network being matched and
the destination is conventionally any: BGP is filtering
NLRI, and the second field has no routing meaning here. It is a
vestige of the access-list being a general-purpose packet
matcher borrowed for a routing job, which is also the shortest
explanation of why it cannot see a prefix-length.
How the result is validated
show bgp ipv4 neighbors 192.0.2.2 received-routes
show bgp ipv4 neighbors 192.0.2.2 routes
show bgp ipv4 neighbors 192.0.2.2 advertised-routes
show bgp ipv4 summary
The first three are the pre-filter, post-filter and outbound
views; received-routes needs soft-reconfiguration inbound on
that neighbour. The summary line tells you the session is up at
all, which is worth confirming before you conclude a filter is
too aggressive.
The configured intent is read back from the VyOS side:
show configuration commands | match 'policy access-list'
show configuration commands | match 'policy prefix-list'
show configuration commands | match 'distribute-list'
And the enforced reality from the FRR side:
vtysh -c 'show ip prefix-list'
vtysh -c 'show running-config'
Compare the two. show configuration commands is what you asked
for; the FRR running-config is what the daemon is applying. When
a filter change appears to have done nothing, that pair of
outputs is where the answer is — usually a filter attached to
the wrong address-family, so it committed cleanly and matched
nothing.
How it fails
The production failure modes the engineer must recognise:
- A prefix-list name is passed to
distribute-list. The node takes a number and resolves it as an access-list. The commit is rejected, which is the good outcome; the bad outcome is the operator concluding the feature is broken. The fix is to use theprefix-list import|export <name>node instead. in/outused instead ofimport/export. The VyOS CLI has only the second pair. This is the single most common transcription error when lifting a configuration from FRR or IOS documentation.- The filter is attached under the neighbour rather than under its address-family.
set protocols bgp neighbor <ip> distribute-list ...is not the path; the filter lives underaddress-family ipv4-unicast. Attaching a v4 policy underipv6-unicastcommits cleanly and filters nothing, which is worse than an error. - The filter is on the wrong neighbour. Diagnosed by comparing
routesagainstreceived-routeson both peers — the one where the two views are identical is the one with no filter running. - The list referenced does not exist. The validator rejects the reference. Define the access-list or prefix-list first, in the same commit if you like, but before the reference is evaluated.
- The filter is too permissive. A catch-all permit at the end turns the list into decoration. Every list ends in an implicit deny; adding an explicit
permit anyremoves the only protection the list provided. - The filter is too aggressive. A prefix the peer legitimately originates is missing from the list. Visible as a route in
received-routesand absent fromroutes, which is the same evidence as a correct rejection — so this one is settled by intent, not by output. - The access-list cannot see prefix-length.
198.51.100.0/24and198.51.100.0/26match the same rule. This is not a misconfiguration to fix in the list; it is the reason to move the policy to a prefix-list.
Rollback
Distribute-list configuration changes are configuration changes. The standard rollback path applies:
comparebeforecommitto see the distribute-list addition.commit-confirm <timeout>for any remote change.rollback N; commit; saveto revert to the previous configuration.load /config/archive/<known-good-file>; commit; saveto revert to a specific snapshot.
The operator who changes a distribute-list on a production router must also know how to roll back the change. The standard rollback is rollback 1; commit; save — the previous configuration had the old distribute-list, so the rollback restores it.
Production discipline
Cross-course references
The Linux course’s XIX-Linux-NetFoundations covers the kernel FIB. The OPNsense course’s XXX-OPNsense-DynamicRouting covers the same FRR distribute-list pattern on the firewall side. The BGP lessons vyos-xxvi-02-as-path (the AS_PATH attribute in detail), vyos-xxviii-01-prefix-list-concept (the prefix-list semantics), and vyos-xxviii-02-prefix-list-config (the prefix-list configuration) cover the broader context. The lesson vyos-xxviii-04-filter-list covers the AS-path-list filter mechanism. The lesson vyos-xxviii-06-filter-troubleshoot walks the debugging of filter failures.
Quiz
Knowledge check · 4 questions
Q1. What does a distribute-list filter when applied to a BGP neighbour?
Q2. A distribute-list using an access-list matches the prefix and the prefix-length.
Q3. An operator configures `distribute-list import 10` where access-list 10 permits the network 198.51.100.0/24. They expected only `198.51.100.0/24` to be accepted. The peer's `/26` and `/28` more-specifics are accepted too. Why?
Access-list 10 rule 10 is `source network 198.51.100.0` with `source inverse-mask 0.0.0.255`. The peer advertises 198.51.100.0/24, 198.51.100.0/26 and 198.51.100.0/28. All three are in the local BGP table.
Q4. An operator configures `set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast distribute-list import 10`, intending to restrict what this router advertises to the upstream. The upstream still receives everything. What is the fix?
The filter is attached in the import direction, so it constrains what the local router accepts from 192.0.2.2. The intent was to constrain what the local router sends. Outbound advertisements are unfiltered.
Passing score: 75%. Answers are checked in this browser.