This document describes the internal architecture of BGPSee, following the lifecycle of the application from startup through peering, message reception, and shutdown.
BGPSee uses three types of threads: a main thread that handles startup and lifecycle, one thread per BGP peer that manages the protocol state machine, and a single output writer thread that serialises JSON to stdout.
graph TD
subgraph "Main Thread"
A[CLI Parsing] --> B[Create BGP Instance]
B --> C[Create Peers]
C --> D[Activate Peers]
D --> E[Wait for Ctrl+D]
E --> F[Deactivate & Free]
end
subgraph "Output Writer Thread"
OQ[output_queue] --> OW[output_writer_thread]
OW --> STDOUT[stdout]
end
subgraph "Peer Thread 1"
P1[select loop] --> FSM1[FSM]
FSM1 --> RX1[recv_msg]
RX1 --> FMT1[format JSON]
FMT1 --> OQ
end
subgraph "Peer Thread 2"
P2[select loop] --> FSM2[FSM]
FSM2 --> RX2[recv_msg]
RX2 --> FMT2[format JSON]
FMT2 --> OQ
end
subgraph "Peer Thread N"
PN[select loop] --> FSMN[FSM]
FSMN --> RXN[recv_msg]
RXN --> FMTN[format JSON]
FMTN --> OQ
end
D --> P1
D --> P2
D --> PN
B --> OQ
The top-level container. Holds the local ASN, router ID, BGP version, an array of up to 256 peer pointers, and a shared output_queue. Created once at startup and owned by the main thread.
Per-peer state. Contains the FSM state (enum bgp_fsm_states), the TCP socket, an array of protocol timers (bgp_local_timer[N_LOCAL_TIMERS]), the peer's thread handle, and two linked-list message queues: ingress_q for messages awaiting FSM processing, and output_q for messages awaiting JSON formatting. Each peer holds a pointer back to the shared output_queue.
A parsed BGP message. Tagged with a type (OPEN, UPDATE, KEEPALIVE, NOTIFICATION) and containing the decoded fields in a union. Messages carry two list nodes (ingress and output) so they can sit on both the ingress and output queues simultaneously.
A mutex+condvar guarded linked list of formatted JSON strings. Peer threads push; the writer thread pops and writes to stdout. This decouples protocol processing from I/O blocking — if stdout is piped to a slow consumer, only the writer thread stalls, not the peer threads that need to send keepalives.
Entry is main() in main.c. After parsing CLI arguments with getopt_long, the main thread:
- Calls
create_bgp_instance()— allocates the instance, initialises theoutput_queue, and starts the output writer thread viaoutput_queue_start(). - Parses each peer argument (
<ip>,<asn>[,<name>]), callingcreate_bgp_peer()for each. This allocates the peer struct, initialises timers withinitialise_local_timers(), sets up the mutex, and links the peer into the instance's peer array. - Calls
activate_bgp_peer()for each peer, which spawns the peer's thread withpthread_create()targetingbgp_peer_thread().
The main thread then blocks reading stdin, waiting for EOF (Ctrl+D) as the shutdown signal.
Each peer thread runs bgp_peer_thread() in bgp.c. This is a select() loop with a 1-second timeout. Each iteration:
- Builds an
fd_setviaget_read_fd_set()containing the TCP socket and (on Linux) the timer file descriptors. - Calls
select()to wait for socket data or timer fires. - If the socket is readable, calls
recv_msg()to parse the incoming BGP message and places it on both theingress_qandoutput_q. - Dispatches to the appropriate FSM state handler.
The FSM states progress as:
IDLE (fsm_state_idle) — Starts the ConnectRetryTimer, calls tcp_connect() to open a TCP connection to port 179. On success, transitions to CONNECT.
CONNECT (fsm_state_connect) — Builds BGP capabilities (MP extensions, 4-byte ASN, route refresh), sends an OPEN message via queue_and_send_open(), starts the HoldTimer, and transitions to OPENSENT.
OPENSENT (fsm_state_opensent) — Waits for the peer's OPEN. Validates the peer ASN, BGP version, hold time, and negotiates 4-byte ASN support. On success, sends a KEEPALIVE, starts the recurring KeepaliveTimer, and transitions to OPENCONFIRM.
OPENCONFIRM (fsm_state_openconfirm) — Sends keepalives on timer fires. Waits for the peer's KEEPALIVE confirming the session. On receipt, transitions to ESTABLISHED.
ESTABLISHED (fsm_state_established) — The steady state. Sends keepalives on timer fires, resets the HoldTimer on any received message (KEEPALIVE or UPDATE). UPDATE messages are parsed and queued for output. A received NOTIFICATION or hold timer expiry closes the session.
When recv_msg() in bgp_message.c reads from the socket, it validates the 16-byte marker and message header, then dispatches to type-specific parsers (parse_open_body, parse_update_body, etc.) that decode path attributes, NLRI prefixes, and capabilities into the bgp_msg structure.
After the FSM processes the message (marking it actioned), print_bgp_msg_and_gc() iterates the output_q, formats each actioned message to a JSON string using jansson (format_msg_json or format_msg_jsonl in bgp_print.c), and pushes the string to the shared output_queue via output_queue_push().
The output writer thread (output_writer_thread in output_queue.c) blocks on pthread_cond_wait() until items arrive, then writes each JSON string to stdout with fflush(). This architecture means that even if stdout blocks for seconds, peer threads continue processing protocol messages and sending keepalives.
BGPSee uses RFC 4271 timers: ConnectRetryTimer, HoldTimer, KeepaliveTimer, and IdleHoldTimer (for reconnection backoff). On Linux, these are implemented with timerfd file descriptors that integrate naturally with select(). On macOS, a polling approach uses clock_gettime(CLOCK_MONOTONIC) to check absolute expiry times each loop iteration — acceptable since BGP timers are all second-resolution (15s-120s) and the select loop runs at least once per second.
Key timer functions: start_timer(), start_timer_recurring(), disarm_timer(), set_timer_value(), and timer_has_fired().
When the main thread detects EOF on stdin:
- Calls
deactivate_all_bgp_peers(), which setspeer->active = 0for each peer and joins the thread. - Each peer thread exits its select loop, sends a CEASE NOTIFICATION (
BGP_ERR_CEASE_ADMIN_SHUT), and closes the socket. - The main thread calls
free_all_bgp_peers()to release per-peer resources. - Calls
free_bgp_instance(), which signalsoutput_queue_shutdown(), joins the writer thread, and frees the instance.
If reconnection is enabled (--reconnect), a failed session arms the IdleHoldTimer with exponential backoff (5s to 120s) instead of exiting the thread. The peer thread re-enters IDLE when the timer fires.