Skip to content

JSON API Reference

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

JSON API Reference

Complete reference for ExaBGP's JSON-encoded messages


Table of Contents


Overview

The JSON encoder provides structured BGP messages to your API program via STDIN.

Configuration:

process receive-routes {
    run /etc/exabgp/api/receive.py;
    encoder json;
    receive {
        parsed;    # Receive parsed BGP messages
        updates;   # Receive route updates
        neighbor-changes;  # Receive session state changes
    }
}

Usage in your program:

import sys
import json

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

    message = json.loads(line)
    print(f"Message type: {message['type']}")

Format:

  • One JSON object per line
  • UTF-8 encoded
  • No newlines within JSON (compact format)
  • Continuous stream (process must read continuously)

Message Structure

Common Fields (All Messages)

Every JSON message includes:

{
  "exabgp": "4.2.25",        // ExaBGP version
  "time": 1699564800.123,    // Unix timestamp (float)
  "host": "hostname",        // Hostname
  "pid": 12345,              // ExaBGP process ID
  "ppid": 1,                 // Parent process ID
  "counter": 1,              // Message counter (increments)
  "type": "update",          // Message type
  "neighbor": {              // Neighbor-specific data
    ...
  }
}

Message types:

  • update - Route announcement/withdrawal
  • state - BGP session state change
  • notification - BGP error/notification
  • keepalive - BGP keepalive received
  • open - BGP OPEN message received
  • refresh - Route refresh message

Message Types

UPDATE

Route announcements and withdrawals.

Structure:

{
  "type": "update",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "asn": {
      "local": 65001,
      "peer": 65000
    },
    "direction": "receive",  // or "send"
    "message": {
      "update": {
        "announce": { ... },  // New/updated routes
        "withdraw": { ... }   // Withdrawn routes
      }
    }
  }
}

STATE

BGP session state changes.

Structure:

{
  "type": "state",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "asn": {
      "local": 65001,
      "peer": 65000
    },
    "state": "up"  // or "down", "connected", etc.
  }
}

Possible states:

  • idle - Not connected
  • connect - Attempting connection
  • active - Connection failed, retrying
  • opensent - OPEN message sent
  • openconfirm - OPEN message received
  • up - Session established βœ…
  • down - Session terminated

NOTIFICATION

BGP error notifications.

{
  "type": "notification",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "message": {
      "notification": {
        "code": 6,
        "subcode": 2,
        "data": "Administrative Reset"
      }
    }
  }
}

Common notification codes:

  • 1 - Message Header Error
  • 2 - OPEN Message Error
  • 3 - UPDATE Message Error
  • 4 - Hold Timer Expired
  • 5 - Finite State Machine Error
  • 6 - Cease

KEEPALIVE

BGP keepalive received (rarely needed to process).

{
  "type": "keepalive",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    }
  }
}

OPEN

BGP OPEN message received (session establishment).

{
  "type": "open",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "message": {
      "open": {
        "version": 4,
        "asn": 65000,
        "hold_time": 180,
        "router_id": "192.168.1.1",
        "capabilities": {
          "multiprotocol": [
            "ipv4 unicast",
            "ipv6 unicast",
            "ipv4 flowspec"
          ],
          "four_bytes_asn": true,
          "route_refresh": true,
          "graceful_restart": false
        }
      }
    }
  }
}

UPDATE Messages

IPv4 Unicast Announcement

{
  "exabgp": "4.2.25",
  "time": 1699564800.0,
  "type": "update",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "asn": {
      "local": 65001,
      "peer": 65000
    },
    "direction": "receive",
    "message": {
      "update": {
        "announce": {
          "ipv4 unicast": {
            "100.10.0.0/24": [
              {
                "next-hop": "192.168.1.1",
                "origin": "igp",
                "as-path": [ 65000 ],
                "med": 100,
                "local-preference": 100,
                "community": [
                  [ 65000, 100 ],
                  [ 65000, 200 ]
                ]
              }
            ]
          }
        }
      }
    }
  }
}

Key points:

  • Address family: "ipv4 unicast" (space, not hyphen)
  • Attributes in array: [ { ... } ] even for single route
  • Communities as nested arrays: [ [ AS, value ], [ AS, value ] ]

IPv4 Unicast Withdrawal

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "withdraw": {
          "ipv4 unicast": {
            "100.10.0.0/24": {}
          }
        }
      }
    }
  }
}

Key points:

  • Empty object {} for withdrawn route
  • No attributes needed

