Skip to main content
RunBook Academy

VyOSXXVIII · BGP Prefix FilteringPrefix filters

Distribute-list — applying an ACL or prefix-list to a BGP neighbour in/out

Intermediate⏱ ~16 minset policy access-listset policy prefix-listset protocols bgp neighbor address-family distribute-listshow bgp ipv4 neighbors received-routesshow bgp ipv4 neighbors routesshow bgp ipv4 neighbors advertised-routesvtysh

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

Not yet marked complete on this device.

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 import and export, not in and out.
  • 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 /24 entries, no ge/le), routes shows the two /24s.
  • The two /26s appear in received-routes and not in routes. 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-listprefix-list
What it referencesA 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 granularityNetwork part onlyNetwork part and prefix-length, with ge / le ranges
Reads back asA number you have to go look upThe name you gave the policy
Use it forMigrating an existing IOS configuration that already uses numbered ACLsEverything 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 the prefix-list import|export <name> node instead.
  • in / out used instead of import / 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 under address-family ipv4-unicast. Attaching a v4 policy under ipv6-unicast commits cleanly and filters nothing, which is worse than an error.
  • The filter is on the wrong neighbour. Diagnosed by comparing routes against received-routes on 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 any removes 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-routes and absent from routes, 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/24 and 198.51.100.0/26 match 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:

  • compare before commit to see the distribute-list addition.
  • commit-confirm <timeout> for any remote change.
  • rollback N; commit; save to revert to the previous configuration.
  • load /config/archive/<known-good-file>; commit; save to 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

  1. Q1. What does a distribute-list filter when applied to a BGP neighbour?

  2. Q2. A distribute-list using an access-list matches the prefix and the prefix-length.

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

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