Skip to content

API Overview

Thomas Mangin edited this page Nov 10, 2025 · 13 revisions

API Overview

Understanding ExaBGP's STDIN/STDOUT API


Table of Contents


What is the ExaBGP API?

The ExaBGP API is a simple STDIN/STDOUT interface that allows your programs to control BGP route announcements and receive BGP updates.

Key characteristics:

  • Language-agnostic: Works with Python, Bash, Ruby, Go, Rust, Node.js, or any language
  • Simple: No libraries needed - just read/write text or JSON
  • Bidirectional: Your program sends commands, receives BGP messages
  • Process-based: ExaBGP spawns your program as a subprocess

Think of it as:

Your Program ↔ STDIN/STDOUT ↔ ExaBGP ↔ BGP Protocol ↔ Router

Why This API Design?

Philosophy

"The best API is no API"

ExaBGP uses UNIX pipes (STDIN/STDOUT) instead of complex libraries.

Advantages:

  • No dependencies: Works with any language that can read/write streams
  • Simple: print() to send commands, readline() to receive
  • Debugging-friendly: Can test manually with echo/cat
  • Language-agnostic: Not tied to Python or any specific ecosystem
  • Process isolation: Your program crashes don't affect ExaBGP
  • Easy integration: Wrap existing tools without modification

Compared to alternatives:

  • Library-based (e.g., GoBGP's gRPC): Requires language-specific client
  • REST API: Adds HTTP overhead, state management complexity
  • File-based: Requires polling, slower updates

STDIN/STDOUT is:

  • Fast (no network/HTTP overhead)
  • Simple (just streams)
  • Universal (every language supports it)

How It Works

Configuration

ExaBGP configuration defines which programs to run:

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

    # API configuration
    api {
        processes [ my-program ];
    }
}

process my-program {
    run /etc/exabgp/api/my-program.py;
    encoder text;  # or 'json'
}

Process Startup

When ExaBGP starts:

  1. ExaBGP spawns /etc/exabgp/api/my-program.py as subprocess
  2. ExaBGP connects to program's STDIN/STDOUT
  3. Program can now:
    • Write to STDOUT → Commands to ExaBGP
    • Read from STDIN → BGP messages from ExaBGP

Communication

┌─────────────────────┐
│  Your Program       │
│                     │
│  print("announce    │  STDOUT ──────┐
│    route ...")      │               │
│                     │               ▼
│  for line in stdin: │  STDIN  ◀──────┬─────────┐
│    process(line)    │                │ ExaBGP  │
└─────────────────────┘                │         │
                                       │ BGP     │
                                       │ Speaker │
                                       └────┬────┘
                                            │
                                            │ BGP Protocol
                                            ▼
                                       ┌─────────┐
                                       │ Router  │
                                       └─────────┘

Text vs JSON Encoders

ExaBGP supports two encoding formats: Text and JSON.

Text Encoder

Use case: Sending commands (announcing/withdrawing routes)

Configuration:

process my-program {
    run /etc/exabgp/api/announce.py;
    encoder text;
}

Format: Human-readable text commands

Example commands:

# Announce
print("announce route 100.10.0.0/24 next-hop self")

# Withdraw
print("withdraw route 100.10.0.0/24")

# FlowSpec
print("announce flow route { match { destination 100.10.0.0/24; } then { discard; } }")

Pros:

  • ✅ Human-readable
  • ✅ Easy to test manually
  • ✅ Simple parsing

Cons:

  • ❌ Harder to parse programmatically
  • ❌ Limited structured data

JSON Encoder

Use case: Receiving BGP updates (routes, session state)

Configuration:

process my-program {
    run /etc/exabgp/api/receive.py;
    encoder json;
    receive {
        parsed;   # Receive parsed BGP messages
        updates;  # Receive route updates
    }
}

Format: JSON lines (one JSON object per line)

Example message:

{
  "exabgp": "4.2.25",
  "time": 1699564800.0,
  "type": "update",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "message": {
      "update": {
        "announce": {
          "ipv4 unicast": {
            "100.10.0.0/24": [
              {
                "next-hop": "192.168.1.2"
              }
            ]
          }
        }
      }
    }
  }
}

Pros:

  • ✅ Structured data
  • ✅ Easy to parse (every language has JSON support)
  • ✅ Complete information

Cons:

  • ❌ Verbose
  • ❌ Harder to read manually

