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>
This commit is contained in:
1 parent
bcfd6e013f
commit
25597ab3ef
16 files changed
+933
-565
No files matched your search
+89
-47
@@ -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)),
|
||||
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user