Add runtime observation and systemd restart adapter
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,562 @@
|
||||
//! Bounded, read-only Linux Generic Netlink transport for WireGuard.
|
||||
//!
|
||||
//! Replies are received into zeroizing buffers. The datagram sender address
|
||||
//! must be kernel port 0; `nlmsg_pid` in request replies is the requester's
|
||||
//! port ID, as Linux Generic Netlink constructs replies using that destination.
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use rustix::event::{PollFd, PollFlags, Timespec, poll};
|
||||
use rustix::fd::OwnedFd;
|
||||
use rustix::io::Errno;
|
||||
use rustix::net::{
|
||||
AddressFamily, RecvFlags, SendFlags, SocketFlags, SocketType, bind, getsockname,
|
||||
netlink::SocketAddrNetlink, recvfrom, sendto, socket_with,
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::netlink::{DecodeContext, DecodeError, DecodeLimits, GetDeviceDecoder};
|
||||
use super::{InterfaceObservation, PeerSetCompleteness, RuntimeObservation, RuntimePeerSet};
|
||||
|
||||
const NETLINK_GENERIC: u32 = 16;
|
||||
const CTRL_FAMILY_ID: u16 = 0x10;
|
||||
const CTRL_CMD_GETFAMILY: u8 = 3;
|
||||
const CTRL_CMD_NEWFAMILY: u8 = 1;
|
||||
const CTRL_ATTR_FAMILY_ID: u16 = 1;
|
||||
const CTRL_ATTR_FAMILY_NAME: u16 = 2;
|
||||
const WG_CMD_GET_DEVICE: u8 = 0;
|
||||
const WG_GENL_VERSION: u8 = 1;
|
||||
const WG_ATTR_IFNAME: u16 = 2;
|
||||
const NLMSG_ERROR: u16 = 2;
|
||||
const NLMSG_DONE: u16 = 3;
|
||||
const NLMSG_REQUEST: u16 = 1;
|
||||
const NLMSG_DUMP: u16 = 0x300;
|
||||
const NLMSG_MULTI: u16 = 2;
|
||||
const MAX_DATAGRAM: usize = 1024 * 1024;
|
||||
const MAX_INTERFACE_NAME: usize = 15;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
|
||||
pub(crate) enum TransportError {
|
||||
#[error("configured WireGuard interface was not found")]
|
||||
NoDevice,
|
||||
#[error("permission denied while reading WireGuard state")]
|
||||
PermissionDenied,
|
||||
#[error("WireGuard netlink response was malformed or incomplete")]
|
||||
Malformed,
|
||||
#[error("WireGuard netlink transport failed")]
|
||||
Transport,
|
||||
#[error("WireGuard netlink request timed out")]
|
||||
Timeout,
|
||||
#[error("WireGuard netlink response exceeded configured limits")]
|
||||
LimitExceeded,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Datagram {
|
||||
bytes: Zeroizing<Vec<u8>>,
|
||||
sender: u32,
|
||||
}
|
||||
|
||||
trait Io {
|
||||
fn now(&self) -> Duration;
|
||||
fn send(&mut self, destination: u32, bytes: &[u8]) -> Result<(), TransportError>;
|
||||
fn receive(&mut self, wait: Duration) -> Result<Datagram, TransportError>;
|
||||
fn port_id(&self) -> u32;
|
||||
}
|
||||
|
||||
struct LinuxIo {
|
||||
socket: OwnedFd,
|
||||
port_id: u32,
|
||||
origin: Instant,
|
||||
}
|
||||
|
||||
impl LinuxIo {
|
||||
fn new() -> Result<Self, TransportError> {
|
||||
let socket = socket_with(
|
||||
AddressFamily::NETLINK,
|
||||
SocketType::DGRAM,
|
||||
SocketFlags::CLOEXEC.union(SocketFlags::NONBLOCK),
|
||||
Some(rustix::net::netlink::GENERIC),
|
||||
)
|
||||
.map_err(map_errno)?;
|
||||
bind(&socket, &SocketAddrNetlink::new(0, 0)).map_err(map_errno)?;
|
||||
let addr = SocketAddrNetlink::try_from(getsockname(&socket).map_err(map_errno)?)
|
||||
.map_err(|_| TransportError::Transport)?;
|
||||
Ok(Self {
|
||||
socket,
|
||||
port_id: addr.pid(),
|
||||
origin: Instant::now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Io for LinuxIo {
|
||||
fn now(&self) -> Duration {
|
||||
self.origin.elapsed()
|
||||
}
|
||||
|
||||
fn port_id(&self) -> u32 {
|
||||
self.port_id
|
||||
}
|
||||
|
||||
fn send(&mut self, destination: u32, bytes: &[u8]) -> Result<(), TransportError> {
|
||||
let sent = sendto(
|
||||
&self.socket,
|
||||
bytes,
|
||||
SendFlags::empty(),
|
||||
&SocketAddrNetlink::new(destination, 0),
|
||||
)
|
||||
.map_err(map_errno)?;
|
||||
if sent == bytes.len() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(TransportError::Transport)
|
||||
}
|
||||
}
|
||||
|
||||
fn receive(&mut self, wait: Duration) -> Result<Datagram, TransportError> {
|
||||
let timeout = Timespec {
|
||||
tv_sec: wait.as_secs().min(i64::MAX as u64) as i64,
|
||||
tv_nsec: wait.subsec_nanos() as i64,
|
||||
};
|
||||
let mut fds = [PollFd::new(&self.socket, PollFlags::IN)];
|
||||
if poll(&mut fds, Some(&timeout)).map_err(map_errno)? == 0 {
|
||||
return Err(TransportError::Timeout);
|
||||
}
|
||||
let mut bytes = Zeroizing::new(vec![0; MAX_DATAGRAM]);
|
||||
let (_, count, address) = recvfrom(
|
||||
&self.socket,
|
||||
&mut bytes[..],
|
||||
RecvFlags::TRUNC.union(RecvFlags::DONTWAIT),
|
||||
)
|
||||
.map_err(map_errno)?;
|
||||
if count > MAX_DATAGRAM {
|
||||
return Err(TransportError::LimitExceeded);
|
||||
}
|
||||
let sender = address
|
||||
.and_then(|a| SocketAddrNetlink::try_from(a).ok())
|
||||
.ok_or(TransportError::Malformed)?
|
||||
.pid();
|
||||
bytes.truncate(count);
|
||||
Ok(Datagram { bytes, sender })
|
||||
}
|
||||
}
|
||||
|
||||
fn map_errno(error: Errno) -> TransportError {
|
||||
if error == Errno::ACCESS || error == Errno::PERM {
|
||||
TransportError::PermissionDenied
|
||||
} else if error == Errno::NOENT || error == Errno::NODEV {
|
||||
TransportError::NoDevice
|
||||
} else if error == Errno::TIMEDOUT {
|
||||
TransportError::Timeout
|
||||
} else {
|
||||
TransportError::Transport
|
||||
}
|
||||
}
|
||||
|
||||
fn map_kernel_error(error: i32) -> TransportError {
|
||||
if error == Errno::ACCESS.raw_os_error() || error == Errno::PERM.raw_os_error() {
|
||||
TransportError::PermissionDenied
|
||||
} else if error == Errno::NOENT.raw_os_error() || error == Errno::NODEV.raw_os_error() {
|
||||
TransportError::NoDevice
|
||||
} else {
|
||||
TransportError::Transport
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn observe_interface(
|
||||
interface: &str,
|
||||
timeout: Duration,
|
||||
limits: DecodeLimits,
|
||||
) -> Result<RuntimeObservation, TransportError> {
|
||||
let mut io = LinuxIo::new()?;
|
||||
observe_with_io(&mut io, interface, timeout, limits)
|
||||
}
|
||||
|
||||
fn observe_with_io(
|
||||
io: &mut impl Io,
|
||||
interface: &str,
|
||||
timeout: Duration,
|
||||
limits: DecodeLimits,
|
||||
) -> Result<RuntimeObservation, TransportError> {
|
||||
if interface.is_empty()
|
||||
|| interface.len() > MAX_INTERFACE_NAME
|
||||
|| interface.as_bytes().contains(&0)
|
||||
|| timeout.is_zero()
|
||||
{
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
let start = io.now();
|
||||
let deadline = start.checked_add(timeout).ok_or(TransportError::Timeout)?;
|
||||
let family = resolve_family(io, deadline)?;
|
||||
let seq = 2;
|
||||
let request = get_device_request(family, seq, io.port_id(), interface);
|
||||
io.send(0, &request)?;
|
||||
let context = DecodeContext {
|
||||
sequence: seq,
|
||||
port_id: io.port_id(),
|
||||
family_id: family,
|
||||
};
|
||||
let mut decoder = GetDeviceDecoder::new(context, interface, limits);
|
||||
loop {
|
||||
let datagram = receive_before(io, deadline)?;
|
||||
if datagram.sender != 0 {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
decoder.push_datagram(&datagram.bytes).map_err(map_decode)?;
|
||||
// Decoder only returns a device after its explicit terminal DONE.
|
||||
if decoder.is_done() {
|
||||
let device = decoder.finish().map_err(map_decode)?;
|
||||
return Ok(RuntimeObservation {
|
||||
observed_at: None,
|
||||
interface: InterfaceObservation::Present {
|
||||
name: device.interface_name,
|
||||
peers: RuntimePeerSet::from_records(
|
||||
PeerSetCompleteness::Complete,
|
||||
device.peers,
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_before(io: &mut impl Io, deadline: Duration) -> Result<Datagram, TransportError> {
|
||||
let remaining = deadline
|
||||
.checked_sub(io.now())
|
||||
.ok_or(TransportError::Timeout)?;
|
||||
if remaining.is_zero() {
|
||||
return Err(TransportError::Timeout);
|
||||
}
|
||||
io.receive(remaining)
|
||||
}
|
||||
|
||||
fn resolve_family(io: &mut impl Io, deadline: Duration) -> Result<u16, TransportError> {
|
||||
let seq = 1;
|
||||
let mut request = message(CTRL_FAMILY_ID, NLMSG_REQUEST, seq, io.port_id());
|
||||
request.extend_from_slice(&[CTRL_CMD_GETFAMILY, 1, 0, 0]);
|
||||
push_attr(&mut request, CTRL_ATTR_FAMILY_NAME, b"wireguard\0");
|
||||
set_len(&mut request);
|
||||
io.send(0, &request)?;
|
||||
let d = receive_before(io, deadline)?;
|
||||
if d.sender != 0 {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
parse_family(&d.bytes, seq, io.port_id())
|
||||
}
|
||||
|
||||
fn parse_family(bytes: &[u8], seq: u32, port: u32) -> Result<u16, TransportError> {
|
||||
if bytes.len() < 20 {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
if read_u32(bytes, 0)? as usize != bytes.len()
|
||||
|| read_u32(bytes, 8)? != seq
|
||||
|| read_u32(bytes, 12)? != port
|
||||
{
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
let message_type = read_u16(bytes, 4)?;
|
||||
if message_type == NLMSG_ERROR {
|
||||
let status = read_u32(bytes, 16)? as i32;
|
||||
if status == 0 {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
return Err(status
|
||||
.checked_neg()
|
||||
.map(map_kernel_error)
|
||||
.ok_or(TransportError::Malformed)?);
|
||||
}
|
||||
if message_type != CTRL_FAMILY_ID {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
if bytes[16] != CTRL_CMD_NEWFAMILY || read_u16(bytes, 18)? != 0 {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
let mut off = 20;
|
||||
while off < bytes.len() {
|
||||
if bytes.len() - off < 4 {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
let len = read_u16(bytes, off)? as usize;
|
||||
let kind = read_u16(bytes, off + 2)? & 0x3fff;
|
||||
if len < 4 || off + len > bytes.len() {
|
||||
return Err(TransportError::Malformed);
|
||||
}
|
||||
if kind == CTRL_ATTR_FAMILY_ID && len == 6 {
|
||||
return read_u16(bytes, off + 4);
|
||||
}
|
||||
let aligned_len = len.checked_add(3).ok_or(TransportError::Malformed)? & !3;
|
||||
off = off
|
||||
.checked_add(aligned_len)
|
||||
.filter(|&end| end <= bytes.len())
|
||||
.ok_or(TransportError::Malformed)?;
|
||||
}
|
||||
Err(TransportError::NoDevice)
|
||||
}
|
||||
|
||||
fn get_device_request(family: u16, seq: u32, port: u32, interface: &str) -> Vec<u8> {
|
||||
let mut out = message(family, NLMSG_REQUEST | NLMSG_DUMP, seq, port);
|
||||
out.extend_from_slice(&[WG_CMD_GET_DEVICE, WG_GENL_VERSION, 0, 0]);
|
||||
let mut interface_name = interface.as_bytes().to_vec();
|
||||
interface_name.push(0);
|
||||
push_attr(&mut out, WG_ATTR_IFNAME, &interface_name);
|
||||
set_len(&mut out);
|
||||
out
|
||||
}
|
||||
fn message(kind: u16, flags: u16, seq: u32, port: u32) -> Vec<u8> {
|
||||
let mut out = vec![0; 16];
|
||||
out[4..6].copy_from_slice(&kind.to_ne_bytes());
|
||||
out[6..8].copy_from_slice(&flags.to_ne_bytes());
|
||||
out[8..12].copy_from_slice(&seq.to_ne_bytes());
|
||||
out[12..16].copy_from_slice(&port.to_ne_bytes());
|
||||
out
|
||||
}
|
||||
fn push_attr(out: &mut Vec<u8>, kind: u16, value: &[u8]) {
|
||||
let len = (4 + value.len()) as u16;
|
||||
out.extend_from_slice(&len.to_ne_bytes());
|
||||
out.extend_from_slice(&kind.to_ne_bytes());
|
||||
out.extend_from_slice(value);
|
||||
while !out.len().is_multiple_of(4) {
|
||||
out.push(0);
|
||||
}
|
||||
}
|
||||
fn set_len(out: &mut [u8]) {
|
||||
let len = out.len() as u32;
|
||||
out[..4].copy_from_slice(&len.to_ne_bytes());
|
||||
}
|
||||
fn read_u16(bytes: &[u8], at: usize) -> Result<u16, TransportError> {
|
||||
bytes
|
||||
.get(at..at + 2)
|
||||
.and_then(|b| b.try_into().ok())
|
||||
.map(u16::from_ne_bytes)
|
||||
.ok_or(TransportError::Malformed)
|
||||
}
|
||||
fn read_u32(bytes: &[u8], at: usize) -> Result<u32, TransportError> {
|
||||
bytes
|
||||
.get(at..at + 4)
|
||||
.and_then(|b| b.try_into().ok())
|
||||
.map(u32::from_ne_bytes)
|
||||
.ok_or(TransportError::Malformed)
|
||||
}
|
||||
fn map_decode(error: DecodeError) -> TransportError {
|
||||
match error {
|
||||
DecodeError::KernelError(n) if n == Errno::NODEV.raw_os_error() => TransportError::NoDevice,
|
||||
DecodeError::KernelError(n)
|
||||
if n == Errno::ACCESS.raw_os_error() || n == Errno::PERM.raw_os_error() =>
|
||||
{
|
||||
TransportError::PermissionDenied
|
||||
}
|
||||
DecodeError::LimitExceeded => TransportError::LimitExceeded,
|
||||
DecodeError::KernelError(_) | DecodeError::DumpFailed => TransportError::Transport,
|
||||
_ => TransportError::Malformed,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ctrl_family_message(sequence: u32, port: u32, family: u16) -> Vec<u8> {
|
||||
let mut out = message(CTRL_FAMILY_ID, 0, sequence, port);
|
||||
out.extend_from_slice(&[CTRL_CMD_NEWFAMILY, 2, 0, 0]);
|
||||
push_attr(&mut out, CTRL_ATTR_FAMILY_ID, &family.to_ne_bytes());
|
||||
set_len(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_family_only_from_matching_kernel_reply() {
|
||||
assert_eq!(parse_family(&ctrl_family_message(1, 55, 42), 1, 55), Ok(42));
|
||||
assert_eq!(
|
||||
parse_family(&ctrl_family_message(2, 55, 42), 1, 55),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_family(&ctrl_family_message(1, 56, 42), 1, 55),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
}
|
||||
|
||||
fn netlink_error(sequence: u32, port: u32, status: i32) -> Vec<u8> {
|
||||
let mut out = message(NLMSG_ERROR, 0, sequence, port);
|
||||
out.extend_from_slice(&status.to_ne_bytes());
|
||||
set_len(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_errors_map_kernel_errno_after_validating_reply_identity() {
|
||||
for errno in [Errno::NOENT, Errno::NODEV] {
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(1, 55, -errno.raw_os_error()), 1, 55),
|
||||
Err(TransportError::NoDevice)
|
||||
);
|
||||
}
|
||||
for errno in [Errno::ACCESS, Errno::PERM] {
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(1, 55, -errno.raw_os_error()), 1, 55),
|
||||
Err(TransportError::PermissionDenied)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(1, 55, -Errno::IO.raw_os_error()), 1, 55),
|
||||
Err(TransportError::Transport)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(1, 55, i32::MIN), 1, 55),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(1, 55, 0), 1, 55),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(2, 55, -Errno::NOENT.raw_os_error()), 1, 55),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_family(&netlink_error(1, 56, -Errno::NOENT.raw_os_error()), 1, 55),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_family(
|
||||
&netlink_error(1, 55, -Errno::NOENT.raw_os_error())[..19],
|
||||
1,
|
||||
55
|
||||
),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_resolution_propagates_kernel_errors_from_injected_transport() {
|
||||
for (errno, expected) in [
|
||||
(Errno::NODEV, TransportError::NoDevice),
|
||||
(Errno::ACCESS, TransportError::PermissionDenied),
|
||||
(Errno::IO, TransportError::Transport),
|
||||
] {
|
||||
let mut fake = FakeIo {
|
||||
port: 55,
|
||||
..FakeIo::default()
|
||||
};
|
||||
fake.incoming
|
||||
.push_back(datagram(netlink_error(1, 55, -errno.raw_os_error()), 0));
|
||||
assert_eq!(
|
||||
resolve_family(&mut fake, Duration::from_secs(1)),
|
||||
Err(expected)
|
||||
);
|
||||
assert_eq!(fake.sent.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_request_targets_only_named_interface_and_dump_flags() {
|
||||
let request = get_device_request(42, 7, 55, "wg-test");
|
||||
assert_eq!(read_u16(&request, 4), Ok(42));
|
||||
assert_eq!(read_u16(&request, 6), Ok(NLMSG_REQUEST | NLMSG_DUMP));
|
||||
assert_eq!(
|
||||
&request[16..20],
|
||||
&[WG_CMD_GET_DEVICE, WG_GENL_VERSION, 0, 0]
|
||||
);
|
||||
assert_eq!(read_u16(&request, 20), Ok(12));
|
||||
assert_eq!(&request[22..24], &2u16.to_ne_bytes());
|
||||
assert_eq!(&request[24..32], b"wg-test\0");
|
||||
assert_eq!(request.len(), 32);
|
||||
assert_eq!(read_u32(&request, 0), Ok(request.len() as u32));
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeIo {
|
||||
clock: Duration,
|
||||
port: u32,
|
||||
sent: Vec<(u32, Vec<u8>)>,
|
||||
incoming: std::collections::VecDeque<Datagram>,
|
||||
}
|
||||
impl Io for FakeIo {
|
||||
fn now(&self) -> Duration {
|
||||
self.clock
|
||||
}
|
||||
fn port_id(&self) -> u32 {
|
||||
self.port
|
||||
}
|
||||
fn send(&mut self, destination: u32, bytes: &[u8]) -> Result<(), TransportError> {
|
||||
self.sent.push((destination, bytes.to_vec()));
|
||||
Ok(())
|
||||
}
|
||||
fn receive(&mut self, wait: Duration) -> Result<Datagram, TransportError> {
|
||||
if let Some(datagram) = self.incoming.pop_front() {
|
||||
Ok(datagram)
|
||||
} else {
|
||||
self.clock += wait;
|
||||
Err(TransportError::Timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn datagram(bytes: Vec<u8>, sender: u32) -> Datagram {
|
||||
Datagram {
|
||||
bytes: Zeroizing::new(bytes),
|
||||
sender,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn injected_transport_rejects_non_kernel_sender_and_bounds_resolution_wait() {
|
||||
let mut fake = FakeIo {
|
||||
port: 55,
|
||||
..FakeIo::default()
|
||||
};
|
||||
fake.incoming
|
||||
.push_back(datagram(ctrl_family_message(1, 55, 42), 22));
|
||||
assert_eq!(
|
||||
resolve_family(&mut fake, Duration::from_secs(1)),
|
||||
Err(TransportError::Malformed)
|
||||
);
|
||||
assert_eq!(fake.sent.len(), 1);
|
||||
assert_eq!(fake.sent[0].0, 0);
|
||||
|
||||
let mut silent = FakeIo {
|
||||
port: 55,
|
||||
..FakeIo::default()
|
||||
};
|
||||
assert_eq!(
|
||||
resolve_family(&mut silent, Duration::from_millis(5)),
|
||||
Err(TransportError::Timeout)
|
||||
);
|
||||
assert_eq!(silent.clock, Duration::from_millis(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn injected_transport_returns_only_a_complete_public_empty_observation() {
|
||||
let mut fake = FakeIo {
|
||||
port: 55,
|
||||
..FakeIo::default()
|
||||
};
|
||||
fake.incoming
|
||||
.push_back(datagram(ctrl_family_message(1, 55, 42), 0));
|
||||
let mut response = message(42, NLMSG_MULTI, 2, 55);
|
||||
response.extend_from_slice(&[WG_CMD_GET_DEVICE, WG_GENL_VERSION, 0, 0]);
|
||||
push_attr(&mut response, 2, b"wg0\0");
|
||||
push_attr(&mut response, 8 | 0x8000, &[]);
|
||||
set_len(&mut response);
|
||||
let mut complete = response;
|
||||
let done_start = complete.len();
|
||||
complete.extend_from_slice(&message(NLMSG_DONE, 0, 2, 55));
|
||||
complete.extend_from_slice(&0i32.to_ne_bytes());
|
||||
set_len(&mut complete[done_start..]);
|
||||
fake.incoming.push_back(datagram(complete, 0));
|
||||
let observation = observe_with_io(
|
||||
&mut fake,
|
||||
"wg0",
|
||||
Duration::from_secs(2),
|
||||
DecodeLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(observation.interface, InterfaceObservation::Present { ref name, ref peers } if name == "wg0" && peers.completeness == PeerSetCompleteness::Complete && peers.peers.is_empty())
|
||||
);
|
||||
assert_eq!(fake.sent.len(), 2);
|
||||
assert!(fake.sent.iter().all(|(destination, _)| *destination == 0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user