Hello BIRD maintainers,

I would like to ask for clarification about BIRD's BGP `merge paths`
equivalence rules.

BIRD 2.19.0+ appears to use an AS_PATH-length-based equivalence check for
BGP
routes when kernel `merge paths` is enabled. In the reproduced cases, BIRD
exported one ECMP route from two BGP paths even though:

1. The two eBGP paths came from different neighboring ASes, and their
AS_PATH
   contents were different.
2. The two iBGP paths had different AS_PATH contents.

The common condition was that both paths had the same AS_PATH length and
otherwise equal attributes. This looks similar to AS-path multipath-relax
behavior in implementations such as FRR, but in FRR this relaxed behavior is
enabled by an explicit `bgp bestpath as-path multipath-relax` option. FRR's
default multipath behavior is stricter: eBGP multipath requires the same
remote
AS, and iBGP multipath requires matching AS_PATH contents.

I am reporting this as a route-merging behavior / correctness concern and
documentation clarification request, not as a security vulnerability or a
strict
RFC 4271 violation.

## Version Tested

I reproduced this on:

```text
BIRD version 2.19.0+branch.master.880200b1f94c
```

The PoC intentionally refuses to run against versions older than 2.19.0.

## Background

BIRD's documentation for the kernel protocol says that `merge paths` merges
best
routes and "equivalent non-best routes" to generate one ECMP route:

```text
With path merging enabled, both best routes and equivalent non-best routes
are
merged during export to generate one ECMP (equal-cost multipath) route for
each
network.
```

The question is what counts as BGP-equivalent for this purpose. The current
documentation does not appear to make clear that equivalence can mean equal
AS_PATH length rather than equal AS_PATH contents or same eBGP neighboring
AS.

For comparison, FRR's default behavior is:

- iBGP paths are multipath-equivalent only when AS_PATH contents match.
- eBGP paths are multipath-equivalent only when the remote AS is the same.
- Different AS_PATHs of equal length are allowed only when the explicit
  `bgp bestpath as-path multipath-relax` knob is enabled.

## Expected Behavior / Documentation

I expected BIRD either to avoid merging such paths by default, or to
document
that `merge paths` intentionally uses an AS_PATH-length-based relaxed
equivalence rule.

For example, I did not expect these two eBGP routes to be treated as
ECMP-equivalent unless this relaxed rule is intentional and documented:

```text
Route A: remote AS 65100, AS_PATH 65100 65200
Route B: remote AS 65300, AS_PATH 65300 65400
```

Likewise, I did not expect these two iBGP routes to be treated as
ECMP-equivalent unless AS_PATH-length-based equivalence is the intended
behavior:

```text
Route A: AS_PATH 65010 65020
Route B: AS_PATH 65030 65040
```

If this relaxed behavior is intentional, it may be helpful to document it
explicitly or expose a configuration knob similar to FRR's AS-path multipath
relax option.

## Actual Behavior

With `merge paths yes` enabled, BIRD exported a merged ECMP route in both
cases.

### Case 1: eBGP paths from different neighboring ASes

The PoC announced the same prefix from two synthetic eBGP peers:

```text
Peer A:
  source:    127.0.0.11
  remote AS: 65100
  AS_PATH:   65100 65200
  next-hop:  10.10.0.11

Peer B:
  source:    127.0.0.12
  remote AS: 65300
  AS_PATH:   65300 65400
  next-hop:  10.10.0.12
```

BIRD showed both BGP routes:

```text
203.0.113.0/24       unicast [poc_peer1 ... from 127.0.0.11] ! (100/?)
[AS65200i]
        via 192.0.2.11 on lo onlink
        BGP.origin: IGP
        BGP.as_path: 65100 65200
        BGP.next_hop: 10.10.0.11
        BGP.local_pref: 100
                     unicast [poc_peer2 ... from 127.0.0.12] (100/?)
[AS65400i]
        via 192.0.2.12 on lo onlink
        BGP.origin: IGP
        BGP.as_path: 65300 65400
        BGP.next_hop: 10.10.0.12
        BGP.local_pref: 100
```

The merged export view for the kernel protocol contained both nexthops:

```text
203.0.113.0/24       unicast [poc_peer1 ... from 127.0.0.11] ! (100/?)
[AS65200i]
        via 192.0.2.11 on lo onlink weight 1
        via 192.0.2.12 on lo onlink weight 1
        BGP.origin: IGP
        BGP.as_path: 65100 65200
        BGP.next_hop: 10.10.0.11
        BGP.local_pref: 100
```

### Case 2: iBGP paths with different AS_PATH contents

The PoC also reproduced the behavior with two iBGP peers:

```text
Peer A:
  source:   127.0.0.11
  AS_PATH:  65010 65020
  next-hop: 10.10.0.11

Peer B:
  source:   127.0.0.12
  AS_PATH:  65030 65040
  next-hop: 10.10.0.12
```

BIRD again exported one merged route:

