Skip to content

ADD PATH

Thomas Mangin edited this page Oct 8, 2026 · 11 revisions

ADD-PATH

ADD-PATH is a BGP capability (RFC 7911) that allows a BGP speaker to advertise multiple paths for the same NLRI (Network Layer Reachability Information) without the need for additional BGP sessions. ExaBGP provides full ADD-PATH support for most address families, enabling path diversity, multipath load balancing, and improved convergence in BGP networks.

Table of Contents


What is ADD-PATH?

ADD-PATH (defined in RFC 7911) enables BGP to advertise multiple paths for the same network prefix to a single peer. Without ADD-PATH, BGP can only send one "best path" per prefix to each neighbor, even if multiple viable paths exist.

The Problem Without ADD-PATH

Traditional BGP behavior:

Route Reflector (RR)
       │
       ├─ Receives 3 paths for 10.0.0.0/8:
       │  1. via AS 100 (best path)
       │  2. via AS 200
       │  3. via AS 300
       │
       └─ Advertises ONLY best path to clients:
          └─▶ Client 1: 10.0.0.0/8 via AS 100
          └─▶ Client 2: 10.0.0.0/8 via AS 100
          └─▶ Client 3: 10.0.0.0/8 via AS 100

Result: Clients lose path diversity!

Problems:

  • Path diversity loss: Clients only see RR's best path, not alternatives
  • Suboptimal routing: Client's best path may differ from RR's best path
  • BGP wedgies: Persistent routing loops in certain topologies
  • Slow convergence: Only one path available; failure requires full re-convergence

The Solution With ADD-PATH

With ADD-PATH enabled:

Route Reflector (RR) with ADD-PATH
       │
       ├─ Receives 3 paths for 10.0.0.0/8:
       │  1. via AS 100 (Path ID: 1)
       │  2. via AS 200 (Path ID: 2)
       │  3. via AS 300 (Path ID: 3)
       │
       └─ Advertises ALL paths to clients:
          └─▶ Client 1: 10.0.0.0/8 via AS 100 (ID:1)
                        10.0.0.0/8 via AS 200 (ID:2)
                        10.0.0.0/8 via AS 300 (ID:3)

Result: Clients have full path visibility and diversity!

Benefits:

  • ✅ Path diversity preserved: Clients see multiple paths
  • ✅ Optimal routing: Each client selects its own best path
  • ✅ Faster convergence: Backup paths pre-installed
  • ✅ Load balancing: Multipath support for traffic engineering

How ADD-PATH Works

Path Identifier Encoding

ADD-PATH extends BGP UPDATE messages to include a Path Identifier for each NLRI:

Traditional BGP UPDATE:
┌─────────────────────────────────┐
│ Prefix: 10.0.0.0/8              │
│ Next-hop: 192.168.1.1           │
│ AS Path: 100 200                │
└─────────────────────────────────┘

ADD-PATH BGP UPDATE:
┌─────────────────────────────────┐
│ Path ID: 1                      │  ← NEW: Unique identifier
│ Prefix: 10.0.0.0/8              │
│ Next-hop: 192.168.1.1           │
│ AS Path: 100 200                │
└─────────────────────────────────┘

Multiple paths for same prefix:
┌─────────────────────────────────┐
│ Path ID: 1                      │
│ Prefix: 10.0.0.0/8              │
│ Next-hop: 192.168.1.1           │
│ AS Path: 100 200                │
└─────────────────────────────────┘

┌─────────────────────────────────┐
│ Path ID: 2                      │
│ Prefix: 10.0.0.0/8              │
│ Next-hop: 192.168.1.2           │
│ AS Path: 100 300                │
└─────────────────────────────────┘

Key Points:

  • Path ID: 32-bit unsigned integer uniquely identifying the path
  • Per-prefix uniqueness: Path IDs must be unique per prefix, per peer
  • Opaque value: No semantic meaning; just an identifier
  • Sender-assigned: Advertising router assigns Path IDs

ADD-PATH Capability Negotiation

[Session Establishment with ADD-PATH]

