status: a session's TCP connection as the kernel sees it; a route in full

When a session advertised a full table to a route collector at two
thousand routes a second, nothing on the status page said whether the
daemon or the peer set that pace. The answer is in the kernel: the
peer's receive window, the queues, and how long the connection was held
up by the window or by the send buffer, which `ss -ti` shows.

This change asks the kernel the same way `ss` does, over the socket
diagnostics netlink family (`NETLINK_SOCK_DIAG`, `SOCK_DIAG_BY_FAMILY`
with the connection's addresses and ports), and reads `tcp_info` and
the congestion control name from the answer. No external program is
run and no unsafe code is needed: the request and the answer are byte
layouts of the kernel's ABI, read field by field, with fields an older
kernel does not send left at zero. A socket on a link-local address is
found by its interface, which the request names.

* `bgp_fib::sockdiag`: the request, the parser, `TcpConnection`, and
  `tcp_connection(local, peer)`; `Ok(None)` where there is no such
  connection or the system is not Linux.
* `GET /api/tcp?peer=ADDRESS` in the HTTP API: the connection of the
  session with that peer, with times in milliseconds, the two windows
  named by what they allow, and the held-up times also as shares of the
  busy time.
* The status page: a "tcp" link on each established session, which
  expands a row below it with the figures, re-asked on every refresh
  while open. The group rows' span is corrected for the "sent" column.

Also on the status page: the attributes of a received route that have
no column of their own were badges with their value in a tooltip, which
is hard to read. Clicking the attrs cell now expands a row under the
route with every attribute by its name (ORIGIN, AS_PATH, NEXT_HOP, MED,
LOCAL_PREF, COMMUNITIES, LARGE_COMMUNITIES, AGGREGATOR and the rest),
the path identifier, the selection rank and the RPKI and ASPA states;
the badges stay. Both expansions use the terms of `ss` and the RFCs
rather than sentences.

Documentation: the endpoint in the HTTP API reference, and the link and
the expansion in the monitoring guide.

Testing: unit tests of the request and of the parser on a reply built
field by field, and of a "no such socket" and a refused answer; a
Linux-only test that asks the kernel about a loopback connection of the
test's own and about one that does not exist; the lab's basics phase
checks the endpoint for the FRR session; the page was checked by hand
against the daemon in the Linux VM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-09 11:41:47 +02:00
1 parent efde277fd8
commit 99a47da2af
8 files changed
+807 -10

No files matched your search

+13 -1
View File
@@ -39,7 +39,13 @@ Below the status line, the page displays the following information:
- Each neighbor, with its session state, time established, next hop,
session properties, number of accepted routes, number of routes
advertised to it (**sent**), rejected announcements, and held routes,
and its last error. Neighbors that need attention are
and its last error. The **tcp** link on an established session expands
a row with the connection as the kernel reports it: round-trip time,
congestion window, the neighbor's receive window and Ferrix's, queues,
retransmissions, throughput, and the share of time the connection was
held up by the neighbor's window or by the local send buffer. This
shows whether a slow advertisement of a table is paced by the neighbor.
Linux only. Neighbors that need attention are
listed first; the others are grouped by role. The IPv4 and IPv6 sessions
of one neighbor, named `NAME-v4` and `NAME-v6`, are listed together,
IPv6 first, and joined by a bar. The **problems only** option hides the neighbors that
@@ -153,6 +159,12 @@ advertise the same routes can therefore have different counts. The number
of routes currently held, because they are RPKI or ASPA invalid or their
next hop is unreachable, is displayed in the neighbor table as **held**.
In the received routes table, the **attrs** column shows the attributes
that have no column of their own as badges. Clicking the cell expands a
row below the route that lists every attribute of the route by its name,
with the path identifier, the selection rank, and the RPKI and ASPA
states.
```json
[{"peer":"transit-a","total":3,"by_reason":{"RPKI invalid":2,"bogon prefix":1},
"recent":[{"prefix":"203.0.113.0/24","reason":"RPKI invalid","as_path":[64496,64511]}]}]
+69
View File
@@ -37,6 +37,7 @@ server returns the following error statuses:
| `/api/routes/rejected` | Rejected routes, by neighbor. |
| `/api/rpki` | RPKI cache, data, and data age. |
| `/api/kernel` | Kernel route installation state and interfaces. |
| `/api/tcp` | The TCP connection of one session, as the kernel reports it. |
| `/api/events` | Session and route changes, as server-sent events. |
| `/metricz` | Prometheus metrics. See [Metrics Reference](@/docs/reference/metrics.md). |
@@ -230,6 +231,74 @@ routes. To return the records for one neighbor, add `?peer=NAME`.
| `refused` | Number of prefixes whose active route the kernel rejected. |
| `errors` | Number of requests that the kernel rejected. |
## TCP Connection
`/api/tcp?peer=ADDRESS` returns what the kernel reports about the TCP
connection of the session with the neighbor at `ADDRESS`, given as
`peer_addr` in `/api/peers` shows it: the round-trip time, the congestion
and receive windows, the queues, the retransmissions, and how long the
connection has been held up by the neighbor's receive window or by the
local send buffer. Linux only; on other systems, and for a session the
kernel has no connection for, `available` is `false` and `reason` says
why.
```json
{
"available": true,
"reason": null,
"connection": {
"state": "established",
"congestion": "cubic",
"rtt_ms": 0.62,
"rtt_variance_ms": 0.11,
"min_rtt_ms": 0.4,
"rto_ms": 201,
"mss": 1428,
"pmtu": 1500,
"cwnd_segments": 10,
"ssthresh_segments": null,
"send_window": 1048576,
"receive_window": 3145728,
"receive_space": 65536,
"send_window_scale": 7,
"receive_window_scale": 10,
"send_queue": 0,
"not_sent": 0,
"recv_queue": 0,
"unacked_segments": 0,
"retrans_segments": 0,
"lost_segments": 0,
"total_retrans": 0,
"bytes_retrans": 0,
"pacing_rate": 2309920000,
"delivery_rate": 184860000,
"bytes_sent": 312993,
"bytes_acked": 312994,
"bytes_received": 190,
"segments_out": 230,
"segments_in": 118,
"busy_ms": 41,
"limited_by_receive_window_ms": 0,
"limited_by_send_buffer_ms": 0,
"limited_by_receive_window_share": 0,
"limited_by_send_buffer_share": 0,
"last_data_sent_ms": 1234,
"last_data_received_ms": 1300,
"last_ack_received_ms": 1234
}
}
```
Times are in milliseconds, sizes in octets, rates in octets per second.
`send_window` is the neighbor's receive window, which bounds what Ferrix
can send; `receive_window` is what Ferrix lets the neighbor send. The two
`_share` values are the fractions of the busy time, 0 to 1, in which the
connection was held up by the neighbor's window and by the local send
buffer. A session that advertises a table to a neighbor that reads it
slowly shows a high `limited_by_receive_window_share`. The status page
shows the same information under the **tcp** link of each established
session.
## Events
`/api/events` is a stream of