Compare commits

17 Commits
Author SHA1 Message Date
Rayhaan JaufeerallyandClaude Fable 5.1 db8edaae8e policy, config, web: the TCP gauges named once, one revalidator, one peer-naming rule
ci / interop (push) Canceled after 0s
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
A code-quality pass over policy, configuration and the status API.
The twenty TCP gauges were spelled out in the struct, the
constructor, the removal, the sampler in the web crate and the status
view; one macro names each once with its help text, `PeerTcp::set`
fills them from a connection, and the view is a `From`. Every metric
and the list that forces them at start come from one declaration,
with a test that every declared family is registered. The two
revalidators, for next hops and for RPKI, share the task body that
scans the table off the sessions' threads and fans the findings out
to every session; the RPKI scan judges a route with the same `Checks`
the import path uses. Which peer a query names is one rule for the
received, rejected and TCP endpoints, so `/api/tcp` takes a name or an
address like the others. The config parser says proto3's "zero is
unset" in three helpers instead of a dozen shapes, and `parse_peer`
is three named parts shorter. The policy's test-only next-hop flag is
named for what it does, `accept_own_host_next_hop`; the "listed, or
else ours" prefix rule and the mark-then-prefer step each exist once.
The status page derives its spanning rows from the table's header,
shares one key-value grid and one detail-row rule, and defines its
helpers before their use. The web crate no longer re-exports views
nothing uses, and the TCP endpoint sits above the test module.

Testing: the TCP view's arithmetic (a share of no busy time is zero,
not NaN), the peer-naming rule, the RPKI scan finding only what a
change can have made invalid, and `/api/tcp` by address, by name and
for an unknown peer. The server and web tests, the HTTP, roles, RPKI
and import-policy integration tests, the docs build and every lab
phase pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 22:44:32 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 4cddd6fb04 export: one rule each for admission, identifiers and what the peer had
A code-quality pass over the exporter. The resync path (gather then
stream) had grown beside the steady-state path (judge then flush)
with its own copies of the admission predicate, the path identifier
rule, the reconciliation of a route the peer already had, the parser
contexts, the withdrawal loop and the advertised gauge. Each is one
function now: `prefix_may_go`, `lowest_free_id`, `Sent::renewed`,
`parser_contexts`, `send_withdrawals`, `within_limit` and
`note_held`. The 3,072-octet ceiling on a route's attributes, which
is what keeps an UPDATE within its size, is a named constant where it
is relied on. The UPDATEs are framed with the packet crate's helpers
instead of a bare type code and a length written by hand. The
positional tuples for a prefix's paths are structs with names. A
mismatched kind of bookkeeping, which nothing should produce, is
asserted in tests and logged in release instead of dropped silently.
Types used only inside the crate are `pub(crate)`. Five doc comments
that had drifted onto neighbours or named former designs are put
right; the exporter's reference encoder no longer breaks the CI doc
build with a link to an item that exists only under test.

Testing: ADD-PATH export has unit tests at last: two routes for a
prefix go with their own identifiers and keep them, and the
withdrawal of one carries its identifier; what is not heard of again
after a reset is withdrawn after the table. The byte-for-byte test of
the encoded UPDATEs covers IPv4 routes with an IPv6 next hop (RFC
8950) too. The test fixtures are shared helpers. The server tests and
the ADD-PATH, transit, limits, roles and deadlock integration tests
pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 22:44:32 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 bef875e50b session: the way out to the peer as a type of its own
A code-quality pass over the per-peer session. The outgoing side of a
session, eight fields served from six places in a two-thousand-line
file, is `Outgoing`: the write buffer, the batches waiting for the
writer task, the task and its word on why it ended. One real race
went with it: when the writer task failed with the queue full, the
session took whichever of two ready branches the select chose, and
half the time ended with an invented send hold timer expiry instead
of the connection's error, which decides whether the peer's routes
are kept over a restart; the writer's own word is taken now. The run
loop's handling of what the rest of the server has to send is a
method that goes by the state machine's state, so that pieces after a
NOTIFICATION in the same batch do not follow it out; `execute_all`
reads one line an action with the sending, delivering and
after-failure steps named. A peer's routes have one way onto the
table at a session's end, whether removed or kept for a restart. An
UPDATE whose attributes cannot be kept ends the session under an
error of its own, `Unstorable`, where a message of ours that cannot
be encoded is dropped. What is kept of a restarting peer is a value,
not seven arguments. The dialer no longer turns a session's end into
a Result and back. The server's screening of a connection is called
that, apart from the collision check's `admit`.

Testing: the back-pressure machinery has unit tests at last: batches
wait while the writer has no room, room goes to the oldest batch, a
piece is counted as the messages in it, and a lost connection is
reported as one while batches wait. The routes module gains tests of
holding at its bound, forgetting and revalidation. The server tests
and the whole integration suite pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 22:44:32 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 25597ab3ef packet, fib, bench: framing helpers, one netlink reader, a mesh with named phases
A code-quality pass over the periphery. The packet crate frames a
message with `begin_message` and `finish_message`, which the
exporter's hand-written UPDATE header can use instead of a bare type
code, and keeps the NLRI reader and writer together as a pair; the
rule that the body's prefixes are IPv4 unicast lives once; a session
with two-octet AS numbers no longer reserves the attributes' wire
copy it never keeps. The TCP diagnostics read netlink with the
netlink module's own attribute walk and helpers instead of copies of
them. The kernel follower learns what the kernel has in one named
step used at start and on relearning, keeps its batch buffers between
wakes, and builds its task in a constructor. The mesh benchmark,
which had grown six phases in one main, is a `Mesh` with named phases
and a `Receiver`, with one report of the sessions for both deadlines;
the collector capabilities, the KEEPALIVE bytes, the draining of a
session and the printing of a table row are helpers shared by the
benches, the examples and the integration tests. The MRT reader uses
the packet crate's attribute splitter.

Testing: a message framed by hand is the one `to_wire` writes; a
short tcp_info from an old kernel leaves the later fields zero; the
synthetic table is the same every run and every announcement decodes
to itself. The packing example gives the same figures on the RIS dump
before and after. The crates' tests, the integration tests, the
kernel tests and every lab phase pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 22:44:32 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 bcfd6e013f rib: the lock-free number in a slot of its own, and the reader in named steps
A code-quality pass over the table. The latest sequence number of
each journal, which readers compare their cursors with without the
shard's lock, was an atomic inside the journal under the lock with
the table holding the other end of an Arc to it, stored on every
route; it is now in a slot beside each shard's lock, `ShardSlot`,
stored once a shard a change while the write guard is still held and
before the readers are woken. "One of each kind of journal" is one
type, `ByKind<T>`, where three hand-written dispatchers and an array
index were. `Reader::poll` is the loop skeleton over `resync_step`
and `follow_pass`; the journal's `tidy` is three named stages;
`live`'s nested Option is an enum that says what an entry says; the
place of a cursor in a journal is found by one function for both the
time chosen for a pass and the pass itself. The attribute set's four
byte-readers share one walk over the raw attributes, with the next
hop decoded once for both of its readers. `best` returns a `Route`
like the other lookups. Docs that described an earlier design (the
reader's `seen`, "every method takes &self", a `Routes::is_empty`
that could not be true) are put right.

Testing: new tests pin the lock-free skip, which nothing checked
directly before: the number equals the journal's after every kind of
change, with and without readers of each kind; a shard passed over is
read again once it has news, from a reader of the whole table and
from one that starts now. The OTC flag and the received-attributes
interning gain tests. The crate's tests, both proptests included, the
server tests, the integration tests, the kernel tests and every lab
phase pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 22:44:32 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 c70a223d0d rib: a reader counts the table once a poll, not once a shard
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s
A router with many peers has a reader of the table's journal for each
session that is sent learned routes, seventy on ddln, and every change
to the table wakes them all. Each pass of a reader went round every
shard, and for each shard took a count on the table (an Arc clone) to
hold it while the shard was read, and gave it back after: two atomic
adds on one cache line, by seventy readers, 256 shards a pass. A
profile of ddln taking a table from the other router had the reader's
poll as the top function at two to three cores, and in the Linux VM
with 64 such sessions half of all CPU time was in the two atomic add
instructions. The sessions writing to the table waited behind it: the
table came in at 58,000 routes a second.

The count is now taken once a poll. And each journal keeps its latest
sequence number in an atomic the table holds the other end of, so that
a reader can tell a shard with nothing new for it from one with
something without the shard's lock, and pass over it: on a quiet
table, which is what the readers see most of the time, a pass is a
load a shard and no lock. An exporter that is sent little of a table,
as one to a peer at an exchange is, no longer makes a verdict of
nothing for a prefix the peer does not hold, which cost a map insert
and a lookup on the way out, and tells a route that came down or
across (RFC 9234) from its attributes' bytes before parsing them.

Testing: the mesh benchmark can have any number of collector sessions
of the first router (BENCH_COLLECTORS): peers in the sense of RFC 9234,
sent learned routes, with the feeders' routes marked as having come
across from a peer, so that each has a reader following the table
while it fills and passes nearly all of it over, as a router with many
peers at exchanges has. In the Linux VM with 64 of them, three rounds
alternating with the previous commit, the routers' exchange of their
tables takes 11 s instead of 20 s, at 86 s of CPU time instead of
155 s; without them it is unchanged. The table's tests, the server
tests, the integration tests, the kernel tests and every lab phase
pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 17:12:37 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 a15efaa26c export: give the leave to gather back once the table is gathered
The last change held a session's leave to gather a table, of which
there are four, until its table had gone out. A peer that reads at the
pace of its receive window takes hours over a table, and two such
peers held two of the four for that long, keeping other sessions from
gathering theirs: they waited for a leave before their first UPDATE.
The leave is for the memory a gather takes; what is still held once
the table is gathered is let go of as it goes out, as it was before
the gather was reworked.

The mesh benchmark's new peer can also come once the first router has
its own peers' routes and the other router's are still arriving
(BENCH_RECEIVER_EARLY=2), as a router's sessions come up after a
restart while its peers' tables arrive; the walk of a full table while
the table fills costs the arriving routes nothing measurable in the
VM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 15:43:48 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 624d20f965 Release 0.2.6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 15:30:07 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 b5ca42244d export: a lighter gather, and UPDATEs to the session by the piece
Two things kept an exporter from its peer. A table being gathered to
go out anew kept a map of every prefix heard, to let a later word on a
prefix supersede an earlier one, and dressed every set of attributes
before the first UPDATE went: a map insert and a parse-and-encode a
route that the peer waited on. And the session's queue took UPDATEs
one by one, a thousand at most, so the exporter stood at the queue
once a hundred kilobytes were in it, behind a writer that takes a
megabyte: the two took turns instead of running together.

The journal gives a table out shard by shard, each prefix once, and a
change meanwhile comes after the table as news of its own, so there is
no second word to keep track of while gathering: the map is gone, and
a prefix the peer is not to have is simply not gathered, as what it
held of it is withdrawn once the table is through. Each set is dressed
as its turn to go comes, so the first UPDATE goes once the table is
gathered; only when the gathered routes alone would be over the
peer's limit are the sets dressed first, to know which cannot be and
take their routes with them, so that none goes if too many would. The
UPDATEs for a set now go to the session in pieces of 64 KiB or so,
each of whole messages, and the queue takes 64 of those: a few
megabytes ahead of the writer, not a hundred kilobytes. The session
counts the messages in a piece by their headers.

Testing: the byte-for-byte test of the encoded UPDATEs now encodes
thirty thousand routes and checks the pieces are of whole messages
and of the size meant. The server tests, the integration tests, the
kernel tests and every lab phase pass. In the Linux VM, three rounds
alternating with the previous commit, a new peer's first UPDATE comes
1.8 s after its session is up instead of 3.1 s, and it has its four
million routes in 3.8 s instead of 4.1 s; the smaller of the two
routers' tables crosses in 3.0 s instead of 4.2 s and the larger in
5.0 s instead of 5.2 s, at the same CPU time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 15:28:40 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 7425b101ed export: gather a table by attributes, then stream it
A table given out anew to a session, at its start or when the journal
had let changes go, was gathered as a verdict for each prefix, each
with the route dressed for the session, and sent once the table was
all there. Dressing a route parses its attributes and encodes them for
the session, so a table of five million routes took seconds before
the first UPDATE went, on one worker thread without a yield. And the
routes that share attributes went out in separate UPDATEs after all:
they were grouped by the dressed form, of which only so many are kept,
and the routes of a set are spread over the whole table, so by the
time the next was reached the form had been let go of and made anew.
The mesh benchmark's new peer got four million routes in 3.8 million
UPDATEs; on a RIPE RIS table, grouped whole, a full table is 160,000
UPDATEs and 21 MB, and grouped in slices of 4,096 routes it is 845,000
UPDATEs and 72 MB, so slices are not the answer either.

The table is now gathered by what dresses its routes: the attribute
set as the table has it, who the route is from, and its IP version.
Nothing is dressed while gathering, which is a map insert a route.
Once the table is all there, each set is dressed once, with the thread
lent to the other tasks every few thousand, and then the sets go out
one after the other, each as few UPDATEs as its routes fit in, with
the peer reading the first while the rest are sent. A prefix heard of
again while the table is read supersedes what was heard before: each
word on a prefix is numbered, and only the latest goes. Path
identifiers are given at gathering, in the order the table has a
prefix's routes, so that they are as before. What was announced before
and not heard of again is withdrawn after the table, as before. The
exporter never blocks the thread on a table: the steady state's batch
is small, and the gathered table is gone through with yields.

Testing: a new test gives a session a table of 300 routes sharing
three sets of attributes, spread over the shards, and checks they go
in three UPDATEs. The mesh benchmark has a new phase in which a peer
takes the first router's table once the mesh is complete, timing the
first UPDATE and the whole; its feeders now negotiate no hold timer,
since they say nothing after their tables, and `BENCH_LOG` prints the
routers' logs. A new example, packing, counts the UPDATEs and octets
a RIS table takes grouped whole and in slices. The server tests, the
integration tests, the kernel tests and every lab phase pass. In the
Linux VM, three rounds alternating with the previous commit, the new
peer's first UPDATE comes 2.9 s after its session is up instead of
5.3 s, and it has all four million routes in 4.0 s instead of 8.0 s,
in 667,000 UPDATEs instead of 3.76 million; the whole run takes
22.4 s of CPU time instead of 26.2 s. The routers' exchange of their
tables, which does not go through this path once their sessions are
up, is unchanged within its noise.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 15:16:18 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 92b9bdd3c5 export: keep the peer reading while the next batch is dressed
An exporter following the table took up to 65,536 changes at a time,
dressed them all, and only then sent them; and before each batch it
slept the 50 ms that lets a run of changes be gathered, whether or not
the table was already far ahead of it. The session's queue holds about
a thousand UPDATEs, so once the batch went out the exporter waited on
the peer reading it, and while the next batch was dressed the peer had
nothing to read. A profile of two routers exchanging their tables,
sampled in 25 ms windows, showed the two stages taking turns: the
exporters at two cores while the sessions were at none, then the other
way round, with a gap of 50 to 75 ms at the start of each cycle for the
sleep. Each task ran at about half a core.

A batch is now 4,096 changes, so that what one batch sends is being
read by the peer while the next is being dressed, and the gathering
sleep is skipped while the reader is behind the table: a run is in hand
already. A reader says whether its last poll caught up.

Testing: the server tests, the integration tests, the kernel tests and
every lab phase pass. On the mesh benchmark in the Linux VM, three
rounds each alternating with the previous commit, the larger table
(4.9 million routes) is across in 5.6 s instead of 6.7 s and the
smaller (2.5 million) in 4.4 s instead of 6.4 s; the whole exchange
takes 7.3 s of wall-clock time instead of 8.6 s, and 18.9 s of CPU time
instead of 20.5 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 14:19:02 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 381eabd886 export: encode a route's attributes once a session
The exporter built each UPDATE as a message: the attributes of the
route's set, parsed from the table, were cloned, had the next hop
added, were sorted, and were handed to the session, which encoded them
to the wire with the rest of the message. For a table whose attribute
sets each cover a few routes that is an encoding of the same octets for
nearly every route, and a copy of the message's bytes on the way to the
writer besides. A profile of a production router taking the other
router's table with ADD-PATH had the exporters at 55% of the CPU time,
their dressing of routes at 23%, and memcpy at 12%.

A route's attributes are now encoded once for a session, when it is
first dressed for it, and kept with where in the octets a NEXT_HOP or
an MP_REACH_NLRI belongs so that the attributes stay in order of type
code. An UPDATE is then its header, the encoded octets with the next
hop or the MP_REACH_NLRI spliced in, and the prefixes: written straight
into a buffer that goes to the session as it is, and from the session
to the writer without a copy. The session's queue takes either a
message or an encoded UPDATE; an encoded one for a session that is no
longer established is dropped, as the exporter made it for the session
as it was. What is kept for a route, now a few hundred octets instead
of a kilobyte, lets twice as many be kept. The exporter's maps hash
with foldhash instead of SipHash, and the parser context for an UPDATE
is made once a flush instead of once an UPDATE.

Testing: a new test encodes a thousand routes with each of the forms
an UPDATE takes (IPv4 with the next hop in its own attribute, IPv6 in
MP_REACH_NLRI, with and without path identifiers, to two-octet and
four-octet AS speakers, over several messages) both ways and checks the
octets are the same byte for byte; the message-building form is kept
for the test. The exporter tests decode what the session is handed.
The server tests, the integration tests, the kernel tests and every
lab phase pass. On the mesh benchmark in the Linux VM, three
rounds each alternating with the previous commit, the two routers'
exchange of 7.4 million routes takes 20.6 s of CPU time instead of
25.4 s (-19%) over 9.0 s of wall-clock time instead of 11.1 s, and the
larger of the two tables is across in 7.1 s instead of 9.3 s. The
smaller table's time varies too much from run to run (5-8 s) for a
difference to be read from it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 13:47:47 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 f1aca37ebe rib: a short row grows by doubling
A prefix's row of paths is given room for as many paths as there are
peers, and from there grows by a quarter at a time, one path at a time
while it is short. A peer sending every path it has (ADD-PATH) gives a
prefix several paths from one peer, so a row goes past the peers' count
from the first few routes and was then reallocated for each path.