Which Encoder Should You Use?

Use encoder text when:

  • Only sending commands (announce/withdraw)
  • Want simple, human-readable format
  • Testing manually

Use encoder json when:

  • Receiving BGP updates from router
  • Need structured data parsing
  • Building complex integrations

Can use both:

# Process 1: Send commands (text)
process announce {
    run /etc/exabgp/api/announce.py;
    encoder text;
}

# Process 2: Receive updates (JSON)
process receive {
    run /etc/exabgp/api/receive.py;
    encoder json;
    receive {
        parsed;
        updates;
    }
}

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

    api {
        processes [ announce, receive ];
    }
}

API Architecture

Components

┌──────────────────────────────────────────────────────┐
│                   Your Program(s)                    │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │  announce.py │  │  receive.py  │  │ health.py  │ │
│  │  (text)      │  │  (json)      │  │ (text)     │ │
│  └──────┬───────┘  └──────┬───────┘  └──────┬─────┘ │
│         │ STDOUT          │ STDIN           │ STDOUT│
└─────────┼─────────────────┼─────────────────┼───────┘
          │                 │                 │
          ▼                 ▼                 ▼
┌──────────────────────────────────────────────────────┐
│                      ExaBGP                          │
│                                                      │
│  ┌─────────────┐   ┌──────────────┐   ┌──────────┐ │
│  │  Process    │   │   BGP State  │   │  Route   │ │
│  │  Manager    │   │   Machine    │   │  Manager │ │
│  └─────────────┘   └──────────────┘   └──────────┘ │
└──────────────────────────┬───────────────────────────┘
                           │ BGP Protocol
                           ▼
                     ┌──────────┐
                     │  Router  │
                     └──────────┘

Process Manager

  • Spawns your programs as subprocesses
  • Monitors process health (restarts if crashed)
  • Routes commands from STDOUT to BGP engine
  • Sends BGP messages to process STDIN

Command Flow

1. Your program: print("announce route 100.10.0.0/24 next-hop self")
2. Process writes to STDOUT
3. ExaBGP reads from process STDOUT
4. ExaBGP parses command
5. ExaBGP generates BGP UPDATE message
6. ExaBGP sends UPDATE to router via TCP 179
7. Router installs route in RIB/FIB

Update Flow

1. Router sends BGP UPDATE to ExaBGP
2. ExaBGP receives UPDATE
3. ExaBGP parses BGP message
4. ExaBGP converts to JSON (if encoder json)
5. ExaBGP writes JSON to process STDIN
6. Your program reads from STDIN
7. Your program processes the update

Basic Examples

Example 1: Simple Route Announcement (Python)

#!/usr/bin/env python3
"""
announce.py - Announce static routes
"""
import sys
import time

# Wait for ExaBGP to be ready
time.sleep(2)

# Announce routes
routes = [
    "100.10.0.0/24",
    "100.20.0.0/24",
]

for route in routes:
    sys.stdout.write(f"announce route {route} next-hop self\n")
    sys.stdout.flush()  # CRITICAL: Must flush!

# Keep process alive
while True:
    time.sleep(60)

Configuration:

process announce {
    run /etc/exabgp/api/announce.py;
    encoder text;
}

Example 2: Health Check (Python)

#!/usr/bin/env python3
"""
healthcheck.py - Announce route only when service is healthy
"""
import sys
import time
import socket

SERVICE_IP = "100.10.0.100"
SERVICE_PORT = 80
CHECK_INTERVAL = 5

def is_healthy():
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(2)
        result = sock.connect_ex((SERVICE_IP, SERVICE_PORT))
        sock.close()
        return result == 0
    except:
        return False

time.sleep(2)
announced = False

while True:
    if is_healthy():
        if not announced:
            sys.stdout.write(f"announce route {SERVICE_IP}/32 next-hop self\n")
            sys.stdout.flush()
            announced = True
    else:
        if announced:
            sys.stdout.write(f"withdraw route {SERVICE_IP}/32\n")
            sys.stdout.flush()
            announced = False

    time.sleep(CHECK_INTERVAL)

Example 3: Receive BGP Updates (Python)