ExaBGP                                  BGP Peer
   │                                        │
   │──── OPEN ─────────────────────────────▶│
   │     Capabilities:                      │
   │     - ADD-PATH:                        │
   │       AFI: IPv4 (1)                    │
   │       SAFI: Unicast (1)                │
   │       Send/Receive: Both (3)           │
   │                                        │
   │◀──── OPEN ─────────────────────────────│
   │     Capabilities:                      │
   │     - ADD-PATH:                        │
   │       AFI: IPv4 (1)                    │
   │       SAFI: Unicast (1)                │
   │       Send/Receive: Both (3)           │
   │                                        │
   │──── KEEPALIVE ────────────────────────▶│
   │◀──── KEEPALIVE ────────────────────────│
   │                                        │
   │  ADD-PATH negotiated for IPv4 unicast  │
   │  Both peers can send/receive multiple  │
   │  paths per prefix                      │

Send/Receive Values:

  • 1: Receive only
  • 2: Send only
  • 3: Send and Receive (both)

Per-AFI/SAFI: ADD-PATH is negotiated separately for each address family.


ExaBGP ADD-PATH Capabilities

ExaBGP implements RFC 7911 ADD-PATH for the ten families listed below:

✅ Send/Receive Support:

  • Send multiple paths per prefix
  • Receive multiple paths per prefix
  • Send+Receive mode (bidirectional)

✅ Supported Address Families:

  • ✅ IPv4 Unicast
  • ✅ IPv6 Unicast
  • ✅ IPv4 Labelled Unicast (ipv4 nlri-mpls)
  • ✅ IPv6 Labelled Unicast (ipv6 nlri-mpls)
  • ✅ VPNv4 (L3VPN)
  • ✅ VPNv6 (L3VPN)
  • ✅ IPv4 FlowSpec (ipv4 flow, 6.0.0+)
  • ✅ IPv6 FlowSpec (ipv6 flow, 6.0.0+)
  • ✅ IPv4 FlowSpec VPN (ipv4 flow-vpn, 6.0.0+)
  • ✅ IPv6 FlowSpec VPN (ipv6 flow-vpn, 6.0.0+)

❌ Not Supported:

  • ❌ IPv4 and IPv6 Multicast
  • ❌ VPLS
  • ❌ EVPN
  • ❌ MVPN
  • ❌ MUP
  • ❌ SR-Policy
  • ❌ Route Target Constraint (RTC)
  • ❌ BGP-LS

These are the ten families ExaBGP offers in the ADD-PATH capability. You can name any family in add-path, but ExaBGP only puts the ten above into the OPEN, and ADD-PATH is only active for a family both OPENs carried. Earlier versions of this page listed multicast and VPLS as supported; they never were.

