VyOSXXVIII · BGP Prefix FilteringPrefix filters
AS-path filter-list — regex matching on AS_PATH for BGP neighbour filtering
What you'll learn
- Explain the AS-path regex semantics FRR implements, including what `_` expands to
- Configure an AS-path-list with the VyOS `set policy as-path-list` syntax
- Bind a filter-list under a neighbour address family with `import` or `export`
- Recognise the canonical patterns for filtering routes from specific upstreams
- Recognise the production failure modes around regex boundaries and the implicit deny
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)
A filter-list is the BGP neighbour’s AS-path-based filter mechanism. It applies an AS-path-list — a regex over the AS_PATH attribute — to the routes flowing in or out of one neighbour address family. It is the complement of the prefix-based filters: a prefix-list matches on the prefix and its length, and answers “which addresses”; a filter-list matches on the AS sequence, and answers “which path did this come by”.
The filter-list is the canonical tool for filtering routes from a specific upstream. The operator who has two upstreams and wants to accept only the routes that came through upstream A uses a filter-list that matches the AS_PATH containing upstream A’s AS number. The filter rejects any route whose AS_PATH does not contain the expected AS.
The regex syntax for matching AS_PATH
The AS-path-list matches against the textual AS_PATH — the same
space-separated sequence of AS numbers you see in the Path
column of show bgp ipv4. The engine is POSIX extended regex
with one addition, and that addition is where almost all of the
confusion lives.
_ is not “any character”
Before compiling the expression, FRR rewrites every _ in it.
From bgpd/bgp_regex.c:
char magic_regexp[] = "(^|[,{}() ]|$)";
So _ is shorthand for “a boundary”: the start of the
string, the end of the string, or one of the characters
, { } ( ) and space. Those brace and parenthesis
characters are not decoration — they are how an AS_SET or a
confederation segment is printed, so _ gets those boundaries
right too.
Two consequences follow, and both are worth holding on to:
_65001_matches AS 65001 anywhere in the path, including as the leftmost AS and as the rightmost AS, because_can match the start and the end of the string. It is the whole-word match, not the middle-only match._is not...really is “any single character”. Writing.65001.where you meant_65001_gives you a regex that requires a character on each side and therefore refuses to match the origin.
The canonical atoms:
| Atom | Meaning | Matches |
|---|---|---|
^ | Start of the AS_PATH | Anchors at the leftmost AS |
$ | End of the AS_PATH | Anchors at the rightmost AS |
_ | A boundary: (^|[,{}() ]|$) | Start, end, space, or an AS_SET/confederation delimiter |
. | Any single character | One character, digit or separator |
[0-9]+ | One or more digits | An AS number |
65001 | The literal characters 65001 | Also matches inside 650010 — this is the classic bug |
.* | Zero or more characters | Anything, including the empty path |
^65001_ | 65001 is the leftmost AS | Routes learned directly from AS 65001 |
_65001$ | 65001 is the rightmost AS | Routes originated by AS 65001 |
_65001_ | 65001 appears as a whole AS anywhere | Routes that traversed AS 65001 |
^$ | The empty AS_PATH | Locally originated routes |
flowchart TD
subgraph AP["AS_PATH: 65001 64512 65010"]
REG1["^65001_ — matches (leftmost is 65001)"]
REG2["_65010$ — matches (rightmost is 65010)"]
REG3["_64512_ — matches (64512 is a whole AS in the path)"]
REG4["_65001_ — ALSO matches (the leading _ matches start-of-string)"]
REG5["^65010 — does NOT match (65010 is not leftmost)"]
REG6["^65001$ — does NOT match (the path is three ASes, not one)"]
end
The fourth line is the one people get wrong. _65001_ is not
restricted to the middle of the path; the leading _ matches
the start of the string just as happily as it matches a space.
The canonical AS-path-list patterns
Pattern 1: Match routes from a specific upstream (AS 65001):
configure
set policy as-path-list FROM-UPSTREAM-A rule 10 action 'permit'
set policy as-path-list FROM-UPSTREAM-A rule 10 regex '_65001_'
set policy as-path-list FROM-UPSTREAM-A rule 10 description 'routes that traversed upstream A'
commit
save
The regex _65001_ matches any AS_PATH in which 65001 appears as a whole AS number. Because _ also matches the start and the end of the string, that covers the leftmost position, the middle, and the origin. It is the pattern for “this route went through AS 65001, wherever in the path that happened”.
Pattern 2: Match routes that originated in a specific AS (AS 65010):
configure
set policy as-path-list FROM-ORIGIN rule 10 action 'permit'
set policy as-path-list FROM-ORIGIN rule 10 regex '_65010$'
set policy as-path-list FROM-ORIGIN rule 10 description 'routes originating in AS 65010'
commit
save
The regex _65010$ matches any AS_PATH whose rightmost AS is 65010 — the origin. The leading _ is what keeps it honest: without it, 65010$ would also match a path ending in 165010.
Pattern 3: Match routes that came through a specific upstream at the leftmost position:
configure
set policy as-path-list FROM-UPSTREAM-A-DIRECT rule 10 action 'permit'
set policy as-path-list FROM-UPSTREAM-A-DIRECT rule 10 regex '^65001_'
set policy as-path-list FROM-UPSTREAM-A-DIRECT rule 10 description 'routes directly from upstream A'
commit
save
The regex ^65001_ matches any AS_PATH whose leftmost AS is 65001, which for an eBGP session means the route came directly from that neighbour rather than being relayed through it. The trailing _ again does the boundary work: ^65001 on its own would also match a path beginning 650012.
Pattern 4: Match routes that did NOT come through a specific upstream (use a deny rule):
configure
set policy as-path-list NOT-FROM-UPSTREAM-A rule 10 action 'deny'
set policy as-path-list NOT-FROM-UPSTREAM-A rule 10 regex '_65001_'
set policy as-path-list NOT-FROM-UPSTREAM-A rule 10 description 'routes that did NOT traverse upstream A'
set policy as-path-list NOT-FROM-UPSTREAM-A rule 20 action 'permit'
set policy as-path-list NOT-FROM-UPSTREAM-A rule 20 regex '.*'
commit
save
Rule 10 denies any route in which 65001 appears as a whole AS. Rule 20 permits everything else. The rules are evaluated in order — VyOS renders the rule number as the FRR seq, so rule ordering is explicit rather than incidental — and the first match wins, which is why the deny has to come first.
Rule 20 is not optional. An AS-path-list ends in an implicit deny, so an inverse filter written with only the deny rule matches nothing and rejects everything. This is the single most common way to lock yourself out of a full table: you meant “everything except AS 65001” and you configured “nothing”.
Applying the filter-list to a BGP neighbour
On VyOS 1.5 the filter-list is bound under the neighbour’s
address family, and the direction is import or export — not
in or out:
configure
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 filter-list import 'FROM-UPSTREAM-A'
commit
save
import filters the routes the peer advertises to the local
router; the local router accepts only the ones whose AS_PATH
matches the AS-path-list.
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list export 'CUSTOMER-ROUTES'
export filters the routes the local router advertises to the
peer; the peer receives only the ones that match.
Three things changed here from the 1.3 form, and all three are commit-time failures rather than silent ones:
- The local ASN is
set protocols bgp system-as 64512, not a level in the path. - The peer is under
set protocols bgp neighbor .... - The filter binding moved under
address-family <af>and takesimport/export. There is noin/outkeyword in this tree, and there is no per-neighbour filter-list outside an address family — which is the right shape, because an AS-path filter for IPv4 has no business applying to your IPv6 sessions.
The interaction with the prefix-list
An AS-path filter and a prefix filter can both apply to the same neighbour and the same direction. They match on different things — the filter-list on the AS_PATH, the prefix-list on the NLRI — and a route must survive both:
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast prefix-list import 'ALLOW-CUSTOMER'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list import 'FROM-UPSTREAM-A'
The local router accepts only routes that:
- Are permitted by the prefix-list
ALLOW-CUSTOMER(NLRI filter). - Are permitted by the AS-path-list
FROM-UPSTREAM-A(AS_PATH filter).
Which tool for which job:
- prefix-list — filter on the prefix and its length.
- filter-list — filter on the AS sequence.
- route-map — anything that needs more than one match condition, or that needs to change an attribute rather than just accept or reject. A route-map can
match as-path <name>and reuse the same AS-path-list, which is how you go from “reject these” to “accept these but with local-preference 50”.
Testing a regex before you commit it
VyOS exposes FRR’s regex search as an operational command, so the matched set can be inspected against the live table before any filter is bound to a neighbour:
show bgp ipv4 regexp _65001_
The command lists every route in the table whose AS_PATH matches
the expression, in the same format as show bgp ipv4. This is
the single most valuable habit in this lesson: the regex is the
part that goes wrong, and this command tells you what it will
select without changing anything.
Once the AS-path-list exists, the complementary command shows what the list itself selects:
show bgp ipv4 filter-list FROM-UPSTREAM-A
A few probes worth running before committing a filter, and what each answers:
# Which routes have 65001 as the leftmost AS?
show bgp ipv4 regexp ^65001_
# Which routes did 65001 originate?
show bgp ipv4 regexp _65001$
# Which routes touched 65001 at all?
show bgp ipv4 regexp _65001_
# Which routes crossed 65001 and then 65010, adjacently?
show bgp ipv4 regexp _65001_65010_
# Which routes are ours?
show bgp ipv4 regexp ^$
Compare the counts. If _65001_ returns dramatically more
routes than ^65001_, most of that traffic is reaching you
through a different neighbour and an import filter on this
session is not the control you thought it was.
The corresponding FRR-side view, when you want to see the compiled list rather than its effect:
vtysh -c 'show bgp as-path-access-list'
The canonical patterns
Pattern 1: Accept only routes from upstream A (inbound):
configure
set policy as-path-list FROM-UPSTREAM-A rule 10 action 'permit'
set policy as-path-list FROM-UPSTREAM-A rule 10 regex '_65001_'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list import 'FROM-UPSTREAM-A'
commit
save
The local router accepts only routes that came through AS 65001.
Pattern 2: Reject routes from upstream B (inbound):
configure
set policy as-path-list NOT-FROM-UPSTREAM-B rule 10 action 'deny'
set policy as-path-list NOT-FROM-UPSTREAM-B rule 10 regex '_65002_'
set policy as-path-list NOT-FROM-UPSTREAM-B rule 20 action 'permit'
set policy as-path-list NOT-FROM-UPSTREAM-B rule 20 regex '.*'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list import 'NOT-FROM-UPSTREAM-B'
commit
save
The local router rejects routes that came through AS 65002 and accepts everything else. Rule 20 is what makes “everything else” mean anything: drop it and the implicit deny at the end of the list rejects the entire table.
Pattern 3: Customer routes — outbound filter to prevent route leaks:
configure
set policy as-path-list CUSTOMER-ROUTES rule 10 action 'permit'
set policy as-path-list CUSTOMER-ROUTES rule 10 regex '^$'
set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list export 'CUSTOMER-ROUTES'
commit
save
The regex ^$ matches the empty AS_PATH, which is what a route
originated by this router carries in the local BGP table —
routes injected with address-family ipv4-unicast network or
generated by an aggregate-address. The export filter therefore
advertises only the local router’s own prefixes and nothing it
learned from anyone else.
The reason ^$ works on export is a timing detail worth being
explicit about: the outbound filter runs against the AS_PATH as
it stands locally, before the local AS is prepended for the
eBGP advertisement. If the filter ran afterwards, ^$ would
never match anything and this whole pattern would be
inexplicable. Debugging tip that follows from it: the path you
see in show bgp ipv4 neighbors 192.0.2.2 advertised-routes
already has the prepend, so do not test your export regex
against that output. Test it with show bgp ipv4 regexp ^$,
which reads the local table.
This is the canonical “send only my own routes” leak-prevention filter. A prefix-list can express the same intent and is easier to audit against an IRR object; the filter-list version does not need updating every time you add a prefix.
The description field on the AS-path-list
The AS-path-list rule takes an optional description, and it is
worth writing — a bare _65001_ two years later tells the next
operator what the rule matches but not why anyone wanted it to:
set policy as-path-list FROM-UPSTREAM-A rule 10 description 'routes that traversed upstream A'
Be clear about where it lives, though. The description is not carried into FRR. VyOS’s policy template emits only
bgp as-path access-list FROM-UPSTREAM-A seq 10 permit _65001_
so the description appears in show configuration commands and
in show policy as-path-list, and is absent from
vtysh -c 'show running-config'. If your debugging habit is to
drop to vtysh, you will not see it there and should not conclude
it was never written.
Note the other half of that rendered line: the VyOS rule number
becomes the FRR seq. Rule numbers are therefore evaluation
order, not labels, and the gap-of-ten convention exists so you
can insert a rule between two existing ones without renumbering
the list.
How the result is validated
show policy as-path-list
show bgp ipv4 regexp _65001_
show bgp ipv4 filter-list FROM-UPSTREAM-A
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
vtysh -c 'show bgp as-path-access-list'
show policy as-path-list is the configured list.
show bgp ipv4 regexp is what a given expression selects from
the live table. show bgp ipv4 filter-list <name> is what the
whole list selects. The three neighbors commands are the
before, after and outbound views — and the received-routes
one has a prerequisite.
Comparing received-routes with routes is the actual proof
that a filter did what you intended: the first is what arrived,
the second is what survived, and the difference is your
filter-list. If both are the same size, the filter is not
attached to the session you think it is.
How it fails
The production failure modes the engineer must recognise:
- The regex has no boundaries. The operator writes
65001instead of_65001_. The bare literal matches the characters65001wherever they appear, including inside650010and165001. The fix is_on both sides, or the appropriate anchor. .used where_was meant..65001.requires a character on each side, so it silently refuses to match the leftmost and the rightmost AS — the two positions you most often care about.- The regex is too permissive.
.*matches everything, including the empty path. As the catch-all rule 20 of an inverse filter that is exactly right; as rule 10 of an import filter it means the filter does nothing at all, and it will look like it is working because routes keep arriving. - The regex is too restrictive.
^65001$matches only a path consisting of the single AS 65001. Real paths are longer, so the filter rejects the whole table. Use^65001_for “learned directly from”,_65001_for “traversed”,_65001$for “originated by”. - The inverse filter has no catch-all. A list with only a
denyrule ends at the implicit deny and rejects everything. This is the same failure as the one above but arrived at from the opposite direction, and it is the more common of the two. - Wrong direction.
importfilters what the peer sends you;exportfilters what you send the peer. Applying an origin-matching filter onexportwhen you meantimportproduces a session that looks healthy and carries nothing. - Wrong tree. A filter-list bound outside
address-family <af>, or written within/outinstead ofimport/export, is rejected at commit. That is the good case. - Rule order. VyOS rule numbers become FRR
seqvalues and first match wins, so apermit .*numbered below adenymakes the deny unreachable. - The list name is referenced but never defined. VyOS’s commit validator catches the dangling reference, so this fails at commit rather than in production.
Rollback
Filter-list changes are configuration changes, and the safe sequence is the ordinary one:
comparebeforecommit, so the diff is on record.commit-confirm 5for any change made over the session the change could break. A filter-list on the wrong session can withdraw your own management route;commit-confirmreverts it for you when you fail to confirm because you are locked out.- To undo a committed change, delete the nodes you added:
configure
delete protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list import 'FROM-UPSTREAM-A'
commit
save
Two cautions on the heavier tools. rollback <N> reverts the
whole configuration to a stored revision and currently
requires a reboot on VyOS — it is not a quiet undo and should
not be the first thing you reach for to remove one filter.
load /config/archive/<file> followed by commit replaces the
entire candidate configuration with the archived one, which
also discards every other change made since that archive was
taken. Both are legitimate; neither is narrow. For a single
filter binding, deleting the node you added is faster and
affects nothing else.
After the rollback, remember that removing an inbound filter
does not by itself bring back the routes it was suppressing. The
peer has already sent them and you discarded them. Either the
session needs a soft reset — reset bgp ipv4 192.0.2.2 soft in
— or soft-reconfiguration inbound has to have been enabled so
the router still has the unfiltered copy to re-evaluate.
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 filter-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), vyos-xxviii-02-prefix-list-config (the prefix-list configuration), and vyos-xxviii-03-distribute-list (the distribute-list) cover the broader context. The lesson vyos-xxviii-06-filter-troubleshoot walks the debugging of filter failures.
Quiz
Knowledge check · 4 questions
Q1. A filter-list is bound to a BGP neighbour address family. What does it match on?
Q2. The regex `_65001_` only matches AS 65001 when it appears in the middle of the AS_PATH, because `_` has to match a space on each side.
Q3. An operator configures an AS-path-list with regex `65001` and no boundaries, intending to match routes that traversed AS 65001. Routes from AS 650010 are also being accepted. Diagnose it and fix it, including how you would verify the fix before committing.
The AS-path-list rule is `set policy as-path-list FROM-UPSTREAM-A rule 10 regex '65001'` and it is bound with `set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list import 'FROM-UPSTREAM-A'`. The peer is advertising paths including `650010 64512 65010` and `165001 64530`. Both are being accepted, because the characters `65001` appear inside both leading AS numbers.
Q4. An operator wants to reject routes that traversed AS 65002 and accept everything else. They configure a single deny rule with regex `_65002_` and bind it inbound. The session stays Established but the prefix count drops to zero. What happened?
The configuration is `set policy as-path-list NOT-FROM-B rule 10 action 'deny'` and `rule 10 regex '_65002_'`, bound with `set protocols bgp neighbor 192.0.2.2 address-family ipv4-unicast filter-list import 'NOT-FROM-B'`. There is no rule 20. `show bgp ipv4 summary` shows the peer Established with 0 prefixes received. Nothing is logged; the commit succeeded.
Passing score: 75%. Answers are checked in this browser.