#!/usr/bin/env python3
"""
receive.py - Process incoming BGP updates
"""
import sys
import json

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

    try:
        msg = json.loads(line)

        # Route announcement
        if msg['type'] == 'update':
            if 'update' in msg['neighbor']['message']:
                update = msg['neighbor']['message']['update']

                # New routes announced
                if 'announce' in update:
                    if 'ipv4 unicast' in update['announce']:
                        routes = update['announce']['ipv4 unicast']
                        for prefix, attrs in routes.items():
                            nexthop = attrs[0]['next-hop']
                            print(f"[RECEIVED] {prefix} via {nexthop}", file=sys.stderr)

                # Routes withdrawn
                if 'withdraw' in update:
                    if 'ipv4 unicast' in update['withdraw']:
                        for prefix in update['withdraw']['ipv4 unicast'].keys():
                            print(f"[WITHDRAWN] {prefix}", file=sys.stderr)

        # Session state change
        elif msg['type'] == 'state':
            state = msg['neighbor']['state']
            print(f"[STATE] BGP session: {state}", file=sys.stderr)

    except Exception as e:
        print(f"[ERROR] {e}", file=sys.stderr)

Configuration:

process receive {
    run /etc/exabgp/api/receive.py;
    encoder json;
    receive {
        parsed;
        updates;
    }
}

Example 4: Bash Script

#!/bin/bash
# announce.sh - Announce routes from bash

# Wait for ExaBGP
sleep 2

# Announce routes
echo "announce route 100.10.0.0/24 next-hop self"
echo "announce route 100.20.0.0/24 next-hop self"

# Keep running
while true; do
    sleep 60
done

Example 5: Go Program

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {
    // Wait for ExaBGP
    time.Sleep(2 * time.Second)

    // Announce routes
    routes := []string{
        "100.10.0.0/24",
        "100.20.0.0/24",
    }

    for _, route := range routes {
        fmt.Printf("announce route %s next-hop self\n", route)
    }

    // Keep alive
    for {
        time.Sleep(60 * time.Second)
    }
}

Process Lifecycle

1. Startup

  1. ExaBGP starts
  2. Reads configuration file
  3. Spawns process(es) defined in process blocks
  4. Connects to process STDIN/STDOUT
  5. Waits for BGP session to establish

Your program should:

  • Sleep 2-5 seconds to wait for BGP session
  • Send initial announcements
  • Enter main loop

2. Running

Your program:

  • Continuously runs (infinite loop or event-driven)
  • Writes commands to STDOUT when needed
  • Reads messages from STDIN if receiving updates
  • Flushes STDOUT after each write

ExaBGP:

  • Monitors process (checks if still running)
  • Routes commands to BGP engine
  • Sends BGP updates to process

3. Shutdown

Graceful shutdown (SIGTERM):

  1. ExaBGP receives SIGTERM
  2. ExaBGP sends SIGTERM to all processes
  3. Processes should exit cleanly
  4. ExaBGP closes BGP sessions (sends NOTIFICATION)
  5. ExaBGP exits

Process crash:

  1. Process exits/crashes
  2. ExaBGP detects (broken pipe or process exit)
  3. ExaBGP can restart process (if respawn enabled)

Configuration for auto-restart:

process my-program {
    run /etc/exabgp/api/my-program.py;
    encoder text;
}

ExaBGP automatically respawns crashed processes.


Communication Flow

Sending Commands (STDOUT → ExaBGP)

# WRONG - Not flushed, nothing happens
print("announce route 100.10.0.0/24 next-hop self")

# CORRECT - Flushed, ExaBGP receives it
sys.stdout.write("announce route 100.10.0.0/24 next-hop self\n")
sys.stdout.flush()

# ALSO CORRECT - Print and flush
print("announce route 100.10.0.0/24 next-hop self")
sys.stdout.flush()

# ALSO CORRECT - Unbuffered mode
print("announce route 100.10.0.0/24 next-hop self", flush=True)

Critical: Always flush STDOUT, or use unbuffered mode:

python3 -u /etc/exabgp/api/announce.py

Receiving Messages (STDIN ← ExaBGP)

# Read lines forever
while True:
    line = sys.stdin.readline()
    if not line:
        break  # EOF
    process(line)

Message types (JSON):

  • type: "state" - BGP session state changes
  • type: "update" - Route announcements/withdrawals
  • type: "notification" - BGP errors
  • type: "keepalive" - Keepalive messages

Command Acknowledgment (ACK Feature)

⚠️ Version Difference: ACK feature availability depends on ExaBGP version.

