Skip to main content
RunBook Academy

VyOSXXVIII · BGP Prefix FilteringPrefix filters

AS-path filter-list — regex matching on AS_PATH for BGP neighbour filtering

Advanced⏱ ~18 minset policy as-path-listshow policy as-path-listshow bgp ipv4 regexpshow bgp ipv4 filter-listshow bgp ipv4 neighbors <ip> received-routesshow bgp ipv4 neighbors <ip> routesshow bgp ipv4 neighbors <ip> advertised-routesvtysh -c show bgp as-path-access-list

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)

Not yet marked complete on this device.

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:

AtomMeaningMatches
^Start of the AS_PATHAnchors at the leftmost AS
$End of the AS_PATHAnchors at the rightmost AS
_A boundary: (^|[,{}() ]|$)Start, end, space, or an AS_SET/confederation delimiter
.Any single characterOne character, digit or separator
[0-9]+One or more digitsAn AS number
65001The literal characters 65001Also matches inside 650010 — this is the classic bug
.*Zero or more charactersAnything, including the empty path
^65001_65001 is the leftmost ASRoutes learned directly from AS 65001
_65001$65001 is the rightmost ASRoutes originated by AS 65001
_65001_65001 appears as a whole AS anywhereRoutes that traversed AS 65001
^$The empty AS_PATHLocally 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 takes import / export. There is no in / out keyword 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:

  1. Are permitted by the prefix-list ALLOW-CUSTOMER (NLRI filter).
  2. 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 65001 instead of _65001_. The bare literal matches the characters 65001 wherever they appear, including inside 650010 and 165001. 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 deny rule 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. import filters what the peer sends you; export filters what you send the peer. Applying an origin-matching filter on export when you meant import produces a session that looks healthy and carries nothing.
  • Wrong tree. A filter-list bound outside address-family <af>, or written with in/out instead of import/export, is rejected at commit. That is the good case.
  • Rule order. VyOS rule numbers become FRR seq values and first match wins, so a permit .* numbered below a deny makes 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:

  • compare before commit, so the diff is on record.
  • commit-confirm 5 for any change made over the session the change could break. A filter-list on the wrong session can withdraw your own management route; commit-confirm reverts 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

  1. Q1. A filter-list is bound to a BGP neighbour address family. What does it match on?

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

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

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