Missing route that is shown in filter used by eBGP
Hello, today I have a related issue with missing route export: - In a bgp instance I use the filter "from_our_place_to_internet" - If I run "show route filter from_our_place_to_internet" all correct routes match - However if I check on the other eBGP end, the route is actually *not* received (it's not filtered, it is really not received) - If I "hack" the filter as follows, bird DOES export the route: # Hack 2025-11-20 if ( net ~ [ 185.203.114.0/23+ ] ) then { print "Exporting 185.203.114.0 to internet"; accept; } - Some thoughts: - The route is direct, static and babel received - Our filter matches RTS_STATIC - However bird seems to "prefer" the direct route - We do not export direct marked routes, but static routes - Is that the cause of the problem? But it is also static... However it does not make sense to me, thus wondering: - Why does bird show the route in the filter output? - Why does bird show not export it via eBGP? - Why does my hack then make it work in the end? Details below, any help appreciated as this is driving me nuts. Best regards, Nico -------------------------------------------------------------------------------- a) the affected route bird> show route all 185.203.114.0/23 Table master4: 185.203.114.0/23 unicast [direct1 2025-11-07] * (240) dev eth4 Type: device univ unicast [place6_10_v4 2025-11-07] (200) dev eth4 Type: static univ unicast [babel1 12:47:44.565 from fe80::3eec:efff:fecb:d81b] (130/96) [00:00:00:00:93:4e:c2:43] via 147.78.194.67 on eth1 Type: Babel univ Babel.metric: 96 Babel.router_id: 00:00:00:00:93:4e:c2:43 bird> b) the show route output bird> show route filter from_our_place_to_internet Table master4: 185.203.114.0/23 unicast [place6_10_v4 2025-11-07] (200) dev eth4 ... c) the filter itself filter from_our_place_to_internet { if(!has_internet_length()) then reject; if(is_default_route()) then reject; if(is_from_local_asn() || is_emitted_by_one_of_our_asn_directly() || should_export_to_internet_from_customer() ) then accept; reject; } ... where the related functions are: function has_internet_length() -> bool { if ((net.type = NET_IP6 && net.len > 48) || (net.type = NET_IP4 && net.len > 24)) then { return false; } return true; } function is_v6_default() -> bool { if (net = ::/0) then return true; return false; } function is_v4_default() -> bool { if (net = 0.0.0.0/0) then return true; return false; } function is_default_route() -> bool { if(is_v6_default() || is_v4_default()) then return true; return false; } function is_from_local_asn() -> bool { if(source = RTS_STATIC) then return true; if(source = RTS_BGP && bgp_path.len = 0) then return true; return false; } function emitted_from_one_of_our_asn() -> bool { if(bgp_path.last ~ our_asn) then return true; return false; } function should_export_to_internet_from_customer() -> bool { # We don't consider non-internet networks if(!has_internet_length()) then return false; if ( (bgp_path.first ~ our_asn) && # received from one of our asn (bgp_path.len = 2) && # one of our ASNs received it (bgp_path.last ~ customer_asn) # one of our customers emitted it and we accept it ) then return true; if ( (bgp_path.len = 1) && # we ASNs received it directly (bgp_path.last ~ customer_asn) # one of our customers emitted it and we accept it ) then return true; return false; } -- Sustainable and modern Infrastructures by ungleich.ch
Hello Nico, unless you have BGP TX Add-Path, you are exporting only the best route, which is the direct one in this case, and that's filtered out. I would suggest to drop the direct protocol preference under the static protocol, at least for this route. Or raising the preference in the static, you may say something like route 185.203.114.0/23 dev eth4 { preference = 250; }; for just this one route and i hope that i haven't botched our own syntax. This should make it work. Maria On Thu, Nov 20, 2025 at 01:59:48PM +0100, Nico Schottelius via Bird-users wrote:
Hello,
today I have a related issue with missing route export:
- In a bgp instance I use the filter "from_our_place_to_internet" - If I run "show route filter from_our_place_to_internet" all correct routes match - However if I check on the other eBGP end, the route is actually *not* received (it's not filtered, it is really not received) - If I "hack" the filter as follows, bird DOES export the route: # Hack 2025-11-20 if ( net ~ [ 185.203.114.0/23+ ] ) then { print "Exporting 185.203.114.0 to internet"; accept; }
- Some thoughts: - The route is direct, static and babel received - Our filter matches RTS_STATIC - However bird seems to "prefer" the direct route - We do not export direct marked routes, but static routes - Is that the cause of the problem? But it is also static...
However it does not make sense to me, thus wondering:
- Why does bird show the route in the filter output? - Why does bird show not export it via eBGP? - Why does my hack then make it work in the end?
Details below, any help appreciated as this is driving me nuts.
Best regards,
Nico
--------------------------------------------------------------------------------
a) the affected route
bird> show route all 185.203.114.0/23 Table master4: 185.203.114.0/23 unicast [direct1 2025-11-07] * (240) dev eth4 Type: device univ unicast [place6_10_v4 2025-11-07] (200) dev eth4 Type: static univ unicast [babel1 12:47:44.565 from fe80::3eec:efff:fecb:d81b] (130/96) [00:00:00:00:93:4e:c2:43] via 147.78.194.67 on eth1 Type: Babel univ Babel.metric: 96 Babel.router_id: 00:00:00:00:93:4e:c2:43 bird>
b) the show route output
bird> show route filter from_our_place_to_internet Table master4: 185.203.114.0/23 unicast [place6_10_v4 2025-11-07] (200) dev eth4 ...
c) the filter itself
filter from_our_place_to_internet { if(!has_internet_length()) then reject; if(is_default_route()) then reject;
if(is_from_local_asn() || is_emitted_by_one_of_our_asn_directly() || should_export_to_internet_from_customer() ) then accept;
reject;
}
... where the related functions are:
function has_internet_length() -> bool { if ((net.type = NET_IP6 && net.len > 48) || (net.type = NET_IP4 && net.len > 24)) then { return false; } return true; }
function is_v6_default() -> bool { if (net = ::/0) then return true; return false; }
function is_v4_default() -> bool { if (net = 0.0.0.0/0) then return true; return false; }
function is_default_route() -> bool { if(is_v6_default() || is_v4_default()) then return true; return false; }
function is_from_local_asn() -> bool { if(source = RTS_STATIC) then return true; if(source = RTS_BGP && bgp_path.len = 0) then return true;
return false; }
function emitted_from_one_of_our_asn() -> bool { if(bgp_path.last ~ our_asn) then return true; return false; }
function should_export_to_internet_from_customer() -> bool { # We don't consider non-internet networks if(!has_internet_length()) then return false;
if ( (bgp_path.first ~ our_asn) && # received from one of our asn (bgp_path.len = 2) && # one of our ASNs received it (bgp_path.last ~ customer_asn) # one of our customers emitted it and we accept it ) then return true;
if ( (bgp_path.len = 1) && # we ASNs received it directly (bgp_path.last ~ customer_asn) # one of our customers emitted it and we accept it ) then return true;
return false; }
-- Sustainable and modern Infrastructures by ungleich.ch
-- Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
Good morning Maria, thanks for the reply, but I am a bit puzzled, in multiple cases: - a) active vs. all routes I was under the impression that filtering allows me to select the best route, i.e. filters apply on "all routes", not on "best routes". Shouldn't it be the case that filters in general operate on all, not already filtered routes? - b) filter output vs. export reality Independent of whether we filter on "all best" or "all routes", in the CLI the filter output shown does include the route - if the filter does not match, it also should not print it - Route attribute background Maybe for understanding *why* the route exists as it exists: - Using a match on RTS_STATIC is somewhat universal and can be used in all of our data centers, as *if* we define a static route, we do want to eBGP export it - The main reason why it was defined in this case to be static is to allow the filter to grab it - otherwise it would actually not be needed at all - The main motivation is to reduce the number of special cases for selecting internet facing routes in the same way everywhere - c) Multi attribute matching So the effective route is seen via direct, static and babel. My assumption in bird would be that I can select (filter) the route using one of the three protocol RTS_ matches to find and export the route. So what I am trying to say is that as a user I'd expect the union of attributes to be matching and not only the the current/best selected. Which seems to be what the filter output in the CLI is doing, versus what bird exports. Does it make sense? Best regards, Nico Maria Matejka via Bird-users <bird-users@network.cz> writes:
Hello Nico,
unless you have BGP TX Add-Path, you are exporting only the best route, which is the direct one in this case, and that’s filtered out.
I would suggest to drop the direct protocol preference under the static protocol, at least for this route. Or raising the preference in the static, you may say something like
route 185.203.114.0/23 dev eth4 { preference = 250; };
for just this one route and i hope that i haven’t botched our own syntax.
This should make it work.
Maria
On Thu, Nov 20, 2025 at 01:59:48PM +0100, Nico Schottelius via Bird-users wrote:
Hello,
today I have a related issue with missing route export:
* In a bgp instance I use the filter “from_our_place_to_internet”
* If I run “show route filter from_our_place_to_internet” all correct routes match
* However if I check on the other eBGP end, the route is actually not received (it’s not filtered, it is really not received)
* If I “hack” the filter as follows, bird DOES export the route: # Hack 2025-11-20 if ( net ~ [ 185.203.114.0/23+ ] ) then { print “Exporting 185.203.114.0 to internet”; accept; }
* Some thoughts:
* The route is direct, static and babel received * Our filter matches RTS_STATIC * However bird seems to “prefer” the direct route * We do not export direct marked routes, but static routes * Is that the cause of the problem? But it is also static…
However it does not make sense to me, thus wondering:
* Why does bird show the route in the filter output? * Why does bird show not export it via eBGP? * Why does my hack then make it work in the end?
Details below, any help appreciated as this is driving me nuts.
Best regards,
Nico
-------------------------------------------------------------------------------------------------------------------------
1 the affected route
bird> show route all 185.203.114.0/23 Table master4: 185.203.114.0/23 unicast [direct1 2025-11-07] * (240) dev eth4 Type: device univ unicast [place6_10_v4 2025-11-07] (200) dev eth4 Type: static univ unicast [babel1 12:47:44.565 from fe80::3eec:efff:fecb:d81b] (130/96) [00:00:00:00:93:4e:c2:43] via 147.78.194.67 on eth1 Type: Babel univ Babel.metric: 96 Babel.router_id: 00:00:00:00:93:4e:c2:43 bird>
2 the show route output
bird> show route filter from_our_place_to_internet Table master4: 185.203.114.0/23 unicast [place6_10_v4 2025-11-07] (200) dev eth4 …
3 the filter itself
filter from_our_place_to_internet { if(!has_internet_length()) then reject; if(is_default_route()) then reject;
if(is_from_local_asn() || is_emitted_by_one_of_our_asn_directly() || should_export_to_internet_from_customer() ) then accept;
reject;
}
… where the related functions are:
function has_internet_length() -> bool { if ((net.type = NET_IP6 && net.len > 48) || (net.type = NET_IP4 && net.len > 24)) then { return false; } return true; }
function is_v6_default() -> bool { if (net = ::/0) then return true; return false; }
function is_v4_default() -> bool { if (net = 0.0.0.0/0) then return true; return false; }
function is_default_route() -> bool { if(is_v6_default() || is_v4_default()) then return true; return false; }
function is_from_local_asn() -> bool { if(source = RTS_STATIC) then return true; if(source = RTS_BGP && bgp_path.len = 0) then return true;
return false; }
function emitted_from_one_of_our_asn() -> bool { if(bgp_path.last ~ our_asn) then return true; return false; }
function should_export_to_internet_from_customer() -> bool { # We don’t consider non-internet networks if(!has_internet_length()) then return false;
if ( (bgp_path.first ~ our_asn) && # received from one of our asn (bgp_path.len = 2) && # one of our ASNs received it (bgp_path.last ~ customer_asn) # one of our customers emitted it and we accept it ) then return true;
if ( (bgp_path.len = 1) && # we ASNs received it directly (bgp_path.last ~ customer_asn) # one of our customers emitted it and we accept it ) then return true;
return false; }
– Sustainable and modern Infrastructures by ungleich.ch
– Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
-- Sustainable and modern Infrastructures by ungleich.ch
Good morning Nico, On Fri, Nov 21, 2025 at 09:29:12AM +0100, Nico Schottelius wrote:
thanks for the reply, but I am a bit puzzled, in multiple cases:
Oh that's ok. BIRD is complex.
- a) active vs. all routes I was under the impression that filtering allows me to select the best route, i.e. filters apply on "all routes", not on "best routes".
Shouldn't it be the case that filters in general operate on all, not already filtered routes?
Well, well, yes and no. Filtering on … - import allows you to select whether a route enters the best route selection process - export to BGP, Babel, OSPF, RIP, Kernel allows you to select whether the best route passes into the protocol - export to Pipe and BGP with Add Path TX allows you to select which of the routes pass into the protocol, regardless of the best route selected - export to BGP with Secondary option allows you to walk over the route list and pick the first route to pass the filter We expect to add some more options to make this even more powerful, e.g. an additional filter block after the iBGP nexthop resolution, or maybe even some customization of best route selection. This should ~~confuse people even more~~ allow people better customization. With that, if you wish to export to BGP "first which passes", you can use `secondary` in the channel config. You need a `sorted` table to do that, iirc.
- b) filter output vs. export reality Independent of whether we filter on "all best" or "all routes", in the CLI the filter output shown does include the route - if the filter does not match, it also should not print it
Well, if you do `show route filter xyz`, then you get all routes matching that filter. Some protocols pick them. You can do `show route export bgp_to_internet.ipv6` to explicitly re-run the export filter in that context. I know that it is kinda confusing, and we should probably improve our documentation to show this properly.
- Route attribute background
Maybe for understanding *why* the route exists as it exists:
- Using a match on RTS_STATIC is somewhat universal and can be used in all of our data centers, as *if* we define a static route, we do want to eBGP export it - The main reason why it was defined in this case to be static is to allow the filter to grab it - otherwise it would actually not be needed at all - The main motivation is to reduce the number of special cases for selecting internet facing routes in the same way everywhere
With that, I would simply lower the preference of `protocol direct` then under 200, maybe to something like 180 which is still more than default babel but less than static, and that should fix your problem. The direct protocol default preference is kinda unreasonably high in your case.
- c) Multi attribute matching So the effective route is seen via direct, static and babel. My assumption in bird would be that I can select (filter) the route using one of the three protocol RTS_ matches to find and export the route.
That is possible with the `sorted` table option and `secondary` bgp channel option.
So what I am trying to say is that as a user I'd expect the union of attributes to be matching and not only the the current/best selected.
Then we failed to set the user expectation correctly.
Which seems to be what the filter output in the CLI is doing, versus what bird exports.
Yup, because e.g. Pipe takes all while BGP takes best by default, and the show route output does not know which export behavior you prefer. To show only best routes, you can add `primary` to the `show route` command.
Does it make sense?
Yes. Welcome to the group of people who got confused by our UI multiple times! We should definitely make it less confusing for future. Thanks for all of this, it's a very valuable feedback for us. Maria -- Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
Hi folks, Anno domini 2025 Maria Matejka via Bird-users scripsit:
- b) filter output vs. export reality Independent of whether we filter on "all best" or "all routes", in the CLI the filter output shown does include the route - if the filter does not match, it also should not print it
Well, if you do `show route filter xyz`, then you get all routes matching that filter. Some protocols pick them. You can do `show route export bgp_to_internet.ipv6` to explicitly re-run the export filter in that context.
I know that it is kinda confusing, and we should probably improve our documentation to show this properly.
A `show route primary filter xyz` should also be equivalent to what's actually exported, right? Kind regards, Max
On Fri, Nov 21, 2025 at 01:37:57PM +0100, Maximilian Wilhelm via Bird-users wrote:
Hi folks,
Anno domini 2025 Maria Matejka via Bird-users scripsit:
- b) filter output vs. export reality Independent of whether we filter on "all best" or "all routes", in the CLI the filter output shown does include the route - if the filter does not match, it also should not print it
Well, if you do `show route filter xyz`, then you get all routes matching that filter. Some protocols pick them. You can do `show route export bgp_to_internet.ipv6` to explicitly re-run the export filter in that context.
I know that it is kinda confusing, and we should probably improve our documentation to show this properly.
A `show route primary filter xyz` should also be equivalent to what's actually exported, right?
No, because a protocol could reject (or even accept) a route on its own, regardless of its export filter. For example an IBGP protocol rejects routes recived from other IBGP protocols. -- Elen sila lumenn' omentielvo Ondrej 'Santiago' Zajicek (email: santiago@crfreenet.org) "To err is human -- to blame it on a computer is even more so."
¡Hola! On Fri, Nov 21, 2025 at 01:37:57PM +0100, Maximilian Wilhelm via Bird-users wrote:
- b) filter output vs. export reality Independent of whether we filter on "all best" or "all routes", in the CLI the filter output shown does include the route - if the filter does not match, it also should not print it
Well, if you do `show route filter xyz`, then you get all routes matching that filter. Some protocols pick them. You can do `show route export bgp_to_internet.ipv6` to explicitly re-run the export filter in that context.
I know that it is kinda confusing, and we should probably improve our documentation to show this properly.
A `show route primary filter xyz` should also be equivalent to what's actually exported, right?
Almost! It is, in that case, equivalent to what _should_ be exported if there was no preexport limitation. The most prominent case is back-export to Pipe and BGP; these two protocols reject their own routes, while others (e.g. Babel) accept them. With that, you need this: show route primary filter xyz preexport bgp_to_internet.ipv6 Starting with BIRD 3, some routes might be also export-pending because BGP runs in different threads than the CLI. This isn't reflected in any output for now because that information is performance sensitive and almost impossible to access ad-hoc safely. We also expect to add protocol contexts, and as soon as this happens, you would need something like show route primary filter xyz preexport bgp_to_internet.ipv6 context { ... } to match `show route export bgp_to_internet.ipv6` output. Last but not least, if you change your filters but call `configure soft`, the affected channels do not reload at all, and therefore to find out which routes were actually exported, you may ask for show route exported bgp_to_internet.ipv6 but if the export filter did any changes, it's already lost because the export filter is forgotten with the reconfiguration. Only if you have `export table on`, then you may ask for show route export table bgp_to_internet.ipv6 and it actually shows not only what has been exported but also it may tell you what has been exported but not yet sent to the peer. Not only that, in BIRD 3, this call works always, and with `export table off`, it shows exactly the routes which have not yet been sent into the TCP socket. And that's been quite an infodump which may have left you even more confused but now at least it's dumped. We should definitely do some major updates to the documentation. Maria -- Maria Matejka (she/her) | BIRD Team Leader | CZ.NIC, z.s.p.o.
Olá, Anno domini 2025 Maria Matejka scripsit: [...]
And that's been quite an infodump which may have left you even more confused but now at least it's dumped.
We should definitely do some major updates to the documentation.
Thanks Maria and Ondrej, that was very insightful and makes sense, when thinking about it, so no confusion here :-D Kind regards, Max
participants (4)
-
Maria Matejka -
Maximilian Wilhelm -
Nico Schottelius -
Ondrej Zajicek