ExaBGP 4.x and Earlier: No ACK

Behavior:

  • Your program writes commands to STDOUT
  • ExaBGP reads commands
  • No response sent back
  • Your program doesn't know if command succeeded
# ExaBGP 4.x - No feedback
print("announce route 100.10.0.0/24 next-hop self")
sys.stdout.flush()
# Command sent, but no way to know if it worked

Problem: Silent failures - bad commands are ignored without notification.


ExaBGP 5.x/main: ACK Feature

Behavior:

  • Your program writes command to STDOUT
  • ExaBGP reads and validates command
  • ExaBGP sends response on your program's STDIN
  • Your program can verify success/failure

Enable ACK:

export exabgp.api.ack=true  # Default in main/5.x

Responses:

  • done\n - Command succeeded
  • error\n - Command failed (syntax error, invalid route, etc.)
  • shutdown\n - ExaBGP is shutting down

Using ACK in Your Program

With select() to read response:

#!/usr/bin/env python3
"""
announce_with_ack.py - Send commands with ACK verification
"""
import sys
import select

def send_command(command):
    """Send command and wait for ACK"""
    # Send command
    sys.stdout.write(command + "\n")
    sys.stdout.flush()

    # Wait for response using select()
    # Timeout after 5 seconds
    ready, _, _ = select.select([sys.stdin], [], [], 5.0)

    if ready:
        response = sys.stdin.readline().strip()
        if response == "done":
            sys.stderr.write(f"[SUCCESS] {command}\n")
            return True
        elif response == "error":
            sys.stderr.write(f"[ERROR] Command failed: {command}\n")
            return False
        elif response == "shutdown":
            sys.stderr.write(f"[SHUTDOWN] ExaBGP is shutting down\n")
            sys.exit(0)
    else:
        sys.stderr.write(f"[TIMEOUT] No response for: {command}\n")
        return False

# Send commands with verification
send_command("announce route 100.10.0.0/24 next-hop self")
send_command("announce route 100.20.0.0/24 next-hop self")

# Keep running
while True:
    import time
    time.sleep(60)

ACK with Error Handling

#!/usr/bin/env python3
"""
robust_announcer.py - Robust command sending with retries
"""
import sys
import select
import time

def send_with_retry(command, max_retries=3):
    """Send command with retry on failure"""
    for attempt in range(max_retries):
        sys.stdout.write(command + "\n")
        sys.stdout.flush()

        ready, _, _ = select.select([sys.stdin], [], [], 5.0)

        if ready:
            response = sys.stdin.readline().strip()

            if response == "done":
                sys.stderr.write(f"[OK] {command}\n")
                return True
            elif response == "error":
                sys.stderr.write(f"[ERROR] Attempt {attempt + 1}/{max_retries}: {command}\n")
                if attempt < max_retries - 1:
                    time.sleep(1)
                    continue
                return False
            elif response == "shutdown":
                sys.stderr.write("[SHUTDOWN] ExaBGP shutting down\n")
                sys.exit(0)
        else:
            sys.stderr.write(f"[TIMEOUT] Attempt {attempt + 1}/{max_retries}\n")
            if attempt < max_retries - 1:
                time.sleep(1)
                continue
            return False

    return False

# Use it
if send_with_retry("announce route 100.10.0.0/24 next-hop self"):
    sys.stderr.write("[INFO] Route announced successfully\n")
else:
    sys.stderr.write("[FATAL] Failed to announce route after retries\n")
    sys.exit(1)

Backward Compatibility (No ACK Mode)

For ExaBGP 4.x or when ACK is disabled:

#!/usr/bin/env python3
"""
announce_no_ack.py - Works with ExaBGP 4.x (no ACK)
"""
import sys
import time

def send_command_no_ack(command):
    """Send command without waiting for ACK"""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()
    # No feedback - just hope it worked
    sys.stderr.write(f"[SENT] {command}\n")

# ExaBGP 4.x style
send_command_no_ack("announce route 100.10.0.0/24 next-hop self")

# Keep running
while True:
    time.sleep(60)

Detecting ACK Support

#!/usr/bin/env python3
"""
detect_ack.py - Auto-detect if ACK is enabled
"""
import sys
import select
import os

def has_ack_support():
    """Test if ExaBGP has ACK enabled"""
    # Check environment variable
    ack = os.getenv('exabgp.api.ack', 'true')
    return ack.lower() == 'true'

