# API Overview **Understanding ExaBGP's STDIN/STDOUT API** --- ## Table of Contents - [What is the ExaBGP API?](#what-is-the-exabgp-api) - [Why This API Design?](#why-this-api-design) - [How It Works](#how-it-works) - [Targeting Specific Neighbors](#targeting-specific-neighbors) - [Text vs JSON Encoders](#text-vs-json-encoders) - [API Versioning (6.0.0+)](#api-versioning-600) - [API Architecture](#api-architecture) - [Basic Examples](#basic-examples) - [Process Lifecycle](#process-lifecycle) - [Communication Flow](#communication-flow) - [Command Acknowledgment (ACK Feature)](#command-acknowledgment-ack-feature) - [What is ACK?](#what-is-ack) - [Why Use ACK?](#why-use-ack) - [Using ACK in Your Program](#using-ack-in-your-program) - [Dynamic ACK Control (Runtime Commands)](#dynamic-ack-control-runtime-commands) - [Error Handling](#error-handling) - [Best Practices](#best-practices) - [Next Steps](#next-steps) --- ## 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: ```ini 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 │ └─────────┘ ``` --- ## Targeting Specific Neighbors When ExaBGP is configured with **multiple BGP neighbors**, you can use **neighbor selectors** to target specific peers with your API commands. ### Basic Syntax ``` neighbor [selector-options] ``` **By default, commands apply to all configured neighbors.** Use the `neighbor` keyword to target specific peer(s) instead of broadcasting to all neighbors. ### Quick Examples ```python # Target single neighbor print("neighbor 127.0.0.1 announce route 10.0.0.0/24 next-hop self") # Target all neighbors print("neighbor * announce route 10.0.0.0/24 next-hop self") # Target multiple neighbors print("neighbor 10.0.0.1,10.0.0.2 announce route 10.0.0.0/24 next-hop self") # Filter by peer ASN print("neighbor 127.0.0.1 peer-as 65000 announce route 10.0.0.0/24 next-hop self") ``` ### Available Selectors You can use these selectors to differentiate between multiple peers: - **`neighbor `** - Required: Target neighbor by IP address - **`local-ip `** - Filter by local IP address (when multiple local IPs) - **`local-as `** - Filter by local ASN (for multi-AS setups) - **`peer-as `** - Filter by peer ASN - **`router-id `** - Filter by router ID - **`family-allowed `** - Filter by address family capabilities (e.g., `ipv4-unicast/ipv6-unicast` for multi-session, or `in-open` for single-session) ### Common Use Cases **Multi-Provider Setup:** ```python # Announce different routes to different providers sys.stdout.write("neighbor 10.0.1.1 announce route 100.10.0.0/24 next-hop self as-path [65001 65001]\n") # Provider A (de-preference) sys.stdout.flush() sys.stdout.write("neighbor 10.0.2.1 announce route 100.10.0.0/24 next-hop self\n") # Provider B (preferred) sys.stdout.flush() ``` **Geographic Load Balancing:** ```python # US datacenter announces to US peers with high preference sys.stdout.write("neighbor 192.168.1.1 announce route 100.10.0.100/32 next-hop self local-preference 200\n") sys.stdout.flush() # EU datacenter announces to EU peers with high preference sys.stdout.write("neighbor 192.168.2.1 announce route 100.10.0.100/32 next-hop self local-preference 200\n") sys.stdout.flush() ``` **ASN-Based Filtering:** ```python # Only announce specific routes to peers in AS 65000 sys.stdout.write("neighbor * peer-as 65000 announce route 10.0.0.0/8 next-hop self\n") sys.stdout.flush() ``` **Address Family Filtering:** ```python # Send FlowSpec rules only to neighbors with FlowSpec capability sys.stdout.write("neighbor * family-allowed ipv4-flowspec announce flow route { match { destination 10.0.0.0/8; } then { discard; } }\n") sys.stdout.flush() # Target multi-session neighbors with dual-stack capability sys.stdout.write("neighbor * family-allowed ipv4-unicast/ipv6-unicast announce route 10.0.0.0/24 next-hop self\n") sys.stdout.flush() ``` ### Configuration Example ```ini # Multiple neighbors sharing one API process 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 [ my-program ]; } } neighbor 192.168.2.1 { router-id 192.168.1.2; local-address 192.168.1.2; local-as 65001; peer-as 65002; api { processes [ my-program ]; } } process my-program { run /etc/exabgp/api/announce.py; encoder text; } ``` With this configuration, `my-program` can selectively announce routes to either neighbor using neighbor selectors. ### Important Notes - 💡 **Default behavior**: Commands without `neighbor` selector apply to **all configured neighbors** - 💡 **Selective targeting**: Use `neighbor` keyword to target specific peer(s) instead of all neighbors - 💡 **Wildcard**: `neighbor *` explicitly targets all configured neighbors (same as default) - 💡 **Multiple IPs**: Use comma-separated IPs (`neighbor 10.0.0.1,10.0.0.2`) to target multiple specific neighbors - 💡 **AND Logic**: When combining selectors, all must match (e.g., `neighbor 127.0.0.1 peer-as 65000` requires both IP and ASN to match) - 💡 **No Match = Silent**: If no neighbor matches your selectors, the command is silently ignored **For detailed examples and more selector options**, see: - [API Commands - Neighbor Selectors](API-Commands#neighbor-selectors-targeting-specific-peers) - [Text API Reference - Neighbor Selectors](Text-API-Reference#neighbor-selectors) --- ## Text vs JSON Encoders ExaBGP supports two encoding formats: **Text** and **JSON**. > **⚠️ Important: Commands are ALWAYS text format** > > - **Commands you send to ExaBGP (STDOUT):** Always text format (`announce route ...`, `withdraw route ...`) > - **Messages from ExaBGP (STDIN):** Can be text or JSON based on `encoder` setting > - The `encoder` setting controls ONLY what format ExaBGP uses when sending messages to your program > - **You CANNOT send JSON commands to ExaBGP** - all commands must be text ### Text Encoder **Use case:** Sending commands + receiving simple text messages **Configuration:** ```ini process my-program { run /etc/exabgp/api/announce.py; encoder text; } ``` **Format:** Human-readable text commands (bidirectional) **Example commands (your program → ExaBGP via STDOUT):** ```python # Commands are ALWAYS text format, regardless of encoder setting print("announce route 100.10.0.0/24 next-hop 192.0.2.1") print("withdraw route 100.10.0.0/24") print("announce flow route { match { destination 100.10.0.0/24; } then { discard; } }") ``` **Pros:** - ✅ Human-readable (both directions) - ✅ Easy to test manually - ✅ Simple for both sending and receiving **Cons:** - ❌ Received messages harder to parse programmatically - ❌ Limited structured data from ExaBGP --- ### JSON Encoder **Use case:** Receiving BGP updates in structured format (routes, session state) **Configuration:** ```ini 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:** ```json { "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? > **Remember:** The `encoder` setting ONLY affects messages FROM ExaBGP TO your program. Commands from your program to ExaBGP are always text. **Use `encoder text` when:** - Only sending commands (announce/withdraw) - Want simple, human-readable messages from ExaBGP - Testing manually - Don't need structured parsing of received messages **Use `encoder json` when:** - Receiving BGP updates from router - Need structured data parsing - Building complex integrations - Want easy programmatic parsing of ExaBGP messages **Can use both (common pattern):** ```ini # 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 Versioning (6.0.0+) ExaBGP 6.0.0 introduces **API versioning** with support for v4 (legacy) and v6 (default) API formats. ### API Versions | Version | Description | Default In | |---------|-------------|------------| | **v6** | JSON-only, new command format | ExaBGP 6.0.0+ | | **v4** | Legacy, supports text/JSON | ExaBGP 4.x, 5.x (compatible) | ### Checking/Setting API Version Use the `api version` command to query or change the API version at runtime: ```python # Query current API version print("api version", flush=True) # Response (JSON): {"api-version": 6, "description": "json-only"} # Switch to v4 legacy mode print("api version 4", flush=True) # Switch back to v6 (json-only) print("api version 6", flush=True) ``` ### Command Format Differences | Action | v4 Format | v6 Format | |--------|-----------|-----------| | Announce to all | `announce route ...` | `peer * announce route ...` | | Announce to specific | `neighbor 1.2.3.4 announce ...` | `peer 1.2.3.4 announce route ...` | | Withdraw | `withdraw route ...` | `peer * withdraw route ...` | | Shutdown | `shutdown` | `daemon shutdown` | | Enable ACK | `enable-ack` | `session ack enable` | | Disable ACK | `disable-ack` | `session ack disable` | | Show neighbors | `show neighbor` | `peer show` | | Show RIB | `show adj-rib in` | `rib show in` | ### JSON Output Differences Some JSON field names changed between v4/v5 and v6: | v4/v5 Key | v6 Key | Context | |-----------|--------|---------| | `ip` | `prefix` | BGP-LS ip-reachability-tlv (now includes /length) | | `sr-adj` | `sr-adjs` | BGP-LS adjacency SID (now array) | | `remote-router-id` | `remote-router-ids` | BGP-LS (now array) | | `sr_capability_flags` | `sr-capability-flags` | BGP-LS (hyphenated) | ### Backward Compatibility - **v4 commands still work in v6** - They're automatically translated - **Bridge mode** - Use `exabgp migrate api --exec` to run legacy scripts unchanged - **Set v4 mode** - Use `api version 4` if your scripts depend on v4 JSON format ### Example: v6 Command Format ```python #!/usr/bin/env python3 """ExaBGP 6.0.0 v6 API format example""" import sys # v6 format: target comes first sys.stdout.write("peer * announce route 10.0.0.0/24 next-hop self\n") sys.stdout.flush() # Target specific peer sys.stdout.write("peer 192.168.1.1 announce route 10.0.1.0/24 next-hop self\n") sys.stdout.flush() # Withdraw from all peers sys.stdout.write("peer * withdraw route 10.0.0.0/24\n") sys.stdout.flush() ``` --- ## 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) ```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:** ```ini process announce { run /etc/exabgp/api/announce.py; encoder text; } ``` --- ### Example 2: Health Check (Python) ```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) ```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:** ```ini process receive { run /etc/exabgp/api/receive.py; encoder json; receive { parsed; updates; } } ``` --- ### Example 4: Bash Script ```bash #!/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 ```go 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:** ```ini process my-program { run /etc/exabgp/api/my-program.py; encoder text; } ``` ExaBGP automatically respawns crashed processes. --- ## Communication Flow ### Sending Commands (STDOUT → ExaBGP) ```python # 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: ```bash python3 -u /etc/exabgp/api/announce.py ``` --- ### Receiving Messages (STDIN ← ExaBGP) ```python # 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) > ✅ **Available in ExaBGP 4.x and 5.x/main** - Default: enabled in both versions ### What is ACK? The ACK (acknowledgment) feature allows your program to receive feedback on whether commands succeeded or failed. **Behavior (when enabled, which is the default):** - 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 **Responses:** - `done\n` - Command succeeded - `error\n` - Command failed (syntax error, invalid route, etc.) - `shutdown\n` - ExaBGP is shutting down **Default Configuration (both 4.x and 5.x/main):** ```bash # ACK is enabled by default in both versions export exabgp.api.ack=true # Default # To disable (for simpler programs that don't check responses): export exabgp.api.ack=false ``` ### 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 ACK is disabled:** - Commands are fire-and-forget - No feedback on success/failure - Simpler code (no need to read responses) - Check ExaBGP logs for errors --- ### Using ACK in Your Program **Robust ACK handling with polling loop:** ```python #!/usr/bin/env python3 """ announce_with_ack.py - Send commands with ACK verification """ import sys import select import time def wait_for_ack(expected_count=1, timeout=30): """ Wait for ACK responses from ExaBGP. Polls STDIN until all expected ACK messages are received. ExaBGP may not respond immediately, so we loop with sleep. Handles both text and JSON encoder formats: - Text: "done", "error", "shutdown" - JSON: {"answer": "done|error|shutdown", "message": "..."} Args: expected_count: Number of ACK messages expected (default: 1) timeout: Total timeout in seconds (default: 30) Returns: True if all ACKs received successfully False if any command failed or timeout occurred Raises: SystemExit: If ExaBGP sends shutdown message """ import json received = 0 start_time = time.time() while received < expected_count: # Check if we've exceeded timeout elapsed = time.time() - start_time if elapsed >= timeout: return False # Poll for data (non-blocking with short timeout) ready, _, _ = select.select([sys.stdin], [], [], 0.1) if ready: line = sys.stdin.readline().strip() # Parse response (could be text or JSON) answer = None if line.startswith('{'): # JSON format: {"answer": "done|error|shutdown", ...} try: data = json.loads(line) answer = data.get('answer') except: pass else: # Text format: done|error|shutdown answer = line if answer == "done": received += 1 elif answer == "error": return False elif answer == "shutdown": raise SystemExit(0) # Ignore other messages (could be BGP updates if receiving) else: # No data yet, sleep briefly before next poll time.sleep(0.1) return True def send_command(command): """Send command and wait for ACK""" sys.stdout.write(command + "\n") sys.stdout.flush() return wait_for_ack(expected_count=1) # Send commands with verification if send_command("announce route 100.10.0.0/24 next-hop self"): sys.stderr.write("[OK] Route 100.10.0.0/24 announced\n") if send_command("announce route 100.20.0.0/24 next-hop self"): sys.stderr.write("[OK] Route 100.20.0.0/24 announced\n") # Keep running while True: time.sleep(60) ``` --- ### When ACK is Disabled **When ACK is disabled (exabgp.api.ack=false):** ```python #!/usr/bin/env python3 """ announce_no_ack.py - Simple announcer when ACK is disabled """ 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 - command is fire-and-forget sys.stderr.write(f"[SENT] {command}\n") # Simpler code when ACK is disabled send_command_no_ack("announce route 100.10.0.0/24 next-hop self") # Keep running while True: time.sleep(60) ``` --- ### Detecting ACK Support ```python #!/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 **ACK is enabled by default in both ExaBGP 4.x and 5.x/main:** ```bash # Default (ACK enabled) exabgp /etc/exabgp/exabgp.conf # Explicitly enable ACK (already the default) export exabgp.api.ack=true exabgp /etc/exabgp/exabgp.conf ``` **Disable ACK (for simpler programs that don't need feedback):** ```bash export exabgp.api.ack=false exabgp /etc/exabgp/exabgp.conf ``` **In healthcheck application:** ```bash # With ACK (default) python -m exabgp healthcheck --cmd "curl -sf http://localhost/health" # Without ACK (simpler mode) python -m exabgp healthcheck --no-ack --cmd "curl -sf http://localhost/health" ``` --- ### When to Use ACK **Use ACK (recommended for production):** - Production deployments - Automated DDoS mitigation - Critical route announcements - Any scenario where silent failures are unacceptable - When you need reliable error handling **Disable ACK when:** - Simple static announcements that don't change - Testing/development with manual verification - Programs that don't need error feedback - Simpler code without response handling --- ### Dynamic ACK Control (Runtime Commands) > ✅ **Available in ExaBGP 5.0.0+** - Control ACK behavior dynamically at runtime In addition to the startup configuration (`exabgp.api.ack=true/false`), ExaBGP 5.0.0+ provides **three API commands** to dynamically control ACK behavior during runtime: #### The Three ACK Control Commands **1. `enable-ack` - Re-enable ACK Responses** ``` enable-ack ``` - Re-enables ACK responses for this API connection - **Sends "done" ACK** for the `enable-ack` command itself - All future commands will receive ACK responses (`done` or `error`) - Use case: Re-enable error checking after disabling it **2. `disable-ack` - Disable ACK with Final Response** ``` disable-ack ``` - Disables ACK responses for this API connection - **Sends "done" ACK** for the `disable-ack` command itself (final ACK) - Future commands will NOT receive ACK responses (fire-and-forget mode) - Use case: Disable ACK gracefully after receiving confirmation **3. `silence-ack` - Immediate Silence (No Response)** ``` silence-ack ``` - Disables ACK responses immediately - **Does NOT send ACK** for the `silence-ack` command itself - Future commands will NOT receive ACK responses (fire-and-forget mode) - Use case: Disable ACK immediately without waiting for final response --- #### Comparison: disable-ack vs silence-ack | Command | Sends ACK for itself? | Future commands ACKed? | Use Case | |---------|----------------------|------------------------|----------| | `enable-ack` | ✅ Yes ("done") | ✅ Yes | Re-enable error checking | | `disable-ack` | ✅ Yes ("done", final ACK) | ❌ No | Graceful transition to fire-and-forget | | `silence-ack` | ❌ No (immediate silence) | ❌ No | Immediate transition to fire-and-forget | **Key difference:** - `disable-ack`: Sends one final "done" response, THEN stops ACKing - `silence-ack`: Stops ACKing immediately (no response to this command) --- #### Example: Dynamic ACK Control ```python #!/usr/bin/env python3 """ dynamic_ack_control.py - Demonstrate runtime ACK control """ import sys import select import time def wait_for_ack(timeout=2): """Wait for ACK response (returns True if 'done', False otherwise)""" ready, _, _ = select.select([sys.stdin], [], [], timeout) if ready: response = sys.stdin.readline().strip() return response == "done" return False def send_command(command, expect_ack=True): """Send command with optional ACK verification""" sys.stdout.write(command + "\n") sys.stdout.flush() if expect_ack: if wait_for_ack(): sys.stderr.write(f"[OK] {command}\n") return True else: sys.stderr.write(f"[FAIL] {command}\n") return False else: sys.stderr.write(f"[SENT] {command} (no ACK expected)\n") return True # Initial state: ACK is enabled by default time.sleep(0.2) # Wait for BGP session establishment # Step 1: Send command with ACK (baseline) send_command("announce route 100.10.0.0/24 next-hop self", expect_ack=True) # Step 2: Disable ACK gracefully (receive final "done") send_command("disable-ack", expect_ack=True) # Gets final ACK # Step 3: Send commands without ACK (fire-and-forget mode) send_command("announce route 100.20.0.0/24 next-hop self", expect_ack=False) send_command("announce route 100.30.0.0/24 next-hop self", expect_ack=False) # Step 4: Re-enable ACK send_command("enable-ack", expect_ack=True) # ACK is back # Step 5: Verify ACK is working again send_command("announce route 100.40.0.0/24 next-hop self", expect_ack=True) # Keep running while True: time.sleep(60) ``` **Output:** ``` [OK] announce route 100.10.0.0/24 next-hop self [OK] disable-ack [SENT] announce route 100.20.0.0/24 next-hop self (no ACK expected) [SENT] announce route 100.30.0.0/24 next-hop self (no ACK expected) [OK] enable-ack [OK] announce route 100.40.0.0/24 next-hop self ``` --- #### Example: silence-ack (Immediate Silence) ```python #!/usr/bin/env python3 """ silence_ack_example.py - Demonstrate immediate ACK silencing """ import sys import select import time def wait_for_ack(timeout=1): """Wait for ACK response""" ready, _, _ = select.select([sys.stdin], [], [], timeout) if ready: response = sys.stdin.readline().strip() return response == "done" return False time.sleep(0.2) # Step 1: Send command with ACK sys.stdout.write("announce route 200.10.0.0/24 next-hop self\n") sys.stdout.flush() if wait_for_ack(): sys.stderr.write("[OK] Route announced with ACK\n") # Step 2: Use silence-ack (NO ACK for this command itself) sys.stdout.write("silence-ack\n") sys.stdout.flush() # Note: No ACK expected for silence-ack itself if wait_for_ack(): sys.stderr.write("[ERROR] Unexpected ACK for silence-ack\n") else: sys.stderr.write("[OK] silence-ack executed (no ACK as expected)\n") # Step 3: Send commands without expecting ACK sys.stdout.write("announce route 200.20.0.0/24 next-hop self\n") sys.stdout.flush() sys.stderr.write("[SENT] Route announced (no ACK expected)\n") # Keep running while True: time.sleep(60) ``` --- #### Use Cases for Dynamic ACK Control **When to use `enable-ack`:** - Re-enable error checking after a batch of fire-and-forget commands - Switch from performance mode to reliability mode - Debugging: Enable ACK temporarily to verify commands **When to use `disable-ack`:** - High-performance bulk announcements where ACK overhead is too high - Graceful transition to fire-and-forget mode - You still want confirmation that ACK was disabled successfully - **Best practice**: Using ExaBGP 5.x/main with non-ACK-aware API programs **When to use `silence-ack`:** - Maximum performance: Skip even the final ACK response - Immediate transition to fire-and-forget mode - Bulk operations where even one ACK adds latency --- #### Best Practice: Non-ACK-Aware Programs on ExaBGP 5.x/main If you're running ExaBGP 5.x/main with legacy API programs that don't handle ACK responses, use `disable-ack` at the start of your program: ```python #!/usr/bin/env python3 """ legacy_program.py - Old API program that doesn't handle ACK """ import sys import time # Best practice: Disable ACK at startup (safe on all ExaBGP versions) # - ExaBGP 5.x/main: Disables ACK after sending final "done" # - ExaBGP 4.x: Prints warning but continues (no harm) sys.stdout.write("disable-ack\n") sys.stdout.flush() time.sleep(0.1) # Give ExaBGP time to process # Now your legacy code works without modification while True: # Legacy code that doesn't expect ACK responses sys.stdout.write("announce route 100.10.0.100/32 next-hop self\n") sys.stdout.flush() # No ACK handling needed - fire-and-forget mode time.sleep(5) ``` **Why this works:** - ✅ **ExaBGP 5.x/main**: ACK is disabled, your program works normally - ⚠️ **ExaBGP 4.x**: Command is ignored with warning, but `exabgp.api.ack=true` default still applies (your program may hang if it doesn't handle ACK) - 💡 **Solution**: Also set `exabgp.api.ack=false` in environment for ExaBGP 4.x compatibility **For maximum compatibility across all ExaBGP versions:** ```bash # Run your program with both methods: # 1. Environment variable (works on 4.x and 5.x) # 2. disable-ack command (works on 5.x, ignored on 4.x) export exabgp.api.ack=false exabgp /etc/exabgp/exabgp.conf ``` Then in your program, still send `disable-ack` as defense-in-depth: ```python # Send disable-ack even if environment variable is set # (no harm, ensures ACK is off regardless of config) sys.stdout.write("disable-ack\n") sys.stdout.flush() time.sleep(0.1) ``` **Example: Bulk Announcement Optimization** ```python #!/usr/bin/env python3 """ bulk_with_ack_control.py - Optimize bulk announcements """ import sys import time time.sleep(0.2) # Step 1: Verify ExaBGP is ready (with ACK) sys.stdout.write("announce route 10.0.0.1/32 next-hop self\n") sys.stdout.flush() # Check ACK... # Step 2: Disable ACK for bulk operation (10,000 routes) sys.stdout.write("silence-ack\n") # Immediate silence for max performance sys.stdout.flush() # Step 3: Bulk announce without ACK overhead for i in range(10000): prefix = f"10.{i//256}.{i%256}.0/24" sys.stdout.write(f"announce route {prefix} next-hop self\n") sys.stdout.flush() # Step 4: Re-enable ACK to verify completion sys.stdout.write("enable-ack\n") sys.stdout.flush() # Check ACK to confirm ExaBGP processed everything # Step 5: Send verification command sys.stdout.write("announce route 10.255.255.0/24 next-hop self\n") sys.stdout.flush() # Check ACK to confirm ExaBGP is still responsive while True: time.sleep(60) ``` --- #### Version Compatibility | Feature | ExaBGP 4.x | ExaBGP 5.x/main | |---------|-----------|-----------------| | `exabgp.api.ack=true/false` (startup config) | ✅ Yes | ✅ Yes | | `enable-ack` (runtime command) | ⚠️ Ignored (warning) | ✅ Yes | | `disable-ack` (runtime command) | ⚠️ Ignored (warning) | ✅ Yes | | `silence-ack` (runtime command) | ⚠️ Ignored (warning) | ✅ Yes | **Backward Compatibility:** - ✅ **Safe to send to older versions**: These commands are safe to send to ExaBGP 4.x - ⚠️ **Warning only**: Older versions will print a warning but will NOT crash or fail - 🎯 **Best practice**: Safe to include in API programs that may run on different ExaBGP versions **Migration notes:** - ExaBGP 4.x: ACK behavior can only be set at startup via environment variable - ExaBGP 5.x/main: ACK behavior can be changed dynamically at runtime via API commands - **Recommended**: Use `disable-ack` at the start of your API program if you don't want to handle ACK responses --- ## Error Handling ### Command Errors (When ACK is Disabled) **ExaBGP logs errors but continues:** ```python # Invalid command (when ACK is disabled) 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 (When ACK is Enabled - Default) **ExaBGP sends error response:** ```python # Invalid command (with ACK enabled - the default) 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:** ```python 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:** ```python if msg['type'] == 'state': if msg['neighbor']['state'] == 'down': # Session dropped - stop announcing? pass ``` --- ## Best Practices ### 1. Always Flush STDOUT ```python sys.stdout.flush() # After every write ``` Or use unbuffered mode: ```python #!/usr/bin/env python3 -u ``` ### 2. Wait for BGP Session ```python time.sleep(2) # Give ExaBGP time to establish session ``` ### 3. Keep Process Alive ```python while True: time.sleep(60) # Don't exit immediately ``` ### 4. Use Structured Logging ```python 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 ```python try: result = check_service() except Exception as e: log(f"Health check error: {e}") # Assume down on error result = False ``` ### 6. Use State Tracking ```python 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 ```python 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 - **[Text API Reference](Text-API-Reference)** - All text commands - **[JSON API Reference](JSON-API-Reference)** - JSON message format - **[API Commands](API-Commands)** - Complete command reference - **[Writing API Programs](Writing-API-Programs)** - Best practices guide - **[Production Best Practices](Production-Best-Practices)** - Production deployment ### Examples - **[Quick Start](Quick-Start)** - Basic examples - **[High Availability](Service-High-Availability)** - Health check patterns - **[DDoS Mitigation](DDoS-Mitigation)** - FlowSpec automation ### Configuration - **[Configuration Syntax](Configuration-Syntax)** - Process configuration - **[Environment Variables](Environment-Variables)** - API environment --- **Ready to dive deeper?** Continue to [Text API Reference](Text-API-Reference) → ---