The four FlowSpec families were added in 6.0.0 (issue #1140). Before that, ipv4 flow and the other flow families were accepted in add-path and then silently left out of the OPEN, so ADD-PATH was never negotiated for them. See FlowSpec with ADD-PATH.

MUP was in the list and has been taken out. Its encoder writes no Path Identifier, so offering ADD-PATH for MUP told the peer to expect four octets in front of every MUP NLRI which ExaBGP then did not send, and the peer mis-framed the rest of the field.

✅ Path Identifier Management:

  • Path ID set per route with path-information, in the configuration or via the API
  • No automatic assignment: a route without path-information is sent with Path ID 0 when ADD-PATH send is negotiated for its family
  • Per-prefix Path ID tracking

✅ API Integration:

  • Advertise routes with Path IDs
  • Receive routes with Path IDs
  • Withdraw specific paths by ID

Implementation:

  • src/exabgp/bgp/message/open/capability/addpath.py (capability)
  • src/exabgp/bgp/neighbor/neighbor.py (neighbour settings)
  • src/exabgp/bgp/message/open/capability/capabilities.py (which families are offered)
  • NLRI encoding/decoding with Path IDs (src/exabgp/bgp/message/update/nlri/inet.py, src/exabgp/bgp/message/update/nlri/flow.py)

RFC Compliance: RFC 7911 fully implemented


Why Use ADD-PATH?

Advantages

Benefit Without ADD-PATH With ADD-PATH
Path Diversity Only best path visible All paths visible
Routing Optimality RR's best path only Each router picks best
Convergence Single path; slow failover Multiple paths; fast failover
Load Balancing Limited (ECMP same AS-path) Flexible (multiple AS-paths)
BGP Wedgies Possible Prevented
Path Exploration Limited Full topology visibility

Use Cases Summary

  1. Route Reflector deployments: Preserve path diversity to clients
  2. Multipath load balancing: Advertise multiple equal-cost paths
  3. Fast convergence: Pre-install backup paths for sub-second failover
  4. Anycast services: Advertise from multiple locations simultaneously
  5. Path diversity for resilience: Multiple independent paths for redundancy

Configuration

Basic ADD-PATH Configuration

Enable ADD-PATH for IPv4 unicast (send and receive):

neighbor 192.168.1.1 {
    router-id 192.168.1.2;
    local-address 192.168.1.2;
    local-as 65001;
    peer-as 65000;

    capability {
        add-path send/receive;  # Enable ADD-PATH bidirectionally
    }

    family {
        ipv4 unicast;
    }

    api {
        processes [ announce-paths ];
    }
}

process announce-paths {
    run python3 /etc/exabgp/api/addpath.py;
    encoder text;
}

Notes:

  • send/receive: Bidirectional ADD-PATH (most common)
  • ADD-PATH applies to all configured families which support it (see the list above)

Send-Only ADD-PATH

Only send multiple paths (don't receive):

neighbor 192.168.1.1 {
    router-id 192.168.1.2;
    local-address 192.168.1.2;
    local-as 65001;
    peer-as 65000;

    capability {
        add-path send;  # Send multiple paths only
    }

    family {
        ipv4 unicast;
    }
}

When to use:

  • ExaBGP advertises multiple paths (e.g., Route Reflector)
  • Peer only needs to receive paths, not send multiple

Receive-Only ADD-PATH

Only receive multiple paths (don't send):

neighbor 192.168.1.1 {
    router-id 192.168.1.2;
    local-address 192.168.1.2;
    local-as 65001;
    peer-as 65000;

    capability {
        add-path receive;  # Receive multiple paths only
    }

    family {
        ipv4 unicast;
    }
}

When to use:

  • ExaBGP receives multiple paths from peer
  • ExaBGP only sends single best path

Multiple Address Families

ADD-PATH for IPv4 and IPv6:

neighbor 2001:db8::1 {
    router-id 192.168.1.2;
    local-address 2001:db8::2;
    local-as 65001;
    peer-as 65000;

    capability {
        add-path send/receive;
    }

    family {
        ipv4 unicast;
        ipv6 unicast;
        ipv4 flow;
        l2vpn vpls;  # carried on the session, but never with ADD-PATH
    }
}

Notes:

  • ADD-PATH negotiated per AFI/SAFI
  • With no add-path { } block, every configured family is a candidate
  • Not all families support ADD-PATH (see the list above). Here the capability covers the two unicast families and, since 6.0.0, ipv4 flow, but not l2vpn vpls

FlowSpec with ADD-PATH (6.0.0+)

ipv4 flow, ipv6 flow, ipv4 flow-vpn and ipv6 flow-vpn can be put in add-path { }, and ADD-PATH is negotiated for them (issue #1140). A flow route takes path-information <id> next to rd in the route block, not inside match:

neighbor 127.0.0.1 {
    router-id 1.2.3.4;
    local-address 127.0.0.1;
    local-as 1;
    peer-as 1;

    capability {
        add-path send/receive;
    }

    family {
        ipv4 flow;
        ipv4 flow-vpn;
    }

    add-path {
        ipv4 flow;
        ipv4 flow-vpn;
    }

    flow {
        route first-path {
            path-information 1;
            match {
                destination 10.0.0.0/24;
            }
            then {
                discard;
            }
        }
        route second-path {
            path-information 2;
            match {
                destination 10.0.0.0/24;
            }
            then {
                rate-limit 9600;
            }
        }
        route vpn-path {
            rd 65000:1;
            path-information 0.0.0.3;
            match {
                destination 10.0.1.0/24;
            }
            then {
                discard;
            }
        }
    }
}

The one-line form and the announce section take it too:

flow {
    route path-information 3 destination 10.0.2.0/24 discard;
}
announce {
    ipv4 {
        flow destination 10.0.3.0/24 path-information 4 discard;
    }
}

Notes:

  • The identifier is written as an integer (1) or as a dotted quad (0.0.0.1)
  • With ADD-PATH negotiated for the family, the identifier is sent in front of the NLRI, and 0 when the route has none configured
  • Two flow routes with the same match and different identifiers are two paths; before 6.0.0 one replaced the other
  • The full example is etc/exabgp/conf-flow-addpath.conf in the source tree

Upgrading from 5.x: with capability { add-path send/receive; } (or send, or receive) and no add-path { } block, every configured family is offered. So from 6.0.0 a neighbour carrying a flow family also offers ADD-PATH for it, which 5.x silently did not. To keep flow without ADD-PATH, list the families you want in an add-path { } block, for example only ipv4 unicast; and ipv6 unicast;.

Disable ADD-PATH

Explicitly disable ADD-PATH:

neighbor 192.168.1.1 {
    router-id 192.168.1.2;
    local-address 192.168.1.2;
    local-as 65001;
    peer-as 65000;

    # No add-path directive = disabled (default)

    family {
        ipv4 unicast;
    }
}

API Usage

Advertise Routes with Path IDs (Text Format)

Announce multiple paths for the same prefix:

#!/usr/bin/env python3
"""Advertise multiple paths using ADD-PATH"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def announce_multiple_paths():
    """Announce 10.0.0.0/8 via two different next-hops"""

    # Path 1: via 192.168.1.10 (Path ID: 1)
    route1 = "announce route 10.0.0.0/8 next-hop 192.168.1.10 path-information 1"
    send(route1)

    # Path 2: via 192.168.1.20 (Path ID: 2)
    route2 = "announce route 10.0.0.0/8 next-hop 192.168.1.20 path-information 2"
    send(route2)

    print("Announced 2 paths for 10.0.0.0/8", file=sys.stderr)

announce_multiple_paths()

The programs on this page read ExaBGP's answer after each command with this send() function, as Reading the answers explains. ExaBGP 6.0 writes these commands with a peer * selector, see How a program talks to ExaBGP.

Syntax:

announce route <prefix> next-hop <ip> path-information <path-id>

Key Points:

  • path-information <id>: Specifies the Path ID (32-bit unsigned integer)
  • Must be unique per prefix
  • If omitted, the route is sent with Path ID 0; ExaBGP does not pick one for you

Withdraw Specific Paths

Withdraw a specific path by Path ID:

#!/usr/bin/env python3
"""Withdraw specific path using ADD-PATH"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def withdraw_specific_path():
    """Withdraw Path ID 1 for 10.0.0.0/8, keep Path ID 2"""

    # Withdraw only path with ID 1
    withdraw = "withdraw route 10.0.0.0/8 path-information 1"
    send(withdraw)

    print("Withdrew path ID 1 (path ID 2 remains active)", file=sys.stderr)

withdraw_specific_path()

Without Path ID: withdraws only the route announced without a Path ID (Path ID 0); the paths announced with an ID stay.

Anycast with Multiple Paths

Advertise same prefix from multiple locations:

#!/usr/bin/env python3
"""Anycast service with ADD-PATH"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def announce_anycast_paths():
    """Announce 192.0.2.1/32 from 3 data centers"""

    paths = [
        ("192.0.2.1/32", "10.0.1.1", 1, "DC1"),
        ("192.0.2.1/32", "10.0.2.1", 2, "DC2"),
        ("192.0.2.1/32", "10.0.3.1", 3, "DC3"),
    ]

    for prefix, nexthop, path_id, dc in paths:
        route = f"announce route {prefix} next-hop {nexthop} path-information {path_id} community 65001:{path_id}"
        send(route)
        print(f"Announced {prefix} from {dc} (Path ID {path_id})", file=sys.stderr)

announce_anycast_paths()

Receive Multiple Paths (JSON Format)

Process multiple received paths:

#!/usr/bin/env python3
"""Receive and process multiple paths via ADD-PATH"""
import sys
import json

def process_received_paths():
    """Process incoming routes with Path IDs"""

    while True:
        line = sys.stdin.readline().strip()
        if not line:
            break

        try:
            msg = json.loads(line)

            if msg.get('type') == 'update':
                # the UPDATE is under neighbor.message.update, announce is keyed by family then next hop
                update = msg['neighbor']['message']['update']
                for family, by_nexthop in update.get('announce', {}).items():
                    for nexthop, routes in by_nexthop.items():
                        for route in routes:
                            prefix = route.get('nlri')
                            path_id = route.get('path-information')

                            print(f"Received: {prefix} via {nexthop} (Path ID: {path_id})", file=sys.stderr)

        except json.JSONDecodeError:
            pass

process_received_paths()

Load Balancing Across Multiple Paths

Use multiple paths for load balancing:

#!/usr/bin/env python3
"""ECMP load balancing with ADD-PATH"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def announce_ecmp_paths():
    """Announce 4 equal-cost paths for load balancing"""

    prefix = "192.168.0.0/16"
    next_hops = [
        "10.0.1.1",
        "10.0.1.2",
        "10.0.1.3",
        "10.0.1.4",
    ]

    for idx, nexthop in enumerate(next_hops, start=1):
        route = (
            f"announce route {prefix} "
            f"next-hop {nexthop} "
            f"path-information {idx} "
            f"med 100 "  # Same MED for ECMP
            f"as-path [ 65001 ]"
        )
        send(route)

    print(f"Announced {len(next_hops)} ECMP paths for {prefix}", file=sys.stderr)

announce_ecmp_paths()

Use Cases

1. Route Reflector with ADD-PATH

Scenario: Preserve path diversity in Route Reflector deployments.

Problem: Traditional RR only advertises best path to clients, hiding alternative paths.

Solution: Enable ADD-PATH on RR to advertise all paths to clients.

ExaBGP does not pass the routes it receives from one neighbour on to another, so it is not a route reflector by itself. A program receiving the paths from the PEs (with api { receive { parsed; update; } }) has to announce each of them again to the clients, with its own path-information.

Architecture:

         Internet
            │
    ┌───────┼───────┐
    │       │       │
  PE1     PE2     PE3
    │       │       │
    └───────┼───────┘
            │
      Route Reflector
      (ExaBGP + program + ADD-PATH)
            │
    ┌───────┼───────┐
    │       │       │
  Client1 Client2 Client3

Without ADD-PATH: Clients see only RR's best path
With ADD-PATH: Clients see paths from PE1, PE2, PE3

Configuration (Route Reflector):

# RR advertises multiple paths to clients
neighbor 192.168.1.10 {  # Client 1
    router-id 192.168.1.254;
    local-address 192.168.1.254;
    local-as 65001;
    peer-as 65001;  # iBGP

    capability {
        add-path send/receive;
    }

    family {
        ipv4 unicast;
    }
}

Benefits:

  • Clients have full path visibility
  • Each client selects optimal path independently
  • No BGP wedgies
  • Faster convergence (backup paths pre-installed)

2. Multipath Load Balancing

Scenario: Advertise multiple paths for ECMP load balancing.

Example:

#!/usr/bin/env python3
"""Load balancing with ADD-PATH"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def setup_load_balancing():
    """Advertise prefix via 4 next-hops for ECMP"""

    prefix = "10.0.0.0/8"
    uplinks = [
        ("10.1.1.1", 1, "ISP-1"),
        ("10.1.1.2", 2, "ISP-2"),
        ("10.1.1.3", 3, "ISP-3"),
        ("10.1.1.4", 4, "ISP-4"),
    ]

    for nexthop, path_id, isp in uplinks:
        route = (
            f"announce route {prefix} "
            f"next-hop {nexthop} "
            f"path-information {path_id} "
            f"as-path [ 65001 ]"
        )
        send(route)

        print(f"Path {path_id}: {prefix} via {isp} ({nexthop})", file=sys.stderr)

setup_load_balancing()

Result: Traffic distributed across 4 uplinks using ECMP.

3. Fast Convergence with Backup Paths

Scenario: Pre-install backup paths for sub-second convergence.

Architecture:

Primary Path:   ExaBGP ──▶ Router A ──▶ Destination
Backup Path:    ExaBGP ──▶ Router B ──▶ Destination

ADD-PATH: Both paths advertised simultaneously
Router pre-installs both, uses primary, switches to backup instantly on failure

Example:

#!/usr/bin/env python3
"""Primary + backup paths with ADD-PATH"""
import sys
import time
import subprocess

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def check_primary_health():
    """Check if primary path is healthy"""
    try:
        result = subprocess.run(
            ["ping", "-c", "1", "-W", "1", "10.0.1.1"],
            capture_output=True
        )
        return result.returncode == 0
    except:
        return False

def announce_paths_with_failover():
    """Announce primary + backup, adjust weights based on health"""

    primary_healthy = check_primary_health()

    if primary_healthy:
        # Primary: lower MED (preferred)
        primary = "announce route 192.168.0.0/16 next-hop 10.0.1.1 path-information 1 med 50"
        backup = "announce route 192.168.0.0/16 next-hop 10.0.2.1 path-information 2 med 100"
    else:
        # Primary failed: set higher MED
        primary = "announce route 192.168.0.0/16 next-hop 10.0.1.1 path-information 1 med 200"
        backup = "announce route 192.168.0.0/16 next-hop 10.0.2.1 path-information 2 med 50"

    send(primary)
    send(backup)

# Monitor and adjust
while True:
    announce_paths_with_failover()
    time.sleep(10)

4. Anycast Service Advertisement

Scenario: Advertise anycast IP from multiple PoPs.

Example:

#!/usr/bin/env python3
"""Anycast DNS with ADD-PATH"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

def announce_anycast_dns():
    """Announce 192.0.2.53 from 3 geographic locations"""

    anycast_ip = "192.0.2.53/32"
    pops = [
        ("10.1.1.1", 1, "US-East", 65001),
        ("10.2.1.1", 2, "EU-West", 65002),
        ("10.3.1.1", 3, "APAC", 65003),
    ]

    for nexthop, path_id, location, asn in pops:
        route = (
            f"announce route {anycast_ip} "
            f"next-hop {nexthop} "
            f"path-information {path_id} "
            f"as-path [ {asn} ] "
            f"community 65000:{path_id}"
        )
        send(route)

        print(f"Anycast {anycast_ip} from {location} (Path {path_id})", file=sys.stderr)

announce_anycast_dns()

Result: All 3 paths advertised; routers select nearest PoP.


Path Identifiers

Path ID Assignment

ExaBGP does not assign Path IDs. The sender of the route chooses them.

No path-information (sent with Path ID 0):

# No path-information specified: with ADD-PATH send negotiated for the family,
# the route goes out with Path ID 0 (four zero octets in front of the NLRI)
sys.stdout.write("announce route 10.0.0.0/8 next-hop 192.168.1.1\n")

Every route announced this way for the same prefix carries the same Path ID 0, so to advertise several paths each one needs its own path-information.

Explicit Path ID:

# Specify the Path ID
sys.stdout.write("announce route 10.0.0.0/8 next-hop 192.168.1.1 path-information 42\n")

Path ID Requirements

Per RFC 7911:

  • 32-bit unsigned integer (0 to 4,294,967,295)
  • Unique per prefix, per peer (not globally unique)
  • Opaque value (no semantic meaning)
  • Sender-assigned (advertising router controls)

Best Practices:

  • Use sequential IDs (1, 2, 3, ...) for simplicity
  • Use meaningful IDs for debugging (e.g., PoP number)
  • Avoid Path ID 0 (some implementations treat as invalid)
  • Document Path ID scheme

Path ID Persistence

Path IDs must remain stable for a given path:

  • Don't change Path ID unless path changes
  • Consistent IDs across restarts (if possible)
  • Withdraw old ID before announcing new ID for same prefix

Monitoring and Verification

Via API (JSON Format)

Monitor received paths with Path IDs:

#!/usr/bin/env python3
"""Monitor ADD-PATH routes"""
import sys
import json

paths_per_prefix = {}

while True:
    line = sys.stdin.readline().strip()
    if not line:
        break

    try:
        msg = json.loads(line)

        if msg.get('type') == 'update':
            update = msg['neighbor']['message']['update']

            # announce: family -> next hop -> list of routes
            for family, by_nexthop in update.get('announce', {}).items():
                for nexthop, routes in by_nexthop.items():
                    for route in routes:
                        prefix = route.get('nlri')
                        path_id = route.get('path-information', 'N/A')

                        paths_per_prefix.setdefault(prefix, set()).add(path_id)
                        print(f"ADD-PATH: {prefix} via {nexthop} (ID: {path_id})", file=sys.stderr)

            # withdraw: family -> list of routes, no next hop
            for family, routes in update.get('withdraw', {}).items():
                for route in routes:
                    prefix = route.get('nlri')
                    path_id = route.get('path-information', 'N/A')

                    paths_per_prefix.get(prefix, set()).discard(path_id)
                    print(f"WITHDRAW: {prefix} Path ID {path_id}", file=sys.stderr)

        # Periodically report paths per prefix
        for prefix, path_ids in paths_per_prefix.items():
            if len(path_ids) > 1:
                print(f"Multi-path: {prefix} has {len(path_ids)} paths: {path_ids}", file=sys.stderr)

    except json.JSONDecodeError:
        pass

The shape is the one ExaBGP emits, as recorded in qa/encoding/conf-addpath.ci: {"nlri": "193.0.2.1/32", "path-information": "1.2.3.4"} under neighbor.message.update.announce["ipv4 unicast"]["10.0.0.1"]. A route learned without ADD-PATH has no path-information key. The API process needs api { receive { update; } } and encoder json to be sent the UPDATEs.

Via Logging

Enable NLRI parsing logs:

[exabgp.log]
level = DEBUG
parser = true

Log output (message part only, the logger adds its own prefix): the parser logs each received NLRI and whether it carried a Path ID. The payload starts with the four octets of the Path ID, here 1 and then 2, for 10.0.0.0/8:

NLRI      ipv4 unicast       with path-information        payload 0000 0001 080A
NLRI      ipv4 unicast       with path-information        payload 0000 0002 080A

These lines were produced by calling ExaBGP's lazynlri formatter (src/exabgp/logger/format.py) with those two NLRI.

Verification Checklist

Verify ADD-PATH negotiation:

# NLRI received with a Path ID (needs the parser logging above)
grep "with path-information" /var/log/exabgp.log

Test multiple path advertisement:

#!/usr/bin/env python3
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

# Announce 2 paths
send("announce route 10.0.0.0/8 next-hop 192.168.1.1 path-information 1")
send("announce route 10.0.0.0/8 next-hop 192.168.1.2 path-information 2")

# Verify peer receives both paths

Limitations and Considerations

ExaBGP Limitations

  1. Address Family Support

    • ✅ Unicast, labelled unicast, L3VPN and FlowSpec (FlowSpec since 6.0.0)
    • ❌ Multicast, VPLS, EVPN, MVPN, MUP, SR-Policy, RTC and BGP-LS: not supported
    • See ExaBGP ADD-PATH Capabilities for the exact list
  2. Path ID Management

    • User must track Path IDs in API processes
    • No automatic Path ID deduplication
    • Path ID scope limited to per-prefix, per-peer

General ADD-PATH Considerations

  1. Memory Overhead

    • Multiple paths stored per prefix
    • More routes in RIB (N paths × M prefixes)
    • Consider memory capacity
  2. CPU Overhead

    • More UPDATE messages to process
    • Path selection runs for each path
    • Consider CPU capacity for high path counts
  3. Peer Support

    • Both peers must negotiate ADD-PATH capability
    • Verify vendor support and limitations
    • Test before production deployment
  4. Path Explosion

    • Large networks: many paths per prefix possible
    • Limit paths advertised (e.g., best N paths)
    • Monitor path counts

Best Practices

✅ Do:

  • Start with send/receive mode for flexibility
  • Use meaningful Path IDs for debugging
  • Monitor path counts per prefix
  • Test with small scale before full deployment
  • Document Path ID assignment scheme

❌ Don't:

  • Advertise unlimited paths (memory/CPU risk)
  • Change Path IDs unnecessarily
  • Assume all vendors support all features
  • Deploy without testing peer behavior

Common Errors and Solutions

Error 1: ADD-PATH Not Negotiated

Symptom: ADD-PATH configured but not negotiated.

ExaBGP does not log a warning when the peer leaves ADD-PATH out of its OPEN; the routes are simply exchanged without Path IDs. Routes which differ only by their Path ID are then one route to that peer (RFC 4271 9.2): it is sent the first, when that one is withdrawn another is sent in its place, and the prefix is withdrawn only with the last of them. Earlier 6.0.0 snapshots sent each as the same UPDATE, and withdrawing one withdrew the prefix while another was still announced. A family named in add-path { } but missing from family { } is dropped, with a debug message (configuration logging): skipping add-path family (<afi>, <safi>) as it is not negotiated.

Causes:

  1. Peer does not support ADD-PATH (RFC 7911)
  2. Peer has ADD-PATH disabled
  3. AFI/SAFI mismatch

Solutions:

# Verify ExaBGP configuration
neighbor 192.168.1.1 {
    capability {
        add-path send/receive;  # Ensure enabled
    }

    family {
        ipv4 unicast;  # Must match peer
    }
}

# Check peer configuration
# Cisco:
router bgp 65000
  neighbor 192.168.1.2 activate
  address-family ipv4 unicast
    neighbor 192.168.1.2 advertise additional-paths all

# Juniper:
protocols {
    bgp {
        group peers {
            family inet {
                unicast {
                    add-path {
                        receive;
                        send {
                            path-count 6;
                        }
                    }
                }
            }
        }
    }
}

Error 2: Duplicate Path IDs

Symptom: Same Path ID used for different paths to same prefix.

Causes:

  1. API process bug
  2. Path ID not updated on path change
  3. Race condition in Path ID assignment

Solutions:

#!/usr/bin/env python3
"""Properly manage Path IDs"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

class PathManager:
    def __init__(self):
        self.next_path_id = 1
        self.prefix_paths = {}  # prefix -> {nexthop: path_id}

    def announce_path(self, prefix, nexthop):
        """Announce path with unique Path ID"""

        if prefix not in self.prefix_paths:
            self.prefix_paths[prefix] = {}

        if nexthop in self.prefix_paths[prefix]:
            # Re-use existing Path ID
            path_id = self.prefix_paths[prefix][nexthop]
        else:
            # Assign new Path ID
            path_id = self.next_path_id
            self.next_path_id += 1
            self.prefix_paths[prefix][nexthop] = path_id

        route = f"announce route {prefix} next-hop {nexthop} path-information {path_id}"
        send(route)

        return path_id

    def withdraw_path(self, prefix, nexthop):
        """Withdraw specific path"""

        if prefix in self.prefix_paths and nexthop in self.prefix_paths[prefix]:
            path_id = self.prefix_paths[prefix][nexthop]
            withdraw = f"withdraw route {prefix} path-information {path_id}"
            send(withdraw)

            del self.prefix_paths[prefix][nexthop]

# Use PathManager to avoid duplicate Path IDs
pm = PathManager()
pm.announce_path("10.0.0.0/8", "192.168.1.1")
pm.announce_path("10.0.0.0/8", "192.168.1.2")

Error 3: Path Not Withdrawn

Symptom: Withdrawn path still appears at peer.

Causes:

  1. Path ID not specified in withdraw
  2. Incorrect Path ID
  3. Peer issue

Solutions:

#!/usr/bin/env python3
"""Correct path withdrawal with Path ID"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

# INCORRECT: withdraws only the route announced without a Path ID (Path ID 0)
send("withdraw route 10.0.0.0/8")

# CORRECT: Withdraws specific path by ID
send("withdraw route 10.0.0.0/8 path-information 1")

# Verify Path ID matches announced path
# Announce:  path-information 42
# Withdraw:  path-information 42  (must match!)

Error 4: Too Many Paths

Symptom: Memory or CPU exhaustion from too many paths.

Causes:

  1. No limit on paths per prefix
  2. Path explosion in large network
  3. API process announcing excessive paths

Solutions:

#!/usr/bin/env python3
"""Limit paths per prefix"""
import sys

def send(command):
    """Write one command, then read lines until ExaBGP answers it."""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    while True:
        line = sys.stdin.readline()
        if not line:  # ExaBGP closed the pipe: it is stopping
            sys.exit(0)
        answer = line.strip()
        if answer == "done":
            return True
        if answer == "error":
            sys.stderr.write(f"ExaBGP refused: {command}\n")
            return False

class LimitedPathManager:
    MAX_PATHS_PER_PREFIX = 4  # Limit to 4 paths

    def __init__(self):
        self.prefix_paths = {}  # prefix -> [(nexthop, path_id), ...]

    def announce_path(self, prefix, nexthop, path_id):
        """Announce path with limit enforcement"""

        if prefix not in self.prefix_paths:
            self.prefix_paths[prefix] = []

        # Check limit
        if len(self.prefix_paths[prefix]) >= self.MAX_PATHS_PER_PREFIX:
            # Remove oldest path
            old_nexthop, old_path_id = self.prefix_paths[prefix].pop(0)
            withdraw = f"withdraw route {prefix} path-information {old_path_id}"
            send(withdraw)
            print(f"Removed oldest path (ID {old_path_id}) due to limit", file=sys.stderr)

        # Announce new path
        self.prefix_paths[prefix].append((nexthop, path_id))
        route = f"announce route {prefix} next-hop {nexthop} path-information {path_id}"
        send(route)

# Enforce path limit
lpm = LimitedPathManager()

See Also


References

RFCs

ExaBGP Documentation

Implementation

  • Source Code: src/exabgp/bgp/message/open/capability/addpath.py
  • Capability Type: 69 (ADD-PATH)
  • ExaBGP Version: 4.x, 5.x/main

Vendor Documentation


Clone this wiki locally