Repository navigation
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.
- What is ADD-PATH?
- How ADD-PATH Works
- ExaBGP ADD-PATH Capabilities
- Why Use ADD-PATH?
- Configuration
- API Usage
- Use Cases
- Path Identifiers
- Monitoring and Verification
- Limitations and Considerations
- Common Errors and Solutions
- See Also
- References
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.
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
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
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
[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 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-informationis 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
| 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 |
- Route Reflector deployments: Preserve path diversity to clients
- Multipath load balancing: Advertise multiple equal-cost paths
- Fast convergence: Pre-install backup paths for sub-second failover
- Anycast services: Advertise from multiple locations simultaneously
- Path diversity for resilience: Multiple independent paths for redundancy
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)
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
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
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 notl2vpn vpls
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.confin 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;.
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;
}
}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 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.
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()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()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()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)
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.
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)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.
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")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 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
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:
passThe 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.
Enable NLRI parsing logs:
[exabgp.log]
level = DEBUG
parser = trueLog 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.
Verify ADD-PATH negotiation:
# NLRI received with a Path ID (needs the parser logging above)
grep "with path-information" /var/log/exabgp.logTest 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-
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
-
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
-
Memory Overhead
- Multiple paths stored per prefix
- More routes in RIB (N paths × M prefixes)
- Consider memory capacity
-
CPU Overhead
- More UPDATE messages to process
- Path selection runs for each path
- Consider CPU capacity for high path counts
-
Peer Support
- Both peers must negotiate ADD-PATH capability
- Verify vendor support and limitations
- Test before production deployment
-
Path Explosion
- Large networks: many paths per prefix possible
- Limit paths advertised (e.g., best N paths)
- Monitor path counts
✅ 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
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:
- Peer does not support ADD-PATH (RFC 7911)
- Peer has ADD-PATH disabled
- 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;
}
}
}
}
}
}
}Symptom: Same Path ID used for different paths to same prefix.
Causes:
- API process bug
- Path ID not updated on path change
- 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")Symptom: Withdrawn path still appears at peer.
Causes:
- Path ID not specified in withdraw
- Incorrect Path ID
- 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!)Symptom: Memory or CPU exhaustion from too many paths.
Causes:
- No limit on paths per prefix
- Path explosion in large network
- 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()- Route Refresh - Route refresh capability
- Graceful Restart - Graceful restart with multiple paths
- API Commands - API command reference
- Configuration Syntax - Configuration guide
- Anycast Management - Anycast with ADD-PATH
- RFC 7911: Advertisement of Multiple Paths in BGP (ADD-PATH)
- RFC 4271: BGP-4 base specification
- RFC 4456: BGP Route Reflection
- RFC Support - All implemented RFCs
- RFC Information - Detailed RFC implementation list
-
Source Code:
src/exabgp/bgp/message/open/capability/addpath.py - Capability Type: 69 (ADD-PATH)
- ExaBGP Version: 4.x, 5.x/main
Getting Started
Configuration
- Configuration Syntax
- Neighbor Configuration
- Directives A-Z
- Syntax Reference
- Templates
- Environment Variables
- Process Configuration
API
- API Overview
- Text API Reference
- JSON API Reference
- API Commands
- Writing API Programs
- Error Handling
- Production Best Practices
Address Families
- Overview
- IPv4 Unicast
- IPv6 Unicast
- FlowSpec
- EVPN
- L3VPN
- BGP-LS
- VPLS
- SRv6 / MUP
- Multicast
- RT Constraint
Features
Use Cases
Tools
Operations
Reference
- Architecture
- Design
- Attribute Reference
- Command Reference
- BGP State Machine
- Capabilities
- Communities
- Examples Index
- Glossary
- RFC Support
Integration
Migration
Community
External