def send_command(command):
    """Send command with or without ACK based on support"""
    sys.stdout.write(command + "\n")
    sys.stdout.flush()

    if has_ack_support():
        # Wait for ACK
        ready, _, _ = select.select([sys.stdin], [], [], 5.0)
        if ready:
            response = sys.stdin.readline().strip()
            return response == "done"
        return False
    else:
        # No ACK - assume success
        return True

# Auto-adapts to ExaBGP version
send_command("announce route 100.10.0.0/24 next-hop self")

Configuration

Enable ACK (main/5.x default):

export exabgp.api.ack=true
exabgp /etc/exabgp/exabgp.conf

Disable ACK (for 4.x compatibility):

export exabgp.api.ack=false
exabgp /etc/exabgp/exabgp.conf

In healthcheck application:

# For ExaBGP 4.x or when ACK disabled
python -m exabgp healthcheck --no-ack --cmd "curl -sf http://localhost/health"

Why Use ACK?

Benefits:

  • Reliability: Know if commands succeeded
  • Error Detection: Catch syntax errors immediately
  • Debugging: Easier to troubleshoot failures
  • Retry Logic: Can retry failed commands
  • Production-Ready: Critical for automated systems

When to use:

  • Production deployments
  • Automated DDoS mitigation
  • Critical route announcements
  • Any scenario where silent failures are unacceptable

When not needed:

  • Simple static announcements
  • Testing/development
  • ExaBGP 4.x compatibility required

Error Handling

Command Errors (Without ACK)

ExaBGP logs errors but continues:

# Invalid command (ExaBGP 4.x behavior)
print("announce route 100.10.0.0/24")  # Missing next-hop

ExaBGP log:

ERROR:    Could not parse command: announce route 100.10.0.0/24

Your program won't know unless you monitor logs.


Command Errors (With ACK)

ExaBGP sends error response:

# Invalid command (ExaBGP 5.x with ACK)
sys.stdout.write("announce route 100.10.0.0/24\n")  # Missing next-hop
sys.stdout.flush()

# Wait for response
ready, _, _ = select.select([sys.stdin], [], [], 5.0)
if ready:
    response = sys.stdin.readline().strip()
    if response == "error":
        sys.stderr.write("[ERROR] Command rejected by ExaBGP\n")
        # Check ExaBGP logs for details

Your program knows immediately that the command failed.


Process Crashes

If your program crashes:

  1. ExaBGP detects (broken pipe)
  2. ExaBGP logs error
  3. ExaBGP restarts process automatically

Best practice:

try:
    main_loop()
except Exception as e:
    sys.stderr.write(f"[ERROR] {e}\n")
    sys.exit(1)

BGP Session Failures

If BGP session drops:

  • ExaBGP continues running
  • Your process continues running
  • Commands are queued until session re-establishes

Receive updates about session state:

if msg['type'] == 'state':
    if msg['neighbor']['state'] == 'down':
        # Session dropped - stop announcing?
        pass

Best Practices

1. Always Flush STDOUT

sys.stdout.flush()  # After every write

Or use unbuffered mode:

#!/usr/bin/env python3 -u

2. Wait for BGP Session

time.sleep(2)  # Give ExaBGP time to establish session

3. Keep Process Alive

while True:
    time.sleep(60)  # Don't exit immediately

4. Use Structured Logging

import sys

def log(message):
    sys.stderr.write(f"[{time.time()}] {message}\n")

log("Service UP - announced route")

STDERR goes to ExaBGP log.

5. Handle Errors Gracefully

try:
    result = check_service()
except Exception as e:
    log(f"Health check error: {e}")
    # Assume down on error
    result = False

6. Use State Tracking

announced = False

if should_announce and not announced:
    announce()
    announced = True
elif not should_announce and announced:
    withdraw()
    announced = False

Avoids redundant announcements.

7. Validate Commands

def announce_route(prefix, nexthop="self"):
    if not is_valid_prefix(prefix):
        log(f"Invalid prefix: {prefix}")
        return
    sys.stdout.write(f"announce route {prefix} next-hop {nexthop}\n")
    sys.stdout.flush()

Next Steps

Detailed References

Examples

Configuration


Ready to dive deeper? Continue to Text API Reference


👻 Ghost written by Claude (Anthropic AI)

Clone this wiki locally