IPv6 Unicast

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "announce": {
          "ipv6 unicast": {
            "2001:db8::/32": [
              {
                "next-hop": "2001:db8::1",
                "origin": "igp",
                "as-path": [ 65000 ]
              }
            ]
          }
        }
      }
    }
  }
}

FlowSpec

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "announce": {
          "ipv4 flowspec": {
            "flow": {
              "destination-prefix": "100.10.0.0/24",
              "source-prefix": "10.0.0.0/8",
              "protocol": [ "=6" ],
              "destination-port": [ "=80" ],
              "tcp-flags": [ "syn" ]
            },
            "attributes": {
              "extended-community": [
                "traffic-rate:0:0"  // Discard
              ]
            }
          }
        }
      }
    }
  }
}

FlowSpec match operators:

  • =80 - Equals 80
  • >1023 - Greater than
  • <1024 - Less than
  • >=1024&<=65535 - Range

FlowSpec actions (extended-community):

  • traffic-rate:0:0 - Discard (rate-limit to 0)
  • traffic-rate:0:1000000 - Rate-limit to 1 Mbps
  • redirect:65001:100 - Redirect to VRF
  • traffic-marking:46 - Mark DSCP

L3VPN

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "announce": {
          "ipv4 mpls-vpn": {
            "65001:100:100.10.0.0/24": [
              {
                "next-hop": "192.168.1.1",
                "origin": "igp",
                "as-path": [ 65000 ],
                "label": 10000,
                "route-distinguisher": "65001:100",
                "extended-community": [
                  "target:65001:100",
                  "origin:65001:100"
                ]
              }
            ]
          }
        }
      }
    }
  }
}

Key format:

  • Prefix format: <RD>:<prefix>
  • Example: 65001:100:100.10.0.0/24

EVPN

EVPN Type 2 (MAC/IP Advertisement):

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "announce": {
          "evpn": {
            "mac-ip-advertisement": {
              "route-distinguisher": "65001:100",
              "ethernet-segment-identifier": "00:00:00:00:00:00:00:00:00:00",
              "ethernet-tag": 0,
              "mac": "aa:bb:cc:dd:ee:ff",
              "ip": "100.10.0.100",
              "label": [ 10000 ],
              "next-hop": "192.168.1.1",
              "extended-community": [
                "target:65001:100",
                "encapsulation:vxlan"
              ]
            }
          }
        }
      }
    }
  }
}

EVPN Type 3 (Inclusive Multicast):

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "announce": {
          "evpn": {
            "inclusive-multicast": {
              "route-distinguisher": "65001:100",
              "ethernet-tag": 0,
              "originating-router": "192.168.1.1",
              "next-hop": "192.168.1.1",
              "extended-community": [
                "target:65001:100"
              ]
            }
          }
        }
      }
    }
  }
}

BGP-LS

{
  "type": "update",
  "neighbor": {
    "message": {
      "update": {
        "announce": {
          "ipv4 link-state": {
            "node": {
              "protocol-id": "isis-level2",
              "identifier": "0000.0000.0001.00",
              "local-node-descriptors": {
                "asn": 65000,
                "bgp-ls-identifier": 0,
                "router-id": "192.168.1.1"
              },
              "attributes": {
                "node-name": "router1",
                "isis-area-identifier": "49.0001"
              }
            }
          }
        }
      }
    }
  }
}

STATE Messages

Session UP

{
  "exabgp": "4.2.25",
  "time": 1699564800.0,
  "type": "state",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "asn": {
      "local": 65001,
      "peer": 65000
    },
    "state": "up"
  }
}

Session DOWN

{
  "type": "state",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "state": "down",
    "reason": "Cease - Administrative Reset"
  }
}

NOTIFICATION Messages

Administrative Reset

{
  "type": "notification",
  "neighbor": {
    "address": {
      "local": "192.168.1.2",
      "peer": "192.168.1.1"
    },
    "message": {
      "notification": {
        "code": 6,
        "subcode": 2,
        "data": "Administrative Reset"
      }
    }
  }
}

Hold Timer Expired

{
  "type": "notification",
  "neighbor": {
    "message": {
      "notification": {
        "code": 4,
        "subcode": 0,
        "data": "Hold Timer Expired"
      }
    }
  }
}

Parsing Patterns

Basic Parser

#!/usr/bin/env python3
import sys
import json

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

    try:
        msg = json.loads(line)

        # Route update
        if msg['type'] == 'update':
            handle_update(msg)

        # Session state
        elif msg['type'] == 'state':
            handle_state(msg)

        # Notification
        elif msg['type'] == 'notification':
            handle_notification(msg)

    except json.JSONDecodeError as e:
        print(f"JSON parse error: {e}", file=sys.stderr)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)