```text
203.0.113.0/24       unicast [poc_peer1 ... from 127.0.0.11] ! (100/?)
[AS65020i]
        via 192.0.2.11 on lo onlink weight 1
        via 192.0.2.12 on lo onlink weight 1
        BGP.origin: IGP
        BGP.as_path: 65010 65020
        BGP.next_hop: 10.10.0.11
        BGP.local_pref: 100
```

## Steps to Reproduce

The attached PoC starts a private BIRD instance, creates two synthetic BGP
peers, advertises the same IPv4 prefix twice, and enables kernel `merge
paths`.

Run the eBGP case:

```sh
python3 poc_bird_bgp_multipath_aspath.py --keep-workdir
```

Run the iBGP case:

```sh
python3 poc_bird_bgp_multipath_aspath.py --case ibgp --keep-workdir
```

Expected reproduced signal:

```text
RESULT: BIRD treated the routes as merge-paths equivalent and produced ECMP
export view.
```

The script exits with status `2` when the behavior is reproduced.

On an unprivileged account, BIRD may log netlink permission errors and the
Linux
kernel table may remain empty. The important evidence is the
`birdc show route export krt_poc 203.0.113.0/24 all` output, because BIRD's
CLI
uses the same `rt_export_merged()` path to construct the merged export
route.

## Source-Level Analysis

BIRD's BGP merge predicate checks AS_PATH length but not full AS_PATH
equality
or same eBGP neighboring AS.

In `proto/bgp/attrs.c`, `bgp_rte_mergable()` checks local preference,
resolvability, LLGR stale state, AS_PATH length, origin, MED, peer type,
and IGP
metric:

```c
/* RFC 4271 9.1.2.2. a)  Use AS path lengths */
if (pri_bgp->cf->compare_path_lengths || sec_bgp->cf->compare_path_lengths)
{
  x = ea_find(pri->attrs->eattrs, EA_CODE(PROTOCOL_BGP, BA_AS_PATH));
  y = ea_find(sec->attrs->eattrs, EA_CODE(PROTOCOL_BGP, BA_AS_PATH));
  p = x ? as_path_getlen(x->u.ptr) : AS_PATH_MAXLEN;
  s = y ? as_path_getlen(y->u.ptr) : AS_PATH_MAXLEN;

  if (p != s)
    return 0;
}
```

It prevents mixing iBGP and eBGP paths:

```c
/* RFC 4271 9.1.2.2. d) Prefer external peers */
if (pri_bgp->is_interior != sec_bgp->is_interior)
  return 0;
```

But after checking IGP metric, the remaining criteria are ignored:

```c
/* Remaining criteria are ignored */

return 1;
```

I did not find a check equivalent to:

- for iBGP: full AS_PATH contents must match, or
- for eBGP: neighboring AS must match.

## Comparison with FRR

FRR explicitly distinguishes relaxed and non-relaxed multipath behavior.

In its default mode:

```c
} else if (peer_new->sort == BGP_PEER_IBGP) {
  if (aspath_cmp(new->attr->aspath, exist->attr->aspath)) {
    *paths_eq = 1;
  }
} else if (peer_new->as == peer_exist->as) {
  *paths_eq = 1;
}
```

FRR only allows different AS_PATH contents of the same length when the
explicit
AS-path multipath relax flag is configured:

```c
} else if (CHECK_FLAG(bgp->flags, BGP_FLAG_ASPATH_MULTIPATH_RELAX)) {
  *paths_eq = 1;
}
```

This makes the relaxed behavior visible to operators as an intentional
configuration choice.

## Impact

When `merge paths` is enabled, BIRD may export ECMP routes whose
contributing
BGP paths would not be considered multipath-equivalent by implementations
such
as FRR unless an explicit AS-path multipath-relax option is enabled.

Potential consequences include:

1. Traffic sharing across different upstream ASes when the operator expected
   same-AS eBGP multipath only.
2. Traffic sharing across iBGP paths that have different AS_PATH contents.
3. Policy surprises when one AS_PATH would normally be considered
   non-equivalent to another even though their lengths match.
4. Interoperability differences compared with implementations such as FRR
that
   require explicit AS-path multipath relaxation.

This impact is configuration-dependent. It requires `merge paths` or
equivalent
multipath export behavior to be enabled. It is not a session-reset issue
and is
not being reported as a security vulnerability.

## Suggested Fix or Clarification

If the current behavior is unintentional, BIRD could make
`bgp_rte_mergable()`
stricter by default:

1. For iBGP routes, require full AS_PATH equality before merging.
2. For eBGP routes, require the same neighboring AS before merging.
3. Optionally add a configuration knob to allow the current relaxed behavior
   explicitly, similar to FRR's `bgp bestpath as-path multipath-relax`.

If the current behavior is intentional, documenting that `merge paths` may
merge
BGP routes with different AS_PATH contents or different eBGP neighboring
ASes
when AS_PATH lengths are equal would help operators understand the policy
impact.

In other words, this may simply be intentional relaxed multipath behavior.
I am
reporting it because the documentation says "equivalent non-best routes" but
does not appear to define that equivalence as AS_PATH-length-based rather
than
AS_PATH-content-based or same-neighboring-AS-based.

Best regards.

Reply via email to