A row shorter than the first allocation's room doubles instead, and
grows by a quarter only from there. Measured on the mesh benchmark, the
reallocations in the table's insert path fall by half; the memory held
is as before, as the rows that reach the quarter-growth are the ones
that were already allocated for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 13:45:15 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 cbc1dc7e44 bench: two routers exchanging their tables with ADD-PATH
The full-table benchmark measures one router taking in tables from its
peers. What two routers of one AS do with each other, each sending the
other every path it has (ADD-PATH both ways with all_paths, as a mesh
of routers that each hold the network's whole view is configured), was
measured nowhere: the exporter following the table's journal while the
sessions fill the table, at the size of the Internet. In production
that exchange took 70 seconds for a table that arrives from the
Internet in 20.

A new benchmark, mesh, starts two servers in one AS with distinct
router IDs, peers them over IPv6 loopback with ADD-PATH both ways and
all_paths, and feeds the first from four synthetic peers and the second
from two over IPv4 loopback (BENCH_PEERS, BENCH_PEERS_B,
BENCH_SCALE_DOWN). It prints how long each router takes to hold its
own peers' routes and then the other's, with the rate of the latter.
BENCH_DEADLINE prints the sessions' state if a router is not there in
time.

The benchmarks now allocate with jemalloc, as the daemon does, except
when memory is being counted: the C library's allocator put its own
locks in the profiles. Two routers on one host have each other's
addresses as next hops, loopback addresses, which the import check
refuses as it refuses a next hop that is this machine; the permissive
import policy, which only tests use and no configuration can ask for,
now takes them.

Testing: the next-hop test covers both policies; the benchmark runs on
macOS and in the Linux VM under perf.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 13:45:15 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 d0f092ff06 session: write to the peer from a task of its own
A session read from and wrote to its socket in one loop: while it was
writing, it was not reading. Writing waits when the peer's receive
window is closed, and the peer's window closes when the peer is not
reading, which it is not while it is writing to us and waiting on our
window in turn. Two routers sending each other every path at once, as
they do with ADD-PATH both ways after a restart, each stopped reading
within seconds and waited on the other until the send hold timer ended
both sessions eight minutes later. The status page's TCP row showed it:
both windows zero, the send queue full, the receive queue unread. The
batching of writes in the previous change made a session spend far
longer writing and so made this certain; it was possible before.

Now each session has a writer task. The session encodes its messages
into batches and hands them to the task through a bounded queue without
ever waiting: a batch the queue has no room for is kept, and while any
is kept the session takes no more messages from the rest of the server
(which then waits, as before, in its own task) but goes on reading the
peer. The writer task's failure, or a write that makes no progress for
the send hold time, comes back to the session as a local failure and
ends it. When the session ends, what is still to go out, a NOTIFICATION
among it, is handed over and the writer shuts our side once it has
written it.

Testing: a new integration test has one peer's table in the server and
a second peer send a table of its own while reading nothing of what the
server sends it, and checks that the server takes the second table all
the same; against the previous commit the test hangs until its timeout.
The send hold timer test now reads the writer's failure. The session
tests, the integration tests, the kernel tests and every lab phase
pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 12:22:06 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 ce1b68ca59 bgp: jemalloc's background thread on Linux only
The last change built jemalloc with its background thread on, so that
freed pages go back to the kernel on time. jemalloc has that thread on
Linux only: on macOS every start of the daemon, and every test that
starts one, printed "option background_thread currently supports
pthread only".

The feature now comes from a Linux-only dependency entry of the daemon
crate, so the macOS build asks for nothing jemalloc cannot do. The
Linux build is as before: measured in the VM with four peers and a
collector, resident memory falls after the load without any setting
in the environment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 12:06:39 +02:00
Rayhaan JaufeerallyandClaude Fable 5.1 9276bc9f7a session: write messages in batches, with TCP_NODELAY
A session wrote every message to its socket as it came: one write and,
with Nagle's algorithm, mostly one segment for each UPDATE, a hundred or
two octets. On the IPv6 cross-connect between the two routers, whose
round trip is 50 microseconds, a million routes took 42 seconds to go
out: the status page's new TCP row showed the connection busy for that
long with neither window nor buffer limiting it, a congestion window
stuck at ten segments, and a smoothed round-trip time of 18 ms. That is
the receiver's delayed acknowledgements, 40 ms at a time, on a stream
of small segments, holding the sender to a few segments a round trip.

Now a session gathers what is waiting for it, up to the outbound queue's
length, in one wake, encodes the messages into one buffer, and writes
the buffer when it reaches 64 KiB or when the actions of the wake are
done; the socket has TCP_NODELAY, so a batch's last, partial segment is
not held back for the acknowledgement of the one before. The
NOTIFICATION that ends a session, including one sent after a panic, is
flushed on its own.

Measured in the Linux VM with four peers' tables going to a collector
over a veth pair, 151 MiB: 121,850 segments instead of 313,773, the
connection busy 0.8 s instead of 2.0 s, and the congestion window grown
to 47 segments instead of 13. Over loopback the transit benchmark is
unchanged at 2.5 s for the second neighbor to have the table: there the
receiver's parsing sets the pace, not the segments.

Testing: the session tests, including the one that sends a NOTIFICATION
after a panic; the integration tests; the kernel tests and every lab
phase.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 11:14:08 +02:00
53 changed files with 5251 additions and 2106 deletions

No files matched your search

Generated
+13 -10
View File
@@ -157,7 +157,7 @@ checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bgp"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-server",
"bgp-web",
@@ -172,7 +172,7 @@ dependencies = [
[[package]]
name = "bgp-bench"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-integration-tests",
"bgp-packet",
@@ -182,12 +182,14 @@ dependencies = [
"bytes",
"flate2",
"stats_alloc",
"tikv-jemallocator",
"tokio",
"tracing-subscriber",
]
[[package]]
name = "bgp-fib"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-packet",
"bgp-primitives",
@@ -205,7 +207,7 @@ dependencies = [
[[package]]
name = "bgp-integration-tests"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-fib",
"bgp-packet",
@@ -224,7 +226,7 @@ dependencies = [
[[package]]
name = "bgp-packet"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-primitives",
"bitfield",
@@ -240,7 +242,7 @@ dependencies = [
[[package]]
name = "bgp-primitives"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bytes",
"nom",
@@ -252,7 +254,7 @@ dependencies = [
[[package]]
name = "bgp-rib"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-packet",
"bgp-primitives",
@@ -267,7 +269,7 @@ dependencies = [
[[package]]
name = "bgp-rpki"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-primitives",
"bytes",
@@ -280,7 +282,7 @@ dependencies = [
[[package]]
name = "bgp-server"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"bgp-fib",
"bgp-packet",
@@ -288,6 +290,7 @@ dependencies = [
"bgp-rib",
"bgp-rpki",
"bytes",
"foldhash",
"futures",
"nix",
"prometheus",
@@ -302,7 +305,7 @@ dependencies = [
[[package]]
name = "bgp-web"
version = "0.2.5"
version = "0.2.6"
dependencies = [
"axum",
"bgp-fib",
+2 -2
View File
@@ -12,7 +12,7 @@ edition = "2024"
homepage = "https://rayhaan.ch"
license = "Apache-2.0"
repository = "https://git.rayhaan.net/rayhaan/ferrix"
version = "0.2.5"
version = "0.2.6"
# Every crate opts in with `[lints] workspace = true`. CI turns warnings into
# errors, so a lint added here is enforced.
@@ -65,7 +65,7 @@ tokio-stream = { version = "0.1.19", features = ["sync"] }
reqwest = { version = "0.13.5", default-features = false, features = ["json"] }
# The daemon's memory allocator; see bin/bgp/src/main.rs. With its
# statistics, which the status page and the metrics show.
tikv-jemallocator = { version = "0.7.0", features = ["stats", "background_threads"] }
tikv-jemallocator = { version = "0.7.0", features = ["stats"] }
tikv-jemalloc-ctl = { version = "0.7.0", features = ["stats"] }
# `cargo build --profile profiling`: the release build, with the line tables
+6
View File
@@ -23,6 +23,12 @@ tracing-subscriber.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator.workspace = true
# jemalloc's background thread returns freed memory to the kernel on time
# (see the memory figures on the status page); jemalloc has it on Linux
# only, and warns at every start elsewhere if asked for it.
[target.'cfg(target_os = "linux")'.dependencies]
tikv-jemallocator = { workspace = true, features = ["background_threads"] }
[features]
# jemalloc's heap profiler, for scripts/profile/full-table.sh with
# HEAP=1: not for a production build, where it costs memory and time.
+8
View File
@@ -16,11 +16,15 @@ bytes.workspace = true
flate2 = "1"
stats_alloc = { version = "0.1.10", optional = true }
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator.workspace = true
[dev-dependencies]
bgp-integration-tests.workspace = true
bgp-rib.workspace = true
bgp-server.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
[features]
# Counts every allocation, to report how much memory a table holds. The
@@ -42,3 +46,7 @@ harness = false
[lints]
workspace = true
[[bench]]
name = "mesh"
harness = false
+3 -17
View File
@@ -18,6 +18,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use bgp_bench::memory::{Region, describe};
use bgp_bench::report::{mib, per_second, row};
use bgp_bench::{Prefixes, Shape, announcements, encoded, env, wire_context};
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
@@ -26,23 +27,6 @@ use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
use bytes::BytesMut;
fn mib(bytes: usize) -> f64 {
bytes as f64 / (1024.0 * 1024.0)
}
fn per_second(count: usize, elapsed: Duration) -> String {
let rate = count as f64 / elapsed.as_secs_f64();
if rate >= 1e6 {
format!("{:.2} M/s", rate / 1e6)
} else {
format!("{:.0} k/s", rate / 1e3)
}
}
fn row(what: &str, elapsed: Duration, detail: String) {
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
}
fn main() {
let peers = env("BENCH_PEERS", 4) as u32;
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
@@ -110,6 +94,8 @@ fn main() {
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 90)
.await
.expect("a session");
// The same bytes `table_stream` would make, from the messages
// already encoded for the decoding above.
let stream: Vec<u8> = messages.iter().flat_map(|m| m.iter().copied()).collect();
senders.push((session, stream));
}
+659
View File
@@ -0,0 +1,659 @@
//! Two routers of one AS, each taking in full tables from its own peers
//! and sending the other every path (ADD-PATH both ways, `all_paths`), as
//! a mesh of routers that each hold the network's whole view do. How
//! long until each router has the other's routes is what this measures:
//! the exporter reading the table's journal while sessions fill the
//! table, which nothing else exercises at this size.
//!
//! Then a new peer takes the first router's table, as a collector or a
//! router coming back does: how soon its first UPDATE arrives and how
//! long the table takes are measured too. It is a peer of the routers'
//! AS over IPv4, so what it gets is the first router's IPv4 routes from
//! its own peers: the other router's are not reflected to it, and IPv6
//! routes need an IPv6 session.
//!
//! Run with `cargo bench -p bgp-bench --bench mesh`. `BENCH_PEERS`
//! (default 4) feed the first router and `BENCH_PEERS_B` (default 2) the
//! second; `BENCH_SCALE_DOWN` shrinks the table. The routers peer over
//! IPv6 loopback and the feeders come over IPv4 loopback; the new peer
//! comes from 127.0.0.2, which Linux has and macOS does not, so that
//! part is skipped on macOS. With `BENCH_RECEIVER_EARLY=1` the new peer
//! comes as the feeders start instead, so that its exporter goes
//! through the table while the table is being filled, as a router's
//! do after a restart. `BENCH_COLLECTORS` (default 0) more peers of the
//! first router, from 127.0.1.0/24, are peers in the sense of RFC 9234
//! told of learned routes, and the feeders' routes are marked as having
//! come across from a peer, so none of them goes: as many exporters
//! following the table while it fills and passing nearly all of it
//! over, as a router with many peers at exchanges has.
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use bgp_bench::report::row;
use bgp_bench::{Prefixes, Shape, announcements, encoded, env, table_stream};
use bgp_integration_tests::peer_config;
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::attributes::PathAttribute;
use bgp_packet::message::msg_type;
use bgp_packet::open::SendReceive;
use bgp_server::config::PeerConfig;
use bgp_server::family::SUPPORTED;
use bgp_server::policy::{ExportPolicy, ImportPolicy, PeerRole};
use bgp_server::session::SessionRegistry;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::tcp::OwnedWriteHalf;
use tokio::task::JoinHandle;
/// An AS number that is in none of the synthetic paths.
const ASN: u32 = 4_200_000_000;
/// Where the peer that takes the whole table connects from.
const RECEIVER: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 2);
/// Where the collectors connect from: this and the addresses after it.
fn collector_address(n: u32) -> Ipv4Addr {
Ipv4Addr::new(127, 0, 1, n as u8)
}
/// The collectors' AS numbers start here.
const COLLECTOR_ASN: u32 = 65000;
/// How often the tables are looked at while they fill.
const POLL: Duration = Duration::from_millis(5);
/// What the routers send each other, and the receiver: every path.
fn every_path() -> Arc<ExportPolicy> {
Arc::new(ExportPolicy {
learned_routes: true,
all_paths: true,
..ExportPolicy::default()
})
}
fn free_port() -> u16 {
std::net::TcpListener::bind((Ipv6Addr::LOCALHOST, 0))
.expect("a port")
.local_addr()
.expect("an address")
.port()
}
/// A router of the mesh, listening for feeders on IPv4 loopback and for
/// the other router on IPv6 loopback at `port`, which it dials at
/// `other_port`, knowing it as `other_name`.
async fn router(
other_name: &'static str,
id: u8,
port: u16,
other_port: u16,
with_receiver: bool,
collectors: u32,
) -> TestServer {
TestServer::start_with(ASN, |c| {
// Two routers of one AS must not share a BGP identifier.
c.router_id = Ipv4Addr::new(192, 0, 2, id);
c.cluster_id = c.router_id;
c.listen_endpoints = vec![
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0),
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), port),
];
c.peers = vec![PeerConfig {
add_path: Some(SendReceive::Both),
import: ImportPolicy::permissive(),
export: every_path(),
..peer_config(
other_name,
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), other_port),
ASN,
)
}];
if with_receiver {
c.peers.push(PeerConfig {
add_path: Some(SendReceive::Send),
export: every_path(),
..peer_config("receiver", SocketAddr::new(RECEIVER.into(), 179), ASN)
});
}
for n in 1..=collectors {
c.peers.push(PeerConfig {
role: Some(PeerRole::Peer),
export: Arc::new(ExportPolicy {
learned_routes: true,
all_paths: false,
..ExportPolicy::default()
}),
..peer_config(
"collector",
SocketAddr::new(collector_address(n).into(), 179),
COLLECTOR_ASN + n,
)
});
}
c.connect_retry = Duration::from_millis(200);
})
.await
.expect("a router")
}
/// Connects `count` feeders to `router`, numbered from `first`, and
/// returns their sessions with the bytes each is to send.
async fn feeders(
router: &TestServer,
prefixes: &Prefixes,
shape: Shape,
first: u32,
count: u32,
marked: bool,
) -> Vec<(TestPeer, Vec<u8>)> {
let mut sessions = Vec::new();
for peer in first..first + count {
let stream = if marked {
// As having come across from a peer (RFC 9234): not for the
// collectors, which are peers too.
let mut updates = announcements(prefixes, shape, peer);
for update in &mut updates {
update
.path_attributes
.push(PathAttribute::OnlyToCustomer(64512 + peer));
}
encoded(&updates)
.iter()
.flat_map(|m| m.iter().copied())
.collect()
} else {
table_stream(prefixes, shape, peer)
};
let mut session = TestPeer::connect(router.addr).await.expect("a connection");
// No hold timer: a feeder has nothing to say after its table.
session
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 0)
.await
.expect("a session");
sessions.push((session, stream));
}
sessions
}
/// Every session of `registry`, one line each, for the panic when a
/// deadline passes: where the routes have got to, and what each session
/// was to be sent.
fn sessions_report(registry: &SessionRegistry) -> Vec<String> {
registry
.list()
.iter()
.map(|s| {
format!(
"{} {:?} export {:?} add_path_send {:?} routes {} rejected {} advertised {}",
s.peer,
s.name,
s.export,
s.add_path_send,
s.counters.routes(),
s.counters.rejected(),
s.counters.advertised()
)
})
.collect()
}
/// How big a run is, from the environment.
struct Settings {
peers_a: u32,
peers_b: u32,
shape: Shape,
collectors: u32,
}
impl Settings {
fn from_env() -> Self {
Self {
peers_a: env("BENCH_PEERS", 4) as u32,
peers_b: env("BENCH_PEERS_B", 2) as u32,
shape: Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1)),
collectors: env("BENCH_COLLECTORS", 0) as u32,
}
}
/// How many routes A's own feeders bring, and B's.
fn own_routes(&self) -> (usize, usize) {
let per_peer = self.shape.prefixes();
(
per_peer * self.peers_a as usize,
per_peer * self.peers_b as usize,
)
}
/// How many routes the receiver is to be sent: A's IPv4 routes from
/// its own peers.
fn receiver_routes(&self) -> usize {
self.shape.ipv4_prefixes * self.peers_a as usize
}
}
/// The two routers, up and peered, with the collectors' sessions once
/// they are connected.
struct Mesh {
settings: Settings,
prefixes: Prefixes,
a: TestServer,
b: TestServer,
/// The collectors' connections, drained, held until the end.
collectors: Vec<(OwnedWriteHalf, JoinHandle<()>)>,
}
/// The feeders sending, from when they began.
struct Feeding {
started: Instant,
/// Each hands its session back once its table is written.
sending: Vec<JoinHandle<TestPeer>>,
}
/// When each router had what: its own peers' routes, and the other's too.
struct Tables {
a_own: Duration,
b_own: Duration,
a_all: Duration,
b_all: Duration,
}
impl Mesh {
/// Starts the routers, with the collectors and receiver configured at
/// A, and waits for them to peer.
async fn start(settings: Settings, prefixes: Prefixes) -> Self {
let (port_a, port_b) = (free_port(), free_port());
let a = router("b", 1, port_a, port_b, true, settings.collectors).await;
let b = router("a", 2, port_b, port_a, false, 0).await;
a.wait_for_sessions(1).await.expect("the mesh session at A");
b.wait_for_sessions(1).await.expect("the mesh session at B");
Self {
settings,
prefixes,
a,
b,
collectors: Vec::new(),
}
}
/// Connects the collectors to A. They are there before the feeders, as
/// a router's peers are before its tables arrive after a restart. An
/// address that is not this machine's ends the connecting, with a
/// word.
async fn connect_collectors(&mut self) {
for n in 1..=self.settings.collectors {
let socket = tokio::net::TcpSocket::new_v4().expect("a socket");
if let Err(e) = socket.bind(SocketAddr::new(collector_address(n).into(), 0)) {
println!(
"collectors: skipped, {} is not ours ({e})",
collector_address(n)
);
break;
}
let stream = socket.connect(self.a.addr).await.expect("A listens");
let mut session = TestPeer::from_stream(stream);
session
.handshake(COLLECTOR_ASN + n, Ipv4Addr::new(192, 0, 3, n as u8), 0)
.await
.expect("A takes a collector's session");
self.collectors.push(session.drain());
}
if !self.collectors.is_empty() {
self.a
.wait_for_sessions(1 + self.collectors.len())
.await
.expect("the collectors' sessions at A");
}
}
/// Connects the feeders to both routers and starts them sending.
async fn feed(&self) -> Feeding {
let Settings {
peers_a,
peers_b,
shape,
..
} = self.settings;
let marked = !self.collectors.is_empty();
let feed_a = feeders(&self.a, &self.prefixes, shape, 1, peers_a, marked).await;
let feed_b = feeders(&self.b, &self.prefixes, shape, 1 + peers_a, peers_b, false).await;
let started = Instant::now();
let sending = feed_a
.into_iter()
.chain(feed_b)
.map(|(mut session, stream)| {
tokio::spawn(async move {
session.send_raw(&stream).await.expect("the router reads");
session
})
})
.collect();
Feeding { started, sending }
}
/// The receiver taking A's table while it fills, if `BENCH_RECEIVER_EARLY`
/// asks for that: as the feeders start (1), or once A has its own
/// peers' routes and B's are still coming (2), as a router's sessions
/// come up after a restart while its peers' tables arrive.
fn early_receiver(&self, deadline: Duration) -> Option<JoinHandle<()>> {
let when = env("BENCH_RECEIVER_EARLY", 0);
if when == 0 {
return None;
}
let (own_a, _) = self.settings.own_routes();
let routes = self.settings.receiver_routes();
let (addr, registry, rib) = (
self.a.addr,
self.a.registry.clone(),
Arc::clone(&self.a.rib),
);
Some(tokio::spawn(async move {
while when == 2 && rib.len() < own_a {
tokio::time::sleep(POLL).await;
}
resync(addr, registry, routes, deadline).await
}))
}
/// Waits until both routers have every route, noting when each had
/// what; or panics once `deadline` has passed since `started`.
async fn wait_for_tables(&self, started: Instant, deadline: Duration) -> Tables {
let (own_a, own_b) = self.settings.own_routes();
let mut a_own = None;
let mut b_own = None;
let mut a_all = None;
let mut b_all = None;
let (a_all, b_all) = loop {
tokio::time::sleep(POLL).await;
let (at_a, at_b) = (self.a.rib.len(), self.b.rib.len());
let now = started.elapsed();
if a_own.is_none() && at_a >= own_a {
a_own = Some(now);
}
if b_own.is_none() && at_b >= own_b {
b_own = Some(now);
}
if a_all.is_none() && at_a >= own_a + own_b {
a_all = Some(now);
}
if b_all.is_none() && at_b >= own_a + own_b {
b_all = Some(now);
}
if let (Some(a_all), Some(b_all)) = (a_all, b_all) {
break (a_all, b_all);
}
if now > deadline {
panic!(
"not all routes arrived: A has {at_a} {:?}, B has {at_b} {:?}",
sessions_report(&self.a.registry),
sessions_report(&self.b.registry)
);
}
};
Tables {
a_own: a_own.unwrap_or_default(),
b_own: b_own.unwrap_or_default(),
a_all,
b_all,
}
}
/// One row for each of the four moments of `tables`, with the rate the
/// routes that made the moment came at.
fn report(&self, tables: &Tables) {
let (own_a, own_b) = self.settings.own_routes();
let rate = |when: Duration, routes: usize| {
format!("{:.2} M routes/s", routes as f64 / when.as_secs_f64() / 1e6)
};
row(
"A has its own peers' routes",
tables.a_own,
rate(tables.a_own, own_a),
);
row(
"B has its own peers' routes",
tables.b_own,
rate(tables.b_own, own_b),
);
row(
"A has B's routes too (B's exporter, A's session)",
tables.a_all,
rate(tables.a_all, own_b),
);
row(
"B has A's routes too (A's exporter, B's session)",
tables.b_all,
rate(tables.b_all, own_a),
);
}
}
impl Feeding {
/// Waits for every feeder to have written its table, and returns the
/// sessions, which are to stay up until the end: a feeder going would
/// have its routes withdrawn from both tables meanwhile.
async fn finished(self) -> Vec<TestPeer> {
let mut feeders = Vec::new();
for session in self.sending {
feeders.push(session.await.expect("a feeder is done"));
}
feeders
}
}
fn main() {
// The routers' logs, with `BENCH_LOG` as a `RUST_LOG` filter.
if let Ok(filter) = std::env::var("BENCH_LOG") {
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
}
let settings = Settings::from_env();
let deadline = Duration::from_secs(env("BENCH_DEADLINE", 600) as u64);
println!(
"== Two routers of AS{ASN}, every path both ways: A fed by {} peers, B by {}, {} prefixes each",
settings.peers_a,
settings.peers_b,
settings.shape.prefixes()
);
let prefixes = Prefixes::generate(settings.shape);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("a runtime");
runtime.block_on(async {
let mut mesh = Mesh::start(settings, prefixes).await;
mesh.connect_collectors().await;
let early_receiver = mesh.early_receiver(deadline);
let feeding = mesh.feed().await;
let tables = mesh.wait_for_tables(feeding.started, deadline).await;
mesh.report(&tables);
let feeders = feeding.finished().await;
match early_receiver {
Some(receiver) => receiver.await.expect("the receiver is done"),
None => {
resync(
mesh.a.addr,
mesh.a.registry.clone(),
mesh.settings.receiver_routes(),
deadline,
)
.await
}
}
drop(feeders);
drop(mesh);
});
}
/// The new peer taking a router's table: counting the UPDATEs it is sent
/// while keeping the session alive.
struct Receiver {
started: Instant,
/// When the first UPDATE arrived, in microseconds after `started`; 0
/// until it has.
first: Arc<AtomicU64>,
updates: Arc<AtomicU64>,
counting: JoinHandle<()>,
keepalives: JoinHandle<()>,
}
impl Receiver {
/// A session with the router at `addr` from [`RECEIVER`], as a
/// collector of every path; or `None`, with a word, where that address
/// is not this machine's to connect from.
async fn connect(addr: SocketAddr) -> Option<TestPeer> {
let socket = tokio::net::TcpSocket::new_v4().expect("a socket");
if let Err(e) = socket.bind(SocketAddr::new(RECEIVER.into(), 0)) {
println!("A new peer taking A's routes: skipped, {RECEIVER} is not ours ({e})");
return None;
}
let stream = socket.connect(addr).await.expect("A listens");
let mut session = TestPeer::from_stream(stream);
session
.handshake_with(
ASN,
Ipv4Addr::new(192, 0, 2, 9),
90,
collector_capabilities(ASN, &SUPPORTED, false),
)
.await
.expect("A takes the receiver's session");
Some(session)
}
/// Starts counting what `session` is sent, from now. Every millionth
/// UPDATE is noted, as is a NOTIFICATION or the connection closing:
/// what went wrong, if the table never all arrives.
fn count_updates(session: TestPeer) -> Self {
let started = Instant::now();
let (reader, mut writer) = session.into_stream().into_split();
// KEEPALIVEs, so that the session outlives the hold time.
let keepalives = tokio::spawn(async move {
let keepalive = TestPeer::keepalive_bytes();
loop {
tokio::time::sleep(Duration::from_secs(20)).await;
if writer.write_all(&keepalive).await.is_err() {
break;
}
}
});
let first = Arc::new(AtomicU64::new(0));
let updates = Arc::new(AtomicU64::new(0));
let counting = tokio::spawn({
let (first, updates) = (Arc::clone(&first), Arc::clone(&updates));
async move {
let mut reader = BufReader::with_capacity(1 << 20, reader);
let mut header = [0u8; 19];
let mut body = vec![0u8; 4096];
while reader.read_exact(&mut header).await.is_ok() {
let length = usize::from(u16::from_be_bytes([header[16], header[17]]));
if reader.read_exact(&mut body[..length - 19]).await.is_err() {
break;
}
match header[18] {
msg_type::UPDATE => {
let _ = first.compare_exchange(
0,
started.elapsed().as_micros() as u64,
Ordering::Relaxed,
Ordering::Relaxed,
);
let n = updates.fetch_add(1, Ordering::Relaxed) + 1;
if n % 1_000_000 == 0 {
println!(
" the receiver has read {n} UPDATEs after {:.2} s",
started.elapsed().as_secs_f64()
);
}
}
msg_type::NOTIFICATION => println!(
"the receiver was sent a NOTIFICATION, error {} subcode {}, after {} UPDATEs",
body[0],
body[1],
updates.load(Ordering::Relaxed)
),
_ => {}
}
}
println!(
"the receiver's connection closed after {} UPDATEs",
updates.load(Ordering::Relaxed)
);
}
});
Self {
started,
first,
updates,
counting,
keepalives,
}
}
/// Waits until the router whose sessions are in `registry` has
/// advertised `routes` routes to the receiver, and returns how many it
/// has; or panics once `deadline` has passed.
async fn wait_for_advertised(
&self,
registry: &SessionRegistry,
routes: usize,
deadline: Duration,
) -> u64 {
loop {
tokio::time::sleep(POLL).await;
let advertised = registry
.list()
.iter()
.find(|s| s.peer.addr.ip() == RECEIVER)
.map_or(0, |s| s.counters.advertised());
if advertised as usize >= routes {
return advertised;
}
if self.started.elapsed() > deadline {
panic!(
"the receiver has {advertised} of {routes} routes and read {} UPDATEs; sessions at A: {:#?}",
self.updates.load(Ordering::Relaxed),
sessions_report(registry)
);
}
}
}
/// Prints the row for the table being all there, `advertised` routes
/// of it, and stops counting.
fn report(self, advertised: u64) {
let took = self.started.elapsed();
self.counting.abort();
self.keepalives.abort();
let first = Duration::from_micros(self.first.load(Ordering::Relaxed));
row(
"A new peer has A's IPv4 routes (A's exporter)",
took,
format!(
"{:.2} M routes/s first UPDATE after {:.0} ms, {} UPDATEs",
advertised as f64 / took.as_secs_f64() / 1e6,
first.as_secs_f64() * 1e3,
self.updates.load(Ordering::Relaxed),
),
);
}
}
/// A new peer takes the table of the router at `addr`, whose sessions
/// are in `registry`: `routes` routes with every path. How long until
/// the first UPDATE, and until all of it is sent.
async fn resync(addr: SocketAddr, registry: SessionRegistry, routes: usize, deadline: Duration) {
let Some(session) = Receiver::connect(addr).await else {
return;
};
let receiver = Receiver::count_updates(session);
let advertised = receiver
.wait_for_advertised(&registry, routes, deadline)
.await;
receiver.report(advertised);
}
+6 -16
View File
@@ -11,30 +11,20 @@ use std::net::Ipv4Addr;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use std::time::Instant;
use bgp_bench::memory::Region;
use bgp_bench::mrt::{Dump, parse_attributes};
use bgp_bench::report::{mib, per_second, row};
use bgp_packet::message::{NlriNextHop, PathAttribute};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::table::{Event, PeerIndex, PeerInfo, RibTable};
/// Bytes allocated and not freed since `region` began; zero when memory
/// is not being counted (see `bgp_bench::memory`).
fn retained(region: &Region) -> i64 {
region.held().unwrap_or(0)
}
fn mib(bytes: i64) -> f64 {
bytes as f64 / (1024.0 * 1024.0)
}
fn per_second(count: usize, elapsed: Duration) -> String {
format!("{:.2} M/s", count as f64 / elapsed.as_secs_f64() / 1e6)
}
fn row(what: &str, elapsed: Duration, detail: String) {
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
/// is not being counted (see `bgp_bench::memory`), and zero too in the
/// odd case of more freed than allocated, which is nothing held.
fn retained(region: &Region) -> usize {
usize::try_from(region.held().unwrap_or(0)).unwrap_or(0)
}
fn dump_path() -> Option<PathBuf> {
+26 -68
View File
@@ -32,32 +32,18 @@
use std::net::{Ipv4Addr, SocketAddr};
use std::time::{Duration, Instant};
use bgp_bench::{Prefixes, Shape, announcements, encoded, env};
use bgp_integration_tests::test_peer::TestPeer;
use bgp_packet::open::{AddPathEntry, Capability, ExtendedNextHopEntry, SendReceive};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_server::family::SUPPORTED;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use bgp_bench::report::mib;
use bgp_bench::{Prefixes, Shape, env, table_stream};
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
use bgp_primitives::constants::AfiSafi;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpSocket;
use tokio::net::tcp::OwnedWriteHalf;
/// A KEEPALIVE message: the marker, the length (19) and the type (4).
const KEEPALIVE: [u8; 19] = {
let mut bytes = [0xff; 19];
bytes[16] = 0;
bytes[17] = 19;
bytes[18] = 4;
bytes
};
/// How often KEEPALIVEs go out while the sessions are held open: well
/// inside the 90 s hold time.
const KEEPALIVE_EVERY: Duration = Duration::from_secs(20);
fn mib(bytes: usize) -> f64 {
bytes as f64 / (1024.0 * 1024.0)
}
#[tokio::main]
async fn main() {
let to: SocketAddr = std::env::var("FEED_TO")
@@ -73,12 +59,7 @@ async fn main() {
let started = Instant::now();
let prefixes = Prefixes::generate(shape);
let tables: Vec<Vec<u8>> = (1..=peers)
.map(|peer| {
encoded(&announcements(&prefixes, shape, peer))
.iter()
.flat_map(|message| message.iter().copied())
.collect()
})
.map(|peer| table_stream(&prefixes, shape, peer))
.collect();
let routes = shape.prefixes() * peers as usize;
let bytes: usize = tables.iter().map(Vec::len).sum();
@@ -109,38 +90,21 @@ async fn main() {
}
let stream = socket.connect(to).await.expect("the daemon listens");
let mut session = TestPeer::from_stream(stream);
let mut capabilities = vec![Capability::FourByteAsn { asn: 212_232 }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
capabilities.push(Capability::AddPath {
entries: [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST]
.into_iter()
.map(|family| AddPathEntry {
afi: family.afi,
safi: family.safi,
send_recv: SendReceive::Receive,
})
.collect(),
});
capabilities.push(Capability::ExtendedNextHop {
entries: vec![ExtendedNextHopEntry {
afi: Afi::IPV4,
safi: Safi::UNICAST,
nh_afi: Afi::IPV6,
}],
});
session
.handshake_with(212_232, Ipv4Addr::new(10, 0, 0, 200), 90, capabilities)
.handshake_with(
212_232,
Ipv4Addr::new(10, 0, 0, 200),
90,
collector_capabilities(
212_232,
&[AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST],
true,
),
)
.await
.expect("the daemon takes the collector's session");
let (mut reader, writer) = session.into_stream().into_split();
draining.push(tokio::spawn(async move {
let mut sink = [0u8; 65536];
while let Ok(n) = reader.read(&mut sink).await {
if n == 0 {
break;
}
}
}));
let (writer, sink) = session.drain();
draining.push(sink);
println!("collector session established");
collector_writer = Some(writer);
}
@@ -166,7 +130,11 @@ async fn main() {
)
.await
.expect("the daemon takes the session");
sessions.push(session.into_stream());
// Whatever the daemon sends back, KEEPALIVEs as a rule, is read
// and dropped, so that the writer is never held up.
let (writer, sink) = session.drain();
draining.push(sink);
sessions.push(writer);
}
println!("{peers} session(s) established; sending");
@@ -174,19 +142,8 @@ async fn main() {
let senders: Vec<_> = sessions
.into_iter()
.zip(tables)
.map(|(stream, table)| {
.map(|(mut writer, table)| {
tokio::spawn(async move {
let (mut reader, mut writer) = stream.into_split();
// Whatever the daemon sends back, KEEPALIVEs as a rule, is
// read and dropped, so that its writer is never held up.
tokio::spawn(async move {
let mut sink = [0u8; 65536];
while let Ok(n) = reader.read(&mut sink).await {
if n == 0 {
break;
}
}
});
let started = Instant::now();
writer.write_all(&table).await.expect("the daemon reads");
(writer, started.elapsed())
@@ -210,12 +167,13 @@ async fn main() {
routes as f64 / elapsed.as_secs_f64() / 1e6
);
let keepalive = TestPeer::keepalive_bytes();
let deadline = Instant::now() + hold;
while Instant::now() < deadline {
tokio::time::sleep(KEEPALIVE_EVERY.min(deadline - Instant::now())).await;
for writer in &mut writers {
// A session the daemon closed is left alone.
let _ = writer.write_all(&KEEPALIVE).await;
let _ = writer.write_all(&keepalive).await;
}
}
println!("sessions closed after {} s", hold.as_secs());
+71
View File
@@ -0,0 +1,71 @@
//! How many UPDATEs, and how many octets, a table takes when the routes
//! that share attributes are grouped only within slices of the table
//! taken in shard order, for a few slice sizes: what streaming a table
//! out in slices costs against grouping it whole.
use std::collections::HashMap;
use std::hash::{BuildHasher, RandomState};
use std::path::PathBuf;
use bgp_bench::mrt::Dump;
fn main() {
let path = std::env::var("BENCH_MRT")
.map(PathBuf::from)
.unwrap_or_else(|_| "crates/bench/data/rrc03-bview.20260915.0000.gz".into());
let dump = Dump::read(&path).expect("a readable dump");
let peers = dump.largest_peers(usize::MAX);
let full: Vec<usize> = peers
.iter()
.copied()
.filter(|&p| dump.routes[p].len() > 800_000)
.collect();
println!("{} peers with a full table", full.len());
let hasher = RandomState::new();
for (what, chosen) in [
("one full table", vec![full[0]]),
("every full table, every path", full.clone()),
] {
// (hash of prefix, attribute set, prefix octets) in shard order.
let mut routes: Vec<(u64, u32, usize)> = chosen
.iter()
.flat_map(|&p| dump.routes[p].iter())
.map(|(prefix, set)| {
(
hasher.hash_one(prefix),
*set,
1 + usize::from(prefix.length()).div_ceil(8) + 4,
)
})
.collect();
routes.sort_unstable_by_key(|r| r.0);
println!("== {what}: {} routes", routes.len());
for slice in [4_096usize, 16_384, 65_536, 262_144, usize::MAX] {
let mut updates = 0usize;
let mut octets = 0usize;
for chunk in routes.chunks(slice.min(routes.len())) {
let mut groups: HashMap<u32, usize> = HashMap::new();
for &(_, set, prefix_octets) in chunk {
*groups.entry(set).or_insert(0) += prefix_octets;
}
for (set, prefix_octets) in groups {
// One UPDATE per 4096 octets of prefixes, roughly.
let attrs = dump.attribute_sets[set as usize].len();
let messages = prefix_octets.div_ceil(4000 - attrs.min(3000)).max(1);
updates += messages;
octets += messages * (23 + attrs) + prefix_octets;
}
}
println!(
" slice {:>9}: {:>9} UPDATEs, {:>5.1} MB on the wire",
if slice == usize::MAX {
"whole".to_string()
} else {
slice.to_string()
},
updates,
octets as f64 / 1e6
);
}
}
}
+83
View File
@@ -16,6 +16,14 @@
pub mod memory;
pub mod mrt;
pub mod report;
// The benchmarks allocate as the daemon does (bin/bgp/src/main.rs), with
// jemalloc; the C library's allocator would put its own locks in the
// profile. Not when memory is counted: that needs the counting one.
#[cfg(all(not(feature = "count-memory"), not(target_env = "msvc")))]
#[global_allocator]
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
use std::collections::HashSet;
use std::net::{Ipv4Addr, Ipv6Addr};
@@ -259,6 +267,16 @@ pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<Update
updates
}
/// The whole table as the peer numbered `peer` sends it, one stream of
/// bytes: [`announcements`], [`encoded`] and run together, for a session
/// that writes the table in one go.
pub fn table_stream(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<u8> {
encoded(&announcements(prefixes, shape, peer))
.iter()
.flat_map(|message| message.iter().copied())
.collect()
}
/// `updates` as they are on the wire, on a session with four-octet AS
/// numbers.
pub fn encoded(updates: &[UpdateMessage]) -> Vec<BytesMut> {
@@ -293,3 +311,68 @@ pub fn env(name: &str, default: usize) -> usize {
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
#[cfg(test)]
mod tests {
use super::*;
/// A small table, enough for every prefix length and attribute to
/// come up.
fn shape() -> Shape {
Shape::INTERNET.scaled_down(1000)
}
/// The benchmarks are compared from one run to the next, so the table
/// they are fed must not change between runs: the generator is
/// seeded, and nothing in it depends on where things are in memory.
#[test]
fn the_table_is_the_same_every_run() {
let shape = shape();
let once = Prefixes::generate(shape);
let again = Prefixes::generate(shape);
assert_eq!(once.ipv4, again.ipv4);
assert_eq!(once.ipv6, again.ipv6);
assert_eq!(once.ipv4.len(), shape.ipv4_prefixes);
assert_eq!(once.ipv6.len(), shape.ipv6_prefixes);
for peer in [1, 2] {
assert_eq!(
table_stream(&once, shape, peer),
table_stream(&again, shape, peer)
);
}
assert_ne!(
table_stream(&once, shape, 1),
table_stream(&once, shape, 2),
"each peer has attributes of its own"
);
}
/// What a peer of the benchmarks sends is well formed, and says what
/// it was meant to: every UPDATE reads back as it was made.
#[test]
fn every_announcement_decodes_to_itself() {
let shape = shape();
let prefixes = Prefixes::generate(shape);
let updates = announcements(&prefixes, shape, 1);
let ctx = wire_context();
let announced: usize = updates
.iter()
.map(|update| {
update.announced_routes.len()
+ update
.path_attributes
.iter()
.map(|attribute| match attribute {
PathAttribute::MpReachNlri(reach) => reach.prefixes.len(),
_ => 0,
})
.sum::<usize>()
})
.sum();
assert_eq!(announced, shape.prefixes());
for (update, wire) in updates.iter().zip(encoded(&updates)) {
let decoded = Message::decode(&ctx, &wire).expect("well formed");
assert_eq!(decoded, Message::Update(update.clone()));
}
}
}
+16 -26
View File
@@ -10,7 +10,7 @@ use std::io::{self, BufReader, Read};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::Path;
use bgp_packet::message::{NlriNextHop, PathAttribute};
use bgp_packet::message::{NlriNextHop, PathAttribute, attr_type, split_attribute};
use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::IpPrefix;
use flate2::read::MultiGzDecoder;
@@ -21,7 +21,6 @@ const TABLE_DUMP_V2: u16 = 13;
const PEER_INDEX_TABLE: u16 = 1;
const RIB_IPV4_UNICAST: u16 = 2;
const RIB_IPV6_UNICAST: u16 = 4;
const MP_REACH_NLRI: u8 = 14;
/// A peer of the collector.
#[derive(Debug, Clone)]
@@ -205,32 +204,23 @@ pub fn parse_attributes(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<N
let mut attributes = Vec::new();
let mut next_hop = None;
while !wire.is_empty() {
if wire.get(1) == Some(&MP_REACH_NLRI) {
let extended = wire[0] & 0x10 != 0;
let (length, header) = if extended {
(
usize::from(u16::from_be_bytes([*wire.get(2)?, *wire.get(3)?])),
4,
)
} else {
(usize::from(*wire.get(2)?), 3)
let (raw, rest) = split_attribute(wire).ok()?;
if raw.type_code == attr_type::MP_REACH_NLRI {
// The value is the next hop's length and the next hop, and
// nothing of the family, reserved octet and prefixes that
// follow in an UPDATE, so the attribute's own parser would
// refuse it.
let (length, address) = raw.value.split_first()?;
if usize::from(*length) != address.len() {
return None;
}
next_hop = Some(NlriNextHop::from_bytes(address)?);
} else {
let ([], attribute) = PathAttribute::from_wire(&ctx, raw.wire).ok()? else {
return None;
};
let value = wire.get(header..header + length)?;
let address = |bytes: &[u8]| Some(Ipv6Addr::from(<[u8; 16]>::try_from(bytes).ok()?));
next_hop = match value {
[4, a, b, c, d] => Some(NlriNextHop::Ipv4(Ipv4Addr::new(*a, *b, *c, *d))),
[16, rest @ ..] => Some(NlriNextHop::Ipv6(address(rest)?)),
[32, rest @ ..] if rest.len() == 32 => Some(NlriNextHop::Ipv6WithLl {
global: address(&rest[..16])?,
link_local: address(&rest[16..])?,
}),
_ => return None,
};
wire = &wire[header + length..];
continue;
attributes.push(attribute);
}
let (rest, attribute) = PathAttribute::from_wire(&ctx, wire).ok()?;
attributes.push(attribute);
wire = rest;
}
Some((attributes, next_hop))
+31
View File
@@ -0,0 +1,31 @@
//! How the benchmarks print what they measured: one row a measurement,
//! what was measured in a column wide enough for the longest of them, the
//! time, and whatever else there is to say about it. The rows are what a
//! run is compared against, so their shape is settled here once.
use std::time::Duration;
/// How wide the column that says what was measured is. The widest row is
/// the mesh benchmark's, so that is what every benchmark lines up to.
pub const WIDTH: usize = 52;
/// `bytes` in mebibytes.
pub fn mib(bytes: usize) -> f64 {
bytes as f64 / (1024.0 * 1024.0)
}
/// `count` things in `elapsed`, as a rate: millions a second where there
/// are that many, thousands otherwise.
pub fn per_second(count: usize, elapsed: Duration) -> String {
let rate = count as f64 / elapsed.as_secs_f64();
if rate >= 1e6 {
format!("{:.2} M/s", rate / 1e6)
} else {
format!("{:.0} k/s", rate / 1e3)
}
}
/// Prints one row: `what` took `elapsed`, and `detail` says more.
pub fn row(what: &str, elapsed: Duration, detail: impl std::fmt::Display) {
println!("{what:<WIDTH$} {:>8.3} s {detail}", elapsed.as_secs_f64());
}
+89 -47
View File
@@ -498,9 +498,53 @@ struct Task {
/// them means looking at every route, and is not done once there are
/// none to find.
keeping: bool,
/// What `follow` takes out of the table, and what it makes of it. Kept
/// from one wake to the next so that their room is not found anew each
/// time the table has something, which is many times a second while
/// it fills.
news: Vec<News>,
ops: Vec<Op>,
}
impl Task {
/// A task that is to keep the kernel in step with `rib`, talking to it
/// over `control` and `resolution` and writing routes over `routes`
/// from a task of its own. Returned with it is where that task
/// reports what became of the routes.
fn new(
rib: Arc<RibTable>,
fib: Fib,
control: Socket,
routes: Socket,
resolution: Option<Socket>,
) -> (Self, mpsc::UnboundedReceiver<Report>) {
let (queue, batches) = mpsc::channel(QUEUE);
let (reports, heard) = mpsc::unbounded_channel();
let config = fib.config();
let task = Self {
config,
rib,
fib,
control,
resolution,
queue: Some(queue),
writer: tokio::spawn(write(routes, batches, reports)),
topology: Topology::new(config.resolution_table, config.resolve_via_default),
resolved: FastHashMap::default(),
plan: Plan::default(),
nexthops: config.nexthop_objects.then(FastHashMap::default),
owned: HashSet::new(),
awaiting: None,
marks: 0,
synced: false,
mended_at: None,
keeping: false,
news: Vec::with_capacity(BATCH),
ops: Vec::with_capacity(BATCH),
};
(task, heard)
}
/// Whether a route the kernel has is one of ours.
fn is_ours(&self, route: &Route) -> bool {
route.protocol == PROTOCOL_BGP
@@ -631,6 +675,19 @@ impl Task {
Ok(true)
}
/// Learns what the kernel has that bears on our routes: the routes
/// that say the way to next hops, its nexthop objects, and the routes
/// of ours it has from before, which are kept for the peers to announce
/// again if `keep` says so. Done at the start, and again whenever what
/// the kernel has is no longer known.
async fn learn(&mut self, keep: bool) -> Result<(), FibError> {
self.learn_routes().await?;
let nexthops = self.kernel_nexthops().await?;
self.adopt_routes(&nexthops, keep).await?;
self.use_nexthops(&nexthops);
Ok(())
}
/// Finds the routes of ours that the kernel has from before.
async fn adopt_routes(
&mut self,
@@ -751,17 +808,21 @@ impl Task {
Ok(Ok((change, route)))
}
/// Hands `ops` over to be written to the kernel, in order. With
/// `whole_table` they complete a whole table, and the kernel is in
/// step once it has done them.
async fn send(&mut self, ops: Vec<Op>, whole_table: bool) -> Result<(), FibError> {
/// Hands `ops` over to be written to the kernel, in order, and leaves
/// the vector empty for the caller to fill again. With `whole_table`
/// they complete a whole table, and the kernel is in step once it has
/// done them.
async fn send(&mut self, ops: &mut Vec<Op>, whole_table: bool) -> Result<(), FibError> {
let mut batch = Batch {
ops: Vec::with_capacity(ops.len()),
requests: Vec::with_capacity(ops.len()),
mark: None,
};
let mut ops = std::collections::VecDeque::from(ops);
while let Some(op) = ops.pop_front() {
// By place rather than by draining: what takes a refused op's
// place is put at the end, and is dealt with in its turn.
let mut at = 0;
while let Some(op) = ops.get(at).copied() {
at += 1;
match self.request(&op).await? {
Ok(request) => {
CHANGES
@@ -777,6 +838,7 @@ impl Task {
Err(errno) => ops.extend(self.refused(&op, errno)?),
}
}
ops.clear();
if whole_table {
self.marks += 1;
batch.mark = Some(self.marks);
@@ -850,7 +912,12 @@ impl Task {
/// hands over what brings the kernel in step with them. Returns whether
/// the reader has read everything the table has for it.
async fn follow(&mut self, reader: &mut Reader) -> Result<bool, FibError> {
let mut news = Vec::with_capacity(BATCH);
// Taken out of `self` while they are worked through, since working
// through them is what the rest of `self` is for.
let mut news = std::mem::take(&mut self.news);
let mut ops = std::mem::take(&mut self.ops);
news.clear();
ops.clear();
// Only what is needed is taken while the table is held.
let progress = reader.poll(BATCH, |event| {
news.push(match event {
@@ -872,9 +939,8 @@ impl Task {
Event::Routes { .. } => return,
});
});
let mut ops = Vec::with_capacity(news.len());
let mut whole_table = false;
for item in news {
for item in news.drain(..) {
match item {
News::Reset => {
self.plan.reset();
@@ -928,7 +994,10 @@ impl Task {
News::Withdrawn(prefix) => ops.extend(self.plan.withdrawn(prefix)),
}
}
self.send(ops, whole_table).await?;
let sent = self.send(&mut ops, whole_table).await;
self.news = news;
self.ops = ops;
sent?;
Ok(progress.caught_up)
}
@@ -957,7 +1026,7 @@ impl Task {
}
Report::Refused { op, errno } => {
let instead = self.refused(&op, errno)?;
self.send(instead.into_iter().collect(), false).await?;
self.send(&mut instead.into_iter().collect(), false).await?;
// A refused route may have named a nexthop object that
// went away. Working the routes out again mends that.
let due = matches!(op, Op::Install { .. })
@@ -1026,18 +1095,14 @@ impl Task {
self.flush(heard).await?;
self.synced = false;
self.plan = Plan::default();
self.learn_routes().await?;
let nexthops = self.kernel_nexthops().await?;
// While routes of an earlier run are being kept for the peers to
// announce again, they are kept through this too.
self.adopt_routes(&nexthops, self.keeping).await?;
self.use_nexthops(&nexthops);
Ok(())
self.learn(self.keeping).await
}
/// Waits until the kernel has done everything handed over so far.
async fn flush(&mut self, heard: &mut mpsc::UnboundedReceiver<Report>) -> Result<(), FibError> {
self.send(vec![], true).await?;
self.send(&mut Vec::new(), true).await?;
while let Some(mark) = self.awaiting {
match heard.recv().await {
Some(Report::Done(done)) if done == mark => self.awaiting = None,
@@ -1090,8 +1155,8 @@ impl Task {
self.plan = Plan::default();
self.adopt_routes(&HashMap::new(), false).await?;
}
let ops = self.plan.clear();
self.send(ops, false).await?;
let mut ops = self.plan.clear();
self.send(&mut ops, false).await?;
self.finish_writer().await
}
@@ -1153,38 +1218,15 @@ pub(crate) async fn run(
resolution,
mut hints,
} = sockets;
let (queue, batches) = mpsc::channel(QUEUE);
let (reports, mut heard) = mpsc::unbounded_channel();
let config = fib.config();
let mut task = Task {
config,
rib,
fib,
control,
resolution,
queue: Some(queue),
writer: tokio::spawn(write(routes, batches, reports)),
topology: Topology::new(config.resolution_table, config.resolve_via_default),
resolved: FastHashMap::default(),
plan: Plan::default(),
nexthops: config.nexthop_objects.then(FastHashMap::default),
owned: HashSet::new(),
awaiting: None,
marks: 0,
synced: false,
mended_at: None,
keeping: false,
};
let (mut task, mut heard) = Task::new(rib, fib, control, routes, resolution);
let config = task.config;
// The socket for news was bound before this: what changes while the
// interfaces are being learned is told there.
task.learn_interfaces().await?;
task.learn_routes().await?;
let nexthops = task.kernel_nexthops().await?;
// Kept for a while, if there is to be any keeping: our table is empty
// until the peers have sent theirs again.
let keep = !config.stale_time.is_zero();
task.adopt_routes(&nexthops, keep).await?;
task.use_nexthops(&nexthops);
task.learn(keep).await?;
let mut release_at = None;
if task.plan.installed() > 0 {
info!(
@@ -1284,12 +1326,12 @@ pub(crate) async fn run(
// taken out only to be put back.
while !task.follow(&mut reader).await? {}
task.keeping = false;
let stale = task.plan.release();
let mut stale = task.plan.release();
info!(
removed = stale.len(),
"Routes of an earlier run that were not announced again are removed"
);
task.send(stale, false).await?;
task.send(&mut stale, false).await?;
task.publish(false);
}
ended = &mut task.writer => return Err(writer_error(ended)),
+22 -9
View File
@@ -29,9 +29,13 @@ pub const KIND_UNICAST: u8 = 1;
/// `RTN_BLACKHOLE`: a route that discards the packets for its prefix.
pub const KIND_BLACKHOLE: u8 = 6;
const HEADER_LEN: usize = 16;
/// The netlink header: length, type, flags, sequence number and the
/// sender's port, four octets each but for the two-octet type and flags.
/// The socket diagnostics family (`sockdiag`) frames its messages the
/// same way.
pub(crate) const HEADER_LEN: usize = 16;
mod message_type {
pub(crate) mod message_type {
pub const ERROR: u16 = 2;
pub const DONE: u16 = 3;
pub const NEW_LINK: u16 = 16;
@@ -48,7 +52,7 @@ mod message_type {
pub const GET_NEXTHOP: u16 = 106;
}
mod flag {
pub(crate) mod flag {
pub const REQUEST: u16 = 0x1;
pub const ACK: u16 = 0x4;
/// Send the sender what everyone else is told of the change.
@@ -76,8 +80,8 @@ pub mod group {
/// joined once the socket is bound.
pub const GROUP_NEXTHOP: u32 = 32;
const AF_INET: u8 = 2;
const AF_INET6: u8 = 10;
pub(crate) const AF_INET: u8 = 2;
pub(crate) const AF_INET6: u8 = 10;
/// `RT_TABLE_COMPAT`: the table's number is in an attribute, being too
/// large for the octet the fixed part has for it.
@@ -219,22 +223,31 @@ pub enum NetlinkError {
Truncated { message_type: u16 },
}
fn ne_u16(bytes: &[u8]) -> u16 {
// The kernel writes its numbers in the machine's byte order. Each reader
// takes the first octets of what it is given, so that a caller can pass
// the rest of a buffer rather than cut the field out first.
pub(crate) fn ne_u16(bytes: &[u8]) -> u16 {
u16::from_ne_bytes([bytes[0], bytes[1]])
}
fn ne_u32(bytes: &[u8]) -> u32 {
pub(crate) fn ne_u32(bytes: &[u8]) -> u32 {
u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
pub(crate) fn ne_u64(bytes: &[u8]) -> u64 {
u64::from_ne_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}
/// Rounds a length up to the four octets everything is aligned to.
fn aligned(length: usize) -> usize {
pub(crate) fn aligned(length: usize) -> usize {
(length + 3) & !3
}
/// The attributes in `bytes`: each its type and value. Stops at what is
/// not an attribute.
fn attributes(mut bytes: &[u8]) -> impl Iterator<Item = (u16, &[u8])> {
pub(crate) fn attributes(mut bytes: &[u8]) -> impl Iterator<Item = (u16, &[u8])> {
std::iter::from_fn(move || {
let header = bytes.first_chunk::<4>()?;
let length = usize::from(ne_u16(header));
+32 -38
View File
@@ -7,6 +7,10 @@
use std::io;
use std::net::{IpAddr, SocketAddr};
use crate::netlink::{
AF_INET, AF_INET6, HEADER_LEN, attributes, flag, message_type, ne_u16, ne_u32, ne_u64,
};
/// A TCP connection as the kernel sees it. Times are in microseconds,
/// sizes in octets, rates in octets per second, unless said otherwise.
/// Fields the running kernel does not report are zero.
@@ -90,17 +94,12 @@ impl TcpConnection {
}
const SOCK_DIAG_BY_FAMILY: u16 = 20;
const NLMSG_ERROR: u16 = 2;
const NLM_F_REQUEST: u16 = 1;
const IPPROTO_TCP: u8 = 6;
const AF_INET: u8 = 2;
const AF_INET6: u8 = 10;
const INET_DIAG_INFO: u16 = 2;
const INET_DIAG_CONG: u16 = 4;
/// `idiag_ext` bits asking for those two.
const EXTENSIONS: u8 = (1 << (INET_DIAG_INFO - 1)) | (1 << (INET_DIAG_CONG - 1));
const NO_COOKIE: u32 = u32::MAX;
const HEADER_LEN: usize = 16;
const REQUEST_LEN: usize = HEADER_LEN + 56;
const DIAG_MSG_LEN: usize = 72;
@@ -113,7 +112,7 @@ pub fn request(local: SocketAddr, peer: SocketAddr, sequence: u32) -> Vec<u8> {
let mut buf = Vec::with_capacity(REQUEST_LEN);
buf.extend_from_slice(&(REQUEST_LEN as u32).to_ne_bytes());
buf.extend_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes());
buf.extend_from_slice(&NLM_F_REQUEST.to_ne_bytes());
buf.extend_from_slice(&flag::REQUEST.to_ne_bytes());
buf.extend_from_slice(&sequence.to_ne_bytes());
buf.extend_from_slice(&0u32.to_ne_bytes());
// inet_diag_req_v2
@@ -162,11 +161,11 @@ pub fn parse(buf: &[u8]) -> io::Result<Answer> {
.get(..HEADER_LEN)
.ok_or_else(|| io::Error::other("short netlink answer"))?;
let length = ne_u32(&header[0..4]) as usize;
let message_type = ne_u16(&header[4..6]);
let kind = ne_u16(&header[4..6]);
let message = buf
.get(HEADER_LEN..length.min(buf.len()))
.ok_or_else(|| io::Error::other("short netlink answer"))?;
if message_type == NLMSG_ERROR {
if kind == message_type::ERROR {
let errno = -(message
.first_chunk::<4>()
.map_or(0, |e| i32::from_ne_bytes(*e)));
@@ -177,7 +176,7 @@ pub fn parse(buf: &[u8]) -> io::Result<Answer> {
Answer::Refused(errno)
});
}
if message_type != SOCK_DIAG_BY_FAMILY || message.len() < DIAG_MSG_LEN {
if kind != SOCK_DIAG_BY_FAMILY || message.len() < DIAG_MSG_LEN {
return Err(io::Error::other("not a socket diagnostic"));
}
let mut connection = TcpConnection {
@@ -186,14 +185,7 @@ pub fn parse(buf: &[u8]) -> io::Result<Answer> {
send_queue: ne_u32(&message[60..64]),
..TcpConnection::default()
};
let mut attributes = &message[DIAG_MSG_LEN..];
while attributes.len() >= 4 {
let length = usize::from(ne_u16(&attributes[0..2]));
let kind = ne_u16(&attributes[2..4]);
if length < 4 || length > attributes.len() {
break;
}
let value = &attributes[4..length];
for (kind, value) in attributes(&message[DIAG_MSG_LEN..]) {
match kind {
INET_DIAG_INFO => fill_from_tcp_info(&mut connection, value),
INET_DIAG_CONG => {
@@ -202,7 +194,6 @@ pub fn parse(buf: &[u8]) -> io::Result<Answer> {
}
_ => {}
}
attributes = &attributes[(length + 3) & !3..];
}
Ok(Answer::Connection(Box::new(connection)))
}
@@ -250,20 +241,6 @@ fn fill_from_tcp_info(c: &mut TcpConnection, info: &[u8]) {
c.receive_window = u32_at(232);
}
fn ne_u16(bytes: &[u8]) -> u16 {
u16::from_ne_bytes([bytes[0], bytes[1]])
}
fn ne_u32(bytes: &[u8]) -> u32 {
u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
fn ne_u64(bytes: &[u8]) -> u64 {
let mut array = [0u8; 8];
array.copy_from_slice(bytes);
u64::from_ne_bytes(array)
}
/// Asks the kernel about the TCP connection from `local` to `peer`.
/// `Ok(None)` if there is no such connection, or on an operating system
/// that cannot be asked.
@@ -304,9 +281,10 @@ pub async fn tcp_connection(
mod tests {
use super::*;
/// A reply as the kernel writes it, for an established connection
/// with a `tcp_info` of the full modern length.
fn reply() -> Vec<u8> {
/// A reply as the kernel writes it, for an established connection,
/// with the first `info_len` octets of a `tcp_info` of the full
/// modern length: an older kernel sends a shorter one.
fn reply(info_len: usize) -> Vec<u8> {
let mut info = vec![0u8; 248];
info[0] = 1; // established
info[6] = 0x7a; // snd_wscale 10, rcv_wscale 7
@@ -326,6 +304,7 @@ mod tests {
put64(&mut info, 168, 1_000_000); // busy
put32(&mut info, 228, 131072); // snd_wnd
put32(&mut info, 232, 262144); // rcv_wnd
info.truncate(info_len);
let cong = b"cubic\0";
let mut message = vec![0u8; DIAG_MSG_LEN];
message[1] = 1;
@@ -345,7 +324,9 @@ mod tests {
let mut buf = Vec::new();
buf.extend_from_slice(&(length as u32).to_ne_bytes());
buf.extend_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes());
buf.extend_from_slice(&[0, 0, 1, 0, 0, 0, 0, 0, 0, 0]);
buf.extend_from_slice(&0u16.to_ne_bytes()); // flags
buf.extend_from_slice(&1u32.to_ne_bytes()); // sequence
buf.extend_from_slice(&0u32.to_ne_bytes()); // port
buf.extend_from_slice(&message);
buf.extend_from_slice(&attrs);
buf
@@ -353,7 +334,7 @@ mod tests {
#[test]
fn a_reply_is_read_field_by_field() {
let Answer::Connection(c) = parse(&reply()).unwrap() else {
let Answer::Connection(c) = parse(&reply(248)).unwrap() else {
panic!("a connection");
};
assert_eq!(c.state_name(), "established");
@@ -367,12 +348,25 @@ mod tests {
assert_eq!((c.send_window, c.receive_window), (131072, 262144));
}
/// Linux 4.x sends 104 octets of `tcp_info`, up to `tcpi_total_retrans`:
/// what is there is read, and what came later is left at zero rather
/// than read from the attribute after it.
#[test]
fn a_short_tcp_info_from_an_old_kernel_leaves_later_fields_zero() {
let Answer::Connection(c) = parse(&reply(104)).unwrap() else {
panic!("a connection");
};
assert_eq!(c.congestion.as_deref(), Some("cubic"));
assert_eq!((c.mss, c.rtt, c.cwnd), (1448, 2300, 42));
assert_eq!((c.busy, c.send_window), (0, 0));
}
#[test]
fn no_such_connection_and_a_refusal_are_told_apart() {
let error = |errno: i32| {
let mut buf = vec![0u8; 20];
buf[0..4].copy_from_slice(&20u32.to_ne_bytes());
buf[4..6].copy_from_slice(&NLMSG_ERROR.to_ne_bytes());
buf[4..6].copy_from_slice(&message_type::ERROR.to_ne_bytes());
buf[16..20].copy_from_slice(&(-errno).to_ne_bytes());
buf
};
+76 -5
View File
@@ -2,16 +2,55 @@ use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_packet::message::{BGP_HEADER_LEN, Message, NotificationMessage, UpdateMessage};
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
use bgp_packet::open::{
AddPathEntry, Capability, ExtendedNextHopEntry, OpenMessage, OpenOption, SendReceive,
};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::parser::ParserContext;
use bytes::BytesMut;
use eyre::{Result, bail, eyre};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::tcp::OwnedWriteHalf;
use tokio::net::{TcpListener, TcpStream};
use tokio::task::JoinHandle;
use crate::TIMEOUT;
/// The capabilities a route collector offers in its OPEN: four-octet AS
/// numbers as `asn`, each of `families`, and to be sent several routes
/// for a prefix (ADD-PATH, RFC 7911) in each of them; with
/// `ipv4_over_ipv6`, IPv4 routes with IPv6 next hops too (RFC 8950), as a
/// collector on an IPv6-only session wants. For the tests and benchmarks
/// that play a collector taking a router's whole table.
pub fn collector_capabilities(
asn: u32,
families: &[AfiSafi],
ipv4_over_ipv6: bool,
) -> Vec<Capability> {
let mut capabilities = vec![Capability::FourByteAsn { asn }];
capabilities.extend(families.iter().copied().map(Capability::multiprotocol));
capabilities.push(Capability::AddPath {
entries: families
.iter()
.map(|family| AddPathEntry {
afi: family.afi,
safi: family.safi,
send_recv: SendReceive::Receive,
})
.collect(),
});
if ipv4_over_ipv6 {
capabilities.push(Capability::ExtendedNextHop {
entries: vec![ExtendedNextHopEntry {
afi: Afi::IPV4,
safi: Safi::UNICAST,
nh_afi: Afi::IPV6,
}],
});
}
capabilities
}
/// A minimal BGP peer for use in tests. Connects over TCP and can exchange
/// framed BGP messages using the `bgp-packet` wire format.
pub struct TestPeer {
@@ -57,6 +96,32 @@ impl TestPeer {
self.stream
}
/// The connection with a task reading and dropping whatever the server
/// sends over it, and the half that is left for writing: for a peer
/// that has nothing to hear, as one feeding a table or taking one by
/// the megabyte has not, and whose writer must never be held up by
/// what it has not read. The task ends with the connection, or when
/// its handle is dropped.
pub fn drain(self) -> (OwnedWriteHalf, JoinHandle<()>) {
let (mut reader, writer) = self.stream.into_split();
let sink = tokio::spawn(async move {
let mut sink = vec![0u8; 1 << 16];
while matches!(reader.read(&mut sink).await, Ok(n) if n > 0) {}
});
(writer, sink)
}
/// A KEEPALIVE as it goes on the wire, for whoever writes to the
/// connection without the framing and has to keep the session alive
/// past its hold time.
pub fn keepalive_bytes() -> Vec<u8> {
let mut wire = BytesMut::new();
Message::KeepAlive
.to_wire(&ParserContext::default(), &mut wire)
.expect("a KEEPALIVE is nineteen octets");
wire.to_vec()
}
/// Our end of the connection: what the server sees the peer as.
pub fn local_addr(&self) -> Result<SocketAddr> {
Ok(self.stream.local_addr()?)
@@ -145,7 +210,9 @@ impl TestPeer {
pub async fn recv_open(&mut self) -> Result<OpenMessage> {
match self.recv().await? {
Message::Open(o) => {
// Enable 4-octet ASN parsing if the peer advertised it.
// AS numbers are four octets wide only if both sides
// offered it (RFC 6793 §4.1): the server's offer is read
// here, ours was noted when the OPEN went out.
let four_byte = o.options.iter().any(|opt| {
matches!(opt, OpenOption::Capabilities(caps)
if caps.iter().any(|c| matches!(c, Capability::FourByteAsn { .. })))
@@ -243,7 +310,8 @@ impl TestPeer {
async fn finish_handshake(&mut self) -> Result<OpenMessage> {
let server_open = self.recv_open().await?;
// Expect server KEEPALIVE
// The server's KEEPALIVE says it accepted our OPEN; anything else
// is its refusal, or a server out of step.
match self.recv().await? {
Message::KeepAlive => {}
other => {
@@ -253,12 +321,15 @@ impl TestPeer {
));
}
}
// Send our KEEPALIVE
// Ours says the same of its OPEN, and the session is established.
self.send(&Message::KeepAlive).await?;
Ok(server_open)
}
/// Send raw bytes without framing — useful for testing invalid input.
/// Writes `bytes` as they are, with no framing of its own: for a test
/// that sends what no message would be, and for the benchmarks, which
/// send a table encoded once as one stream rather than message by
/// message.
pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<()> {
self.stream.write_all(bytes).await?;
Ok(())
+8 -46
View File
@@ -7,16 +7,15 @@ use std::sync::Arc;
use std::time::Duration;
use bgp_integration_tests::announcement::{Announcement, withdrawal};
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_peer::{TestPeer, collector_capabilities};
use bgp_integration_tests::test_server::TestServer;
use bgp_integration_tests::{peer_config, prefix, wait_until};
use bgp_packet::message::{
AsPathAttribute, Message, MpReachNlriPathAttribute, NlriNextHop, OriginPathAttribute,
PathAttribute, UpdateMessage,
};
use bgp_packet::open::{AddPathEntry, Capability, ExtendedNextHopEntry, OpenMessage, SendReceive};
use bgp_packet::open::{AddPathEntry, Capability, OpenMessage, SendReceive};
use bgp_primitives::constants::AfiSafi;
use bgp_primitives::constants::{Afi, Safi};
use bgp_primitives::ip_prefix::Nlri;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
@@ -350,25 +349,16 @@ async fn test_every_route_goes_to_a_collector_told_apart_by_path_identifiers() -
b.announce(route(&[PEER_ASN + 1, 64510])).await?;
server.wait_for_routes(2).await?;
let mut capabilities = vec![Capability::FourByteAsn { asn: COLLECTOR_ASN }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
capabilities.push(Capability::AddPath {
entries: vec![AddPathEntry {
afi: AfiSafi::IPV4_UNICAST.afi,
safi: AfiSafi::IPV4_UNICAST.safi,
send_recv: SendReceive::Receive,
}],
});
let mut collector = TestPeer::connect(server.addr).await?;
collector
.handshake_with(
COLLECTOR_ASN,
Ipv4Addr::new(10, 0, 0, 100),
90,
capabilities,
collector_capabilities(COLLECTOR_ASN, &SUPPORTED, false),
)
.await?;
collector.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
collector.ctx.add_path = SUPPORTED.to_vec();
// Both routes, told apart.
let mut held = Held::new();
@@ -437,25 +427,16 @@ async fn test_a_large_table_reaches_a_collector_that_was_there_first() -> Result
c.connect_retry = Duration::from_secs(3600);
})
.await?;
let mut capabilities = vec![Capability::FourByteAsn { asn: COLLECTOR_ASN }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
capabilities.push(Capability::AddPath {
entries: vec![AddPathEntry {
afi: AfiSafi::IPV4_UNICAST.afi,
safi: AfiSafi::IPV4_UNICAST.safi,
send_recv: SendReceive::Receive,
}],
});
let mut collector = TestPeer::connect(server.addr).await?;
collector
.handshake_with(
COLLECTOR_ASN,
Ipv4Addr::new(10, 0, 0, 100),
90,
capabilities,
collector_capabilities(COLLECTOR_ASN, &SUPPORTED, false),
)
.await?;
collector.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
collector.ctx.add_path = SUPPORTED.to_vec();
server.wait_for_sessions(1).await?;
// Two feeders, each sending the same prefixes a hundred to an UPDATE.
@@ -518,35 +499,16 @@ async fn test_both_families_reach_a_collector_over_one_ipv6_session() -> Result<
c.connect_retry = Duration::from_secs(3600);
})
.await?;
let mut capabilities = vec![Capability::FourByteAsn { asn: COLLECTOR_ASN }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
capabilities.push(Capability::AddPath {
entries: [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST]
.into_iter()
.map(|family| AddPathEntry {
afi: family.afi,
safi: family.safi,
send_recv: SendReceive::Receive,
})
.collect(),
});
capabilities.push(Capability::ExtendedNextHop {
entries: vec![ExtendedNextHopEntry {
afi: Afi::IPV4,
safi: Safi::UNICAST,
nh_afi: Afi::IPV6,
}],
});
let mut collector = TestPeer::connect(server.addrs[1]).await?;
collector
.handshake_with(
COLLECTOR_ASN,
Ipv4Addr::new(10, 0, 0, 100),
90,
capabilities,
collector_capabilities(COLLECTOR_ASN, &SUPPORTED, true),
)
.await?;
collector.ctx.add_path = vec![AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST];
collector.ctx.add_path = SUPPORTED.to_vec();
server.wait_for_sessions(1).await?;
let session = &server.registry.list()[0];
assert!(session.extended_next_hop, "RFC 8950 agreed");
@@ -0,0 +1,92 @@
//! A peer that sends us a table while not reading what we send it must
//! not stop us reading it: two routers sending each other their tables at
//! once would otherwise each wait on the other for ever.
use std::net::Ipv4Addr;
use std::sync::Arc;
use std::time::Duration;
use bgp_integration_tests::announcement::Announcement;
use bgp_integration_tests::prefix;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_server::policy::ExportPolicy;
use eyre::Result;
const LOCAL_ASN: u32 = 64601;
const ROUTES: u32 = 100_000;
fn nth(n: u32) -> bgp_primitives::ip_prefix::IpPrefix {
let [a, b, c, _] = (0x0a00_0000u32 + n * 256).to_be_bytes();
prefix(&format!("{a}.{b}.{c}.0/24"))
}
#[tokio::test]
async fn a_peer_that_does_not_read_is_still_read() -> Result<()> {
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.export = Arc::new(ExportPolicy {
learned_routes: true,
all_paths: false,
..ExportPolicy::default()
});
})
.await?;
// One peer's table, for the server to pass on.
let mut first = server
.established(64701, Ipv4Addr::new(10, 0, 0, 1))
.await?;
// A route to an UPDATE, each with attributes of its own, as a real
// table has them: passing it on is then megabytes, not a few hundred
// UPDATEs that the socket buffers would take whole.
for i in 0..ROUTES {
first
.announce(
Announcement::of([nth(i).into()], &[64701, 64510])
.with_communities(&[((i >> 16) as u16 + 1, i as u16)]),
)
.await?;
}
server.wait_for_routes(ROUTES as usize).await?;
// A second peer, which the server starts sending that table to as
// soon as it is up, sends a table of its own and reads nothing back:
// the server's writes to it fill its socket and stop. The server has
// to go on reading it all the same. The peer's receive buffer is
// small, as a real link's window would be, so that this happens with
// the first few hundred kilobytes and not after the loopback's
// megabytes.
let socket = tokio::net::TcpSocket::new_v4()?;
socket.set_recv_buffer_size(16 * 1024)?;
let stream = socket.connect(server.addr).await?;
let mut second = TestPeer::from_stream(stream);
second
.handshake(64702, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
server.wait_for_sessions(2).await?;
// Sent over a couple of seconds, so that the server's export to the
// peer is well under way while the peer is still sending.
let sending = async {
for start in (0..ROUTES).step_by(100) {
second
.announce(Announcement::of(
(start..start + 100).map(|i| nth(i).into()),
&[64702, 64510],
))
.await?;
if start % 2000 == 0 {
tokio::time::sleep(Duration::from_millis(40)).await;
}
}
Ok::<_, eyre::Report>(())
};
tokio::time::timeout(Duration::from_secs(30), sending)
.await
.expect("the server reads the peer's table though the peer reads nothing")?;
tokio::time::timeout(
Duration::from_secs(30),
server.wait_for_routes(2 * ROUTES as usize),
)
.await
.expect("every route is in the table")?;
Ok(())
}
@@ -52,6 +52,27 @@ async fn test_status_and_peers() -> Result<()> {
peers[0]["address_families"],
serde_json::json!(["IPv4 unicast", "IPv6 unicast"])
);
// The session's TCP connection, by the address the peer list gives.
// What the kernel says of it depends on the platform; that it was
// asked, and why it had nothing to say if it had not, does not.
let peer_addr = peers[0]["peer_addr"].as_str().unwrap();
let tcp = get_json(&client, &format!("{base}/api/tcp?peer={peer_addr}")).await?;
let available = tcp["available"].as_bool().expect("available is a bool");
if available {
assert!(tcp["connection"].is_object(), "{tcp}");
} else {
assert!(tcp["reason"].is_string(), "{tcp}");
}
// The address alone names the peer too, as it does elsewhere.
let by_address = get_json(&client, &format!("{base}/api/tcp?peer=127.0.0.1")).await?;
assert_eq!(by_address["available"], tcp["available"]);
// A peer there is no session with is a bad request, not an answer.
let nobody = client
.get(format!("{base}/api/tcp?peer=nobody"))
.send()
.await?;
assert_eq!(nobody.status(), 400);
Ok(())
}
+15 -3
View File
@@ -18,7 +18,6 @@ use bgp_primitives::constants::{AfiSafi, IpVersion, Safi};
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_primitives::parser::{BgpParserError, EncodeError, ParserContext};
use crate::message::write_nlri;
use crate::wire::{
Parsed, list, malformed, parse_afi_safi, parse_asn, put_afi_safi, put_asn, put_length, records,
whole,
@@ -833,6 +832,19 @@ pub(crate) fn parse_nlri_list(
list(buf, |buf| parse_nlri(version, with_path_id, buf))
}
/// Writes prefixes as `parse_nlri_list` reads them: for each, its path
/// identifier if the session uses them for the family (RFC 7911 §3), its
/// length in bits, and the bytes that length covers.
pub fn put_nlri_list(nlri: &[Nlri], with_path_id: bool, out: &mut BytesMut) {
for n in nlri {
if with_path_id {
out.put_u32(n.path_id);
}
out.put_u8(n.prefix.length());
out.put(n.prefix.bytes());
}
}
/// The IP version of the prefixes that make up the NLRI of `family`.
///
/// Any family can be named in MP_REACH_NLRI and MP_UNREACH_NLRI, but only
@@ -897,7 +909,7 @@ impl MpReachNlriPathAttribute {
out.put_u8(self.next_hop.wire_len());
self.next_hop.to_wire(out);
out.put_u8(0);
write_nlri(&self.prefixes, ctx.has_add_path(self.family), out);
put_nlri_list(&self.prefixes, ctx.has_add_path(self.family), out);
}
}
@@ -919,6 +931,6 @@ impl MpUnreachNlriPathAttribute {
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
put_afi_safi(self.family, out);
write_nlri(&self.prefixes, ctx.has_add_path(self.family), out);
put_nlri_list(&self.prefixes, ctx.has_add_path(self.family), out);
}
}
+78 -39
View File
@@ -15,7 +15,7 @@ use crate::errors::DecodeError;
use crate::errors::MsgHeaderSubcode;
use crate::errors::OpenMsgSubcode;
use crate::open::OpenMessage;
use crate::wire::{fill_length, put_length};
use crate::wire::{body_has_path_id, fill_length, put_length};
/// A sentinel AS number used to denote that the actual AS number is provided as
/// a 4 byte value in the capabilities instead.
@@ -30,10 +30,15 @@ pub const BGP_HEADER_LEN: u16 = 19;
/// Maximum BGP message length per RFC 4271 §4.1.
pub const BGP_MAX_MSG_LEN: u16 = 4096;
const BGP_MARKER: [u8; 16] = [0xFF; 16];
/// The sixteen octets of ones that open every message (RFC 4271 §4.1).
/// Whoever frames a message by hand wants [`begin_message`] and
/// [`finish_message`] rather than this.
pub const BGP_MARKER: [u8; 16] = [0xFF; 16];
/// BGP message type codes (RFC 4271 §4.1, RFC 2918).
mod msg_type {
/// BGP message type codes (RFC 4271 §4.1, RFC 2918), for whoever frames
/// a message with [`begin_message`] or tells messages apart by their
/// header.
pub mod msg_type {
pub const OPEN: u8 = 1;
pub const UPDATE: u8 = 2;
pub const NOTIFICATION: u8 = 3;
@@ -162,23 +167,10 @@ impl Message {
/// cannot be encoded, `out` is left as it was.
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
let start = out.len();
out.put_slice(&BGP_MARKER);
// The length, filled in once the body is written. It counts the
// header too.
let length_at = out.len();
out.put_u16(0);
out.put_u8(self.type_code());
let written = self.body_to_wire(ctx, out).and_then(|()| {
let length = out.len() - start;
put_length(
out,
length_at,
2,
length,
BGP_MAX_MSG_LEN.into(),
"BGP message",
)
});
let length_at = begin_message(out, self.type_code());
let written = self
.body_to_wire(ctx, out)
.and_then(|()| finish_message(out, length_at));
if let Err(e) = written {
out.truncate(start);
return Err(e);
@@ -215,6 +207,35 @@ impl Message {
}
}
/// Writes the header of a message of type `kind` to the end of `out`,
/// with its length left at zero, and returns where the length goes. The
/// body follows, and then [`finish_message`] fills the length in: for
/// whoever builds a message out of pieces that are not a [`Message`], as
/// an exporter streaming a table does.
pub fn begin_message(out: &mut BytesMut, kind: u8) -> usize {
out.put_slice(&BGP_MARKER);
let length_at = out.len();
out.put_u16(0);
out.put_u8(kind);
length_at
}
/// Fills in the length of the message whose header [`begin_message`] put
/// at `length_at`, counting the header too, once the body is written. A
/// message longer than [`BGP_MAX_MSG_LEN`] cannot be sent, and is an
/// error; `out` is left for the caller to cut back.
pub fn finish_message(out: &mut BytesMut, length_at: usize) -> Result<(), EncodeError> {
let length = out.len() - length_at + BGP_MARKER.len();
put_length(
out,
length_at,
2,
length,
BGP_MAX_MSG_LEN.into(),
"BGP message",
)
}
/// Decodes the body of an OPEN, with the NOTIFICATION that reports a
/// failure (RFC 4271 §6.2).
fn decode_open(body: &[u8]) -> Result<OpenMessage, DecodeError> {
@@ -256,7 +277,10 @@ pub struct Decoded {
pub message: Message,
/// The errors in an UPDATE that were handled without resetting the
/// session (RFC 7606). Empty for a well-formed UPDATE and for every
/// other message.
/// other message. These are
/// [`DecodedUpdate::errors`](crate::update_decode::DecodedUpdate::errors),
/// named here for the one message that can have them, since this is
/// the result for any message.
pub update_errors: Vec<UpdateError>,
/// See [`crate::update_decode::DecodedUpdate::attributes_wire`].
pub attributes_wire: Option<Vec<u8>>,
@@ -332,14 +356,13 @@ impl UpdateMessage {
/// Writes the body of the UPDATE (what follows the 19-octet header).
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
// The prefixes in the body are IPv4 unicast (RFC 4271 §4.3).
let with_path_id = ctx.has_add_path(AfiSafi::IPV4_UNICAST);
let with_path_id = body_has_path_id(ctx);
// Each field's two-octet length is filled in once the field is
// written.
let withdrawn = out.len();
out.put_u16(0);
write_nlri(&self.withdrawn_routes, with_path_id, out);
put_nlri_list(&self.withdrawn_routes, with_path_id, out);
fill_length(out, withdrawn, 2, u16::MAX.into(), "withdrawn routes")?;
let attributes = out.len();
@@ -350,23 +373,14 @@ impl UpdateMessage {
fill_length(out, attributes, 2, u16::MAX.into(), "path attributes")?;
// The NLRI take up the rest of the message.
write_nlri(&self.announced_routes, with_path_id, out);
put_nlri_list(&self.announced_routes, with_path_id, out);
Ok(())
}
}
/// Writes prefixes as an UPDATE names them: for each, its path identifier
/// if the session uses them for the family (RFC 7911 §3), its length in
/// bits, and the bytes that length covers.
pub(crate) fn write_nlri(nlri: &[Nlri], with_path_id: bool, out: &mut BytesMut) {
for n in nlri {
if with_path_id {
out.put_u32(n.path_id);
}
out.put_u8(n.prefix.length());
out.put(n.prefix.bytes());
}
}
/// The name [`put_nlri_list`] went by before it moved next to the parser
/// it is the pair of; the exporter still uses it.
pub use crate::attributes::put_nlri_list as write_nlri;
/// A parser failure as text for a log line.
pub(crate) fn describe(e: &nom::Err<BgpParserError<&[u8]>>) -> String {
@@ -409,7 +423,7 @@ mod tests {
str::FromStr,
};
use bytes::BytesMut;
use bytes::{BufMut, BytesMut};
use eyre::{Result, eyre};
use crate::message::{
@@ -1133,6 +1147,31 @@ mod tests {
Ok(())
}
/// A message framed by hand, as the exporter frames an UPDATE it
/// builds from pieces, is the message `to_wire` writes; and one that
/// grows past the maximum is refused with its length filled in.
#[test]
fn a_message_framed_by_hand_is_the_one_to_wire_writes() {
use crate::message::{begin_message, finish_message, msg_type};
let ctx = default_v6_context();
let mut whole = BytesMut::new();
Message::Update(UpdateMessage::end_of_rib(AfiSafi::IPV4_UNICAST))
.to_wire(&ctx, &mut whole)
.unwrap();
let mut by_hand = BytesMut::new();
let length_at = begin_message(&mut by_hand, msg_type::UPDATE);
assert_eq!(length_at, 16);
by_hand.put_u32(0);
finish_message(&mut by_hand, length_at).unwrap();
assert_eq!(by_hand, whole);
let mut too_long = BytesMut::new();
let length_at = begin_message(&mut too_long, msg_type::UPDATE);
too_long.put_bytes(0, 4096);
assert!(finish_message(&mut too_long, length_at).is_err());
}
#[test]
fn test_message_rejects_bad_marker() {
let ctx = default_v6_context();
+13 -8
View File
@@ -19,7 +19,7 @@
//! be acted on, with treat-as-withdraw and attribute discard already carried
//! out, together with a report of what was wrong, for the log.
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::Nlri;
use bgp_primitives::parser::ParserContext;
@@ -29,6 +29,7 @@ use crate::message::{
MpUnreachNlriPathAttribute, NotAnAttribute, PathAttribute, RawAttribute, UpdateMessage,
defined_flags, describe, parse_nlri_list, split_attribute,
};
use crate::wire::body_has_path_id;
/// How an error in an UPDATE was handled, short of resetting the session.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -62,7 +63,9 @@ pub struct UpdateError {
pub struct DecodedUpdate {
/// The UPDATE after treat-as-withdraw and attribute discard.
pub update: UpdateMessage,
/// Empty when the UPDATE was well formed.
/// What was wrong, in the order met; empty when the UPDATE was well
/// formed. Where a message of any type was decoded this is
/// [`Decoded::update_errors`](crate::message::Decoded::update_errors).
pub errors: Vec<UpdateError>,
/// The attributes in `update`, as they were on the wire, for a table
/// that keeps attributes in that form and need not write them out
@@ -102,9 +105,7 @@ fn reset(subcode: UpdateMsgSubcode, data: Vec<u8>, detail: String) -> DecodeErro
/// error here cannot be recovered from: the prefixes are what
/// treat-as-withdraw would act on.
fn prefixes(ctx: &ParserContext, field: &str, buf: &[u8]) -> Result<Vec<Nlri>, DecodeError> {
// The prefixes in the body are IPv4 unicast (RFC 4271 §4.3).
let with_path_id = ctx.has_add_path(AfiSafi::IPV4_UNICAST);
match parse_nlri_list(IpVersion::Ipv4, with_path_id, buf) {
match parse_nlri_list(IpVersion::Ipv4, body_has_path_id(ctx), buf) {
Ok((_, prefixes)) => Ok(prefixes),
Err(e) => Err(reset(
UpdateMsgSubcode::InvalidNetworkField,
@@ -234,7 +235,10 @@ struct AttributeReader<'a> {
attributes: Vec<PathAttribute>,
errors: Vec<UpdateError>,
/// The bytes of the attributes that are kept and are a property of a
/// route: all but MP_REACH_NLRI and MP_UNREACH_NLRI.
/// route: all but MP_REACH_NLRI and MP_UNREACH_NLRI. Left empty on a
/// session with two-octet AS numbers, whose bytes no table keeps (see
/// [`DecodedUpdate::attributes_wire`]), so that nothing is copied or
/// reserved for them.
kept_wire: Vec<u8>,
/// The type codes met so far, one bit each.
seen: [u64; 4],
@@ -246,7 +250,7 @@ impl<'a> AttributeReader<'a> {
ctx,
attributes: Vec::new(),
errors: Vec::new(),
kept_wire: Vec::with_capacity(wire_len),
kept_wire: Vec::with_capacity(if ctx.four_octet_asn { wire_len } else { 0 }),
seen: [0; 4],
}
}
@@ -291,7 +295,7 @@ impl<'a> AttributeReader<'a> {
route_property,
} = self.judge(&raw)?
{
if route_property {
if route_property && self.ctx.four_octet_asn {
self.kept_wire.extend_from_slice(raw.wire);
}
self.attributes.push(attribute);
@@ -494,6 +498,7 @@ mod tests {
use std::net::Ipv4Addr;
use crate::message::PathAttributeFlags;
use bgp_primitives::constants::AfiSafi;
use bgp_primitives::ip_prefix::IpPrefix;
use super::*;
+10 -1
View File
@@ -9,7 +9,7 @@ use nom::IResult;
use nom::number::complete::{be_u8, be_u16, be_u32};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::parser::{BgpParserError, EncodeError};
use bgp_primitives::parser::{BgpParserError, EncodeError, ParserContext};
use crate::message::AS_TRANS;
@@ -91,6 +91,15 @@ pub(crate) fn put_asn(four_octet: bool, asn: u32, out: &mut BytesMut) {
}
}
/// Whether the prefixes in the body of an UPDATE carry path identifiers
/// on a session with `ctx`. The body holds IPv4 unicast routes and nothing
/// else (RFC 4271 §4.3), so that is the family whose ADD-PATH agreement
/// decides it; every other family's prefixes are in MP_REACH_NLRI and
/// MP_UNREACH_NLRI, which name their family.
pub(crate) fn body_has_path_id(ctx: &ParserContext) -> bool {
ctx.has_add_path(AfiSafi::IPV4_UNICAST)
}
/// Parses an address family as the multiprotocol attributes and most
/// capabilities name it: a two-octet AFI and a one-octet SAFI.
pub(crate) fn parse_afi_safi(buf: &[u8]) -> Parsed<'_, AfiSafi> {
+112 -34
View File
@@ -13,7 +13,7 @@
use std::hash::BuildHasher;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use bgp_packet::message::{NlriNextHop, PathAttribute, attr_type, split_attribute};
use bgp_packet::message::{NlriNextHop, PathAttribute, RawAttribute, attr_type, split_attribute};
use bgp_primitives::parser::{EncodeError, ParserContext};
use bytes::{BufMut, BytesMut};
use foldhash::fast::RandomState;
@@ -21,6 +21,10 @@ use hashbrown::HashTable;
const OPTIONAL: u8 = 0x80;
/// The type of an AS_PATH segment whose AS numbers are in order; the other
/// type, AS_SET, is an unordered handful (RFC 4271 §4.3).
const AS_SEQUENCE: u8 = 2;
/// What route selection reads from a set of attributes (RFC 4271 §9.1.2.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectionKey {
@@ -116,7 +120,6 @@ impl SelectionKey {
/// Reads an AS_PATH value with four-octet AS numbers.
fn scan_as_path(mut segments: &[u8]) -> Option<PathScan> {
const AS_SEQUENCE: u8 = 2;
let mut hops = 0usize;
let (mut first, mut last) = (0, 0);
let mut at_start = true;
@@ -150,7 +153,6 @@ fn scan_as_path(mut segments: &[u8]) -> Option<PathScan> {
/// AS numbers to `out`, and returns whether any segment is not a sequence.
/// Stops at what is not a segment.
fn read_as_path(mut segments: &[u8], out: &mut Vec<u32>) -> bool {
const AS_SEQUENCE: u8 = 2;
let mut has_set = false;
while let [kind, count, rest @ ..] = segments {
let Some((asns, rest)) = rest.split_at_checked(usize::from(*count) * 4) else {
@@ -187,43 +189,58 @@ impl AttrSet {
&self.wire
}
/// The attributes one by one, as they lie in the stored bytes. The
/// bytes were checked when the set was made, so this is all of them;
/// it stops at the first that does not split as an attribute all the
/// same, rather than go on from the middle of one.
fn raw_attributes(&self) -> impl Iterator<Item = RawAttribute<'_>> {
let mut wire = &*self.wire;
std::iter::from_fn(move || {
let (attribute, rest) = split_attribute(wire).ok()?;
wire = rest;
Some(attribute)
})
}
/// The value of the attribute of type `type_code`, if the set has one.
fn attribute(&self, type_code: u8) -> Option<&[u8]> {
self.raw_attributes()
.find(|attribute| attribute.type_code == type_code)
.map(|attribute| attribute.value)
}
/// The route's next hop: the one that came in MP_REACH_NLRI, or else
/// the NEXT_HOP attribute. Read off the stored bytes without building
/// anything else, for those who ask it of every route.
pub fn next_hop(&self) -> Option<NlriNextHop> {
let mut plain = None;
let mut wire = &*self.wire;
while let Ok((attribute, rest)) = split_attribute(wire) {
for attribute in self.raw_attributes() {
match attribute.type_code {
attr_type::MP_REACH_NLRI => {
// Kept as the next hop's length and the next hop.
let (length, address) = attribute.value.split_first()?;
return NlriNextHop::from_bytes(address)
.filter(|_| usize::from(*length) == address.len());
}
attr_type::MP_REACH_NLRI => return decode_next_hop(attribute.value),
attr_type::NEXT_HOP => plain = NlriNextHop::from_bytes(attribute.value),
_ => {}
}
wire = rest;
}
plain
}
/// Whether the route carries the OTC attribute (RFC 9234): it came
/// down from a provider or across from a peer, and is to go on down
/// only. Read off the stored bytes without building anything else,
/// for those who ask it of every route: a session to a peer or a
/// provider asks it of most of the table, to pass over them.
pub fn is_only_to_customer(&self) -> bool {
self.attribute(attr_type::ONLY_TO_CUSTOMER).is_some()
}
/// Whether the route carries the BLACKHOLE community (RFC 7999), which
/// asks for the packets to its prefix to be dropped. Read off the
/// stored bytes without building anything else, for those who ask it
/// of every route.
pub fn is_blackhole(&self) -> bool {
const BLACKHOLE: [u8; 4] = [0xFF, 0xFF, 0x02, 0x9A];
let mut wire = &*self.wire;
while let Ok((attribute, rest)) = split_attribute(wire) {
if attribute.type_code == attr_type::COMMUNITIES {
let (communities, _) = attribute.value.as_chunks::<4>();
return communities.contains(&BLACKHOLE);
}
wire = rest;
}
false
self.attribute(attr_type::COMMUNITIES)
.is_some_and(|communities| communities.as_chunks::<4>().0.contains(&BLACKHOLE))
}
/// Puts the AS numbers of the route's path in `out`, in place of what
@@ -237,14 +254,10 @@ impl AttrSet {
/// else, for those who ask it of every route.
pub fn as_path(&self, out: &mut Vec<u32>) -> bool {
out.clear();
let mut wire = &*self.wire;
while let Ok((attribute, rest)) = split_attribute(wire) {
if attribute.type_code == attr_type::AS_PATH {
return read_as_path(attribute.value, out);
}
wire = rest;
match self.attribute(attr_type::AS_PATH) {
Some(segments) => read_as_path(segments, out),
None => false,
}
false
}
/// The attributes, parsed, and the next hop if it came in
@@ -291,7 +304,8 @@ fn encode(
Ok(())
}
/// Writes a next hop as an MP_REACH_NLRI that holds nothing else.
/// Writes a next hop as an MP_REACH_NLRI that holds nothing else: the
/// next hop's length, and the next hop.
fn encode_next_hop(next_hop: &NlriNextHop, out: &mut BytesMut) {
out.put_u8(OPTIONAL);
out.put_u8(attr_type::MP_REACH_NLRI);
@@ -300,6 +314,13 @@ fn encode_next_hop(next_hop: &NlriNextHop, out: &mut BytesMut) {
next_hop.to_wire(out);
}
/// Reads the next hop out of the value of an MP_REACH_NLRI as
/// [`encode_next_hop`] writes it. `None` if the value is not one.
fn decode_next_hop(value: &[u8]) -> Option<NlriNextHop> {
let (length, address) = value.split_first()?;
NlriNextHop::from_bytes(address).filter(|_| usize::from(*length) == address.len())
}
/// Parses path attributes in the form [`AttrSet`] keeps and table dumps
/// use (RFC 6396 §4.3.4): four-octet AS numbers, and an MP_REACH_NLRI that
/// holds only a next hop. `None` if they are malformed.
@@ -310,11 +331,7 @@ fn parse(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
while !wire.is_empty() {
let (raw, rest) = split_attribute(wire).ok()?;
if raw.type_code == attr_type::MP_REACH_NLRI {
let (length, address) = raw.value.split_first()?;
if usize::from(*length) != address.len() {
return None;
}
next_hop = Some(NlriNextHop::from_bytes(address)?);
next_hop = Some(decode_next_hop(raw.value)?);
} else {
let (_, attribute) = PathAttribute::from_wire(&ctx, raw.wire).ok()?;
attributes.push(attribute);
@@ -634,6 +651,67 @@ mod tests {
);
}
/// The OTC attribute is read off the stored bytes, wherever in them it
/// lies.
#[test]
fn otc_is_told_from_the_bytes() {
let store = AttrStore::new();
let without = store.intern(&attributes(vec![3333]), None).unwrap();
assert!(!without.is_only_to_customer());
let mut with = attributes(vec![3333]);
with.insert(1, PathAttribute::OnlyToCustomer(3333));
assert!(store.intern(&with, None).unwrap().is_only_to_customer());
// After COMMUNITIES, which is the last of the usual attributes.
let mut after = attributes(vec![3333]);
after.push(PathAttribute::OnlyToCustomer(3333));
assert!(store.intern(&after, None).unwrap().is_only_to_customer());
}
/// What a session received, with attributes of our own added after it,
/// is one set with the parsed form of the whole; the bytes are not
/// parsed to find that out, so the bytes must say the same thing.
#[test]
fn received_attributes_with_ours_added_are_the_same_set_as_the_parsed_form() {
let store = AttrStore::new();
let ctx = four_octet();
let received = attributes(vec![65001, 65002]);
let mut wire = BytesMut::new();
encode(&received, None, &mut wire).unwrap();
let ours = [
PathAttribute::OnlyToCustomer(65001),
PathAttribute::LocalPref(LocalPrefPathAttribute(150)),
];
let mut added = BytesMut::new();
for attribute in &ours {
attribute.to_wire(&ctx, &mut added).unwrap();
}
let next_hop = NlriNextHop::Ipv6("2001:db8::1".parse().unwrap());
let mut whole = received.clone();
whole.extend(ours.iter().cloned());
let parsed = store.intern(&whole, Some(&next_hop)).unwrap();
let from_bytes = store
.intern_received_with(&wire, &added, Some(&next_hop))
.unwrap();
assert!(Arc::ptr_eq(&parsed, &from_bytes));
assert!(from_bytes.is_only_to_customer());
assert_eq!(from_bytes.next_hop(), Some(next_hop));
// Without a next hop too.
let parsed = store.intern(&whole, None).unwrap();
let from_bytes = store.intern_received_with(&wire, &added, None).unwrap();
assert!(Arc::ptr_eq(&parsed, &from_bytes));
// An attribute of ours of a type the peer sent is not a set.
let mut repeated = BytesMut::new();
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(1))
.to_wire(&ctx, &mut repeated)
.unwrap();
assert!(store.intern_received_with(&wire, &repeated, None).is_none());
}
/// Two of the same attribute are not a set: its readers could each
/// take a different one.
#[test]
+284 -206
View File
@@ -95,7 +95,7 @@ struct Entry {
}
/// One journal of one shard.
struct Journal {
pub(super) struct Journal {
kind: Kind,
/// The sequence number of the latest change. Counts every change this
/// journal is for, whether or not anyone was reading.
@@ -171,25 +171,35 @@ impl Journal {
}
}
/// Forgets that `entry`'s prefix had gone, if `entry` is what said so.
fn drop_entry(gone: &mut HashMap<IpPrefix, u64, RandomState>, entry: &Entry) {
/// Voids the note that `entry`'s prefix had gone, if `entry` is what
/// said so: an entry that is dropped must not leave the note behind,
/// or the prefix would be given out as gone on the strength of a later
/// entry that says something else.
fn void_gone_note(gone: &mut HashMap<IpPrefix, u64, RandomState>, entry: &Entry) {
if gone.get(&entry.prefix) == Some(&entry.seq) {
gone.remove(&entry.prefix);
}
}
/// What `entry` says, if it still says something: it is the latest
/// change of its prefix. The row, or `None` for a prefix that has gone.
fn live<'a>(
&self,
entry: &Entry,
rows: &'a HashTable<Row>,
hasher: &RandomState,
) -> Option<Option<&'a Row>> {
/// What `entry` says: something only if it is the latest change of its
/// prefix.
fn says<'a>(&self, entry: &Entry, rows: &'a HashTable<Row>, hasher: &RandomState) -> Says<'a> {
let hash = hasher.hash_one(entry.prefix);
match rows.find(hash, |row| row.prefix == entry.prefix) {
Some(row) => (row.seqs.of(self.kind) == entry.seq).then_some(Some(row)),
None => (self.gone.get(&entry.prefix) == Some(&entry.seq)).then_some(None),
Some(row) if *row.seqs.of(self.kind) == entry.seq => Says::Row(row),
None if self.gone.get(&entry.prefix) == Some(&entry.seq) => Says::Gone,
_ => Says::Nothing,
}
}
/// Where a reader whose cursor is `cursor` is to read from.
fn place_of(&self, cursor: u64) -> Place {
if cursor < self.floor {
Place::Lagged
} else if cursor == self.seq {
Place::Nothing
} else {
Place::From(self.entries.partition_point(|entry| entry.seq <= cursor))
}
}
@@ -199,7 +209,26 @@ impl Journal {
if self.entries.len() <= self.limits.tidy_at {
return;
}
// What every reader has passed can go.
self.drop_passed();
// Entries for prefixes that have a row are at most as many as the
// rows once the dead ones are out. Entries for prefixes that have
// gone are not counted in: they are what a reader that has stopped
// reading makes pile up. Twice that, so that the pass over every
// entry that finding the dead ones takes is made only once the
// journal has doubled, and the cost per change stays small.
let limit = 2 * (rows.len() + self.limits.slack);
if self.entries.len() <= limit {
return;
}
self.drop_dead(rows, hasher);
if self.entries.len() <= limit {
return;
}
self.drop_oldest_half();
}
/// Drops what every reader has passed.
fn drop_passed(&mut self) {
let passed = self
.cursors
.iter()
@@ -210,40 +239,57 @@ impl Journal {
if front.seq > passed {
break;
}
Self::drop_entry(&mut self.gone, front);
Self::void_gone_note(&mut self.gone, front);
self.entries.pop_front();
}
}
// Entries for prefixes that have a row are at most as many as the
// rows once the dead ones are out. Entries for prefixes that have
// gone are not counted in: they are what a reader that has stopped
// reading makes pile up.
let limit = 2 * (rows.len() + self.limits.slack);
if self.entries.len() <= limit {
return;
}
// Dead entries: their prefix has changed again since. This looks
// every entry up, but only once the journal has doubled, so the
// cost per change stays small.
/// Drops the dead entries: those whose prefix has changed again since,
/// so that a later entry says what they said. This looks every entry
/// up.
fn drop_dead(&mut self, rows: &HashTable<Row>, hasher: &RandomState) {
let mut entries = std::mem::take(&mut self.entries);
entries.retain(|entry| self.live(entry, rows, hasher).is_some());
entries.retain(|entry| !matches!(self.says(entry, rows, hasher), Says::Nothing));
self.entries = entries;
}
if self.entries.len() <= limit {
return;
}
// Still too long: a reader is not reading while prefixes come and
// go, or more prefixes went at once than are left. The older half
// goes, and a reader that had not read it loses its place. Half,
// so that readers that are only a little behind keep theirs, and
// so that getting a long journal down to size takes few passes.
/// Drops the older half of the entries without regard to the readers,
/// for a journal that is too long even without its dead entries: a
/// reader is not reading while prefixes come and go, or more prefixes
/// went at once than are left. A reader that had not read what goes
/// loses its place. Half, so that readers that are only a little
/// behind keep theirs, and so that getting a long journal down to size
/// takes few passes.
fn drop_oldest_half(&mut self) {
for entry in self.entries.drain(..self.entries.len() / 2) {
Self::drop_entry(&mut self.gone, &entry);
Self::void_gone_note(&mut self.gone, &entry);
self.floor = entry.seq;
}
}
}
/// What an entry says; see [`Journal::says`].
enum Says<'a> {
/// The prefix's row, whose latest change the entry is.
Row(&'a Row),
/// The prefix has no route any more, and the entry is what says so.
Gone,
/// Nothing: the prefix has changed again since, and a later entry has
/// it.
Nothing,
}
/// Where a reader is to read a journal from; see [`Journal::place_of`].
enum Place {
/// Entries the reader had not read were dropped; it has lost its
/// place.
Lagged,
/// The reader has read everything up to the latest change.
Nothing,
/// The index of the first entry the reader has not read.
From(usize),
}
/// Which of a shard's two journals.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Kind {
@@ -253,29 +299,36 @@ pub(super) enum Kind {
All,
}
/// The sequence numbers a row remembers, one for each journal: of the
/// latest change of its best route, and of any of its routes.
#[derive(Debug, Clone, Copy)]
pub(super) struct Seqs {
best: u64,
all: u64,
/// One of something for each [`Kind`] of journal.
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct ByKind<T> {
best: T,
all: T,
}
impl Seqs {
fn of(self, kind: Kind) -> u64 {
impl<T> ByKind<T> {
pub(super) fn of(&self, kind: Kind) -> &T {
match kind {
Kind::Best => self.best,
Kind::All => self.all,
Kind::Best => &self.best,
Kind::All => &self.all,
}
}
pub(super) fn of_mut(&mut self, kind: Kind) -> &mut T {
match kind {
Kind::Best => &mut self.best,
Kind::All => &mut self.all,
}
}
}
/// The sequence numbers a row remembers, one for each journal: of the
/// latest change of its best route, and of any of its routes.
pub(super) type Seqs = ByKind<u64>;
/// The two journals of a shard, written together: every change to a row is
/// a change of one of its routes, and some are a change of its best route.
pub(super) struct Journals {
best: Journal,
all: Journal,
}
pub(super) type Journals = ByKind<Journal>;
impl Journals {
pub(super) fn new(limits: Limits) -> Self {
@@ -285,18 +338,13 @@ impl Journals {
}
}
fn of(&self, kind: Kind) -> &Journal {
match kind {
Kind::Best => &self.best,
Kind::All => &self.all,
}
}
fn of_mut(&mut self, kind: Kind) -> &mut Journal {
match kind {
Kind::Best => &mut self.best,
Kind::All => &mut self.all,
}
/// Copies the journals' sequence numbers to `latest`, which the table
/// keeps outside the shard's lock for readers to look at
/// ([`RibTable::latest_seq`]). Called with the lock held, after every
/// change to the shard.
pub(super) fn publish(&self, latest: &ByKind<AtomicU64>) {
latest.best.store(self.best.seq, Ordering::Release);
latest.all.store(self.all.seq, Ordering::Release);
}
/// The time by the table's clock that the changes made from now on
@@ -362,18 +410,11 @@ pub(super) struct Mark(u64, u64);
pub struct Routes<'a>(&'a Row);
impl<'a> Routes<'a> {
/// The routes: at least one, since a prefix with none has no row, and
/// is given out as [`Event::Withdrawn`] instead.
pub fn iter(&self) -> impl Iterator<Item = RouteRef<'a>> + use<'a> {
self.0.routes()
}
/// How many there are: at least one.
pub fn len(&self) -> usize {
self.0.paths.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl std::fmt::Debug for Routes<'_> {
@@ -424,19 +465,15 @@ pub struct Progress {
}
enum Phase {
/// Giving out the whole table; `next` is the shard to do next, and
/// `announced` whether [`Event::Reset`] has been given. `upto` is the
/// time on the table's clock the table is given out as of: the shards
/// are gone through one at a time while the table changes, and the
/// place taken in each shard's journal is as of that one time, so that
/// what follows is consistent from shard to shard. A change after it
/// that a later shard's rows already show is read again from the
/// Giving out the whole table; `next` is the shard to do next. `upto`
/// is the time on the table's clock the table is given out as of, and
/// is taken when [`Event::Reset`] is given, so `None` until then: the
/// shards are gone through one at a time while the table changes, and
/// the place taken in each shard's journal is as of that one time, so
/// that what follows is consistent from shard to shard. A change after
/// it that a later shard's rows already show is read again from the
/// journal; that is harmless.
Resync {
next: usize,
announced: bool,
upto: u64,
},
Resync { next: usize, upto: Option<u64> },
/// Following the journals; `next` is the shard to read next, and
/// `upto` the time by the table's clock up to which this pass reads
/// (see [`Entry::stamp`]).
@@ -447,9 +484,11 @@ impl Phase {
/// The beginning of a resynchronisation.
const RESYNC: Self = Self::Resync {
next: 0,
announced: false,
upto: 0,
upto: None,
};
/// The beginning of a pass over the journals.
const FOLLOW: Self = Self::Follow { next: 0, upto: 0 };
}
/// Follows the routes of a table: the best of each prefix, or all of them.
@@ -466,7 +505,10 @@ pub struct Reader {
/// is for the shard's benefit.
cursors: Vec<u64>,
phase: Phase,
/// The table's version when the reader was last found caught up.
/// The table's version as it was when the current pass over the
/// journals began. [`Reader::changed`] waits while the version is still
/// this one: whatever was written since the pass began has moved it
/// on, and is there to be read.
seen: u64,
caught_up: bool,
}
@@ -482,7 +524,7 @@ impl RibTable {
/// those who want the news and not the table. If it falls too far
/// behind it is reset like any other, and is then given the table.
pub fn reader_from_now(self: &Arc<Self>) -> Reader {
let mut reader = self.register(Kind::Best, Phase::Follow { next: 0, upto: 0 });
let mut reader = self.register(Kind::Best, Phase::FOLLOW);
for (index, shard) in self.shards.iter().enumerate() {
let shard = read(shard);
reader.set_cursor(index, &shard, shard.journals.best.seq);
@@ -598,107 +640,128 @@ impl Reader {
pub fn poll(&mut self, budget: usize, mut visit: impl FnMut(Event<'_>)) -> Progress {
// A call that may give nothing out would never get anywhere.
let budget = budget.max(1);
let shards = self.table.shards.len();
// Held once for the call: a count taken on the table for every
// shard of every pass, by every reader, was the most of what a
// router with many peers did.
let table = Arc::clone(&self.table);
let mut events = 0;
loop {
match self.phase {
Phase::Resync {
next,
announced,
upto,
} => {
let upto = if announced {
upto
} else {
self.forget_place();
visit(Event::Reset);
events += 1;
self.table.settled()
};
if next == shards {
visit(Event::Synced);
events += 1;
self.phase = Phase::Follow { next: 0, upto: 0 };
continue;
}
events += self.scan(next, upto, &mut visit);
self.phase = Phase::Resync {
next: next + 1,
announced: true,
upto,
};
if events >= budget {
return self.progress(events, false);
}
let done = match self.phase {
Phase::Resync { next, upto } => {
self.resync_step(&table, next, upto, budget, &mut events, &mut visit)
}
Phase::Follow { next, upto } => {
// Read before the pass: a change made during it shows as
// a newer version, and `changed` then returns at once.
// And the pass reads up to one time in every shard
// alike, chosen so that about the budget is read.
let (version, upto) = if next == 0 {
(
self.table.version.load(Ordering::Acquire),
self.time_for(budget - events),
)
} else {
(self.seen, upto)
};
self.seen = version;
let mut shard = next;
let mut lagged = false;
let mut consumed_any = false;
let mut more_somewhere = false;
while shard < shards {
if events >= budget {
self.phase = Phase::Follow { next: shard, upto };
return self.progress(events, false);
}
match self.drain(shard, upto, &mut visit) {
Drained::Lagged => {
lagged = true;
break;
}
Drained::Read {
events: read,
consumed,
more,
} => {
events += read;
consumed_any |= consumed;
more_somewhere |= more;
}
}
shard += 1;
}
if lagged {
self.phase = Phase::RESYNC;
continue;
}
self.phase = Phase::Follow { next: 0, upto: 0 };
if next == 0 && !consumed_any && more_somewhere {
// A whole pass got nowhere, though there is more:
// it is all beyond the earliest change still under
// way, which no pass can read past until it ends.
// Asking again at once would spin until then. The
// change wakes the readers when it ends.
return self.progress(events, true);
}
if next != 0 || more_somewhere {
// This pass took up where an earlier call stopped,
// so the shards before that point have not been
// looked at in this call; or some shard had more
// than its share, or changes later than this
// pass's time. Go round again from the first.
if events >= budget {
return self.progress(events, false);
}
continue;
}
return self.progress(events, true);
self.follow_pass(&table, next, upto, budget, &mut events, &mut visit)
}
};
if let Some(progress) = done {
return progress;
}
}
}
/// One step of a resynchronisation: the shard `next`, or the end of it
/// once every shard has been given out. Returns what to tell the
/// caller if the budget is spent, and `None` to go on.
fn resync_step(
&mut self,
table: &RibTable,
next: usize,
upto: Option<u64>,
budget: usize,
events: &mut usize,
visit: &mut impl FnMut(Event<'_>),
) -> Option<Progress> {
let upto = upto.unwrap_or_else(|| {
self.forget_place();
visit(Event::Reset);
*events += 1;
table.settled()
});
if next == table.shards.len() {
visit(Event::Synced);
*events += 1;
self.phase = Phase::FOLLOW;
return None;
}
*events += self.scan(table, next, upto, visit);
self.phase = Phase::Resync {
next: next + 1,
upto: Some(upto),
};
(*events >= budget).then(|| self.progress(*events, false))
}
/// A pass over the journals from shard `next` on, or the rest of one
/// that an earlier call cut short. Returns what to tell the caller if
/// the pass is to be the call's last, and `None` to go round again.
fn follow_pass(
&mut self,
table: &RibTable,
next: usize,
upto: u64,
budget: usize,
events: &mut usize,
visit: &mut impl FnMut(Event<'_>),
) -> Option<Progress> {
// Read before the pass: a change made during it shows as a newer
// version, and `changed` then returns at once. And the pass reads
// up to one time in every shard alike, chosen so that about the
// budget is read.
let (version, upto) = if next == 0 {
(
table.version.load(Ordering::Acquire),
self.time_for(budget - *events),
)
} else {
(self.seen, upto)
};
self.seen = version;
let mut consumed_any = false;
let mut more_somewhere = false;
for shard in next..table.shards.len() {
if *events >= budget {
self.phase = Phase::Follow { next: shard, upto };
return Some(self.progress(*events, false));
}
// Nothing new in this shard, by the number that is read
// without its lock: it is left alone, and so are the sessions
// writing to it.
if self.cursors[shard] == table.latest_seq(shard, self.kind) {
continue;
}
match self.drain(table, shard, upto, visit) {
Drained::Lagged => {
self.phase = Phase::RESYNC;
return None;
}
Drained::Read {
events: read,
consumed,
more,
} => {
*events += read;
consumed_any |= consumed;
more_somewhere |= more;
}
}
}
self.phase = Phase::FOLLOW;
if next == 0 && !consumed_any && more_somewhere {
// A whole pass got nowhere, though there is more: it is all
// beyond the earliest change still under way, which no pass
// can read past until it ends. Asking again at once would spin
// until then. The change wakes the readers when it ends.
return Some(self.progress(*events, true));
}
if next != 0 || more_somewhere {
// This pass took up where an earlier call stopped, so the
// shards before that point have not been looked at in this
// call; or some shard had more than its share, or changes
// later than this pass's time. Go round again from the first.
return (*events >= budget).then(|| self.progress(*events, false));
}
Some(self.progress(*events, true))
}
fn progress(&mut self, events: usize, caught_up: bool) -> Progress {
@@ -706,6 +769,11 @@ impl Reader {
Progress { events, caught_up }
}
/// Whether the last [`Reader::poll`] read everything there was.
pub fn caught_up(&self) -> bool {
self.caught_up
}
/// Waits until there may be something to read. Returns at once if the
/// last [`Reader::poll`] did not catch up.
pub async fn changed(&mut self) {
@@ -760,8 +828,13 @@ impl Reader {
/// as of `upto` on the table's clock, in one hold of the shard: what
/// changed after that time is in the journal after that place, whether
/// or not the rows given out show it already.
fn scan(&mut self, index: usize, upto: u64, visit: &mut impl FnMut(Event<'_>)) -> usize {
let table = Arc::clone(&self.table);
fn scan(
&mut self,
table: &RibTable,
index: usize,
upto: u64,
visit: &mut impl FnMut(Event<'_>),
) -> usize {
let shard = read(&table.shards[index]);
for row in &shard.rows {
visit(self.event_for(row));
@@ -797,13 +870,15 @@ impl Reader {
// many changes were made with it.
let mut earliest = None;
for (index, shard) in self.table.shards.iter().enumerate() {
let shard = read(shard);
let journal = shard.journals.of(self.kind);
let cursor = self.cursors[index];
if cursor < journal.floor || cursor == journal.seq {
if cursor == self.table.latest_seq(index, self.kind) {
continue;
}
let start = journal.entries.partition_point(|entry| entry.seq <= cursor);
let shard = read(shard);
let journal = shard.journals.of(self.kind);
let Place::From(start) = journal.place_of(cursor) else {
continue;
};
if let Some(next) = journal.entries.get(start) {
earliest = Some(earliest.map_or(next.stamp, |e: u64| e.min(next.stamp)));
}
@@ -817,23 +892,27 @@ impl Reader {
/// Reads a shard's journal from this reader's place, no further than
/// the changes made by `upto` on the table's clock.
fn drain(&mut self, index: usize, upto: u64, visit: &mut impl FnMut(Event<'_>)) -> Drained {
let table = Arc::clone(&self.table);
fn drain(
&mut self,
table: &RibTable,
index: usize,
upto: u64,
visit: &mut impl FnMut(Event<'_>),
) -> Drained {
let shard = read(&table.shards[index]);
let journal = shard.journals.of(self.kind);
let mut cursor = self.cursors[index];
if cursor < journal.floor {
return Drained::Lagged;
}
if cursor == journal.seq {
return Drained::Read {
events: 0,
consumed: false,
more: false,
};
}
let start = journal.entries.partition_point(|entry| entry.seq <= cursor);
let start = match journal.place_of(cursor) {
Place::Lagged => return Drained::Lagged,
Place::Nothing => {
return Drained::Read {
events: 0,
consumed: false,
more: false,
};
}
Place::From(start) => start,
};
let mut events = 0;
let mut more = false;
for entry in journal.entries.range(start..) {
@@ -842,13 +921,12 @@ impl Reader {
break;
}
cursor = entry.seq;
let event = match journal.live(entry, &shard.rows, &shard.hasher) {
Some(Some(row)) => self.event_for(row),
Some(None) => Event::Withdrawn {
let event = match journal.says(entry, &shard.rows, &shard.hasher) {
Says::Row(row) => self.event_for(row),
Says::Gone => Event::Withdrawn {
prefix: entry.prefix,
},
// The prefix has changed again; a later entry has it.
None => continue,
Says::Nothing => continue,
};
visit(event);
events += 1;
+106 -5
View File
@@ -8,7 +8,7 @@ use proptest::prelude::*;
use super::super::tests::{Fixture, Route, external, internal, prefix, route};
use super::super::{AttrSet, PeerIndex, PeerInfo, RibTable};
use super::{Event, Limits, Reader};
use super::{Event, Kind, Limits, Reader, read};
/// What a follower would keep: its own copy of the best routes, brought
/// up to date from the events it is given.
@@ -53,7 +53,10 @@ impl Mirror {
}
Event::Routes { prefix, routes } => {
self.routes_events += 1;
assert!(!routes.is_empty(), "a prefix with no routes is withdrawn");
assert!(
routes.iter().next().is_some(),
"a prefix with no routes is withdrawn"
);
self.routes.insert(
prefix,
sorted(
@@ -115,9 +118,6 @@ impl Mirror {
);
}
}
}
impl Mirror {
/// For the mirror of a follower of all routes: it holds every route in
/// the table, and knows which is best.
#[track_caller]
@@ -213,6 +213,107 @@ fn a_reader_from_now_is_given_the_changes_and_not_the_table() {
assert_eq!((best, withdrawn), (vec![new], vec![old]));
}
/// The number a reader checks without a shard's lock must be the journal's
/// own after every kind of change, whether or not anyone is reading: a
/// reader skips a shard on the strength of it, and a shard with news that
/// said it had none would never be read until the next change.
#[test]
fn the_lock_free_number_is_the_journals_after_every_kind_of_change() {
let mut fixture = Fixture::new(&[external(1), external(2)]);
let prefixes: Vec<IpPrefix> = (0..16).map(nth_prefix).collect();
let agree = |table: &RibTable| {
for (index, shard) in table.shards.iter().enumerate() {
let shard = read(shard);
for kind in [Kind::Best, Kind::All] {
assert_eq!(
table.latest_seq(index, kind),
shard.journals.of(kind).seq,
"shard {index}, {kind:?}"
);
}
}
};
// Without a reader, with one of each kind, and with both.
for readers in 0..4 {
let _best = (readers & 1 != 0).then(|| fixture.table.reader());
let _all = (readers & 2 != 0).then(|| fixture.table.reader_of_all_routes());
agree(&fixture.table);
for p in &prefixes {
fixture.announce(0, p, &Route::via(&[1]));
}
agree(&fixture.table);
// The same prefixes again, with other attributes.
for p in &prefixes {
fixture.announce(0, p, &Route::via(&[1, 2]));
}
agree(&fixture.table);
// A second route for each, then the last route of one taken away.
for p in &prefixes {
fixture.announce(1, p, &Route::via(&[2, 3]));
}
agree(&fixture.table);
fixture.withdraw(0, &prefixes[0]);
agree(&fixture.table);
fixture.withdraw(1, &prefixes[0]);
agree(&fixture.table);
fixture.table.remove_peer(fixture.peers[1]);
agree(&fixture.table);
fixture.peers[1] = fixture.table.add_peer(external(2)).unwrap();
fixture
.table
.mark_stale(fixture.peers[0], &[IpVersion::Ipv4]);
agree(&fixture.table);
fixture
.table
.sweep_stale(fixture.peers[0], &[IpVersion::Ipv4]);
agree(&fixture.table);
assert!(
fixture.table.is_empty(),
"round {readers} left routes behind"
);
}
}
/// A shard that had nothing new for a reader is passed over without its
/// lock. The next change to it must bring the reader back.
#[test]
fn a_shard_passed_over_is_read_again_once_it_has_news() {
let fixture = Fixture::on(RibTable::new(1), &[external(1)]);
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
mirror.catch_up(&mut reader, 100);
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
fixture.announce(0, &prefix("10.0.0.0/8"), &Route::via(&[1]));
let mut events = Vec::new();
let progress = reader.poll(100, |event| events.push(format!("{event:?}")));
assert!(progress.caught_up);
assert_eq!(events.len(), 1, "{events:?}");
assert!(events[0].starts_with("Best"), "{events:?}");
}
/// As [`a_shard_passed_over_is_read_again_once_it_has_news`], for a reader
/// that begins where the journals are rather than with the table.
#[test]
fn a_shard_passed_over_by_a_reader_from_now_is_read_again_once_it_has_news() {
let fixture = Fixture::on(RibTable::new(1), &[external(1)]);
fixture.announce(0, &prefix("192.0.2.0/24"), &Route::via(&[1]));
let mut reader = fixture.table.reader_from_now();
assert!(reader.poll(100, |_| panic!("no event expected")).caught_up);
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
fixture.announce(0, &prefix("10.0.0.0/8"), &Route::via(&[1]));
let mut events = Vec::new();
let progress = reader.poll(100, |event| events.push(format!("{event:?}")));
assert!(progress.caught_up);
assert_eq!(events.len(), 1, "{events:?}");
assert!(events[0].starts_with("Best"), "{events:?}");
}
/// A change to a route that is not the best is nothing to a follower of
/// the best routes, and is what a follower of all routes is there for.
#[test]
+47 -20
View File
@@ -313,10 +313,21 @@ struct Shard {
journals: journal::Journals,
}
/// The routing table. Shared between threads by reference; every method
/// takes `&self`.
/// A shard, and beside its lock what is read without taking it.
struct ShardSlot {
lock: RwLock<Shard>,
/// The sequence number of each of the shard's journals as of the last
/// change to the shard, stored by the change while it still holds the
/// lock ([`RibTable::change`]). A reader looks at it first, and passes
/// over a shard with nothing new for it without stopping the shard's
/// writers.
latest: journal::ByKind<AtomicU64>,
}
/// The routing table. Shared between threads by reference: nothing takes
/// `&mut self`, and the readers ([`Reader`]) keep an `Arc` of their own.
pub struct RibTable {
shards: Box<[RwLock<Shard>]>,
shards: Box<[ShardSlot]>,
hasher: RandomState,
attrs: AttrStore,
/// Which peer numbers are in use. Changing the peer list also takes
@@ -357,15 +368,16 @@ impl RibTable {
let hasher = RandomState::default();
Self {
shards: (0..shards.max(1))
.map(|_| {
RwLock::new(Shard {
.map(|_| ShardSlot {
lock: RwLock::new(Shard {
rows: HashTable::new(),
routes: 0,
peers: Vec::new(),
hasher: hasher.clone(),
scratch: select::Scratch::default(),
journals: journal::Journals::new(limits),
})
}),
latest: journal::ByKind::default(),
})
.collect(),
hasher,
@@ -516,7 +528,7 @@ impl RibTable {
/// How many routes of `peer` are stale; see [`RibTable::mark_stale`].
/// Looks at every prefix in the table.
#[cfg(test)]
pub(super) fn count_stale(&self, peer: PeerIndex) -> usize {
fn count_stale(&self, peer: PeerIndex) -> usize {
self.shards
.iter()
.map(|shard| {
@@ -616,18 +628,22 @@ impl RibTable {
/// journal one of them follows was written to.
fn change<'a, T>(
&'a self,
parts: impl IntoIterator<Item = (&'a RwLock<Shard>, T)>,
parts: impl IntoIterator<Item = (&'a ShardSlot, T)>,
mut apply: impl FnMut(&mut Shard, T),
) {
let change = self.begin_change();
let mut news = false;
for (shard, part) in parts {
let mut shard = write(shard);
for (slot, part) in parts {
let mut shard = write(slot);
shard.journals.set_stamp(change.stamp);
let before = shard.journals.mark();
apply(&mut shard, part);
shard.tidy_journals();
news |= shard.journals.has_news_since(before);
// Once per change, not once per route; and before the lock is
// let go, so that a reader that sees the new number and takes
// the lock finds the entries behind it.
shard.journals.publish(&slot.latest);
}
drop(change);
if news {
@@ -651,9 +667,18 @@ impl RibTable {
}
}
/// The sequence number of shard `shard`'s journal of `kind` as of the
/// last change to the shard, without the shard's lock.
fn latest_seq(&self, shard: usize, kind: journal::Kind) -> u64 {
self.shards[shard]
.latest
.of(kind)
.load(std::sync::atomic::Ordering::Acquire)
}
/// The time on the clock up to which every change is complete in every
/// shard: everything before the earliest one still under way.
pub(super) fn settled(&self) -> u64 {
fn settled(&self) -> u64 {
let in_progress = self
.in_progress
.lock()
@@ -696,11 +721,8 @@ impl RibTable {
}
/// The best route for `prefix`: who it is from, and its attributes.
pub fn best(&self, prefix: &IpPrefix) -> Option<(PeerIndex, Arc<AttrSet>)> {
self.with_row(prefix, |row| {
let best = &row.paths[row.best()];
(best.peer(), Arc::clone(&best.attrs))
})
pub fn best(&self, prefix: &IpPrefix) -> Option<Route> {
self.with_row(prefix, |row| row.route(row.best()).to_owned())
}
/// Every route for `prefix`.
@@ -798,14 +820,14 @@ impl RibTable {
}
}
fn read(shard: &RwLock<Shard>) -> RwLockReadGuard<'_, Shard> {
fn read(shard: &ShardSlot) -> RwLockReadGuard<'_, Shard> {
// A panic under the lock cannot leave a row half changed in a way that
// is unsafe to read; the poison is ignored, as for the server's tables.
shard.read().unwrap_or_else(PoisonError::into_inner)
shard.lock.read().unwrap_or_else(PoisonError::into_inner)
}
fn write(shard: &RwLock<Shard>) -> RwLockWriteGuard<'_, Shard> {
shard.write().unwrap_or_else(PoisonError::into_inner)
fn write(shard: &ShardSlot) -> RwLockWriteGuard<'_, Shard> {
shard.lock.write().unwrap_or_else(PoisonError::into_inner)
}
/// A change to the table that is under way; see [`RibTable::begin_change`].
@@ -873,6 +895,11 @@ fn push_tight<T>(vec: &mut Vec<T>, value: T, expected: usize) {
let first = expected.min(FIRST_ROOM);
let more = if vec.capacity() < first {
first - vec.len()
} else if vec.capacity() < FIRST_ROOM {
// A peer that sends several routes for a prefix (ADD-PATH)
// takes a row past the peers' count: doubled while small,
// and from there by a quarter.
vec.capacity()
} else {
(vec.len() / 4).max(1)
};
+1 -1
View File
@@ -162,7 +162,7 @@ impl Fixture {
/// Which peer (by order added) the best route for `prefix` is from.
fn best(&self, prefix: &IpPrefix) -> Option<usize> {
let (peer, _) = self.table.best(prefix)?;
let peer = self.table.best(prefix)?.peer;
self.peers.iter().position(|p| *p == peer)
}
}
+1
View File
@@ -8,6 +8,7 @@ repository.workspace = true
version.workspace = true
[dependencies]
foldhash.workspace = true
bgp-packet.workspace = true
bgp-primitives.workspace = true
bgp-rib.workspace = true
+14
View File
@@ -54,6 +54,14 @@ impl From<IpAddr> for PeerAddress {
}
}
/// Whether `wanted`, as someone typed it, names the peer at `address`
/// that is configured as `name`, if it is: by that name, or by the address
/// as [`PeerAddress::is_called`] reads it. The one rule for every place a
/// peer is asked for by name.
pub fn names_peer(wanted: &str, name: Option<&str>, address: PeerAddress) -> bool {
name == Some(wanted) || address.is_called(wanted)
}
/// The address, and the interface by name if there is one: `fe80::1%eth0`.
/// An interface that is no longer there goes by its number.
impl fmt::Display for PeerAddress {
@@ -325,6 +333,12 @@ mod tests {
assert!(there.is_called("fe80::1"));
assert!(!there.is_called("fe80::1%other"));
assert!(!there.is_called("fe80::2"));
// By its configured name as well, where it has one.
assert!(names_peer("upstream", Some("upstream"), there));
assert!(names_peer("fe80::1", Some("upstream"), there));
assert!(names_peer("fe80::1", None, there));
assert!(!names_peer("upstream", None, there));
assert!(!names_peer("other", Some("upstream"), there));
assert_eq!(
socket_text(peer.to_dial().unwrap()),
format!("[fe80::1%{name}]:179")
File diff suppressed because it is too large. Load diff
+166 -138
View File
@@ -11,6 +11,7 @@ use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_rpki::RpkiConfig;
use protobuf::EnumOrUnknown;
use crate::address::{Endpoint, PeerAddress};
use crate::error::{ConfigError, ExportProblem, ImportProblem, OriginateProblem, PeerProblem};
@@ -141,20 +142,28 @@ impl Config {
let listen_endpoints = parse_listen_endpoints(&proto.listen_endpoints)?;
let defaults = PeerDefaults {
export: Arc::new(match proto.export.as_ref() {
Some(export) => parse_export(export)?,
None => ExportPolicy::default(),
}),
export_limits: match proto.export_limits.as_ref() {
Some(limits) => parse_export_limits(limits)
.map_err(ConfigError::ExportLimits)?
.map(Arc::new),
None => None,
},
graceful_restart: match proto.graceful_restart.as_ref() {
Some(own) => GracefulRestartConfig::default().overridden_by(own)?,
None => GracefulRestartConfig::default(),
},
export: Arc::new(
proto
.export
.as_ref()
.map(parse_export)
.transpose()?
.unwrap_or_default(),
),
export_limits: proto
.export_limits
.as_ref()
.map(parse_export_limits)
.transpose()
.map_err(ConfigError::ExportLimits)?
.flatten()
.map(Arc::new),
graceful_restart: proto
.graceful_restart
.as_ref()
.map(|own| GracefulRestartConfig::default().overridden_by(own))
.transpose()?
.unwrap_or_default(),
};
let mut peers = parse_peers(proto.peers, proto.local_asn, &defaults)?;
check_roles(
@@ -179,16 +188,7 @@ impl Config {
None => None,
};
let rpki = match proto.rpki.as_ref() {
Some(rpki) => Some(parse_rpki(rpki)?),
None => None,
};
let connect_retry = if proto.connect_retry_seconds == 0 {
DEFAULT_CONNECT_RETRY
} else {
Duration::from_secs(proto.connect_retry_seconds as u64)
};
let rpki = proto.rpki.as_ref().map(parse_rpki).transpose()?;
Ok(Self {
router_id,
@@ -197,7 +197,7 @@ impl Config {
hold_time_secs: DEFAULT_HOLD_TIME_SECS,
listen_endpoints,
peers,
connect_retry,
connect_retry: seconds(proto.connect_retry_seconds).unwrap_or(DEFAULT_CONNECT_RETRY),
collision_detect_established_state: proto.collision_detect_established_state,
accept_unconfigured_peers: proto.accept_unconfigured_peers,
originate,
@@ -398,20 +398,37 @@ fn parse_identifier(text: &str) -> Option<Ipv4Addr> {
.filter(|id| !id.is_unspecified())
}
/// A number the configuration has as zero is a field left out: the
/// protocol buffer has no way of telling the two apart, and nothing
/// configured here is meaningfully zero.
fn given(n: u32) -> Option<u32> {
(n > 0).then_some(n)
}
/// A time given in seconds, if it was given.
fn seconds(n: u32) -> Option<Duration> {
given(n).map(|n| Duration::from_secs(n.into()))
}
/// A list of prefixes, if any are listed: an empty list leaves the setting
/// out rather than allowing nothing.
fn parse_prefixes_if_any(
ranges: &[proto::PrefixRange],
) -> Result<Option<PrefixSet>, ImportProblem> {
if ranges.is_empty() {
return Ok(None);
}
parse_prefixes(ranges).map(Some)
}
fn parse_kernel(kernel: &proto::Kernel) -> FibConfig {
let defaults = FibConfig::default();
FibConfig {
// The kernel has no table 0 to put routes in.
table: match kernel.table {
0 => defaults.table,
table => table,
},
table: given(kernel.table).unwrap_or(defaults.table),
metric: kernel.metric.unwrap_or(defaults.metric),
nexthop_objects: !kernel.without_nexthop_objects,
resolution_table: match kernel.resolution_table {
0 => defaults.resolution_table,
table => table,
},
resolution_table: given(kernel.resolution_table).unwrap_or(defaults.resolution_table),
resolve_via_default: kernel.resolve_via_default_route,
stale_time: kernel.stale_seconds.map_or(defaults.stale_time, |seconds| {
Duration::from_secs(seconds.into())
@@ -452,12 +469,8 @@ fn parse_export_limits(
limits: &proto::ExportLimits,
) -> Result<Option<ExportLimits>, ImportProblem> {
let limits = ExportLimits {
prefixes: if limits.prefixes.is_empty() {
None
} else {
Some(parse_prefixes(&limits.prefixes)?)
},
max_prefixes: Some(limits.max_prefixes).filter(|limit| *limit > 0),
prefixes: parse_prefixes_if_any(&limits.prefixes)?,
max_prefixes: given(limits.max_prefixes),
};
Ok((limits != ExportLimits::default()).then_some(limits))
}
@@ -472,38 +485,17 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerPro
return Err(PeerProblem::RemoteAsn);
}
let hold_time_secs = match peer.hold_time_seconds {
None => DEFAULT_HOLD_TIME_SECS,
// RFC 4271 §4.2: zero, or at least three seconds.
Some(1 | 2) => return Err(PeerProblem::HoldTime),
Some(secs) => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime)?,
};
let local_address = if peer.local_address.is_empty() {
None
} else {
let ip = IpAddr::from_str(&peer.local_address)
.map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))?;
if ip.is_ipv4() != endpoint.ip.is_ipv4() {
return Err(PeerProblem::AddressFamilyMismatch);
}
Some(ip)
};
let families = if peer.address_families.is_empty() {
FamilySet::supported()
} else {
peer.address_families
.iter()
.map(|family| match family.enum_value() {
Ok(proto::AddressFamily::IPV4_UNICAST) => Ok(AfiSafi::IPV4_UNICAST),
Ok(proto::AddressFamily::IPV6_UNICAST) => Ok(AfiSafi::IPV6_UNICAST),
Ok(proto::AddressFamily::ADDRESS_FAMILY_UNSPECIFIED) | Err(_) => {
Err(PeerProblem::AddressFamily)
}
})
.collect::<Result<_, _>>()?
};
let hold_time_secs = parse_hold_time(peer.hold_time_seconds)?;
let local_address = (!peer.local_address.is_empty())
.then(|| {
IpAddr::from_str(&peer.local_address)
.map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))
})
.transpose()?;
if local_address.is_some_and(|ip| ip.is_ipv4() != endpoint.ip.is_ipv4()) {
return Err(PeerProblem::AddressFamilyMismatch);
}
let families = parse_families(&peer.address_families)?;
let add_path = match peer.add_path.enum_value() {
Ok(proto::AddPath::ADD_PATH_OFF) => None,
@@ -513,20 +505,14 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerPro
Err(_) => return Err(PeerProblem::AddPath),
};
let import = match peer.import.as_ref() {
Some(import) => parse_import(import)?,
None => ImportPolicy::default(),
};
let import = peer
.import
.as_ref()
.map(parse_import)
.transpose()?
.unwrap_or_default();
let role = match peer.role.enum_value() {
Ok(proto::Role::ROLE_UNSPECIFIED) => None,
Ok(proto::Role::PROVIDER) => Some(PeerRole::Provider),
Ok(proto::Role::CUSTOMER) => Some(PeerRole::Customer),
Ok(proto::Role::PEER) => Some(PeerRole::Peer),
Ok(proto::Role::ROUTE_SERVER) => Some(PeerRole::RouteServer),
Ok(proto::Role::ROUTE_SERVER_CLIENT) => Some(PeerRole::RouteServerClient),
Err(_) => return Err(PeerProblem::Role),
};
let role = parse_role(peer.role)?;
if peer.require_role && role.is_none() {
return Err(PeerProblem::RequireRoleWithoutRole);
}
@@ -550,24 +536,74 @@ fn parse_peer(peer: Peer, defaults: &PeerDefaults) -> Result<PeerConfig, PeerPro
import,
role,
require_role: peer.require_role,
export: match peer.export.as_ref() {
Some(own) => Arc::new(parse_export(own)?),
None => Arc::clone(&defaults.export),
},
export_limits: match peer.export_limits.as_ref() {
Some(own) => parse_export_limits(own)
.map_err(PeerProblem::ExportLimits)?
.map(Arc::new),
None => defaults.export_limits.clone(),
},
graceful_restart: match peer.graceful_restart.as_ref() {
Some(own) => defaults.graceful_restart.overridden_by(own)?,
None => defaults.graceful_restart,
},
export: peer
.export
.as_ref()
.map(parse_export)
.transpose()?
.map_or_else(|| Arc::clone(&defaults.export), Arc::new),
// A block of the peer's own takes the place of the top-level one,
// an empty block included: that lifts the limits.
export_limits: peer
.export_limits
.as_ref()
.map(parse_export_limits)
.transpose()
.map_err(PeerProblem::ExportLimits)?
.map_or_else(|| defaults.export_limits.clone(), |own| own.map(Arc::new)),
graceful_restart: peer
.graceful_restart
.as_ref()
.map(|own| defaults.graceful_restart.overridden_by(own))
.transpose()?
.unwrap_or(defaults.graceful_restart),
groups: peer.groups,
reflector_client: peer.route_reflector_client,
passive: peer.passive,
expected_routes: Some(peer.expected_routes).filter(|n| *n > 0),
expected_routes: given(peer.expected_routes),
})
}
/// The hold time a peer is offered: the default where none is given.
fn parse_hold_time(seconds: Option<u32>) -> Result<u16, PeerProblem> {
match seconds {
None => Ok(DEFAULT_HOLD_TIME_SECS),
// RFC 4271 §4.2: zero, or at least three seconds.
Some(1 | 2) => Err(PeerProblem::HoldTime),
Some(secs) => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime),
}
}
/// The address families offered to a peer: every one there is, unless
/// some are listed.
fn parse_families(
families: &[EnumOrUnknown<proto::AddressFamily>],
) -> Result<FamilySet, PeerProblem> {
if families.is_empty() {
return Ok(FamilySet::supported());
}
families
.iter()
.map(|family| match family.enum_value() {
Ok(proto::AddressFamily::IPV4_UNICAST) => Ok(AfiSafi::IPV4_UNICAST),
Ok(proto::AddressFamily::IPV6_UNICAST) => Ok(AfiSafi::IPV6_UNICAST),
Ok(proto::AddressFamily::ADDRESS_FAMILY_UNSPECIFIED) | Err(_) => {
Err(PeerProblem::AddressFamily)
}
})
.collect()
}
/// What the peer is to us, if that was said.
fn parse_role(role: EnumOrUnknown<proto::Role>) -> Result<Option<PeerRole>, PeerProblem> {
Ok(match role.enum_value() {
Ok(proto::Role::ROLE_UNSPECIFIED) => None,
Ok(proto::Role::PROVIDER) => Some(PeerRole::Provider),
Ok(proto::Role::CUSTOMER) => Some(PeerRole::Customer),
Ok(proto::Role::PEER) => Some(PeerRole::Peer),
Ok(proto::Role::ROUTE_SERVER) => Some(PeerRole::RouteServer),
Ok(proto::Role::ROUTE_SERVER_CLIENT) => Some(PeerRole::RouteServerClient),
Err(_) => return Err(PeerProblem::Role),
})
}
@@ -584,7 +620,6 @@ fn parse_rpki(rpki: &proto::Rpki) -> Result<RpkiConfig, ConfigError> {
port => parse_endpoint(&cache.address, port),
})
.collect::<Result<_, _>>()?;
let seconds = |seconds: u32| (seconds > 0).then(|| Duration::from_secs(seconds.into()));
Ok(RpkiConfig {
caches,
refresh: seconds(rpki.refresh_seconds),
@@ -595,20 +630,20 @@ fn parse_rpki(rpki: &proto::Rpki) -> Result<RpkiConfig, ConfigError> {
fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProblem> {
let defaults = ImportPolicy::default();
// A length of zero is the field left out.
let longest = |field, value: u32, version: IpVersion, default: u8| match value {
0 => Ok(default),
value => u8::try_from(value)
.ok()
.filter(|length| *length <= version.bits())
.ok_or(ImportProblem::PrefixLength {
field,
value,
longest: version.bits(),
}),
let longest = |field, value: u32, version: IpVersion, default: u8| {
given(value).map_or(Ok(default), |value| {
u8::try_from(value)
.ok()
.filter(|length| *length <= version.bits())
.ok_or(ImportProblem::PrefixLength {
field,
value,
longest: version.bits(),
})
})
};
let prefixes = parse_prefixes(&import.prefixes)?;
let prefixes = parse_prefixes_if_any(&import.prefixes)?;
let rules = import
.rules
.iter()
@@ -622,11 +657,8 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
.collect::<Result<Vec<_>, _>>()?;
Ok(ImportPolicy {
max_prefixes: Some(import.max_prefixes).filter(|limit| *limit > 0),
hold_down: match import.hold_down_seconds {
0 => defaults.hold_down,
seconds => Duration::from_secs(seconds.into()),
},
max_prefixes: given(import.max_prefixes),
hold_down: seconds(import.hold_down_seconds).unwrap_or(defaults.hold_down),
accept_bogons: import.accept_bogons,
accept_default_route: import.accept_default_route,
max_ipv4_length: longest(
@@ -646,6 +678,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
accept_aspa_invalid: import.accept_aspa_invalid,
accept_as_sets: import.accept_as_sets,
accept_third_party_next_hop: import.accept_third_party_next_hop,
accept_own_host_next_hop: false,
accept_blackholes: import.accept_blackholes,
own_prefixes: Arc::default(),
originated: Arc::default(),
@@ -659,13 +692,13 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
local_pref: import.local_pref,
rules,
reflection: None,
max_as_path_length: match import.max_as_path_length {
0 => defaults.max_as_path_length,
length => u16::try_from(length).map_err(|_| ImportProblem::PathLength(length))?,
},
prefixes: Some(prefixes).filter(|prefixes| !prefixes.is_empty()),
origin_asns: Some(import.origin_asns.iter().copied().collect())
.filter(|asns: &std::collections::HashSet<u32>| !asns.is_empty()),
max_as_path_length: given(import.max_as_path_length)
.map(|length| u16::try_from(length).map_err(|_| ImportProblem::PathLength(length)))
.transpose()?
.unwrap_or(defaults.max_as_path_length),
prefixes,
origin_asns: (!import.origin_asns.is_empty())
.then(|| import.origin_asns.iter().copied().collect()),
})
}
@@ -680,17 +713,17 @@ fn parse_prefixes(ranges: &[proto::PrefixRange]) -> Result<PrefixSet, ImportProb
problem,
})?;
let longest = prefix.address_family().bits();
let max_length = match range.max_length {
0 => prefix.length(),
max_length => u8::try_from(max_length)
// Left out, the prefix itself is the longest allowed.
let max_length = given(range.max_length).map_or(Ok(prefix.length()), |max_length| {
u8::try_from(max_length)
.ok()
.filter(|max| (prefix.length()..=longest).contains(max))
.ok_or(ImportProblem::MaxLength {
prefix: range.prefix.clone(),
max_length,
longest,
})?,
};
})
})?;
prefixes.allow(prefix, max_length);
}
Ok(prefixes)
@@ -699,21 +732,16 @@ fn parse_prefixes(ranges: &[proto::PrefixRange]) -> Result<PrefixSet, ImportProb
fn parse_rule(rule: &proto::ImportRule) -> Result<ImportRule, ImportProblem> {
Ok(ImportRule {
communities: Patterns::parse(&rule.match_communities)?,
prefixes: if rule.match_prefixes.is_empty() {
None
} else {
Some(parse_prefixes(&rule.match_prefixes)?)
},
prefixes: parse_prefixes_if_any(&rule.match_prefixes)?,
local_pref: rule.local_pref,
marks: Marks::parse(&[], &rule.add_communities, &rule.add_large_communities)?,
})
}
fn parse_port(port: u32) -> Result<u16, ConfigError> {
match port {
0 => Ok(DEFAULT_BGP_PORT),
port => u16::try_from(port).map_err(|_| ConfigError::Port(port)),
}
given(port).map_or(Ok(DEFAULT_BGP_PORT), |port| {
u16::try_from(port).map_err(|_| ConfigError::Port(port))
})
}
fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr, ConfigError> {
File diff suppressed because it is too large. Load diff
+12 -18
View File
@@ -18,7 +18,7 @@ use tracing::{info, warn};
use crate::collision::Direction;
use crate::config::PeerConfig;
use crate::connection::{CLOSED_AT_OUR_REQUEST, PeerSettings, ServerContext, run_session};
use crate::connection::{PeerSettings, ServerContext, run_session};
use crate::session::DialState;
/// Upper bound for exponential backoff between connection attempts.
@@ -102,7 +102,9 @@ async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry:
registry.set_dial_state(&peer.name, DialState::Connecting);
info!(peer = %peer.name, endpoint = %peer.endpoint, "Connecting to peer");
let ended = match tokio::time::timeout(CONNECT_TIMEOUT, connect(&peer)).await {
// Why the attempt ended, and whether that is a failure worth a
// line in the log.
let (reason, failed) = match tokio::time::timeout(CONNECT_TIMEOUT, connect(&peer)).await {
Ok(Ok(stream)) => match stream.peer_addr() {
Ok(peer_addr) => {
info!(peer = %peer.name, endpoint = %peer.endpoint, "TCP connected, starting session");
@@ -121,25 +123,17 @@ async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry:
if end.established {
backoff = connect_retry;
}
if end.result.is_ok() {
Ok(())
} else {
Err(end.reason())
}
let failed = end.result.is_err();
(end.reason(), failed)
}
Err(e) => Err(format!("connection lost at once: {e}")),
Err(e) => (format!("connection lost at once: {e}"), true),
},
Ok(Err(e)) => Err(format!("connect failed: {e}")),
Err(_) => Err(format!("connect timed out after {CONNECT_TIMEOUT:?}")),
};
let reason = match ended {
Ok(()) => CLOSED_AT_OUR_REQUEST.to_string(),
Err(reason) => {
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
reason
}
Ok(Err(e)) => (format!("connect failed: {e}"), true),
Err(_) => (format!("connect timed out after {CONNECT_TIMEOUT:?}"), true),
};
if failed {
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
}
// Not all at the same moment, when many peers went away together.
let wait = jittered(backoff);
+7 -3
View File
@@ -41,6 +41,9 @@ pub enum SessionError {
Decode(#[from] DecodeError),
#[error("could not encode a message: {0}")]
Encode(#[from] EncodeError),
/// A received UPDATE's attributes could not be kept in the table.
#[error(transparent)]
Unstorable(#[from] crate::routes::Unstorable),
/// The peer sent nothing for as long as the pre-negotiation hold time.
#[error("timed out waiting for a message in the {0} state")]
Timeout(State),
@@ -101,9 +104,10 @@ impl SessionError {
pub fn notification(&self) -> Option<NotificationMessage> {
let error = match self {
Self::Io(_) | Self::SendHoldTimerExpired(_) | Self::NotifiedByPeer(_) => return None,
// Something of ours could not be written out, which is a bug
// here: as for a panic, the session is reset.
Self::Encode(_) => BgpError::Cease(CeaseSubcode::AdminReset),
// Something of ours could not be written out, or the peer's
// routes could not be kept, which is a bug here: as for a
// panic, the session is reset.
Self::Encode(_) | Self::Unstorable(_) => BgpError::Cease(CeaseSubcode::AdminReset),
Self::Decode(e) => {
return Some(NotificationMessage {
error: e.notification.clone(),
+287 -8
View File
@@ -35,10 +35,13 @@ use std::net::{IpAddr, Ipv4Addr};
use bgp_packet::message::{
AsPathAttribute, AsPathSegment, LocalPrefPathAttribute, MpReachNlriPathAttribute,
MpUnreachNlriPathAttribute, NextHopPathAttribute, NlriNextHop, PathAttribute, UpdateMessage,
attr_type, begin_message, finish_message, msg_type, put_nlri_list,
};
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::Nlri;
use bgp_primitives::parser::ParserContext;
use bgp_rib::table::{DEFAULT_LOCAL_PREF, PeerIndex};
use bytes::{BufMut, Bytes, BytesMut};
use crate::advertise::{Towards, ipv6_next_hop};
use crate::marks::{self, Control, Named, communities_of, well_known_allow};
@@ -47,7 +50,7 @@ use crate::policy::{ExportPolicy, PeerRole};
/// The session a route may go out on, as far as that decides whether it
/// does and as what.
#[derive(Debug, Clone, Copy)]
pub struct Egress<'a> {
pub(crate) struct Egress<'a> {
pub towards: Towards,
pub peer_asn: u32,
/// The peer's number in the routing table: what its own routes there
@@ -66,7 +69,7 @@ pub struct Egress<'a> {
/// Route reflection (RFC 4456), as far as one session goes.
#[derive(Debug, Clone, Copy)]
pub struct Reflection {
pub(crate) struct Reflection {
/// The cluster this router reflects routes for.
pub cluster_id: Ipv4Addr,
/// Whether the peer is a route reflector client of ours.
@@ -78,7 +81,7 @@ pub struct Reflection {
/// Where a learned route came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Source {
pub(crate) struct Source {
pub peer: PeerIndex,
/// Whether that peer is in our own AS.
pub ibgp: bool,
@@ -92,7 +95,7 @@ pub struct Source {
/// A route ready to be announced on a session: its attributes, but for
/// the ones that depend on which prefixes go with them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outgoing {
pub(crate) struct Outgoing {
pub attributes: Vec<PathAttribute>,
pub next_hop: NlriNextHop,
}
@@ -393,15 +396,155 @@ pub(crate) fn finish(attributes: &mut Vec<PathAttribute>, towards: &Towards) {
/// lengths every UPDATE has.
const UPDATE_ROOM: usize = 4096 - 19 - 4;
/// The most octets a route's attributes may take on the wire for the
/// route to go out at all. A message is 4,096 octets at most (RFC 4271
/// §4.1) and must hold the attributes, the next hop and at least one
/// prefix; three quarters of it leaves [`UPDATE_ROOM`] a thousand octets
/// for the next hop and the prefixes, which is some forty IPv6 prefixes
/// with path identifiers a message, enough to keep a table's UPDATEs few.
pub(crate) const ATTRIBUTES_AT_MOST: usize = 3072;
/// The most octets a prefix takes in an UPDATE: a path identifier, a
/// length, and the address.
fn nlri_octets(routes: IpVersion) -> usize {
4 + 1 + usize::from(routes.bits()) / 8
}
/// A route's attributes as they go out on one session, encoded once and
/// kept: what every UPDATE announcing a route with them carries, less the
/// next hop, which goes with the prefixes. `at_next_hop` and
/// `at_mp_reach` are where in `bytes` a NEXT_HOP (type 3) and an
/// MP_REACH_NLRI (type 14) go, so that the attributes stay in order of
/// type code.
#[derive(Debug)]
pub(crate) struct EncodedAttributes {
bytes: Bytes,
at_next_hop: usize,
at_mp_reach: usize,
}
impl EncodedAttributes {
/// Encodes `attributes`, which are `finish`ed, for a session with
/// `four_octet_asn`. `None` if they cannot be encoded.
pub(crate) fn encode(attributes: &[PathAttribute], four_octet_asn: bool) -> Option<Self> {
let ctx = ParserContext {
four_octet_asn,
ibgp: false,
add_path: Vec::new(),
};
let mut out = BytesMut::with_capacity(256);
let mut at_next_hop = None;
let mut at_mp_reach = None;
for attribute in attributes {
let code = attribute.type_code();
if code > attr_type::NEXT_HOP && at_next_hop.is_none() {
at_next_hop = Some(out.len());
}
if code > attr_type::MP_REACH_NLRI && at_mp_reach.is_none() {
at_mp_reach = Some(out.len());
}
attribute.to_wire(&ctx, &mut out).ok()?;
}
Some(Self {
at_next_hop: at_next_hop.unwrap_or(out.len()),
at_mp_reach: at_mp_reach.unwrap_or(out.len()),
bytes: out.freeze(),
})
}
pub(crate) fn octets(&self) -> usize {
self.bytes.len()
}
/// The UPDATE messages, complete with their headers, that announce
/// `routes`, all of IP version `version`, with these attributes and
/// `next_hop`: as few as they fit in, in pieces of [`ENCODED_PIECE`]
/// octets or so, each of whole messages. `ctx` is the session's, and
/// says whether path identifiers go with the family's prefixes
/// (RFC 7911).
///
/// What `announcements`, kept under test as the reference, builds as
/// messages, built as bytes: the attributes are not parsed, cloned or
/// encoded again for each message.
pub(crate) fn announcements(
&self,
next_hop: NlriNextHop,
version: IpVersion,
routes: &[Nlri],
ctx: &ParserContext,
) -> Vec<BytesMut> {
// Attributes that would leave no room for a prefix do not go out
// at all; see `Learned::dressed`.
debug_assert!(self.octets() < ATTRIBUTES_AT_MOST);
let room = UPDATE_ROOM.saturating_sub(self.octets() + NEXT_HOP_ROOM);
let at_once = (room / nlri_octets(version)).max(1);
let in_body = matches!(next_hop, NlriNextHop::Ipv4(_)) && version == IpVersion::Ipv4;
let with_path_id = ctx.add_path.contains(&AfiSafi::unicast(version));
let mut pieces = Vec::new();
let mut out = BytesMut::with_capacity(ENCODED_PIECE.min(routes.len() * 64 + 4096));
for routes in routes.chunks(at_once) {
{
let length_at = begin_message(&mut out, msg_type::UPDATE);
// No withdrawn routes.
out.put_u16(0);
let attributes_at = out.len();
out.put_u16(0);
if in_body {
let NlriNextHop::Ipv4(next_hop) = next_hop else {
unreachable!("in_body means an IPv4 next hop");
};
out.put_slice(&self.bytes[..self.at_next_hop]);
PathAttribute::NextHop(NextHopPathAttribute(next_hop))
.to_wire(ctx, &mut out)
.expect("a next hop encodes");
out.put_slice(&self.bytes[self.at_next_hop..]);
} else {
out.put_slice(&self.bytes[..self.at_mp_reach]);
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
family: AfiSafi::unicast(version),
next_hop,
prefixes: routes.to_vec(),
})
.to_wire(ctx, &mut out)
.expect("a chunk that fits encodes");
out.put_slice(&self.bytes[self.at_mp_reach..]);
}
let attributes_len = out.len() - attributes_at - 2;
out[attributes_at..attributes_at + 2]
.copy_from_slice(&(attributes_len as u16).to_be_bytes());
if in_body {
put_nlri_list(routes, with_path_id, &mut out);
}
// As many prefixes went in as fit beside the attributes.
finish_message(&mut out, length_at).expect("a chunk that fits is a message");
}
if out.len() >= ENCODED_PIECE {
pieces.push(out.split());
}
}
if !out.is_empty() {
pieces.push(out);
}
pieces
}
}
/// What the next hop takes, in its own attribute or with the prefixes,
/// and a little to spare.
const NEXT_HOP_ROOM: usize = 64;
/// How many octets of UPDATEs go to a session in one piece: what the
/// session hands its writer at a time. A piece holds whole messages,
/// so it runs over by at most one.
pub(crate) const ENCODED_PIECE: usize = 64 * 1024;
/// The UPDATEs that announce `routes`, all of IP version `version`, as
/// `outgoing`: as few as they fit in. `attributes_octets` is how long
/// `outgoing`'s attributes are on this session's wire.
///
/// What [`EncodedAttributes::announcements`] does, built as messages:
/// kept as what its bytes are checked against.
#[cfg(test)]
pub(crate) fn announcements(
outgoing: &Outgoing,
version: IpVersion,
@@ -409,9 +552,6 @@ pub(crate) fn announcements(
towards: &Towards,
attributes_octets: usize,
) -> Vec<UpdateMessage> {
// What the next hop takes, in its own attribute or with the prefixes,
// and a little to spare.
const NEXT_HOP_ROOM: usize = 64;
let room = UPDATE_ROOM.saturating_sub(attributes_octets + NEXT_HOP_ROOM);
let at_once = (room / nlri_octets(version)).max(1);
let in_body = matches!(outgoing.next_hop, NlriNextHop::Ipv4(_)) && version == IpVersion::Ipv4;
@@ -484,7 +624,7 @@ mod tests {
use std::net::Ipv4Addr;
use bgp_packet::message::{
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity, Message,
MultiExitDiscPathAttribute, OriginPathAttribute, PathAttributeFlags,
};
use bgp_primitives::ip_prefix::IpPrefix;
@@ -549,6 +689,145 @@ mod tests {
PathAttribute::Communities(CommunitiesPathAttribute(communities.to_vec()))
}
/// What `EncodedAttributes::announcements` writes is, byte for byte,
/// what the messages `announcements` builds encode to: with the next
/// hop in its own attribute (IPv4) and in MP_REACH_NLRI (IPv6, and
/// IPv4 with an IPv6 next hop, RFC 8950), with and without path
/// identifiers, to two-octet and four-octet AS speakers (so with the
/// attributes around NEXT_HOP, MP_REACH_NLRI and AS4_PATH in order),
/// and over as many messages when the routes do not fit in one.
#[test]
fn encoded_announcements_are_the_messages_on_the_wire() {
use std::net::Ipv6Addr;
// The routes' IP version, their next hop, and whether the session
// takes IPv4 routes with IPv6 next hops.
let layouts = [
(
IpVersion::Ipv4,
NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1)),
false,
),
(
IpVersion::Ipv6,
NlriNextHop::Ipv6WithLl {
global: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1),
link_local: Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1),
},
false,
),
(
IpVersion::Ipv4,
NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
true,
),
];
for four_octet_asn in [true, false] {
for with_path_id in [false, true] {
for (version, next_hop, extended_next_hop) in layouts {
let towards = Towards {
four_octet_asn,
extended_next_hop,
..towards(None, false)
};
let attributes = vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![4_200_000_000, 3356, 64700]),
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(10)),
communities(&[(3356, 100), (64700, 1)]),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity {
global_admin: 64700,
data_1: 1,
data_2: 2,
}],
}),
];
let routes: Vec<Nlri> = (0..30_000u32)
.map(|i| Nlri {
prefix: match version {
IpVersion::Ipv4 => IpPrefix::new(
version,
Ipv4Addr::from(0x0a00_0000 + i * 256).octets(),
24,
),
IpVersion::Ipv6 => IpPrefix::new(
version,
Ipv6Addr::new(0x2001, 0xdb8, i as u16, 0, 0, 0, 0, 0).octets(),
48,
),
}
.unwrap(),
path_id: if with_path_id { i + 1 } else { 0 },
})
.collect();
let ctx = ParserContext {
four_octet_asn,
ibgp: false,
add_path: if with_path_id {
vec![AfiSafi::unicast(version)]
} else {
Vec::new()
},
};
let mut finished = attributes.clone();
finish(&mut finished, &towards);
let encoded = EncodedAttributes::encode(&finished, four_octet_asn).unwrap();
let bytes = encoded.announcements(next_hop, version, &routes, &ctx);
let outgoing = Outgoing {
attributes,
next_hop,
};
let messages =
announcements(&outgoing, version, &routes, &towards, encoded.octets());
let expected: Vec<BytesMut> = messages
.into_iter()
.map(|update| {
let mut out = BytesMut::new();
Message::Update(update).to_wire(&ctx, &mut out).unwrap();
out
})
.collect();
let what = format!(
"{version:?} with next hop {next_hop:?}, path ids {with_path_id}, \
four-octet {four_octet_asn}"
);
assert!(expected.len() > 1, "{what}: more than one message");
for update in &expected {
assert!(update.len() <= 4096, "{what}: a message fits");
}
// In pieces of whole messages, each of about the size
// meant, all but the last at least that.
assert!(bytes.len() > 1, "{what}: more than one piece");
for (n, piece) in bytes.iter().enumerate() {
assert!(
n == bytes.len() - 1
|| (ENCODED_PIECE..ENCODED_PIECE + 4096).contains(&piece.len()),
"{what}: piece {n} is {} octets",
piece.len()
);
let mut at = 0;
while at < piece.len() {
let length =
usize::from(u16::from_be_bytes([piece[at + 16], piece[at + 17]]));
at += length;
}
assert_eq!(at, piece.len(), "{what}: piece {n} is of whole messages");
}
let got: Vec<u8> = bytes
.iter()
.flat_map(|piece| piece.iter().copied())
.collect();
let want: Vec<u8> = expected.iter().flat_map(|m| m.iter().copied()).collect();
assert_eq!(got, want, "{what}: the same octets");
}
}
}
}
struct Case<'a> {
policy: &'a ExportPolicy,
role: Option<PeerRole>,
+15 -1
View File
@@ -15,7 +15,8 @@
use bgp_packet::community::{CommunityError, parse_large, parse_standard};
use bgp_packet::message::{
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity, PathAttribute,
CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
LocalPrefPathAttribute, PathAttribute,
};
/// A standard community, or all those with one half or both left open:
@@ -186,6 +187,19 @@ impl Marks {
}
}
/// Marks `attributes` as a policy or one of its rules says: with `marks`
/// on their communities, and with `local_pref` as their LOCAL_PREF if one
/// is given, in place of whatever they had.
pub fn mark(attributes: &mut Vec<PathAttribute>, marks: &Marks, local_pref: Option<u32>) {
marks.apply(attributes);
if let Some(local_pref) = local_pref {
put(
attributes,
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
);
}
}
/// Takes the standard communities out of `attributes`.
fn take_communities(attributes: &mut Vec<PathAttribute>) -> Vec<(u16, u16)> {
let at = attributes
+285 -316
View File
@@ -2,391 +2,296 @@
use std::sync::LazyLock;
use bgp_fib::sockdiag::TcpConnection;
use bgp_packet::message::Message;
#[cfg(test)]
use prometheus::core::Collector;
use prometheus::{
GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, register_gauge_vec,
register_int_counter, register_int_counter_vec, register_int_gauge, register_int_gauge_vec,
};
pub static MESSAGES_SENT: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
/// What a declared metric puts on the registry: one collector for a
/// metric, and one for each gauge of a group of them.
#[cfg(test)]
trait Collectors {
fn collectors(&self) -> Vec<Box<dyn Collector>>;
}
#[cfg(test)]
impl<T: Collector + Clone + 'static> Collectors for T {
fn collectors(&self) -> Vec<Box<dyn Collector>> {
vec![Box::new(self.clone())]
}
}
/// Declares the metrics: a static for each, made on first use, and
/// [`init`], which forces every one of them so that nothing is left out
/// of a scrape for not having been used yet. Each is declared once, here,
/// so that the two cannot drift apart.
macro_rules! metrics {
($(
$(#[$meta:meta])*
$name:ident: $type:ty = $register:expr;
)*) => {
$(
$(#[$meta])*
pub static $name: LazyLock<$type> =
LazyLock::new(|| $register.expect("metric registration"));
)*
/// Every metric declared above, as the collectors the registry
/// holds, for the tests to check it against.
#[cfg(test)]
fn declared() -> Vec<Box<dyn Collector>> {
let mut all = Vec::new();
$(all.extend($name.collectors());)*
all
}
/// Forces registration of every metric so they appear in scrapes
/// before their first observation: a counter that is not there
/// until it first counts hides that first count from `increase()`.
pub fn init() {
$(LazyLock::force(&$name);)*
BUILD_INFO
.with_label_values(&[env!("CARGO_PKG_VERSION")])
.set(1);
}
};
}
metrics! {
MESSAGES_SENT: IntCounterVec = register_int_counter_vec!(
"bgp_messages_sent_total",
"BGP messages sent, by type",
&["type"]
)
.expect("metric registration")
});
pub static MESSAGES_RECEIVED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
MESSAGES_RECEIVED: IntCounterVec = register_int_counter_vec!(
"bgp_messages_received_total",
"BGP messages received, by type",
&["type"]
)
.expect("metric registration")
});
pub static UPDATE_ERRORS: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
UPDATE_ERRORS: IntCounterVec = register_int_counter_vec!(
"bgp_update_errors_total",
"Errors in received UPDATE messages that did not reset the session, by how they were handled (RFC 7606)",
&["handling"]
)
.expect("metric registration")
});
pub static SESSIONS_ESTABLISHED_TOTAL: LazyLock<IntCounter> = LazyLock::new(|| {
register_int_counter!(
);
SESSIONS_ESTABLISHED_TOTAL: IntCounter = register_int_counter!(
"bgp_sessions_established_total",
"Sessions that reached Established"
)
.expect("metric registration")
});
pub static SESSIONS_ESTABLISHED: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
);
SESSIONS_ESTABLISHED: IntGauge = register_int_gauge!(
"bgp_sessions_established",
"Sessions currently in Established"
)
.expect("metric registration")
});
pub static ROUTES_REJECTED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
ROUTES_REJECTED: IntCounterVec = register_int_counter_vec!(
"bgp_routes_rejected_total",
"Routes announced by a peer and refused by its import policy, by why",
&["peer", "reason"]
)
.expect("metric registration")
});
pub static PEER_ROUTES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
PEER_ROUTES: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_routes",
"Routes of an established peer that are in the routing table",
&["peer"]
)
.expect("metric registration")
});
pub static PEER_ADVERTISED: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
PEER_ADVERTISED: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_advertised_routes",
"Routes announced to an established peer and not withdrawn since",
&["peer"]
)
.expect("metric registration")
});
/// A session's TCP connection as the kernel reports it (`TCP_INFO`),
/// sampled when the metrics are scraped. Seconds, octets, segments, and
/// octets per second; the busy, limited and byte figures are cumulative
/// over the connection's life.
pub struct PeerTcp {
pub rtt_seconds: GaugeVec,
pub min_rtt_seconds: GaugeVec,
pub rtt_variance_seconds: GaugeVec,
pub cwnd_segments: IntGaugeVec,
pub mss_bytes: IntGaugeVec,
pub send_window_bytes: IntGaugeVec,
pub receive_window_bytes: IntGaugeVec,
pub send_queue_bytes: IntGaugeVec,
pub not_sent_bytes: IntGaugeVec,
pub receive_queue_bytes: IntGaugeVec,
pub unacked_segments: IntGaugeVec,
pub retransmitted_segments: IntGaugeVec,
pub lost_segments: IntGaugeVec,
pub busy_seconds: GaugeVec,
pub receive_window_limited_seconds: GaugeVec,
pub send_buffer_limited_seconds: GaugeVec,
pub sent_bytes: IntGaugeVec,
pub received_bytes: IntGaugeVec,
pub delivery_rate_bytes_per_second: IntGaugeVec,
pub pacing_rate_bytes_per_second: IntGaugeVec,
}
impl PeerTcp {
/// Forgets the peer's series, when its session has ended.
pub fn remove(&self, label: &str) {
for gauge in [
&self.rtt_seconds,
&self.min_rtt_seconds,
&self.rtt_variance_seconds,
&self.busy_seconds,
&self.receive_window_limited_seconds,
&self.send_buffer_limited_seconds,
] {
let _ = gauge.remove_label_values(&[label]);
}
for gauge in [
&self.cwnd_segments,
&self.mss_bytes,
&self.send_window_bytes,
&self.receive_window_bytes,
&self.send_queue_bytes,
&self.not_sent_bytes,
&self.receive_queue_bytes,
&self.unacked_segments,
&self.retransmitted_segments,
&self.lost_segments,
&self.sent_bytes,
&self.received_bytes,
&self.delivery_rate_bytes_per_second,
&self.pacing_rate_bytes_per_second,
] {
let _ = gauge.remove_label_values(&[label]);
}
}
}
pub static PEER_TCP: LazyLock<PeerTcp> = LazyLock::new(|| {
let seconds = |name: &str, help: &str| {
register_gauge_vec!(format!("bgp_peer_tcp_{name}"), help, &["peer"])
.expect("metric registration")
};
let count = |name: &str, help: &str| {
register_int_gauge_vec!(format!("bgp_peer_tcp_{name}"), help, &["peer"])
.expect("metric registration")
};
PeerTcp {
rtt_seconds: seconds(
"rtt_seconds",
"Smoothed round-trip time of the session's TCP connection",
),
min_rtt_seconds: seconds(
"min_rtt_seconds",
"Least round-trip time seen on the connection",
),
rtt_variance_seconds: seconds(
"rtt_variance_seconds",
"Round-trip time variance of the connection",
),
cwnd_segments: count("cwnd_segments", "Congestion window, in segments"),
mss_bytes: count("mss_bytes", "Maximum segment size"),
send_window_bytes: count(
"send_window_bytes",
"The peer's receive window: what it lets us send",
),
receive_window_bytes: count(
"receive_window_bytes",
"Our receive window: what we let the peer send",
),
send_queue_bytes: count(
"send_queue_bytes",
"Octets written and not yet acknowledged by the peer",
),
not_sent_bytes: count("not_sent_bytes", "Octets written and not yet sent"),
receive_queue_bytes: count("receive_queue_bytes", "Octets received and not yet read"),
unacked_segments: count("unacked_segments", "Segments sent and not yet acknowledged"),
retransmitted_segments: count(
"retransmitted_segments",
"Segments retransmitted over the connection's life",
),
lost_segments: count("lost_segments", "Segments thought lost now"),
busy_seconds: seconds(
"busy_seconds",
"Time the connection had something to send, over its life",
),
receive_window_limited_seconds: seconds(
"receive_window_limited_seconds",
"Of the busy time, time held up by the peer's receive window",
),
send_buffer_limited_seconds: seconds(
"send_buffer_limited_seconds",
"Of the busy time, time held up by our send buffer being full",
),
sent_bytes: count("sent_bytes", "Octets sent over the connection's life"),
received_bytes: count(
"received_bytes",
"Octets received over the connection's life",
),
delivery_rate_bytes_per_second: count(
"delivery_rate_bytes_per_second",
"Delivery rate the kernel measured most recently",
),
pacing_rate_bytes_per_second: count(
"pacing_rate_bytes_per_second",
"Rate the kernel paces the connection at",
),
}
});
/// The allocator's account of the daemon's memory, by `kind`: allocated,
/// active, metadata, resident, mapped, retained (jemalloc's `stats.*`).
pub static MEMORY: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
PEER_TCP: PeerTcp = PeerTcp::new();
/// The allocator's account of the daemon's memory, by `kind`: allocated,
/// active, metadata, resident, mapped, retained (jemalloc's `stats.*`).
MEMORY: IntGaugeVec = register_int_gauge_vec!(
"bgp_memory_bytes",
"The daemon's memory as its allocator accounts for it, by kind",
&["kind"]
)
.expect("metric registration")
});
pub static PEER_MAX_PREFIXES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
PEER_MAX_PREFIXES: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_max_prefixes",
"The most routes a configured peer may have (import.max_prefixes), where there is a limit",
&["peer"]
)
.expect("metric registration")
});
pub static PREFIX_LIMIT_EXCEEDED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
PREFIX_LIMIT_EXCEEDED: IntCounterVec = register_int_counter_vec!(
"bgp_prefix_limit_exceeded_total",
"Sessions ended because the peer went over its limit of routes",
&["peer"]
)
.expect("metric registration")
});
pub static EXPORT_LIMIT_EXCEEDED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
EXPORT_LIMIT_EXCEEDED: IntCounterVec = register_int_counter_vec!(
"bgp_export_limit_exceeded_total",
"Sessions ended because more routes were to be announced to the peer than its export limit allows",
&["peer"]
)
.expect("metric registration")
});
pub static RPKI_VRPS: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
RPKI_VRPS: IntGaugeVec = register_int_gauge_vec!(
"bgp_rpki_vrps",
"Validated ROA payloads in use for route origin validation, by address family",
&["family"]
)
.expect("metric registration")
});
pub static RPKI_ASPAS: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
);
RPKI_ASPAS: IntGauge = register_int_gauge!(
"bgp_rpki_aspas",
"Customer ASes with an ASPA in use for AS path verification"
)
.expect("metric registration")
});
pub static RPKI_UPDATES: LazyLock<IntCounter> = LazyLock::new(|| {
register_int_counter!("bgp_rpki_updates_total", "Times the VRPs or ASPAs changed")
.expect("metric registration")
});
pub static CONNECTIONS_REFUSED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
RPKI_UPDATES: IntCounter = register_int_counter!(
"bgp_rpki_updates_total",
"Times the VRPs or ASPAs changed"
);
CONNECTIONS_REFUSED: IntCounterVec = register_int_counter_vec!(
"bgp_connections_refused_total",
"Incoming connections closed without a session, by why",
&["reason"]
)
.expect("metric registration")
});
pub static COLLISIONS: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
COLLISIONS: IntCounterVec = register_int_counter_vec!(
"bgp_connection_collisions_total",
"Connection collisions resolved, by which connection was closed",
&["closed"]
)
.expect("metric registration")
});
pub static LOCAL_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!("bgp_local_routes", "Locally originated routes")
.expect("metric registration")
});
pub static RIB_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
);
LOCAL_ROUTES: IntGauge = register_int_gauge!(
"bgp_local_routes",
"Locally originated routes"
);
RIB_ROUTES: IntGauge = register_int_gauge!(
"bgp_rib_routes",
"Routes learned from peers, counting each peer's separately"
)
.expect("metric registration")
});
pub static RIB_PREFIXES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
);
RIB_PREFIXES: IntGauge = register_int_gauge!(
"bgp_rib_prefixes",
"Prefixes with at least one route, and so with a best route"
)
.expect("metric registration")
});
pub static RIB_ATTRIBUTE_SETS: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
);
RIB_ATTRIBUTE_SETS: IntGauge = register_int_gauge!(
"bgp_rib_attribute_sets",
"Distinct sets of path attributes held for the routes"
)
.expect("metric registration")
});
pub static RIB_JOURNAL_ENTRIES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
);
RIB_JOURNAL_ENTRIES: IntGauge = register_int_gauge!(
"bgp_rib_journal_entries",
"Changes of best route waiting for a follower to read them"
)
.expect("metric registration")
});
pub static ROUTES_EXPORTED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
);
ROUTES_EXPORTED: IntCounterVec = register_int_counter_vec!(
"bgp_routes_exported_total",
"Routes learned from one peer and announced to, or withdrawn from, another",
&["change"]
)
.expect("metric registration")
});
/// Whether each configured peer's session is up: what to alert on when a
/// peer goes away, which a count of sessions cannot say.
pub static PEER_UP: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
/// Whether each configured peer's session is up: what to alert on when a
/// peer goes away, which a count of sessions cannot say.
PEER_UP: IntGaugeVec = register_int_gauge_vec!(
"bgp_peer_up",
"Whether the session with a configured peer is Established (1) or not (0)",
&["peer"]
)
.expect("metric registration")
});
pub static BUILD_INFO: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
);
BUILD_INFO: IntGaugeVec = register_int_gauge_vec!(
"bgp_build_info",
"Always 1; the version of the daemon is in the label",
&["version"]
)
.expect("metric registration")
});
);
}
/// Forces registration of every metric so they appear in scrapes before
/// their first observation: a counter that is not there until it first
/// counts hides that first count from `increase()`.
pub fn init() {
LazyLock::force(&MESSAGES_SENT);
LazyLock::force(&MESSAGES_RECEIVED);
LazyLock::force(&UPDATE_ERRORS);
LazyLock::force(&SESSIONS_ESTABLISHED_TOTAL);
LazyLock::force(&SESSIONS_ESTABLISHED);
LazyLock::force(&ROUTES_REJECTED);
LazyLock::force(&PEER_ROUTES);
LazyLock::force(&PEER_MAX_PREFIXES);
LazyLock::force(&PREFIX_LIMIT_EXCEEDED);
LazyLock::force(&EXPORT_LIMIT_EXCEEDED);
LazyLock::force(&RPKI_VRPS);
LazyLock::force(&RPKI_ASPAS);
LazyLock::force(&RPKI_UPDATES);
LazyLock::force(&CONNECTIONS_REFUSED);
LazyLock::force(&COLLISIONS);
LazyLock::force(&LOCAL_ROUTES);
LazyLock::force(&RIB_ROUTES);
LazyLock::force(&RIB_PREFIXES);
LazyLock::force(&RIB_ATTRIBUTE_SETS);
LazyLock::force(&RIB_JOURNAL_ENTRIES);
LazyLock::force(&ROUTES_EXPORTED);
LazyLock::force(&PEER_UP);
LazyLock::force(&PEER_ADVERTISED);
LazyLock::force(&PEER_TCP);
LazyLock::force(&MEMORY);
BUILD_INFO
.with_label_values(&[env!("CARGO_PKG_VERSION")])
.set(1);
/// The type of a gauge of [`PeerTcp`] by what it holds: a time, in
/// seconds, or a count.
macro_rules! tcp_gauge {
(seconds) => {
GaugeVec
};
(count) => {
IntGaugeVec
};
}
/// Registers a gauge of [`PeerTcp`], with the peer as its one label.
macro_rules! register_tcp_gauge {
(seconds, $name:expr, $help:expr) => {
register_gauge_vec!($name, $help, &["peer"])
};
(count, $name:expr, $help:expr) => {
register_int_gauge_vec!($name, $help, &["peer"])
};
}
/// What the kernel's figure becomes on the gauge: microseconds become
/// seconds, and a count is clamped into what a gauge holds.
macro_rules! tcp_gauge_value {
(seconds, $microseconds:expr) => {
$microseconds as f64 / 1e6
};
(count, $count:expr) => {
i64::try_from($count).unwrap_or(i64::MAX)
};
}
/// Declares the gauges of [`PeerTcp`], each named once with its help text
/// and the field of [`TcpConnection`] it is set from: the struct, how it is
/// made, and what sets and removes a peer's series all follow from the one
/// list. The gauge `name` is `bgp_peer_tcp_name`.
macro_rules! peer_tcp {
($connection:ident; $(
$name:ident: $kind:ident from $source:ident = $help:literal,
)*) => {
/// A session's TCP connection as the kernel reports it (`TCP_INFO`),
/// sampled when the metrics are scraped. Seconds, octets, segments, and
/// octets per second; the busy, limited and byte figures are cumulative
/// over the connection's life.
pub struct PeerTcp {
$(pub $name: tcp_gauge!($kind),)*
}
#[cfg(test)]
impl Collectors for PeerTcp {
fn collectors(&self) -> Vec<Box<dyn Collector>> {
vec![$(Box::new(self.$name.clone()),)*]
}
}
impl PeerTcp {
fn new() -> prometheus::Result<Self> {
Ok(Self {
$($name: register_tcp_gauge!(
$kind,
concat!("bgp_peer_tcp_", stringify!($name)),
$help
)?,)*
})
}
/// Sets the peer's gauges from what the kernel reports of its
/// connection.
pub fn set(&self, label: &str, $connection: &TcpConnection) {
let label = [label];
$(
self.$name
.with_label_values(&label)
.set(tcp_gauge_value!($kind, u64::from($connection.$source)));
)*
}
/// Forgets the peer's series, when its session has ended.
pub fn remove(&self, label: &str) {
$(let _ = self.$name.remove_label_values(&[label]);)*
}
}
};
}
peer_tcp! {
connection;
rtt_seconds: seconds from rtt = "Smoothed round-trip time of the session's TCP connection",
min_rtt_seconds: seconds from min_rtt = "Least round-trip time seen on the connection",
rtt_variance_seconds: seconds from rtt_variance = "Round-trip time variance of the connection",
cwnd_segments: count from cwnd = "Congestion window, in segments",
mss_bytes: count from mss = "Maximum segment size",
send_window_bytes: count from send_window = "The peer's receive window: what it lets us send",
receive_window_bytes: count from receive_window = "Our receive window: what we let the peer send",
send_queue_bytes: count from send_queue = "Octets written and not yet acknowledged by the peer",
not_sent_bytes: count from not_sent = "Octets written and not yet sent",
receive_queue_bytes: count from recv_queue = "Octets received and not yet read",
unacked_segments: count from unacked = "Segments sent and not yet acknowledged",
retransmitted_segments: count from total_retrans = "Segments retransmitted over the connection's life",
lost_segments: count from lost = "Segments thought lost now",
busy_seconds: seconds from busy = "Time the connection had something to send, over its life",
receive_window_limited_seconds: seconds from limited_by_receive_window = "Of the busy time, time held up by the peer's receive window",
send_buffer_limited_seconds: seconds from limited_by_send_buffer = "Of the busy time, time held up by our send buffer being full",
sent_bytes: count from bytes_sent = "Octets sent over the connection's life",
received_bytes: count from bytes_received = "Octets received over the connection's life",
delivery_rate_bytes_per_second: count from delivery_rate = "Delivery rate the kernel measured most recently",
pacing_rate_bytes_per_second: count from pacing_rate = "Rate the kernel paces the connection at",
}
pub fn message_type_name(msg: &Message) -> &'static str {
@@ -398,3 +303,67 @@ pub fn message_type_name(msg: &Message) -> &'static str {
Message::RouteRefresh(_) => "route_refresh",
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
/// Every metric family the declaration lists is on the registry once
/// `init` has run, each under a name of its own: the registry refuses
/// a second collector for any of them. The registry is the process's,
/// so what else is on it is not looked at.
#[test]
fn init_registers_every_declared_metric_family() {
init();
let declared = declared();
let names: HashSet<String> = declared
.iter()
.flat_map(|collector| collector.desc())
.map(|desc| desc.fq_name.clone())
.collect();
assert_eq!(names.len(), declared.len(), "a name is declared twice");
for collector in declared {
assert!(matches!(
prometheus::register(collector),
Err(prometheus::Error::AlreadyReg)
));
}
// A metric without labels is in a scrape from the start, which is
// what `init` is for; one with labels is there once it has one.
let scraped: HashSet<String> = prometheus::gather()
.iter()
.map(|family| family.name().to_string())
.collect();
for name in [
"bgp_sessions_established",
"bgp_rib_routes",
"bgp_build_info",
] {
assert!(scraped.contains(name), "{name} is not in a scrape");
}
}
/// The kernel's microseconds become seconds, and its counts are set as
/// they are; a peer whose session ended loses every series.
#[test]
fn a_connection_sets_and_a_removal_clears_every_gauge() {
let tcp = &*PEER_TCP;
let connection = TcpConnection {
rtt: 1500,
cwnd: 10,
bytes_sent: 1 << 40,
..TcpConnection::default()
};
tcp.set("metrics-test", &connection);
let label = ["metrics-test"];
assert_eq!(tcp.rtt_seconds.with_label_values(&label).get(), 0.0015);
assert_eq!(tcp.cwnd_segments.with_label_values(&label).get(), 10);
assert_eq!(tcp.sent_bytes.with_label_values(&label).get(), 1 << 40);
tcp.remove("metrics-test");
// There is nothing left to remove.
assert!(tcp.rtt_seconds.remove_label_values(&label).is_err());
assert!(tcp.sent_bytes.remove_label_values(&label).is_err());
}
}
+42 -34
View File
@@ -29,9 +29,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use bgp_packet::message::{
AS_TRANS, AsPathAttribute, LocalPrefPathAttribute, NlriNextHop, PathAttribute,
};
use bgp_packet::message::{AS_TRANS, AsPathAttribute, NlriNextHop, PathAttribute};
use bgp_packet::open::BgpRole;
use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::IpPrefix;
@@ -91,6 +89,10 @@ pub struct ImportPolicy {
/// who never asked for it. A route server's routes always have its
/// clients as next hops, and are taken either way.
pub accept_third_party_next_hop: bool,
/// Take a next hop that is on this host: a loopback address, or one of
/// our own. For routers that share a host, as those of a test do. Not
/// a setting; only [`ImportPolicy::permissive`] has it.
pub accept_own_host_next_hop: bool,
/// Take routes with the BLACKHOLE community for single addresses, and
/// have the kernel drop the traffic to them (RFC 7999).
pub accept_blackholes: bool,
@@ -170,13 +172,7 @@ impl ImportRule {
}
pub fn apply(&self, attributes: &mut Vec<PathAttribute>) {
self.marks.apply(attributes);
if let Some(local_pref) = self.local_pref {
marks::put(
attributes,
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
);
}
marks::mark(attributes, &self.marks, self.local_pref);
}
}
@@ -199,6 +195,7 @@ impl Default for ImportPolicy {
accept_aspa_invalid: false,
accept_as_sets: false,
accept_third_party_next_hop: false,
accept_own_host_next_hop: false,
accept_blackholes: false,
own_prefixes: Arc::default(),
originated: Arc::default(),
@@ -228,6 +225,7 @@ impl ImportPolicy {
accept_aspa_invalid: true,
accept_as_sets: true,
accept_third_party_next_hop: true,
accept_own_host_next_hop: true,
accept_blackholes: false,
..Self::default()
}
@@ -250,13 +248,7 @@ impl ImportPolicy {
if let Some(scrub) = self.scrub {
scrub.apply(attributes);
}
self.marks.apply(attributes);
if let Some(local_pref) = self.local_pref {
marks::put(
attributes,
PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)),
);
}
marks::mark(attributes, &self.marks, self.local_pref);
}
}
@@ -654,11 +646,24 @@ impl ImportPolicy {
return Err(Rejection::BogonPrefix);
}
}
self.listed_or_ours(prefix, PrefixSet::allows)
}
/// Whether `prefix` is one the peer may announce by its list, where it
/// has one, and else not one of ours. `inside` says what being in a
/// set means: [`PrefixSet::allows`] for a route, which is bound by the
/// lengths allowed, [`PrefixSet::covers`] for a blackhole request,
/// which is for one address whatever the lengths.
fn listed_or_ours(
&self,
prefix: &IpPrefix,
inside: fn(&PrefixSet, &IpPrefix) -> bool,
) -> Result<(), Rejection> {
match &self.prefixes {
Some(allowed) if !allowed.allows(prefix) => Err(Rejection::PrefixNotAllowed),
Some(allowed) if !inside(allowed, prefix) => Err(Rejection::PrefixNotAllowed),
// Said to be this peer's to announce, ours or not.
Some(_) => Ok(()),
None if self.own_prefixes.allows(prefix) => Err(Rejection::OwnPrefix),
None if inside(&self.own_prefixes, prefix) => Err(Rejection::OwnPrefix),
None => Ok(()),
}
}
@@ -684,12 +689,7 @@ impl ImportPolicy {
if !self.accept_bogons && is_bogon_prefix(prefix) {
return Err(Rejection::BogonPrefix);
}
match &self.prefixes {
Some(allowed) if !allowed.covers(prefix) => Err(Rejection::PrefixNotAllowed),
Some(_) => Ok(()),
None if self.own_prefixes.covers(prefix) => Err(Rejection::OwnPrefix),
None => Ok(()),
}
self.listed_or_ours(prefix, PrefixSet::covers)
}
/// Judges the next hop of routes of IP version `routes`.
@@ -727,10 +727,10 @@ impl ImportPolicy {
let mut is_peer = false;
for address in named {
let usable = !address.is_unspecified()
&& !address.is_loopback()
&& !address.is_multicast()
&& address != IpAddr::V4(Ipv4Addr::BROADCAST)
&& address != from.local_address;
&& (self.accept_own_host_next_hop
|| (!address.is_loopback() && address != from.local_address));
if !usable {
return Err(Rejection::NextHopUnusable);
}
@@ -1548,13 +1548,7 @@ mod tests {
);
// Nowhere at all, whatever the policy.
let lenient = ImportPolicy::permissive();
for nowhere in [
"0.0.0.0",
"127.0.0.1",
"224.0.0.5",
"255.255.255.255",
"192.0.2.1",
] {
for nowhere in ["0.0.0.0", "224.0.0.5", "255.255.255.255"] {
for policy in [&policy, &lenient] {
assert_eq!(
check(policy, &v4(nowhere), &EXTERNAL),
@@ -1563,6 +1557,20 @@ mod tests {
);
}
}
// This machine: nowhere for traffic to go, unless the policy is
// the permissive one, for routers that share a host.
for ourselves in ["127.0.0.1", "192.0.2.1"] {
assert_eq!(
check(&policy, &v4(ourselves), &EXTERNAL),
Err(Rejection::NextHopUnusable),
"{ourselves}"
);
assert_eq!(
check(&lenient, &v4(ourselves), &EXTERNAL),
Ok(()),
"{ourselves}"
);
}
// Where it may be someone else: said so, a route server, our own AS.
assert_eq!(check(&lenient, &v4("192.0.2.3"), &EXTERNAL), Ok(()));
for from in [
+14 -40
View File
@@ -29,9 +29,9 @@ use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::table::{PeerIndex, RibTable};
use tokio::task::JoinHandle;
use tracing::{info, warn};
use crate::address::PeerAddress;
use crate::rpki::{by_rib_peer, group_by_peer, revalidate_all};
use crate::session::SessionRegistry;
/// How long after the interfaces change the table is looked through:
@@ -130,37 +130,18 @@ pub(crate) fn spawn_revalidator(
continue;
};
let sessions = registry.list();
// By peer, for looking up as often as there are routes.
let mut neighbors: Vec<Option<Neighbor>> = Vec::new();
for session in &sessions {
let at = session.rib_peer.as_usize();
if neighbors.len() <= at {
neighbors.resize(at + 1, None);
}
neighbors[at] = Some(neighbor_at(session.peer.addr));
}
let table = Arc::clone(&rib);
// Looks at every route: not for a thread that sessions run on.
let found =
tokio::task::spawn_blocking(move || now_unreachable(&table, &topology, &neighbors))
.await;
let mut suspects = match found {
Ok(suspects) => suspects,
Err(e) => {
warn!(err = %e, "Looking for routes whose next hop is gone failed");
HashMap::new()
}
};
info!(
now_unreachable = suspects.values().map(Vec::len).sum::<usize>(),
sessions = sessions.len(),
"The machine's interfaces or routes changed; looking at next hops again"
let neighbors = by_rib_peer(
sessions
.iter()
.map(|session| (session.rib_peer, neighbor_at(session.peer.addr))),
);
// Every session, also those with nothing found: each has its
// held routes to look at again.
for session in sessions {
session.revalidate(suspects.remove(&session.rib_peer).unwrap_or_default());
}
revalidate_all(
sessions,
&rib,
"The machine's interfaces or routes changed; looking at next hops again",
move |rib| now_unreachable(rib, &topology, &neighbors),
)
.await;
}
})
}
@@ -211,14 +192,7 @@ fn now_unreachable(
.is_ok()
})
});
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new();
for route in routes {
by_peer.entry(route.peer).or_default().push(Nlri {
prefix: route.prefix,
path_id: route.path_id,
});
}
by_peer
group_by_peer(routes)
}
#[cfg(test)]
@@ -370,7 +344,7 @@ mod tests {
}
fn best(&self, route: &str) -> Option<PeerIndex> {
self.table.best(&prefix(route)).map(|(peer, _)| peer)
self.table.best(&prefix(route)).map(|best| best.peer)
}
}
+31 -26
View File
@@ -244,6 +244,30 @@ pub struct Kept {
pub until: SystemTime,
}
impl Kept {
/// What was kept of a peer: `routes` routes of the IP versions
/// `versions`, under `rib_peer`, of the peer at `addr` called `name`.
/// Which restart they are of and until when they are kept are the
/// table's to say, in [`Restarts::keep`].
pub(crate) fn new(
addr: SocketAddr,
name: Option<String>,
rib_peer: PeerIndex,
versions: Vec<IpVersion>,
routes: usize,
) -> Self {
Self {
id: 0,
addr,
rib_peer,
name,
versions,
routes,
until: SystemTime::UNIX_EPOCH,
}
}
}
/// What there is of graceful restart while the server runs: the routes
/// kept of peers that are restarting, and how far the peers are with
/// sending their routes after this daemon started.
@@ -298,33 +322,14 @@ impl Restarts {
})
}
/// Notes that the routes of `peer` under `rib_peer` have been kept
/// (`routes` of them, of the IP versions `versions`) until `for_at_most`
/// from now, and has them removed then unless the peer is back.
#[allow(clippy::too_many_arguments)]
pub(crate) fn keep(
self: &Arc<Self>,
peer: PeerKey,
addr: SocketAddr,
name: Option<String>,
rib_peer: PeerIndex,
versions: Vec<IpVersion>,
routes: usize,
for_at_most: Duration,
) {
/// Notes that `kept` says what routes of `peer` have been kept, until
/// `for_at_most` from now, and has them removed then unless the peer
/// is back.
pub(crate) fn keep(self: &Arc<Self>, peer: PeerKey, mut kept: Kept, for_at_most: Duration) {
let id = self.ids.fetch_add(1, Ordering::Relaxed);
self.kept.locked().insert(
peer,
Kept {
id,
addr,
rib_peer,
name,
versions,
routes,
until: SystemTime::now() + for_at_most,
},
);
kept.id = id;
kept.until = SystemTime::now() + for_at_most;
self.kept.locked().insert(peer, kept);
let restarts = Arc::clone(self);
tokio::spawn(async move {
tokio::time::sleep(for_at_most).await;
+260 -1
View File
@@ -21,6 +21,14 @@ use crate::rpki::Checks;
/// than a peer sending the whole Internet has; a bound all the same.
const MAX_HELD: usize = 200_000;
/// Why an UPDATE could not be applied: the attributes its routes were to
/// be kept with could not be written in the form the table keeps them in.
/// Attributes that were decoded from a message always can be, so this is
/// a bug here, and not anything the peer did.
#[derive(Debug, thiserror::Error)]
#[error("could not keep the routes' attributes: {0}")]
pub struct Unstorable(#[from] pub(crate) EncodeError);
/// What applying an UPDATE did.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Applied {
@@ -113,7 +121,7 @@ pub(crate) fn apply(
held: &mut Held,
update: &UpdateMessage,
attributes_wire: Option<&[u8]>,
) -> Result<Applied, EncodeError> {
) -> Result<Applied, Unstorable> {
let mut applying = Applying {
table,
import,
@@ -598,3 +606,254 @@ pub(crate) fn revalidate(
}
applied
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use bgp_packet::message::{AsPathAttribute, NextHopPathAttribute, OriginPathAttribute};
use bgp_rib::table::PeerInfo;
use bgp_rpki::{Vrp, VrpTable};
use super::*;
use crate::rejections::RejectionLog;
const PEER_ASN: u32 = 65001;
const PEER_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
fn prefix(text: &str) -> IpPrefix {
IpPrefix::try_from(text).unwrap()
}
fn nlri(text: &str) -> Nlri {
prefix(text).into()
}
fn attrs_of(route: &str) -> Arc<AttrSet> {
RibTable::new(1)
.attrs()
.intern(&announcement(route).path_attributes, None)
.unwrap()
}
/// An UPDATE as the peer announces `route`, originating it itself.
fn announcement(route: &str) -> UpdateMessage {
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(PEER_ADDRESS)),
],
announced_routes: vec![nlri(route)],
}
}
/// VRPs that allow `route` to be originated by `asn` and nobody else.
fn vrps(route: &str, asn: u32) -> VrpTable {
let prefix = prefix(route);
VrpTable::new([Vrp {
prefix,
max_length: prefix.length(),
asn,
}])
}
/// A table with the peer in it, and what is needed to apply its
/// UPDATEs by a permissive policy, checking only against `vrps`.
struct Fixture {
table: RibTable,
peer: PeerIndex,
policy: ImportPolicy,
rejections: RejectionLog,
}
impl Fixture {
fn new() -> Self {
let table = RibTable::new(1);
let peer = table
.add_peer(PeerInfo {
asn: PEER_ASN,
router_id: Ipv4Addr::new(10, 0, 0, 2),
address: PEER_ADDRESS.into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap();
Self {
table,
peer,
policy: ImportPolicy::permissive(),
rejections: RejectionLog::default(),
}
}
fn with_import<T>(
&self,
vrps: Option<&VrpTable>,
held: &mut Held,
work: impl FnOnce(&RibTable, &Import<'_>, &mut Held) -> T,
) -> T {
let import = Import {
peer: self.peer,
from: RouteSource {
peer_asn: PEER_ASN,
local_asn: 65000,
ibgp: false,
four_octet_asn: true,
role: None,
peer_address: PEER_ADDRESS.into(),
local_address: Ipv4Addr::new(192, 0, 2, 9).into(),
extended_next_hop: false,
},
policy: &self.policy,
checks: Checks {
vrps,
..Checks::NONE
},
rejections: &self
.rejections
.of_peer(IpAddr::from(PEER_ADDRESS).into(), "test"),
};
work(&self.table, &import, held)
}
fn announce(&self, vrps: Option<&VrpTable>, held: &mut Held, route: &str) -> Applied {
self.with_import(vrps, held, |table, import, held| {
apply(table, import, held, &announcement(route), None).unwrap()
})
}
fn revalidate(
&self,
vrps: Option<&VrpTable>,
held: &mut Held,
suspects: &[Nlri],
) -> Applied {
self.with_import(vrps, held, |table, import, held| {
revalidate(table, import, held, suspects)
})
}
}
/// Past the bound, a new route is dropped and the overflow noted, so
/// that the peer can be asked for it later; one already held is still
/// replaced, since that takes no more room.
#[test]
fn holding_stops_at_the_bound_and_says_so() {
let mut held = Held::default();
let attrs = attrs_of("198.51.100.0/24");
for n in 0..MAX_HELD as u32 {
let [a, b, c, d] = (0x0a00_0000 + n).to_be_bytes();
held.hold(
IpPrefix::new(IpVersion::Ipv4, [a, b, c, d], 32)
.unwrap()
.into(),
&attrs,
);
}
assert_eq!(held.len(), MAX_HELD);
assert!(!held.take_overflowed());
held.hold(nlri("198.51.100.0/24"), &attrs);
assert_eq!(held.len(), MAX_HELD);
assert!(held.take_overflowed());
assert!(
!held.take_overflowed(),
"noted once, until it happens again"
);
let other = attrs_of("203.0.113.0/24");
held.hold(nlri("10.0.0.0/32"), &other);
assert_eq!(held.len(), MAX_HELD);
assert!(!held.take_overflowed());
assert!(Arc::ptr_eq(&held.routes[&nlri("10.0.0.0/32")], &other));
}
#[test]
fn forgetting_takes_out_what_is_named_and_nothing_else() {
let mut held = Held::default();
let attrs = attrs_of("198.51.100.0/24");
held.hold(nlri("198.51.100.0/24"), &attrs);
held.hold(nlri("203.0.113.0/24"), &attrs);
held.forget(&[nlri("198.51.100.0/24"), nlri("192.0.2.0/24")]);
assert_eq!(held.len(), 1);
assert!(held.routes.contains_key(&nlri("203.0.113.0/24")));
held.forget(&[nlri("203.0.113.0/24")]);
held.forget(&[nlri("203.0.113.0/24")]);
assert_eq!(held.len(), 0);
}
/// A route the VRPs find invalid stays out of the table but is not
/// lost: when the VRPs change in its favour it goes in, without the
/// peer having to send it again.
#[test]
fn a_held_route_is_released_once_the_checks_pass_it() {
let fixture = Fixture::new();
let mut held = Held::default();
let route = "198.51.100.0/24";
let against = vrps(route, PEER_ASN + 1);
let applied = fixture.announce(Some(&against), &mut held, route);
assert_eq!(applied.rejected, 1);
assert_eq!(applied.outcome.added, 0);
assert!(fixture.table.is_empty());
assert_eq!(held.len(), 1);
// Nothing has changed in the route's favour yet.
let applied = fixture.revalidate(Some(&against), &mut held, &[]);
assert_eq!(applied, Applied::default());
assert_eq!(held.len(), 1);
let for_it = vrps(route, PEER_ASN);
let applied = fixture.revalidate(Some(&for_it), &mut held, &[]);
assert_eq!(applied.outcome.added, 1);
assert_eq!(applied.rejected, 0);
assert_eq!(held.len(), 0);
assert_eq!(fixture.table.routes(&prefix(route)).len(), 1);
}
/// The other way round: a route in the table that the VRPs come to
/// find invalid is taken out and held, and comes back once they
/// change again.
#[test]
fn a_route_turned_invalid_is_held_and_comes_back() {
let fixture = Fixture::new();
let mut held = Held::default();
let route = "198.51.100.0/24";
let for_it = vrps(route, PEER_ASN);
let applied = fixture.announce(Some(&for_it), &mut held, route);
assert_eq!(applied.outcome.added, 1);
assert_eq!(fixture.table.len(), 1);
let against = vrps(route, PEER_ASN + 1);
let applied = fixture.revalidate(Some(&against), &mut held, &[nlri(route)]);
assert_eq!(applied.rejected, 1);
assert_eq!(applied.outcome.removed, 1);
assert!(fixture.table.is_empty());
assert_eq!(held.len(), 1);
let applied = fixture.revalidate(Some(&for_it), &mut held, &[]);
assert_eq!(applied.outcome.added, 1);
assert_eq!(held.len(), 0);
assert_eq!(fixture.table.len(), 1);
}
/// Without anything to check against there is nothing to look at
/// again: what is held stays held, and what is in the table stays.
#[test]
fn revalidation_without_checks_changes_nothing() {
let fixture = Fixture::new();
let mut held = Held::default();
let route = "198.51.100.0/24";
fixture.announce(Some(&vrps(route, PEER_ASN + 1)), &mut held, route);
assert_eq!(held.len(), 1);
let applied = fixture.revalidate(None, &mut held, &[nlri(route)]);
assert_eq!(applied, Applied::default());
assert_eq!(held.len(), 1);
assert!(fixture.table.is_empty());
}
}
+203 -66
View File
@@ -22,7 +22,7 @@ use std::sync::Arc;
use std::time::Duration;
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::table::{AttrSet, PeerIndex, RibTable};
use bgp_rib::table::{AttrSet, PeerIndex, RibTable, Route};
use bgp_rpki::{Algorithm, AspaTable, Changed, PathValidity, Rpki, Update, Validity, VrpTable};
use tokio::task::JoinHandle;
use tracing::{info, warn};
@@ -239,52 +239,92 @@ pub(crate) fn spawn_revalidator(
RPKI_UPDATES.inc();
tokio::time::sleep(SETTLE).await;
let sessions: Vec<(SessionHandle, Subject)> = registry
// The sessions whose routes are subject to the checks at all.
let sessions: Vec<SessionHandle> = registry
.list()
.into_iter()
.filter_map(|session| {
let subject = session.validated()?;
Some((session, subject))
})
.filter(|session| session.validated().is_some())
.collect();
// By peer, for looking up as often as there are routes.
let mut subjects: Vec<Option<Subject>> = Vec::new();
for (session, subject) in &sessions {
let at = session.rib_peer.as_usize();
if subjects.len() <= at {
subjects.resize(at + 1, None);
}
subjects[at] = Some(*subject);
}
let (vrps, aspas) = (update.vrps.len(), update.aspas.len());
let rib = Arc::clone(&rib);
// Looks at every route, or at every prefix at least: not for a
// thread that sessions run on.
let found =
tokio::task::spawn_blocking(move || newly_invalid(&rib, &update, &subjects)).await;
let mut suspects = match found {
Ok(suspects) => suspects,
Err(e) => {
warn!(err = %e, "Looking for routes that are now invalid failed");
HashMap::new()
}
};
info!(
vrps,
aspas,
now_invalid = suspects.values().map(Vec::len).sum::<usize>(),
sessions = sessions.len(),
"RPKI data changed; revalidating routes"
let subjects = by_rib_peer(
sessions
.iter()
.filter_map(|session| Some((session.rib_peer, session.validated()?))),
);
// Every session, also those with nothing found: each has its
// held routes to look at again.
for (session, _) in sessions {
session.revalidate(suspects.remove(&session.rib_peer).unwrap_or_default());
}
let what = format!(
"RPKI data changed, to {} VRPs and {} ASPAs; revalidating routes",
update.vrps.len(),
update.aspas.len()
);
revalidate_all(sessions, &rib, &what, move |rib| {
newly_invalid(rib, &update, &subjects)
})
.await;
}
})
}
/// `what` of each session, by its peer's number in the table, for looking
/// up as often as there are routes; `None` where no session has that
/// number.
pub(crate) fn by_rib_peer<T>(sessions: impl IntoIterator<Item = (PeerIndex, T)>) -> Vec<Option<T>> {
let mut by_peer: Vec<Option<T>> = Vec::new();
for (peer, what) in sessions {
let at = peer.as_usize();
if by_peer.len() <= at {
by_peer.resize_with(at + 1, || None);
}
by_peer[at] = Some(what);
}
by_peer
}
/// `routes` as what each session is to take out of the table, by peer.
pub(crate) fn group_by_peer(
routes: impl IntoIterator<Item = Route>,
) -> HashMap<PeerIndex, Vec<Nlri>> {
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new();
for route in routes {
by_peer.entry(route.peer).or_default().push(Nlri {
prefix: route.prefix,
path_id: route.path_id,
});
}
by_peer
}
/// One round of revalidation, for whichever data changed: `find` looks
/// through the table for the routes that are to go, by peer, and every one
/// of `sessions` is told of its own, also those with nothing found, since
/// each has its held routes to look at again. `what` is for the log.
pub(crate) async fn revalidate_all<F>(
sessions: Vec<SessionHandle>,
rib: &Arc<RibTable>,
what: &str,
find: F,
) where
F: FnOnce(&RibTable) -> HashMap<PeerIndex, Vec<Nlri>> + Send + 'static,
{
let table = Arc::clone(rib);
// Looks at every route, or at every prefix at least: not for a thread
// that sessions run on.
let found = tokio::task::spawn_blocking(move || find(&table)).await;
let mut suspects = match found {
Ok(suspects) => suspects,
Err(e) => {
warn!(err = %e, "Looking through the table for routes to take out failed");
HashMap::new()
}
};
info!(
found = suspects.values().map(Vec::len).sum::<usize>(),
sessions = sessions.len(),
"{what}"
);
for session in sessions {
session.revalidate(suspects.remove(&session.rib_peer).unwrap_or_default());
}
}
/// The routes in the table that `data` finds invalid, by peer. `subjects`
/// says, for each peer by its number in the table, which checks its routes
/// are subject to.
@@ -306,42 +346,54 @@ fn newly_invalid(
path.iter().any(|asn| changed.aspa_customers.contains(asn))
};
let path = RefCell::new(Vec::new());
// What each peer's routes are checked against: the data, for the
// checks its routes are subject to.
let checks: Vec<Option<Checks<'_>>> = subjects
.iter()
.map(|subject| {
subject.map(|subject| Checks {
vrps: subject.origin.then_some(&*data.vrps),
aspas: subject.path.map(|check| (&*data.aspas, check)),
next_hops: None,
})
})
.collect();
let routes = rib.routes_where(usize::MAX, |route| {
let Some(Some(subject)) = subjects.get(route.peer.as_usize()) else {
let Some(Some(checks)) = checks.get(route.peer.as_usize()) else {
return false;
};
if subject.origin
&& vrps_may_matter
&& changed.is_none_or(|changed| changed.vrps.covers(&route.prefix))
{
let origin = route.attrs.key().origin();
if data.vrps.validate(&route.prefix, origin) == Validity::Invalid {
return true;
}
}
if let Some(check) = subject.path
&& aspas_may_matter
{
let path = &mut *path.borrow_mut();
let has_set = route.attrs.as_path(path);
return changed.is_none_or(|changed| on_changed_path(changed, path))
&& check.verify(&data.aspas, path, has_set) == PathValidity::Invalid;
}
false
let path = &mut *path.borrow_mut();
// Only the checks whose verdict the change can have altered for
// this route are made.
let checks = Checks {
vrps: checks.vrps.filter(|_| {
vrps_may_matter && changed.is_none_or(|changed| changed.vrps.covers(&route.prefix))
}),
aspas: checks.aspas.filter(|_| {
aspas_may_matter
&& changed.is_none_or(|changed| {
route.attrs.as_path(path);
on_changed_path(changed, path)
})
}),
next_hops: None,
};
checks.rejection(&route.prefix, route.attrs, path).is_some()
});
let mut by_peer: HashMap<PeerIndex, Vec<Nlri>> = HashMap::new();
for route in routes {
by_peer.entry(route.peer).or_default().push(Nlri {
prefix: route.prefix,
path_id: route.path_id,
});
}
by_peer
group_by_peer(routes)
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use bgp_packet::message::{
AsPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
};
use bgp_rib::table::PeerInfo;
use bgp_rpki::Vrp;
use super::*;
fn aspas() -> AspaTable {
@@ -463,4 +515,89 @@ mod tests {
})
);
}
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// Where it is known which VRPs came or went, only the routes those
/// cover are looked at: the others are as valid or invalid as they
/// were, and were dealt with then. Without that knowledge every route
/// is looked at; and a peer whose routes are not subject to the check
/// has none found.
#[test]
fn only_routes_a_change_can_have_made_invalid_are_found() {
let table = RibTable::new(2);
let peer = table
.add_peer(PeerInfo {
asn: 65001,
router_id: Ipv4Addr::new(10, 0, 0, 1),
address: Ipv4Addr::new(192, 0, 2, 1).into(),
interface: 0,
ibgp: false,
reflector_client: false,
})
.unwrap();
let announce = |route: &str, origin: u32| {
let attrs = table
.attrs()
.intern(
&[
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![65001, origin]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
],
None,
)
.unwrap();
table.announce(peer, &attrs, &[prefix(route)]);
};
announce("10.0.0.0/24", 65010);
announce("10.1.0.0/24", 65011);
// Both prefixes are someone else's to originate.
let vrp = |covered: &str| Vrp {
prefix: prefix(covered),
max_length: 24,
asn: 65009,
};
let update = Update {
version: 2,
vrps: Arc::new(VrpTable::new([vrp("10.0.0.0/24"), vrp("10.1.0.0/24")])),
aspas: Arc::new(AspaTable::default()),
changed: Some(Changed {
vrps: Arc::new(VrpTable::new([vrp("10.1.0.0/24")])),
aspa_customers: Arc::default(),
}),
};
let origins = by_rib_peer([(
peer,
Subject {
origin: true,
path: None,
},
)]);
let found = newly_invalid(&table, &update, &origins);
assert_eq!(
found.get(&peer).map(Vec::as_slice),
Some(&[Nlri::from(prefix("10.1.0.0/24"))][..])
);
let whole = Update {
changed: None,
..update
};
let found = newly_invalid(&table, &whole, &origins);
let routes = found.get(&peer).expect("the peer's routes");
assert_eq!(routes.len(), 2);
assert!(routes.contains(&Nlri::from(prefix("10.0.0.0/24"))));
let paths_only = by_rib_peer([(
peer,
Subject {
origin: false,
path: None,
},
)]);
assert!(newly_invalid(&table, &whole, &paths_only).is_empty());
}
}
+5 -4
View File
@@ -237,7 +237,7 @@ impl BgpServer {
};
match result {
Ok((stream, peer_addr)) => {
match admit(&self.config, &context, peer_addr.into(), &connections) {
match screen(&self.config, &context, peer_addr.into(), &connections) {
Ok((peer, slot)) => {
let context = Arc::clone(&context);
tokio::spawn(serve(stream, peer_addr, context, peer, slot));
@@ -461,9 +461,10 @@ fn listen(addr: SocketAddr) -> io::Result<TcpListener> {
socket.listen(LISTEN_BACKLOG)
}
/// Decides whether to take a connection from `address`, and if so with what
/// settings.
fn admit(
/// Screens a connection from `address`: whether to take it, and if so
/// with what settings. Not the collision check, which is
/// [`crate::collision::CollisionTable::admit`].
fn screen(
config: &Config,
context: &ServerContext,
address: PeerAddress,
+24 -9
View File
@@ -17,6 +17,7 @@ use std::time::{Duration, SystemTime};
use bgp_packet::message::Message;
use bgp_primitives::ip_prefix::Nlri;
use bgp_rib::table::PeerIndex;
use bytes::BytesMut;
use tokio::sync::{broadcast, mpsc, watch};
use crate::address::{Endpoint, PeerAddress, socket_text};
@@ -199,7 +200,7 @@ pub struct SessionHandle {
pub reflector_client: bool,
/// The peer's BGP Identifier.
pub peer_router_id: Ipv4Addr,
outbound: mpsc::Sender<Message>,
outbound: mpsc::Sender<Outbound>,
/// Where the session is asked to end, if it listens for that.
ending: Option<mpsc::UnboundedSender<SessionError>>,
/// Which of the RPKI's checks the peer's routes are subject to, if
@@ -219,7 +220,7 @@ impl SessionHandle {
local_addr: SocketAddr,
negotiated: &Negotiated,
rib_peer: PeerIndex,
outbound: mpsc::Sender<Message>,
outbound: mpsc::Sender<Outbound>,
) -> Self {
Self {
peer,
@@ -294,8 +295,6 @@ impl SessionHandle {
}
}
/// Whether `other` is a handle to this same session, and not to another
/// session with the same peer.
/// What the peer goes by in the metrics: its configured name, or its
/// address.
pub fn metric_label(&self) -> String {
@@ -304,6 +303,8 @@ impl SessionHandle {
.unwrap_or_else(|| crate::address::PeerAddress::from(self.peer.addr).to_string())
}
/// Whether `other` is a handle to this same session, and not to another
/// session with the same peer.
pub fn is_same_session(&self, other: &SessionHandle) -> bool {
self.outbound.same_channel(&other.outbound)
}
@@ -312,10 +313,28 @@ impl SessionHandle {
/// space if the session's writer is behind.
pub async fn send(&self, msg: Message) -> Result<(), SessionClosed> {
self.outbound
.send(msg)
.send(Outbound::Message(msg))
.await
.map_err(|_| SessionClosed(self.peer))
}
/// Sends the peer a piece of one or more UPDATEs that are already
/// encoded, headers and all, as the exporter builds them.
pub async fn send_encoded(&self, piece: BytesMut) -> Result<(), SessionClosed> {
self.outbound
.send(Outbound::Encoded(piece))
.await
.map_err(|_| SessionClosed(self.peer))
}
}
/// What the rest of the server gives a session to send: a message, or a
/// piece of one or more UPDATEs already encoded, headers and all, which
/// goes to the peer as it is once the session is established.
#[derive(Debug)]
pub enum Outbound {
Message(Message),
Encoded(BytesMut),
}
#[derive(Debug, Clone)]
@@ -441,10 +460,6 @@ impl SessionRegistry {
self.shutting_down.load(Ordering::Relaxed)
}
/// Closes every session politely: each peer is sent Cease /
/// Administrative Shutdown (RFC 4486) so it can drop the session at once
/// instead of discovering a dead TCP connection. Waits up to `grace` for
/// the sessions to finish; returns how many were still open.
/// Closes every connection because the daemon is going, as `leave`
/// says: for good, or to restart (see [`Leave`]). Waits up to `grace`
/// for the sessions to finish; returns how many were still open.
+241 -183
View File
@@ -9,6 +9,7 @@ use axum::Json;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use bgp_fib::sockdiag::TcpConnection;
use bgp_packet::community;
use bgp_packet::message::{
AsPathAttribute, ExtendedCommunity, NlriNextHop, OriginPathAttribute, PathAttribute,
@@ -18,7 +19,8 @@ use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::table::{AttrSet, PeerIndex, RibTable, Route, RouteRef, SelectionKey};
use bgp_rib::{LocalRoute, RibStopped};
use bgp_rpki::{CacheState, Update};
use bgp_server::address::{PeerAddress, socket_text};
use bgp_server::address::{names_peer, socket_text};
use bgp_server::family::FamilySet;
use bgp_server::policy::PeerRole;
use bgp_server::restart::{Agreed, Restarts};
use bgp_server::rpki::PathCheck;
@@ -148,51 +150,46 @@ impl From<&Agreed> for GracefulRestartView {
impl From<&SessionHandle> for PeerView {
fn from(s: &SessionHandle) -> Self {
let families =
|families: &FamilySet| Some(families.iter().map(|f| f.to_string()).collect());
Self {
name: s.name.clone(),
peer_addr: socket_text(s.peer.addr),
peer_asn: s.peer.asn,
state: "Established".into(),
direction: Some(s.direction.as_str().into()),
local_addr: Some(socket_text(s.local_addr)),
established_at_unix: Some(unix_secs(s.established_at)),
uptime_secs: Some(s.established_at.elapsed().map(|d| d.as_secs()).unwrap_or(0)),
four_octet_asn: Some(s.four_octet_asn),
extended_next_hop: Some(s.extended_next_hop),
address_families: Some(s.families.iter().map(|f| f.to_string()).collect()),
add_path_receive: Some(s.add_path_receive.iter().map(|f| f.to_string()).collect()),
add_path_send: Some(s.add_path_send.iter().map(|f| f.to_string()).collect()),
address_families: families(&s.families),
add_path_receive: families(&s.add_path_receive),
add_path_send: families(&s.add_path_send),
routes: Some(s.counters.routes()),
rejected_routes: Some(s.counters.rejected()),
advertised_routes: Some(s.counters.advertised()),
held_routes: Some(s.counters.held()),
expected_routes: None,
role: s.role.map(PeerRole::as_str),
role_confirmed: s.role.map(|_| s.role_confirmed),
last_error: None,
retry_at_unix: None,
graceful_restart: s.graceful_restart.as_ref().map(GracefulRestartView::from),
stale_routes: Some(s.counters.stale()).filter(|stale| *stale > 0),
stale_until_unix: None,
..Self::known(
s.name.clone(),
socket_text(s.peer.addr),
s.peer.asn,
"Established",
)
}
}
}
impl PeerView {
fn from_configured(peer: &ConfiguredPeer, state: &DialState) -> Self {
let (state_name, last_error, retry_at_unix) = match state {
DialState::Idle => ("Idle", None, None),
DialState::Connecting => ("Connecting", None, None),
DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
DialState::Passive { last_error } => ("Passive", last_error.clone(), None),
};
/// What is known of any peer, with or without a session: who it is
/// and where it stands. Everything else is `None`, for the caller to
/// fill in what it knows besides.
fn known(name: Option<String>, peer_addr: String, peer_asn: u32, state: &str) -> Self {
Self {
name: Some(peer.name.clone()),
peer_addr: peer.endpoint.with_port(),
peer_asn: peer.remote_asn,
state: state_name.into(),
name,
peer_addr,
peer_asn,
state: state.into(),
direction: None,
local_addr: None,
established_at_unix: None,
@@ -206,16 +203,39 @@ impl PeerView {
rejected_routes: None,
advertised_routes: None,
held_routes: None,
expected_routes: peer.expected_routes,
role: peer.role.map(PeerRole::as_str),
expected_routes: None,
role: None,
role_confirmed: None,
last_error,
retry_at_unix,
last_error: None,
retry_at_unix: None,
graceful_restart: None,
stale_routes: None,
stale_until_unix: None,
}
}
fn from_configured(peer: &ConfiguredPeer, state: &DialState) -> Self {
let (state_name, last_error, retry_at_unix) = match state {
DialState::Idle => ("Idle", None, None),
DialState::Connecting => ("Connecting", None, None),
DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
DialState::Passive { last_error } => ("Passive", last_error.clone(), None),
};
Self {
expected_routes: peer.expected_routes,
role: peer.role.map(PeerRole::as_str),
last_error,
retry_at_unix,
..Self::known(
Some(peer.name.clone()),
peer.endpoint.with_port(),
peer.remote_asn,
state_name,
)
}
}
}
/// Configured peers first (with their live session when established), then
@@ -541,9 +561,7 @@ impl PeerNames {
fn matching(&self, wanted: &str) -> Vec<PeerIndex> {
self.peers
.iter()
.filter(|(_, (peer, name, _))| {
name.as_deref() == Some(wanted) || PeerAddress::from(peer.addr).is_called(wanted)
})
.filter(|(_, (peer, name, _))| names_peer(wanted, name.as_deref(), peer.addr.into()))
.map(|(index, _)| *index)
.collect()
}
@@ -1226,7 +1244,7 @@ pub async fn rejected_routes(
query
.peer
.as_deref()
.is_none_or(|wanted| record.label() == wanted || address.is_called(wanted))
.is_none_or(|wanted| names_peer(wanted, Some(record.label()), *address))
})
.map(|(address, record)| RejectionsView {
peer: record.label().to_string(),
@@ -1256,6 +1274,168 @@ pub async fn rejected_routes(
Json(peers)
}
/// `GET /api/tcp?peer=NAME`: what the kernel knows of the TCP connection of
/// the session with that peer.
#[derive(Debug, Deserialize)]
pub struct TcpQuery {
/// The peer whose session it is: its configured name, or its address.
/// The address with its port, as `peer_addr` of `/api/peers` has it,
/// names it too.
pub peer: String,
}
/// A session's TCP connection, as the kernel reports it (Linux only).
/// Times in milliseconds, sizes in octets, rates in octets per second.
#[derive(Debug, Serialize)]
pub struct TcpView {
pub state: &'static str,
pub congestion: Option<String>,
pub rtt_ms: f64,
pub rtt_variance_ms: f64,
pub min_rtt_ms: f64,
pub rto_ms: f64,
pub mss: u32,
pub pmtu: u32,
pub cwnd_segments: u32,
pub ssthresh_segments: Option<u32>,
/// The peer's receive window: what it lets us send.
pub send_window: u32,
/// Our receive window: what we let the peer send.
pub receive_window: u32,
pub receive_space: u32,
pub send_window_scale: u8,
pub receive_window_scale: u8,
/// Octets written by us and not yet acknowledged, and of those the
/// octets not even sent yet.
pub send_queue: u32,
pub not_sent: u32,
pub recv_queue: u32,
pub unacked_segments: u32,
pub retrans_segments: u32,
pub lost_segments: u32,
pub total_retrans: u32,
pub bytes_retrans: u64,
pub pacing_rate: u64,
pub delivery_rate: u64,
pub bytes_sent: u64,
pub bytes_acked: u64,
pub bytes_received: u64,
pub segments_out: u32,
pub segments_in: u32,
/// How long the connection had something to send, and of that how
/// long it was held up by the peer's window and by our send buffer.
pub busy_ms: f64,
pub limited_by_receive_window_ms: f64,
pub limited_by_send_buffer_ms: f64,
/// The same two, as shares of the busy time, 0 to 1.
pub limited_by_receive_window_share: f64,
pub limited_by_send_buffer_share: f64,
pub last_data_sent_ms: u32,
pub last_data_received_ms: u32,
pub last_ack_received_ms: u32,
}
impl From<&TcpConnection> for TcpView {
fn from(c: &TcpConnection) -> Self {
// The kernel's times are in microseconds.
let ms = |us: u64| us as f64 / 1000.0;
// A connection that was never busy was never held up: a share of
// nothing is nothing, not a division by zero.
let share = |part: u64| {
if c.busy == 0 {
0.0
} else {
part as f64 / c.busy as f64
}
};
Self {
state: c.state_name(),
congestion: c.congestion.clone(),
rtt_ms: ms(c.rtt.into()),
rtt_variance_ms: ms(c.rtt_variance.into()),
min_rtt_ms: ms(c.min_rtt.into()),
rto_ms: ms(c.rto.into()),
mss: c.mss,
pmtu: c.pmtu,
cwnd_segments: c.cwnd,
ssthresh_segments: c.ssthresh,
send_window: c.send_window,
receive_window: c.receive_window,
receive_space: c.receive_space,
send_window_scale: c.send_window_scale,
receive_window_scale: c.receive_window_scale,
send_queue: c.send_queue,
not_sent: c.not_sent,
recv_queue: c.recv_queue,
unacked_segments: c.unacked,
retrans_segments: c.retrans,
lost_segments: c.lost,
total_retrans: c.total_retrans,
bytes_retrans: c.bytes_retrans,
pacing_rate: c.pacing_rate,
delivery_rate: c.delivery_rate,
bytes_sent: c.bytes_sent,
bytes_acked: c.bytes_acked,
bytes_received: c.bytes_received,
segments_out: c.segments_out,
segments_in: c.segments_in,
busy_ms: ms(c.busy),
limited_by_receive_window_ms: ms(c.limited_by_receive_window),
limited_by_send_buffer_ms: ms(c.limited_by_send_buffer),
limited_by_receive_window_share: share(c.limited_by_receive_window),
limited_by_send_buffer_share: share(c.limited_by_send_buffer),
last_data_sent_ms: c.last_data_sent_ms,
last_data_received_ms: c.last_data_received_ms,
last_ack_received_ms: c.last_ack_received_ms,
}
}
}
/// The answer to `/api/tcp`.
#[derive(Debug, Serialize)]
pub struct TcpAnswer {
/// Whether the kernel could be asked and knew the connection.
pub available: bool,
/// Why not, if not.
pub reason: Option<String>,
pub connection: Option<TcpView>,
}
pub async fn tcp(
State(state): State<AppState>,
Query(query): Query<TcpQuery>,
) -> Result<Json<TcpAnswer>, QueryError> {
let session = state
.registry
.list()
.into_iter()
.find(|s| {
names_peer(&query.peer, s.name.as_deref(), s.peer.addr.into())
|| socket_text(s.peer.addr) == query.peer
})
.ok_or_else(|| QueryError::Bad(format!("no session with {}", query.peer)))?;
let unavailable = |reason: String| TcpAnswer {
available: false,
reason: Some(reason),
connection: None,
};
let answer =
match bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await {
Ok(Some(c)) => TcpAnswer {
available: true,
reason: None,
connection: Some(TcpView::from(&c)),
},
Ok(None) => unavailable(if cfg!(target_os = "linux") {
"the kernel has no such connection".to_string()
} else {
"only on Linux".to_string()
}),
Err(e) => unavailable(format!("the kernel could not be asked: {e}")),
};
Ok(Json(answer))
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
@@ -1580,156 +1760,34 @@ mod tests {
.map(|(name, value)| (name.to_string(), value.to_string()));
assert_eq!(other, expected);
}
}
/// `GET /api/tcp?peer=ADDRESS:PORT`: what the kernel knows of the TCP
/// connection of the session with that peer, as `/api/peers` names it.
#[derive(Debug, Deserialize)]
pub struct TcpQuery {
pub peer: String,
}
/// A session's TCP connection, as the kernel reports it (Linux only).
/// Times in milliseconds, sizes in octets, rates in octets per second.
#[derive(Debug, Serialize)]
pub struct TcpView {
pub state: &'static str,
pub congestion: Option<String>,
pub rtt_ms: f64,
pub rtt_variance_ms: f64,
pub min_rtt_ms: f64,
pub rto_ms: f64,
pub mss: u32,
pub pmtu: u32,
pub cwnd_segments: u32,
pub ssthresh_segments: Option<u32>,
/// The peer's receive window: what it lets us send.
pub send_window: u32,
/// Our receive window: what we let the peer send.
pub receive_window: u32,
pub receive_space: u32,
pub send_window_scale: u8,
pub receive_window_scale: u8,
/// Octets written by us and not yet acknowledged, and of those the
/// octets not even sent yet.
pub send_queue: u32,
pub not_sent: u32,
pub recv_queue: u32,
pub unacked_segments: u32,
pub retrans_segments: u32,
pub lost_segments: u32,
pub total_retrans: u32,
pub bytes_retrans: u64,
pub pacing_rate: u64,
pub delivery_rate: u64,
pub bytes_sent: u64,
pub bytes_acked: u64,
pub bytes_received: u64,
pub segments_out: u32,
pub segments_in: u32,
/// How long the connection had something to send, and of that how
/// long it was held up by the peer's window and by our send buffer.
pub busy_ms: f64,
pub limited_by_receive_window_ms: f64,
pub limited_by_send_buffer_ms: f64,
/// The same two, as shares of the busy time, 0 to 1.
pub limited_by_receive_window_share: f64,
pub limited_by_send_buffer_share: f64,
pub last_data_sent_ms: u32,
pub last_data_received_ms: u32,
pub last_ack_received_ms: u32,
}
/// The answer to `/api/tcp`.
#[derive(Debug, Serialize)]
pub struct TcpAnswer {
/// Whether the kernel could be asked and knew the connection.
pub available: bool,
/// Why not, if not.
pub reason: Option<String>,
pub connection: Option<TcpView>,
}
pub async fn tcp(
State(state): State<AppState>,
Query(query): Query<TcpQuery>,
) -> Result<Json<TcpAnswer>, QueryError> {
let session = state
.registry
.list()
.into_iter()
.find(|s| socket_text(s.peer.addr) == query.peer)
.ok_or_else(|| QueryError::Bad(format!("no session with {}", query.peer)))?;
let answer =
match bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await {
Ok(Some(c)) => {
let ms = |us: u64| us as f64 / 1000.0;
let share = |part: u64| {
if c.busy == 0 {
0.0
} else {
part as f64 / c.busy as f64
}
};
TcpAnswer {
available: true,
reason: None,
connection: Some(TcpView {
state: c.state_name(),
congestion: c.congestion.clone(),
rtt_ms: ms(c.rtt.into()),
rtt_variance_ms: ms(c.rtt_variance.into()),
min_rtt_ms: ms(c.min_rtt.into()),
rto_ms: ms(c.rto.into()),
mss: c.mss,
pmtu: c.pmtu,
cwnd_segments: c.cwnd,
ssthresh_segments: c.ssthresh,
send_window: c.send_window,
receive_window: c.receive_window,
receive_space: c.receive_space,
send_window_scale: c.send_window_scale,
receive_window_scale: c.receive_window_scale,
send_queue: c.send_queue,
not_sent: c.not_sent,
recv_queue: c.recv_queue,
unacked_segments: c.unacked,
retrans_segments: c.retrans,
lost_segments: c.lost,
total_retrans: c.total_retrans,
bytes_retrans: c.bytes_retrans,
pacing_rate: c.pacing_rate,
delivery_rate: c.delivery_rate,
bytes_sent: c.bytes_sent,
bytes_acked: c.bytes_acked,
bytes_received: c.bytes_received,
segments_out: c.segments_out,
segments_in: c.segments_in,
busy_ms: ms(c.busy),
limited_by_receive_window_ms: ms(c.limited_by_receive_window),
limited_by_send_buffer_ms: ms(c.limited_by_send_buffer),
limited_by_receive_window_share: share(c.limited_by_receive_window),
limited_by_send_buffer_share: share(c.limited_by_send_buffer),
last_data_sent_ms: c.last_data_sent_ms,
last_data_received_ms: c.last_data_received_ms,
last_ack_received_ms: c.last_ack_received_ms,
}),
}
}
Ok(None) => TcpAnswer {
available: false,
reason: Some(if cfg!(target_os = "linux") {
"the kernel has no such connection".to_string()
} else {
"only on Linux".to_string()
}),
connection: None,
},
Err(e) => TcpAnswer {
available: false,
reason: Some(format!("the kernel could not be asked: {e}")),
connection: None,
},
/// The kernel's microseconds are shown as milliseconds, and the time
/// held up as a share of the busy time; a connection that was never
/// busy has a share of zero, not of NaN.
#[test]
fn a_tcp_view_has_milliseconds_and_shares_of_the_busy_time() {
let idle = TcpConnection {
rtt: 1500,
busy: 0,
limited_by_receive_window: 0,
..TcpConnection::default()
};
Ok(Json(answer))
let view = TcpView::from(&idle);
assert_eq!(view.rtt_ms, 1.5);
assert_eq!(view.busy_ms, 0.0);
assert_eq!(view.limited_by_receive_window_share, 0.0);
assert_eq!(view.limited_by_send_buffer_share, 0.0);
let busy = TcpConnection {
busy: 4_000_000,
limited_by_receive_window: 1_000_000,
limited_by_send_buffer: 3_000_000,
..TcpConnection::default()
};
let view = TcpView::from(&busy);
assert_eq!(view.busy_ms, 4000.0);
assert_eq!(view.limited_by_receive_window_ms, 1000.0);
assert_eq!(view.limited_by_receive_window_share, 0.25);
assert_eq!(view.limited_by_send_buffer_share, 0.75);
}
}
+2 -7
View File
@@ -28,11 +28,6 @@ use serde::Serialize;
use tokio::net::TcpListener;
use tracing::info;
pub use api::{
AttrSummary, ConfigView, KernelView, LocalRouteView, PeerView, ReceivedRouteView,
RejectedRouteView, RejectionsView, RoutesPage, RpkiView, StatusView,
};
/// Static facts about the router shown in status output.
#[derive(Debug, Clone)]
pub struct RouterInfo {
@@ -98,8 +93,8 @@ impl RouterInfo {
}
}
/// Best-effort host name without pulling in a dependency: ask the `hostname`
/// utility, which exists on every platform we run on.
/// The machine's host name as the kernel has it, for display; a machine
/// without one, or with one that is not text, is "unknown host".
fn hostname() -> String {
nix::unistd::gethostname()
.ok()
+4 -65
View File
@@ -63,70 +63,9 @@ async fn sample_tcp(state: &AppState) {
let label = session.metric_label();
let connection =
bgp_fib::sockdiag::tcp_connection(session.local_addr, session.peer.addr).await;
let Ok(Some(c)) = connection else {
tcp.remove(&label);
continue;
};
let seconds = |us: u64| us as f64 / 1e6;
let int = |n: u64| i64::try_from(n).unwrap_or(i64::MAX);
let label = [label.as_str()];
tcp.rtt_seconds
.with_label_values(&label)
.set(seconds(c.rtt.into()));
tcp.min_rtt_seconds
.with_label_values(&label)
.set(seconds(c.min_rtt.into()));
tcp.rtt_variance_seconds
.with_label_values(&label)
.set(seconds(c.rtt_variance.into()));
tcp.cwnd_segments
.with_label_values(&label)
.set(c.cwnd.into());
tcp.mss_bytes.with_label_values(&label).set(c.mss.into());
tcp.send_window_bytes
.with_label_values(&label)
.set(c.send_window.into());
tcp.receive_window_bytes
.with_label_values(&label)
.set(c.receive_window.into());
tcp.send_queue_bytes
.with_label_values(&label)
.set(c.send_queue.into());
tcp.not_sent_bytes
.with_label_values(&label)
.set(c.not_sent.into());
tcp.receive_queue_bytes
.with_label_values(&label)
.set(c.recv_queue.into());
tcp.unacked_segments
.with_label_values(&label)
.set(c.unacked.into());
tcp.retransmitted_segments
.with_label_values(&label)
.set(c.total_retrans.into());
tcp.lost_segments
.with_label_values(&label)
.set(c.lost.into());
tcp.busy_seconds
.with_label_values(&label)
.set(seconds(c.busy));
tcp.receive_window_limited_seconds
.with_label_values(&label)
.set(seconds(c.limited_by_receive_window));
tcp.send_buffer_limited_seconds
.with_label_values(&label)
.set(seconds(c.limited_by_send_buffer));
tcp.sent_bytes
.with_label_values(&label)
.set(int(c.bytes_sent));
tcp.received_bytes
.with_label_values(&label)
.set(int(c.bytes_received));
tcp.delivery_rate_bytes_per_second
.with_label_values(&label)
.set(int(c.delivery_rate));
tcp.pacing_rate_bytes_per_second
.with_label_values(&label)
.set(int(c.pacing_rate));
match connection {
Ok(Some(c)) => tcp.set(&label, &c),
_ => tcp.remove(&label),
}
}
}
+42 -28
View File
@@ -68,12 +68,15 @@ table.data th { background: var(--head); font-weight: bold; position: sticky; to
table.data > tbody > tr:hover > td { background: var(--hover); }
table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
table.data tr.group > td { background: var(--band); font-weight: bold; padding: 3px 6px; }
table.data tr.tcp-row > td { background: var(--band); padding: 6px 10px 8px 28px; }
/* A row of detail under a row of the table: a session's TCP connection, a
route in full. The route's is indented past the rank column. */
table.data tr.detail > td { background: var(--band); padding: 6px 10px 8px 28px; }
table.data tr.route-detail > td { padding-left: 40px; }
table.data td.attrs { cursor: pointer; }
table.data td.attrs:hover { background: var(--hover); }
table.data tr.route-detail > td { background: var(--band); padding: 6px 10px 8px 40px; }
.tcp-grid { display: grid; grid-template-columns: max-content 1fr; gap: 1px 16px; font-size: 12px; }
.tcp-grid > div:nth-child(odd) { color: var(--fg-soft); white-space: nowrap; }
/* Names and values, two columns, as the detail rows show them. */
.kv-grid { display: grid; grid-template-columns: max-content 1fr; gap: 1px 16px; font-size: 12px; }
.kv-grid > div:nth-child(odd) { color: var(--fg-soft); white-space: nowrap; }
a.tcp-link { color: var(--dim); font-size: 11.5px; text-decoration: none; border: 1px solid var(--line); border-radius: 3px; padding: 0 4px; margin-left: 4px; }
a.tcp-link:hover, a.tcp-link.open { color: var(--link); border-color: var(--link); }
table.data tr.best > td { background: var(--best-bg); }
@@ -276,6 +279,17 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix);
}
const fmtWhen = unix => new Date(unix * 1000).toLocaleString();
const fmtOctets = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`;
const fmtBits = n => { const b = n * 8; return b >= 1e9 ? `${(b / 1e9).toFixed(2)} Gbit/s` : b >= 1e6 ? `${(b / 1e6).toFixed(1)} Mbit/s` : `${(b / 1e3).toFixed(0)} kbit/s`; };
const fmtMs = n => n >= 1000 ? `${(n / 1000).toFixed(1)} s` : `${n.toFixed(n < 10 ? 2 : 0)} ms`;
const ago = ms => ms >= 60000 ? `${Math.round(ms / 60000)}m` : ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
// A cell across every column of the table that `body` is the body of,
// and a row of one: for a heading, a note or a detail row. The width is
// the table's header's, so a column added there is covered here.
const fullCell = (body, html, cls = '') => `<td colspan="${body.closest('table').querySelectorAll('thead th').length}"${cls ? ` class="${cls}"` : ''}>${html}</td>`;
const fullRow = (body, html, rowClass = '', cellClass = '') => `<tr${rowClass ? ` class="${rowClass}"` : ''}>${fullCell(body, html, cellClass)}</tr>`;
// Names and values in two columns, for a detail row.
const kvGrid = rows => `<div class="kv-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
// What a community of ours asks for where control communities are on
// (control_communities), or ''. They are acted on where a route leaves
@@ -498,8 +512,10 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
'route server': ['RS', 'route server of an Internet exchange'],
'route server client': ['RSC', 'client of a route server we run'],
};
// A router of our own AS, by its AS number.
const isIbgp = p => p.peer_asn === (state.status || {}).local_asn;
function roleGlyph(p) {
if (p.peer_asn === (state.status || {}).local_asn) return glyph('iBGP', 'a router of our own AS');
if (isIbgp(p)) return glyph('iBGP', 'a router of our own AS');
const r = ROLE_GLYPH[p.role];
if (!r) return glyph('?', 'no role configured', 'amber');
const confirmed = p.role_confirmed ? '; the peer says so too (RFC 9234)' : p.state === 'Established' ? '; by our configuration, the peer has not said what it is' : '';
@@ -531,7 +547,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
}
const ROLE_ORDER = ['iBGP', 'provider', 'route server', 'peer', 'customer', 'route server client', 'none'];
function peerGroup(p) {
if (p.peer_asn === (state.status || {}).local_asn) return 'iBGP';
if (isIbgp(p)) return 'iBGP';
return p.role || 'none';
}
const GROUP_TITLE = { iBGP: 'Our own routers', provider: 'Providers', 'route server': 'Route servers', peer: 'Peers', customer: 'Customers', 'route server client': 'Route server clients', none: 'Without a role' };
@@ -542,7 +558,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const down = peers.filter(p => peerRank(p) === 0).length;
const waiting = peers.filter(p => p.state === 'Passive').length;
$('peers-count').textContent = `${up} of ${peers.length} established` + (down ? `, ${down} down` : '') + (waiting ? `, ${waiting} waiting` : '');
if (!peers.length) { setHTML(b, '<tr><td colspan="10" class="dim">No peers configured and none connected.</td></tr>'); return; }
if (!peers.length) { setHTML(b, fullRow(b, 'No peers configured and none connected.', '', 'dim')); return; }
const problemsOnly = $('peers-problems').checked;
// The IPv4 and IPv6 sessions of one neighbour, named <stem>-v4 and
// <stem>-v6, are one neighbour to the reader: listed together, IPv6
@@ -585,16 +601,16 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
// they are to us.
const problems = peers.filter(p => peerRank(p) <= 1).sort(byName);
if (problems.length) {
html += `<tr class="group"><td colspan="10"><span class="red">Needing attention</span> (${problems.length})</td></tr>` + rows(problems);
html += fullRow(b, `<span class="red">Needing attention</span> (${problems.length})`, 'group') + rows(problems);
}
if (!problemsOnly) {
for (const group of ROLE_ORDER) {
const members = peers.filter(p => peerRank(p) >= 2 && peerGroup(p) === group).sort(byName);
if (!members.length) continue;
html += `<tr class="group"><td colspan="10">${GROUP_TITLE[group]} (${members.length})</td></tr>` + rows(members);
html += fullRow(b, `${GROUP_TITLE[group]} (${members.length})`, 'group') + rows(members);
}
} else if (!problems.length) {
html += '<tr><td colspan="10" class="dim">Nothing needs attention.</td></tr>';
html += fullRow(b, 'Nothing needs attention.', '', 'dim');
}
setHTML(b, html);
renderTcpRows();
@@ -610,10 +626,6 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const open = state.tcpOpen.has(p.peer_addr);
return `<a href="#" class="tcp-link${open ? ' open' : ''}" data-peer="${esc(p.peer_addr)}" title="TCP_INFO for this session's socket">tcp</a>`;
}
const fmtOctets = n => n >= 1048576 ? `${(n / 1048576).toFixed(1)} MiB` : n >= 1024 ? `${(n / 1024).toFixed(1)} KiB` : `${n} B`;
const fmtBits = n => { const b = n * 8; return b >= 1e9 ? `${(b / 1e9).toFixed(2)} Gbit/s` : b >= 1e6 ? `${(b / 1e6).toFixed(1)} Mbit/s` : `${(b / 1e3).toFixed(0)} kbit/s`; };
const fmtMs = n => n >= 1000 ? `${(n / 1000).toFixed(1)} s` : `${n.toFixed(n < 10 ? 2 : 0)} ms`;
const ago = ms => ms >= 60000 ? `${Math.round(ms / 60000)}m` : ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
function tcpDetail(a) {
if (!a.available) return `<span class="dim">${esc(a.reason || 'n/a')}</span>`;
const c = a.connection;
@@ -634,24 +646,25 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
['bytes', `${kv('sent', fmtOctets(c.bytes_sent))} ${kv('acked', fmtOctets(c.bytes_acked))} ${kv('received', fmtOctets(c.bytes_received))} ${kv('segs', `${fmtCount(c.segments_out)}↑ ${fmtCount(c.segments_in)}↓`)}`],
['last', `${kv('snd', ago(c.last_data_sent_ms))} ${kv('rcv', ago(c.last_data_received_ms))} ${kv('ack', ago(c.last_ack_received_ms))}`],
];
return `<div class="tcp-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
return kvGrid(rows);
}
async function renderTcpRows() {
const b = $('peers-body');
for (const addr of state.tcpOpen) {
const tr = document.querySelector(`#peers-body tr[data-peer-addr="${CSS.escape(addr)}"]`);
const tr = b.querySelector(`tr[data-peer-addr="${CSS.escape(addr)}"]`);
if (!tr) continue;
let row = tr.nextElementSibling;
if (!row || !row.classList.contains('tcp-row')) {
row = document.createElement('tr');
row.className = 'tcp-row';
row.innerHTML = '<td colspan="10"><span class="dim">asking the kernel…</span></td>';
row.className = 'detail tcp-row';
row.innerHTML = fullCell(b, '<span class="dim">asking the kernel…</span>');
tr.after(row);
}
try {
const answer = await getJSON(`/api/tcp?peer=${encodeURIComponent(addr)}`);
row.innerHTML = `<td colspan="10">${tcpDetail(answer)}</td>`;
row.innerHTML = fullCell(b, tcpDetail(answer));
} catch (e) {
row.innerHTML = `<td colspan="10"><span class="red">${esc(String(e))}</span></td>`;
row.innerHTML = fullCell(b, `<span class="red">${esc(String(e))}</span>`);
}
}
}
@@ -790,7 +803,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
function renderLocal() {
const b = $('local-body'); $('local-count').textContent = state.local.length;
if (!state.local.length) { setHTML(b, '<tr><td colspan="7" class="dim">This router originates no routes.</td></tr>'); return; }
if (!state.local.length) { setHTML(b, fullRow(b, 'This router originates no routes.', '', 'dim')); return; }
const local = state.status ? state.status.local_asn : '?';
setHTML(b, state.local.map(r => `<tr>
<td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td>
@@ -826,7 +839,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
: `${shown}${st ? `, for ${fmtCount(st.received_prefixes)} prefixes` : ''}`;
renderPager();
if (!state.received.length) {
setHTML(b, `<tr><td colspan="12" class="dim">${looking ? 'No route matches.' : 'No routes received from peers.'}</td></tr>`); return;
setHTML(b, fullRow(b, looking ? 'No route matches.' : 'No routes received from peers.', '', 'dim')); return;
}
setHTML(b, state.received.map(r => { const s = r.summary; return `<tr class="${r.best ? 'best' : ''}" data-route="${esc(routeKey(r))}">
<td class="nowrap">${rankGlyph(r)}${r.stale ? glyph('stale', 'Kept from a session of the peer’s that ended (graceful restart), and not sent again since', 'amber') : ''}</td>
@@ -863,18 +876,19 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
if (r.path_id) rows.push(['PATH_ID', `${r.path_id}`]);
rows.push(['selection', `${r.best ? 'best' : `rank ${r.rank}`}${r.stale ? ', stale' : ''}`]);
rows.push(['rpki', r.rpki ? esc(r.rpki) : DASH], ['aspa', r.aspa ? esc(r.aspa) : DASH]);
return `<div class="tcp-grid mono">${rows.map(([k, v]) => `<div>${k}</div><div>${v}</div>`).join('')}</div>`;
return kvGrid(rows);
}
function renderRouteDetails() {
const b = $('received-body');
for (const key of state.routeOpen) {
const r = state.received.find(r => routeKey(r) === key);
const tr = document.querySelector(`#received-body tr[data-route="${CSS.escape(key)}"]`);
const tr = b.querySelector(`tr[data-route="${CSS.escape(key)}"]`);
if (!r || !tr) continue;
const next = tr.nextElementSibling;
if (next && next.classList.contains('route-detail')) continue;
const row = document.createElement('tr');
row.className = 'route-detail';
row.innerHTML = `<td colspan="12">${routeDetail(r)}</td>`;
row.className = 'detail route-detail';
row.innerHTML = fullCell(b, routeDetail(r));
tr.after(row);
}
}
@@ -961,7 +975,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const total = state.rejected.reduce((n, p) => n + p.total, 0);
const announcements = n => `${fmtCount(n)} announcement${n === 1 ? '' : 's'}`;
$('rejected-count').textContent = total ? `${announcements(total)} from ${state.rejected.length} peer${state.rejected.length === 1 ? '' : 's'}${chosen ? `; showing ${chosen}` : ''}` : '0';
if (!peers.length) { setHTML(b, `<tr><td colspan="11" class="dim">${chosen ? `Nothing ${esc(chosen)} announced has been rejected.` : 'Nothing a peer announced has been rejected.'}</td></tr>`); return; }
if (!peers.length) { setHTML(b, fullRow(b, chosen ? `Nothing ${esc(chosen)} announced has been rejected.` : 'Nothing a peer announced has been rejected.', '', 'dim')); return; }
setHTML(b, peers.map(p => {
// Alike: the same prefix, for the same reason, by the same path.
const alike = new Map();
@@ -974,7 +988,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const lines = [...alike.values()].sort((a, c) => c.at_unix - a.at_unix).slice(0, REJECTED_SHOWN);
const more = alike.size > REJECTED_SHOWN
? ` <span class="dim">${REJECTED_SHOWN} of ${alike.size} shown; <a href="/api/routes/rejected?peer=${encodeURIComponent(p.peer)}">the latest ${p.recent.length} as json</a>.</span>` : '';
return `<tr class="group"><td colspan="11"><a href="#received" class="peer-link" data-peer="${esc(p.peer)}">${esc(p.peer)}</a> <span class="dim" style="font-weight: normal">${esc(p.peer_addr)}</span>: <span style="font-weight: normal">${announcements(p.total)} rejected</span> ${reasonBar(p.by_reason, p.total)}${more}</td></tr>` +
return fullRow(b, `<a href="#received" class="peer-link" data-peer="${esc(p.peer)}">${esc(p.peer)}</a> <span class="dim" style="font-weight: normal">${esc(p.peer_addr)}</span>: <span style="font-weight: normal">${announcements(p.total)} rejected</span> ${reasonBar(p.by_reason, p.total)}${more}`, 'group') +
lines.map(r => { const s = r.summary; return `<tr><td></td>
<td class="mono">${esc(r.prefix)}${r.path_id ? ` <span class="dim">path ${r.path_id}</span>` : ''}</td>
<td class="mono path">${clamp(asPath(r.as_path))}</td><td>${esc(r.reason)}</td>
@@ -55,7 +55,7 @@ To build and install the daemon:
bgp --version
```
The command displays the installed version, for example `bgp 0.2.5`.
The command displays the installed version, for example `bgp 0.2.6`.
## Building a Static Executable
@@ -55,11 +55,11 @@ Ferrix provides the following capabilities:
## Release Status
> [!IMPORTANT]
> Ferrix 0.2.5 is an early release, in production use on one small
> Ferrix 0.2.6 is an early release, in production use on one small
> network. Ferrix is tested against FRR, BIRD, and Routinator in an
> automated interoperability lab and against real Linux kernels.
Ferrix 0.2.5 does not support the following features:
Ferrix 0.2.6 does not support the following features:
- Per-client best path selection for route servers, and ADD-PATH for
advertised routes.
+5 -3
View File
@@ -233,9 +233,11 @@ routes. To return the records for one neighbor, add `?peer=NAME`.
## 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
`/api/tcp?peer=PEER` returns what the kernel reports about the TCP
connection of the session with the neighbor `PEER`, named as for
`/api/routes/received`: its configured name, or its address, with or
without the port as `peer_addr` in `/api/peers` shows it. The report
covers 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
+3
View File
@@ -195,6 +195,9 @@ if [ "$collector" = 1 ]; then
last=$now_held
done
echo "collector: $(advertised) routes advertised after $(since "$start") s"
collector_addr=$(status peers '.[] | select(.name == "collector") | .peer_addr')
run curl -sf "http://127.0.0.1:8080/api/tcp?peer=$collector_addr" \
| jq -r '.connection | "collector tcp: busy \(.busy_ms / 1000 | . * 10 | round / 10) s, rwnd-limited \(.limited_by_receive_window_share * 100 | round)%, rtt \(.rtt_ms) ms, cwnd \(.cwnd_segments), sent \(.bytes_sent / 1048576 | round) MiB in \(.segments_out) segments"'
fi
memory_report() {
rss=$(awk '/VmRSS/ { printf "%.0f", $2 / 1024 }' "/proc/$daemon_pid/status")