Parse IPv4 Unicast Routes

def handle_update(msg):
    """Process UPDATE messages"""
    if 'update' not in msg['neighbor']['message']:
        return

    update = msg['neighbor']['message']['update']

    # Announced routes
    if 'announce' in update:
        if 'ipv4 unicast' in update['announce']:
            routes = update['announce']['ipv4 unicast']
            for prefix, attrs_list in routes.items():
                for attrs in attrs_list:
                    nexthop = attrs.get('next-hop', 'unknown')
                    med = attrs.get('med', 0)
                    print(f"[ANNOUNCE] {prefix} via {nexthop} (MED {med})",
                          file=sys.stderr)

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

Parse FlowSpec Rules

def handle_flowspec(msg):
    """Process FlowSpec updates"""
    if 'update' not in msg['neighbor']['message']:
        return

    update = msg['neighbor']['message']['update']

    if 'announce' in update:
        if 'ipv4 flowspec' in update['announce']:
            flows = update['announce']['ipv4 flowspec']

            for flow, attrs in flows.items():
                # Parse match conditions
                dest = flow.get('destination-prefix', 'any')
                src = flow.get('source-prefix', 'any')
                port = flow.get('destination-port', ['any'])[0]
                proto = flow.get('protocol', ['any'])[0]

                # Parse action
                ext_comm = attrs['attributes'].get('extended-community', [])
                action = 'unknown'
                for comm in ext_comm:
                    if comm.startswith('traffic-rate'):
                        rate = comm.split(':')[2]
                        action = f"rate-limit {rate} bps" if rate != '0' else 'discard'
                    elif comm.startswith('redirect'):
                        action = f"redirect {comm}"

                print(f"[FLOWSPEC] {src} -> {dest}:{port} proto:{proto} action:{action}",
                      file=sys.stderr)

Parse Session State

def handle_state(msg):
    """Process STATE messages"""
    peer = msg['neighbor']['address']['peer']
    state = msg['neighbor']['state']

    if state == 'up':
        print(f"[STATE] BGP session with {peer} is UP", file=sys.stderr)
    elif state == 'down':
        reason = msg['neighbor'].get('reason', 'unknown')
        print(f"[STATE] BGP session with {peer} is DOWN: {reason}",
              file=sys.stderr)
    else:
        print(f"[STATE] BGP session with {peer}: {state}", file=sys.stderr)

Track Announced Routes

class RouteTracker:
    def __init__(self):
        self.routes = {}  # prefix -> attributes

    def update(self, msg):
        if 'update' not in msg['neighbor']['message']:
            return

        update = msg['neighbor']['message']['update']

        # Add/update announced routes
        if 'announce' in update:
            if 'ipv4 unicast' in update['announce']:
                routes = update['announce']['ipv4 unicast']
                for prefix, attrs_list in routes.items():
                    self.routes[prefix] = attrs_list[0]
                    print(f"[TRACKER] Added {prefix}, total routes: {len(self.routes)}",
                          file=sys.stderr)

        # Remove withdrawn routes
        if 'withdraw' in update:
            if 'ipv4 unicast' in update['withdraw']:
                routes = update['withdraw']['ipv4 unicast']
                for prefix in routes.keys():
                    if prefix in self.routes:
                        del self.routes[prefix]
                        print(f"[TRACKER] Removed {prefix}, total routes: {len(self.routes)}",
                              file=sys.stderr)

    def get_all_routes(self):
        return self.routes

# Usage
tracker = RouteTracker()
while True:
    line = sys.stdin.readline()
    msg = json.loads(line)
    if msg['type'] == 'update':
        tracker.update(msg)

Complete Examples

Example 1: Log All Updates

#!/usr/bin/env python3
"""
log_updates.py - Log all BGP updates to file
"""
import sys
import json
import time

LOG_FILE = '/var/log/exabgp-updates.log'

def log(message):
    timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
    with open(LOG_FILE, 'a') as f:
        f.write(f"{timestamp} {message}\n")

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

    try:
        msg = json.loads(line)

        if msg['type'] == 'update':
            update = msg['neighbor']['message']['update']
            peer = msg['neighbor']['address']['peer']

            if 'announce' in update:
                if 'ipv4 unicast' in update['announce']:
                    for prefix in update['announce']['ipv4 unicast'].keys():
                        log(f"ANNOUNCE from {peer}: {prefix}")

            if 'withdraw' in update:
                if 'ipv4 unicast' in update['withdraw']:
                    for prefix in update['withdraw']['ipv4 unicast'].keys():
                        log(f"WITHDRAW from {peer}: {prefix}")

        elif msg['type'] == 'state':
            peer = msg['neighbor']['address']['peer']
            state = msg['neighbor']['state']
            log(f"STATE {peer}: {state}")

    except Exception as e:
        log(f"ERROR: {e}")

Example 2: Trigger Actions on Routes

#!/usr/bin/env python3
"""
route_trigger.py - Trigger actions when specific routes appear
"""
import sys
import json
import subprocess

WATCH_PREFIXES = [
    '100.10.0.0/24',
    '100.20.0.0/24'
]

def trigger_action(prefix, action):
    """Run script when route changes"""
    if action == 'announce':
        subprocess.run(['/etc/exabgp/scripts/on_route_add.sh', prefix])
    elif action == 'withdraw':
        subprocess.run(['/etc/exabgp/scripts/on_route_remove.sh', prefix])

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

    try:
        msg = json.loads(line)

        if msg['type'] == 'update':
            update = msg['neighbor']['message']['update']

            # Check announcements
            if 'announce' in update:
                if 'ipv4 unicast' in update['announce']:
                    for prefix in update['announce']['ipv4 unicast'].keys():
                        if prefix in WATCH_PREFIXES:
                            print(f"[TRIGGER] Route announced: {prefix}", file=sys.stderr)
                            trigger_action(prefix, 'announce')

            # Check withdrawals
            if 'withdraw' in update:
                if 'ipv4 unicast' in update['withdraw']:
                    for prefix in update['withdraw']['ipv4 unicast'].keys():
                        if prefix in WATCH_PREFIXES:
                            print(f"[TRIGGER] Route withdrawn: {prefix}", file=sys.stderr)
                            trigger_action(prefix, 'withdraw')

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

Example 3: Prometheus Metrics Exporter

#!/usr/bin/env python3
"""
metrics.py - Export BGP metrics for Prometheus
"""
import sys
import json
from prometheus_client import start_http_server, Counter, Gauge

# Metrics
routes_announced = Counter('bgp_routes_announced_total', 'Total routes announced')
routes_withdrawn = Counter('bgp_routes_withdrawn_total', 'Total routes withdrawn')
active_routes = Gauge('bgp_active_routes', 'Number of active routes')
session_state = Gauge('bgp_session_up', 'BGP session state (1=up, 0=down)', ['peer'])

# Start metrics server
start_http_server(9101)

route_count = 0

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

    try:
        msg = json.loads(line)

        if msg['type'] == 'update':
            update = msg['neighbor']['message']['update']

            if 'announce' in update:
                if 'ipv4 unicast' in update['announce']:
                    count = len(update['announce']['ipv4 unicast'])
                    routes_announced.inc(count)
                    route_count += count
                    active_routes.set(route_count)

            if 'withdraw' in update:
                if 'ipv4 unicast' in update['withdraw']:
                    count = len(update['withdraw']['ipv4 unicast'])
                    routes_withdrawn.inc(count)
                    route_count -= count
                    active_routes.set(route_count)

        elif msg['type'] == 'state':
            peer = msg['neighbor']['address']['peer']
            state = msg['neighbor']['state']
            session_state.labels(peer=peer).set(1 if state == 'up' else 0)

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

Best Practices

1. Always Parse Safely

# Good - safe access
nexthop = attrs.get('next-hop', 'unknown')

# Bad - can raise KeyError
nexthop = attrs['next-hop']

2. Handle Missing Fields

# Address family may not exist
if 'ipv4 unicast' in update.get('announce', {}):
    routes = update['announce']['ipv4 unicast']

3. Process Continuously

# Good - continuous loop
while True:
    line = sys.stdin.readline()
    if not line:
        break
    process(line)

# Bad - read once and exit
line = sys.stdin.readline()
process(line)
# Process exits, ExaBGP restarts it constantly

4. Log to STDERR

# STDOUT is for commands to ExaBGP
# STDERR is for logging
print(f"[INFO] Received route", file=sys.stderr)

5. Catch JSON Errors

try:
    msg = json.loads(line)
except json.JSONDecodeError:
    print(f"[ERROR] Invalid JSON: {line}", file=sys.stderr)
    continue

See Also


Need to send commands? See Text API Reference β†’


πŸ‘» Ghost written by Claude (Anthropic AI)

Clone this wiki locally