implement auth, improve session stats

This commit is contained in:
ktims committed 2026-10-23 14:11:16 -07:00
1 parent bdf5455a3f
commit 8962406c57
7 files changed
+1214 -90

No files matched your search

Generated
+210 -3
View File
@@ -88,6 +88,15 @@ version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "bytemuck"
version = "1.25.2"
@@ -184,6 +193,21 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
@@ -195,6 +219,15 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "defmt"
version = "1.2.0"
@@ -223,7 +256,18 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0"
dependencies = [
"thiserror",
"thiserror 2.0.21",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
]
[[package]]
@@ -404,6 +448,15 @@ dependencies = [
"serde",
]
[[package]]
name = "hybrid-array"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"typenum",
]
[[package]]
name = "indexmap"
version = "2.14.2"
@@ -412,6 +465,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
"serde",
"serde_core",
]
[[package]]
@@ -429,6 +484,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -499,6 +563,16 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "md-5"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
dependencies = [
"cfg-if 1.0.5",
"digest",
]
[[package]]
name = "memchr"
version = "2.8.3"
@@ -576,6 +650,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
@@ -750,19 +833,25 @@ dependencies = [
"bytemuck",
"byteorder",
"clap",
"digest",
"env_logger",
"futures 0.3.34",
"humantime",
"humantime-serde",
"itertools",
"itertools 0.12.1",
"log",
"md-5",
"nix",
"nom",
"nom-derive",
"proc-bitfield",
"rand",
"serde",
"serde_bytes",
"serde_valid",
"serde_yaml_ng",
"sha1",
"subtle",
"tokio",
"tokio-timer",
]
@@ -795,6 +884,16 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde_bytes"
version = "0.11.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
@@ -815,6 +914,59 @@ dependencies = [
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_valid"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a812672d5d6c94c0fa540e62dc318728410c78f40d60ae6c07afc845616da5f6"
dependencies = [
"indexmap",
"itertools 0.14.0",
"num-traits",
"regex",
"serde",
"serde_json",
"serde_valid_derive",
"serde_valid_literal",
"thiserror 1.0.69",
"unicode-segmentation",
]
[[package]]
name = "serde_valid_derive"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539cd22aaf48cb9a8932c8b7f5d54f51719b2f826564f9da1a9902ee411d753c"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
"quote",
"strsim",
"syn 3.0.9",
]
[[package]]
name = "serde_valid_literal"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433e602b939d2e8e8b47f30f919b27a984183974079de2262ca876c2162657c8"
dependencies = [
"regex",
]
[[package]]
name = "serde_yaml_ng"
version = "0.10.0"
@@ -828,6 +980,17 @@ dependencies = [
"unsafe-libyaml",
]
[[package]]
name = "sha1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
dependencies = [
"cfg-if 1.0.5",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
@@ -872,6 +1035,12 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
@@ -905,13 +1074,33 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.21",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
@@ -975,12 +1164,24 @@ dependencies = [
"tokio-executor",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
@@ -1033,3 +1234,9 @@ dependencies = [
"quote",
"syn 2.0.119",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+6
View File
@@ -8,18 +8,24 @@ atomic = "0.6.0"
bytemuck = { version = "1.14.1", features = ["derive"] }
byteorder = "1.5.0"
clap = { version = "4", features = ["derive", "env"] }
digest = "0.11.3"
env_logger = "0.11.1"
futures = "0.3.30"
humantime = "2.4.0"
humantime-serde = "1.1.1"
itertools = "0.12.1"
log = "0.4.20"
md-5 = "0.11.0"
nix = { version = "0.31.3", features = ["fs", "socket"] }
nom = "7.1.3"
nom-derive = "0.10.1"
proc-bitfield = "0.3.1"
rand = "0.8.5"
serde = { version = "1.0.229", features = ["derive"] }
serde_bytes = "0.11.19"
serde_valid = "3.2.0"
serde_yaml_ng = "0.10.0"
sha1 = "0.11.0"
subtle = "2.6"
tokio = { version = "1.35.1", features = ["net", "full"] }
tokio-timer = "0.2.13"
+31
View File
@@ -39,3 +39,34 @@ peers:
# desired_min_tx: 1s
# required_min_rx: 1s
# detect_mult: 5
#
# Authentication (RFC 5880 section 6.7), per peer and off by default. Both ends must be configured identically; a
# peer that does not authenticate as configured is discarded. `key_id` is a number (0-255) that must match the remote's.
# Keys are used verbatim (not hashed or derived), so they must be exactly the length shown. Give them as a string;
# use YAML double-quoted escapes (\x00, \x1b, ...) for bytes that are not printable. Strings are UTF-8, so only bytes
# 0x00-0x7f can be written this way. To use bytes 0x80-0xff, give the key as a list of integers instead, one per
# byte (hex is fine), with the same length requirement:
# key: [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x80, 0xff]
#
# Keyed MD5: `key` is exactly 16 bytes. `meticulous` (default true) selects Meticulous Keyed MD5, in which every
# packet must carry a newer sequence number; the remote must be set to the same mode.
# - address: 192.0.2.4
# auth:
# type: md5
# key_id: 1
# key: "0123456789abcdef"
# meticulous: true
#
# Keyed SHA1: `key` is exactly 20 bytes. `meticulous` as above.
# - address: 192.0.2.5
# auth:
# type: sha1
# key_id: 1
# key: "0123456789abcdef\x00\x01\x7f\x1b"
#
# Simple password: 1 to 16 bytes, sent in the clear. Provides no protection against an attacker on the path.
# - address: 192.0.2.6
# auth:
# type: password
# key_id: 1
# password: "secret"
+643 -54
View File
@@ -1,16 +1,24 @@
use digest::Digest;
use log::warn;
use std::{
ffi::OsStr,
fmt::{self, Display},
io::Cursor,
io::{Cursor, Write},
ops::{Div, Mul},
time,
};
use bytemuck::NoUninit;
use byteorder::{BigEndian, WriteBytesExt};
use nom::{bytes::complete::take, multi::many_m_n, number::complete::be_u8, IResult};
use nom::{
bytes::complete::take,
multi::many_m_n,
number::complete::{be_u32, be_u8},
IResult,
};
use nom_derive::{NomBE, Parse};
use proc_bitfield::*;
use subtle::ConstantTimeEq;
#[repr(u8)]
#[derive(ConvRaw, Debug, NomBE, PartialEq, Eq, Clone, Copy, NoUninit)]
@@ -84,6 +92,21 @@ pub enum BfdAuthType {
MetKeyedSHA1 = 5,
Reserved,
}
impl Display for BfdAuthType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::None => "none",
Self::SimplePassword => "password",
Self::KeyedMD5 => "md5",
Self::MetKeyedMD5 => "meticulous-md5",
Self::KeyedSHA1 => "sha1",
Self::MetKeyedSHA1 => "meticulous-sha1",
Self::Reserved => "reserved",
})
}
}
#[repr(transparent)]
#[derive(Debug, NomBE, PartialEq, Eq, Clone, Copy, NoUninit, Hash)]
pub struct BfdDiscriminator(pub u32);
@@ -181,7 +204,7 @@ impl Div<BfdInterval> for u32 {
}
bitfield! {
#[derive(NomBE)]
#[derive(Clone, Copy, NomBE)]
pub struct BfdFlags(pub u32): Debug {
pub vers: u8 @ 29..=31,
pub diag: u8 [try_get BfdDiagnostic] @ 24..=28,
@@ -197,67 +220,211 @@ bitfield! {
}
}
#[derive(Debug)]
pub struct BfdAuthSimplePassword(Vec<u8>);
#[derive(Clone, Debug, PartialEq)]
pub struct BfdAuthSimplePassword {
pub key_id: u8,
pub password: Vec<u8>,
}
impl<'a> Parse<&'a [u8]> for BfdAuthSimplePassword {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self, nom::error::Error<&'a [u8]>> {
let (i, res) = many_m_n(1, 16, be_u8)(i)?;
Ok((i, Self(res)))
let (i, key_id) = be_u8(i)?;
let (i, password) = many_m_n(1, 16, be_u8)(i)?;
Ok((i, Self { key_id, password }))
}
}
#[derive(Debug, NomBE)]
impl BfdAuthSimplePassword {
fn serialize(&self, wtr: &mut impl Write) -> Result<(), std::io::Error> {
wtr.write_u8(self.key_id)?;
wtr.write_all(&self.password)
}
}
/// Meticulous and non-meticulous keyed authentication share a wire format and differ only in the
/// Auth Type value and sequence number handling.
#[derive(Clone, Debug, PartialEq)]
pub struct BfdAuthKeyedMD5 {
key_id: u8,
_reserved: u8,
seq: u32,
digest: [u8; 16],
pub meticulous: bool,
pub key_id: u8,
pub seq: u32,
pub digest: [u8; 16],
}
#[derive(Debug, NomBE)]
pub struct BfdAuthKeyedSHA1 {
key_id: u8,
_reserved: u8,
seq: u32,
hash: [u8; 20],
}
impl BfdAuthKeyedMD5 {
fn parse_with(i: &[u8], meticulous: bool) -> IResult<&[u8], Self> {
let (i, key_id) = be_u8(i)?;
let (i, _reserved) = be_u8(i)?;
let (i, seq) = be_u32(i)?;
let (i, digest) = take(16usize)(i)?;
let digest = digest.try_into().expect("took 16 bytes");
Ok((
i,
Self {
meticulous,
key_id,
seq,
digest,
},
))
}
#[derive(Debug, NomBE)]
#[nom(Selector = "BfdAuthType", Complete)]
pub enum BfdAuthData {
#[nom(Selector = "BfdAuthType::SimplePassword")]
SimplePassword(BfdAuthSimplePassword),
#[nom(Selector = "BfdAuthType::KeyedMD5")]
KeyedMD5(BfdAuthKeyedMD5),
#[nom(Selector = "BfdAuthType::MetKeyedMD5")]
MetKeyedMD5(BfdAuthKeyedMD5),
#[nom(Selector = "BfdAuthType::KeyedSHA1")]
KeyedSHA1(BfdAuthKeyedSHA1),
#[nom(Selector = "BfdAuthType::MetKeyedSHA1")]
MetKeyedSHA1(BfdAuthKeyedSHA1),
}
impl BfdAuthData {
fn parse_be_with_length(
i: &[u8],
auth_type: BfdAuthType,
auth_len: u8,
) -> IResult<&[u8], Self> {
let (new_i, data) = take(auth_len.saturating_sub(2))(i)?;
let (_leftovers, retval) = BfdAuthData::parse_be(data, auth_type)?;
Ok((new_i, retval))
fn serialize(&self, wtr: &mut impl Write) -> Result<(), std::io::Error> {
wtr.write_u8(self.key_id)?;
wtr.write_u8(0)?;
wtr.write_u32::<BigEndian>(self.seq)?;
wtr.write_all(&self.digest)
}
}
#[derive(Debug, NomBE)]
pub struct BfdAuth {
auth_type: BfdAuthType,
auth_len: u8,
#[nom(Parse = "{ |i| BfdAuthData::parse_be_with_length(i, auth_type, auth_len) }")]
auth_data: BfdAuthData,
#[derive(Clone, Debug, PartialEq)]
pub struct BfdAuthKeyedSHA1 {
pub meticulous: bool,
pub key_id: u8,
pub seq: u32,
pub digest: [u8; 20],
}
#[derive(Debug, NomBE)]
impl BfdAuthKeyedSHA1 {
fn parse_with(i: &[u8], meticulous: bool) -> IResult<&[u8], Self> {
let (i, key_id) = be_u8(i)?;
let (i, _reserved) = be_u8(i)?;
let (i, seq) = be_u32(i)?;
let (i, digest) = take(20usize)(i)?;
let digest = digest.try_into().expect("took 20 bytes");
Ok((
i,
Self {
meticulous,
key_id,
seq,
digest,
},
))
}
fn serialize(&self, wtr: &mut impl Write) -> Result<(), std::io::Error> {
wtr.write_u8(self.key_id)?;
wtr.write_u8(0)?;
wtr.write_u32::<BigEndian>(self.seq)?;
wtr.write_all(&self.digest)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum BfdAuth {
SimplePassword(BfdAuthSimplePassword),
KeyedMD5(BfdAuthKeyedMD5),
KeyedSHA1(BfdAuthKeyedSHA1),
}
impl<'a> Parse<&'a [u8]> for BfdAuth {
fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self, nom::error::Error<&'a [u8]>> {
let (i, auth_type) = BfdAuthType::parse_be(i)?;
let (i, auth_len) = be_u8(i)?;
let (rest, data) = take(auth_len.saturating_sub(2))(i)?;
let auth = match auth_type {
BfdAuthType::SimplePassword => {
Self::SimplePassword(BfdAuthSimplePassword::parse(data)?.1)
}
BfdAuthType::KeyedMD5 => Self::KeyedMD5(BfdAuthKeyedMD5::parse_with(data, false)?.1),
BfdAuthType::MetKeyedMD5 => Self::KeyedMD5(BfdAuthKeyedMD5::parse_with(data, true)?.1),
BfdAuthType::KeyedSHA1 => Self::KeyedSHA1(BfdAuthKeyedSHA1::parse_with(data, false)?.1),
BfdAuthType::MetKeyedSHA1 => {
Self::KeyedSHA1(BfdAuthKeyedSHA1::parse_with(data, true)?.1)
}
BfdAuthType::None | BfdAuthType::Reserved => {
return Err(nom::Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Verify,
)))
}
};
Ok((rest, auth))
}
}
impl BfdAuth {
pub fn auth_type(&self) -> BfdAuthType {
match self {
Self::SimplePassword(_) => BfdAuthType::SimplePassword,
Self::KeyedMD5(d) if d.meticulous => BfdAuthType::MetKeyedMD5,
Self::KeyedMD5(_) => BfdAuthType::KeyedMD5,
Self::KeyedSHA1(d) if d.meticulous => BfdAuthType::MetKeyedSHA1,
Self::KeyedSHA1(_) => BfdAuthType::KeyedSHA1,
}
}
/// Length of the authentication section including the type and length bytes (RFC 5880 4.2-4.4)
pub fn auth_len(&self) -> u8 {
match self {
Self::SimplePassword(p) => 3 + p.password.len() as u8,
Self::KeyedMD5(_) => 24,
Self::KeyedSHA1(_) => 28,
}
}
/// Sequence number of the keyed variants; simple password has none
pub fn seq(&self) -> Option<u32> {
match self {
Self::SimplePassword(_) => None,
Self::KeyedMD5(d) => Some(d.seq),
Self::KeyedSHA1(d) => Some(d.seq),
}
}
pub fn set_seq(&mut self, seq: u32) {
match self {
Self::SimplePassword(_) => {}
Self::KeyedMD5(d) => d.seq = seq,
Self::KeyedSHA1(d) => d.seq = seq,
}
}
pub fn meticulous(&self) -> bool {
match self {
Self::SimplePassword(_) => false,
Self::KeyedMD5(d) => d.meticulous,
Self::KeyedSHA1(d) => d.meticulous,
}
}
pub fn key_id(&self) -> u8 {
match self {
Self::SimplePassword(d) => d.key_id,
Self::KeyedMD5(d) => d.key_id,
Self::KeyedSHA1(d) => d.key_id,
}
}
/// The Auth Key/Digest field; holds the key before signing and the digest on the wire
pub fn digest_field(&self) -> Option<&[u8]> {
match self {
Self::SimplePassword(_) => None,
Self::KeyedMD5(d) => Some(&d.digest),
Self::KeyedSHA1(d) => Some(&d.digest),
}
}
fn digest_field_mut(&mut self) -> Option<&mut [u8]> {
match self {
Self::SimplePassword(_) => None,
Self::KeyedMD5(d) => Some(&mut d.digest),
Self::KeyedSHA1(d) => Some(&mut d.digest),
}
}
fn serialize(&self, wtr: &mut impl Write) -> Result<(), std::io::Error> {
wtr.write_u8(self.auth_type().into())?;
wtr.write_u8(self.auth_len())?;
match self {
Self::SimplePassword(ad) => ad.serialize(wtr),
Self::KeyedMD5(ad) => ad.serialize(wtr),
Self::KeyedSHA1(ad) => ad.serialize(wtr),
}
}
}
#[derive(Clone, Debug, NomBE)]
pub struct BfdPacket {
pub flags: BfdFlags,
pub my_disc: BfdDiscriminator,
@@ -270,9 +437,10 @@ pub struct BfdPacket {
}
impl BfdPacket {
pub fn serialize(&self) -> Result<Box<[u8]>, std::io::Error> {
// TODO: serialize auth
let buf = [0u8; 24];
pub fn serialize(&self) -> Result<Vec<u8>, std::io::Error> {
let auth_len = self.auth.as_ref().map_or(0, BfdAuth::auth_len);
let buf_len = 24 + auth_len as usize;
let buf = Vec::with_capacity(buf_len);
let mut wtr = Cursor::new(buf);
wtr.write_u32::<BigEndian>(self.flags.0)?;
wtr.write_u32::<BigEndian>(self.my_disc.0)?;
@@ -281,7 +449,109 @@ impl BfdPacket {
wtr.write_u32::<BigEndian>(self.required_min_rx.0)?;
wtr.write_u32::<BigEndian>(self.required_min_echo_rx.0)?;
Ok(Box::new(wtr.into_inner()))
if let Some(ad) = &self.auth {
ad.serialize(&mut wtr)?;
}
let buf = wtr.into_inner();
assert_eq!(buf.len(), buf_len);
Ok(buf)
}
fn digest<D: Digest>(&self) -> std::io::Result<digest::Output<D>> {
Ok(D::digest(self.serialize()?))
}
/// Digest of the serialised packet as it currently stands, i.e. with whatever is in the digest field
fn compute_digest(&self) -> std::io::Result<Option<Vec<u8>>> {
Ok(match &self.auth {
Some(BfdAuth::KeyedMD5(_)) => Some(self.digest::<md5::Md5>()?.as_slice().to_vec()),
Some(BfdAuth::KeyedSHA1(_)) => Some(self.digest::<sha1::Sha1>()?.as_slice().to_vec()),
Some(BfdAuth::SimplePassword(_)) | None => None,
})
}
/// Replaces the key in the digest field with the digest of the packet (RFC 5880 6.7.3)
pub fn sign(&mut self) -> std::io::Result<()> {
if let Some(digest) = self.compute_digest()? {
if let Some(slot) = self.auth.as_mut().and_then(BfdAuth::digest_field_mut) {
slot.copy_from_slice(&digest);
}
}
Ok(())
}
pub fn verify(&self, expected: &BfdAuth) -> std::io::Result<bool> {
if !self.flags.auth_present() {
warn!("Auth present flag not set; expected authentication. Failed.");
return Ok(false);
};
if let Some(ad) = &self.auth {
// If the received BFD Control packet does not contain an
// Authentication Section, or the Auth Type is not correct,
// then the received packet MUST be discarded.
match (ad, expected) {
(BfdAuth::SimplePassword(sp), BfdAuth::SimplePassword(expected)) => {
// If the Auth Key ID field does not match the ID of a configured
// password, the received packet MUST be discarded.
if sp.key_id != expected.key_id {
warn!(
"Received key id `{}` doesn't match expected key id `{}`",
sp.key_id, expected.key_id
);
return Ok(false);
}
// If the Auth Len field is not equal to the length of the password
// selected by the key ID, plus three, the packet MUST be discarded.
// NB: implied by vec comparison
//
// If the Password field does not match the password selected by the
// key ID, the packet MUST be discarded.
Ok(sp.password.ct_eq(&expected.password).into())
}
(BfdAuth::KeyedMD5(_), BfdAuth::KeyedMD5(_))
| (BfdAuth::KeyedSHA1(_), BfdAuth::KeyedSHA1(_)) => {
if ad.auth_type() != expected.auth_type() {
warn!(
"Received auth type {:?} doesn't match expected {:?}",
ad.auth_type(),
expected.auth_type()
);
return Ok(false);
}
// If the Auth Key ID field does not match the ID of a configured
// authentication key, the received packet MUST be discarded.
if ad.key_id() != expected.key_id() {
warn!(
"Received key id `{}` doesn't match expected key id `{}`",
ad.key_id(),
expected.key_id()
);
return Ok(false);
}
// Replace the contents of the Auth Key/Digest field with the
// authentication key selected by the received Auth Key ID field.
let mut keyed = self.clone();
match (
keyed.auth.as_mut().and_then(BfdAuth::digest_field_mut),
expected.digest_field(),
) {
(Some(slot), Some(key)) => slot.copy_from_slice(key),
_ => return Ok(false),
}
let computed = keyed.compute_digest()?.unwrap_or_default();
Ok(computed.ct_eq(ad.digest_field().unwrap_or_default()).into())
}
_ => {
warn!("Received auth type doesn't match expected, authentication failed");
Ok(false)
}
}
} else {
warn!("Missing auth data, authentication failed");
Ok(false)
}
}
}
@@ -558,4 +828,323 @@ mod tests {
assert_eq!(i.checked_mul(4), Some(BfdInterval::from_secs(4000)));
assert_eq!(i.checked_mul(5), None);
}
const MD5_KEY: [u8; 16] = *b"0123456789abcdef";
const SHA1_KEY: [u8; 20] = *b"0123456789abcdefghij";
// Independently computed (hashlib) over the full packet with the key in the digest field
const MD5_MET_DIGEST: &str = "d739c4fa15bc2468315d20302f2081d5";
const MD5_NONMET_DIGEST: &str = "d0a336d884839041be9e4d99aa713044";
const SHA1_MET_DIGEST: &str = "b47f39e2b986e3c5ba7525244885cf6628d59974";
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn md5_auth(meticulous: bool, key_id: u8, seq: u32, key: [u8; 16]) -> BfdAuth {
BfdAuth::KeyedMD5(BfdAuthKeyedMD5 {
meticulous,
key_id,
seq,
digest: key,
})
}
fn sha1_auth(meticulous: bool, key_id: u8, seq: u32, key: [u8; 20]) -> BfdAuth {
BfdAuth::KeyedSHA1(BfdAuthKeyedSHA1 {
meticulous,
key_id,
seq,
digest: key,
})
}
fn password_auth(key_id: u8, password: &[u8]) -> BfdAuth {
BfdAuth::SimplePassword(BfdAuthSimplePassword {
key_id,
password: password.to_vec(),
})
}
fn auth_packet(auth: Option<BfdAuth>) -> BfdPacket {
let len = 24 + auth.as_ref().map_or(0, BfdAuth::auth_len);
BfdPacket {
flags: BfdFlags(0)
.with_vers(1)
.with_state(BfdState::Up as u8)
.with_detect_mult(3)
.with_length(len)
.with_auth_present(auth.is_some()),
my_disc: BfdDiscriminator(1),
your_disc: BfdDiscriminator(2),
desired_min_tx: BfdInterval::from_millis(300),
required_min_rx: BfdInterval::from_millis(300),
required_min_echo_rx: BfdInterval::from_micros(0),
auth,
}
}
/// Sign, serialise and re-parse, as a packet crosses the wire
fn over_the_wire(auth: BfdAuth) -> BfdPacket {
let mut p = auth_packet(Some(auth));
p.sign().unwrap();
parse(&p.serialize().unwrap())
}
#[test]
fn auth_accessors() {
let md5 = md5_auth(true, 7, 9, MD5_KEY);
assert_eq!(md5.auth_type(), BfdAuthType::MetKeyedMD5);
assert_eq!(md5.auth_len(), 24);
assert_eq!(
(md5.seq(), md5.key_id(), md5.meticulous()),
(Some(9), 7, true)
);
assert_eq!(
md5_auth(false, 0, 0, MD5_KEY).auth_type(),
BfdAuthType::KeyedMD5
);
let sha1 = sha1_auth(true, 1, 2, SHA1_KEY);
assert_eq!(sha1.auth_type(), BfdAuthType::MetKeyedSHA1);
assert_eq!(sha1.auth_len(), 28);
assert_eq!(
sha1_auth(false, 0, 0, SHA1_KEY).auth_type(),
BfdAuthType::KeyedSHA1
);
let pw = password_auth(3, b"secret");
assert_eq!(pw.auth_type(), BfdAuthType::SimplePassword);
assert_eq!(pw.auth_len(), 9);
assert_eq!((pw.seq(), pw.key_id(), pw.meticulous()), (None, 3, false));
assert!(pw.digest_field().is_none());
}
#[test]
fn set_seq_only_affects_keyed() {
let mut md5 = md5_auth(true, 1, 0, MD5_KEY);
md5.set_seq(42);
assert_eq!(md5.seq(), Some(42));
let mut sha1 = sha1_auth(true, 1, 0, SHA1_KEY);
sha1.set_seq(43);
assert_eq!(sha1.seq(), Some(43));
let mut pw = password_auth(1, b"pw");
pw.set_seq(44);
assert_eq!(pw.seq(), None);
}
#[test]
fn keyed_auth_wire_roundtrip() {
for auth in [
md5_auth(false, 7, 0xdead_beef, MD5_KEY),
md5_auth(true, 7, 1, MD5_KEY),
sha1_auth(false, 255, u32::MAX, SHA1_KEY),
sha1_auth(true, 0, 0, SHA1_KEY),
] {
let p = auth_packet(Some(auth.clone()));
let bytes = p.serialize().unwrap();
assert_eq!(bytes.len(), 24 + auth.auth_len() as usize);
assert_eq!(bytes[3] as usize, bytes.len());
assert_eq!(bytes[24], u8::from(auth.auth_type()));
assert_eq!(bytes[25], auth.auth_len());
assert_eq!(parse(&bytes).auth, Some(auth));
}
}
#[test]
fn password_auth_wire_roundtrip() {
for pw in [&b"a"[..], b"secret", b"0123456789abcdef"] {
let auth = password_auth(5, pw);
let p = auth_packet(Some(auth.clone()));
let bytes = p.serialize().unwrap();
assert_eq!(bytes.len(), 24 + 3 + pw.len());
assert_eq!(bytes[24..27], [1, 3 + pw.len() as u8, 5]);
assert_eq!(&bytes[27..], pw);
assert_eq!(parse(&bytes).auth, Some(auth));
}
}
#[test]
fn parse_rejects_bad_auth_sections() {
let mut base = UP_PACKET.to_vec();
base[1] |= 0x04;
// Auth Type 0 (None) and an unassigned type are not valid with the A bit set
for auth_type in [0u8, 6] {
let mut bytes = base.clone();
bytes.extend_from_slice(&[auth_type, 24, 1, 0, 0, 0, 0, 1]);
bytes.extend_from_slice(&[0; 16]);
assert!(BfdPacket::parse(&bytes).is_err(), "type {auth_type}");
}
// Truncated digest
let mut bytes = base.clone();
bytes.extend_from_slice(&[2, 24, 1, 0, 0, 0, 0, 1]);
bytes.extend_from_slice(&[0; 8]);
assert!(BfdPacket::parse(&bytes).is_err());
}
#[test]
fn sign_matches_known_md5_digest() {
let mut p = auth_packet(Some(md5_auth(true, 7, 9, MD5_KEY)));
assert_eq!(p.serialize().unwrap()[..4], [0x20, 0xc4, 0x03, 0x30]);
p.sign().unwrap();
let digest = p.auth.as_ref().unwrap().digest_field().unwrap();
assert_eq!(hex(digest), MD5_MET_DIGEST);
}
#[test]
fn sign_matches_known_sha1_digest() {
let mut p = auth_packet(Some(sha1_auth(true, 7, 9, SHA1_KEY)));
p.sign().unwrap();
let digest = p.auth.as_ref().unwrap().digest_field().unwrap();
assert_eq!(hex(digest), SHA1_MET_DIGEST);
}
#[test]
fn meticulous_flag_changes_digest() {
let mut p = auth_packet(Some(md5_auth(false, 7, 9, MD5_KEY)));
p.sign().unwrap();
let digest = p.auth.as_ref().unwrap().digest_field().unwrap();
assert_eq!(hex(digest), MD5_NONMET_DIGEST);
}
#[test]
fn sign_does_not_leak_key() {
for (auth, key) in [
(md5_auth(true, 7, 9, MD5_KEY), &MD5_KEY[..]),
(sha1_auth(true, 7, 9, SHA1_KEY), &SHA1_KEY[..]),
] {
let bytes = over_the_wire(auth).serialize().unwrap();
assert!(!bytes.windows(key.len()).any(|w| w == key));
}
}
#[test]
fn sign_is_a_noop_without_keyed_auth() {
let mut none = auth_packet(None);
none.sign().unwrap();
assert_eq!(
none.serialize().unwrap(),
parse(&UP_PACKET).serialize().unwrap()
);
let mut pw = auth_packet(Some(password_auth(1, b"secret")));
let before = pw.serialize().unwrap();
pw.sign().unwrap();
assert_eq!(pw.serialize().unwrap(), before);
}
#[test]
fn verify_accepts_valid_keyed_packets() {
for (sent, expected) in [
(md5_auth(true, 7, 9, MD5_KEY), md5_auth(true, 7, 0, MD5_KEY)),
(
md5_auth(false, 7, 9, MD5_KEY),
md5_auth(false, 7, 0, MD5_KEY),
),
(
sha1_auth(true, 7, 9, SHA1_KEY),
sha1_auth(true, 7, 0, SHA1_KEY),
),
(
sha1_auth(false, 7, 9, SHA1_KEY),
sha1_auth(false, 7, 0, SHA1_KEY),
),
] {
assert!(over_the_wire(sent).verify(&expected).unwrap());
}
}
#[test]
fn verify_does_not_depend_on_expected_seq() {
let p = over_the_wire(md5_auth(true, 7, 9, MD5_KEY));
assert!(p.verify(&md5_auth(true, 7, 12345, MD5_KEY)).unwrap());
}
#[test]
fn verify_rejects_wrong_key() {
let mut other = MD5_KEY;
other[15] ^= 1;
let p = over_the_wire(md5_auth(true, 7, 9, MD5_KEY));
assert!(!p.verify(&md5_auth(true, 7, 0, other)).unwrap());
let mut other = SHA1_KEY;
other[0] ^= 1;
let p = over_the_wire(sha1_auth(true, 7, 9, SHA1_KEY));
assert!(!p.verify(&sha1_auth(true, 7, 0, other)).unwrap());
}
#[test]
fn verify_rejects_wrong_key_id() {
let p = over_the_wire(md5_auth(true, 7, 9, MD5_KEY));
assert!(!p.verify(&md5_auth(true, 8, 0, MD5_KEY)).unwrap());
let p = over_the_wire(sha1_auth(true, 7, 9, SHA1_KEY));
assert!(!p.verify(&sha1_auth(true, 8, 0, SHA1_KEY)).unwrap());
}
#[test]
fn verify_rejects_meticulous_mismatch() {
let p = over_the_wire(md5_auth(true, 7, 9, MD5_KEY));
assert!(!p.verify(&md5_auth(false, 7, 0, MD5_KEY)).unwrap());
let p = over_the_wire(sha1_auth(false, 7, 9, SHA1_KEY));
assert!(!p.verify(&sha1_auth(true, 7, 0, SHA1_KEY)).unwrap());
}
#[test]
fn verify_rejects_auth_type_mismatch() {
let md5 = over_the_wire(md5_auth(true, 7, 9, MD5_KEY));
assert!(!md5.verify(&sha1_auth(true, 7, 0, SHA1_KEY)).unwrap());
assert!(!md5.verify(&password_auth(7, b"secret")).unwrap());
let sha1 = over_the_wire(sha1_auth(true, 7, 9, SHA1_KEY));
assert!(!sha1.verify(&md5_auth(true, 7, 0, MD5_KEY)).unwrap());
let pw = over_the_wire(password_auth(7, b"secret"));
assert!(!pw.verify(&md5_auth(true, 7, 0, MD5_KEY)).unwrap());
}
#[test]
fn verify_rejects_tampered_packet() {
let sent = over_the_wire(md5_auth(true, 7, 9, MD5_KEY));
let expected = md5_auth(true, 7, 0, MD5_KEY);
assert!(sent.verify(&expected).unwrap());
let mut p = sent.clone();
p.my_disc = BfdDiscriminator(99);
assert!(!p.verify(&expected).unwrap());
let mut p = sent.clone();
p.flags = p.flags.with_state(BfdState::Down as u8);
assert!(!p.verify(&expected).unwrap());
let mut p = sent.clone();
p.desired_min_tx = BfdInterval::from_micros(1);
assert!(!p.verify(&expected).unwrap());
let mut p = sent.clone();
p.auth.as_mut().unwrap().set_seq(10);
assert!(!p.verify(&expected).unwrap());
let mut p = sent;
if let Some(BfdAuth::KeyedMD5(d)) = p.auth.as_mut() {
d.digest[0] ^= 1;
}
assert!(!p.verify(&expected).unwrap());
}
#[test]
fn verify_rejects_missing_auth() {
let expected = md5_auth(true, 7, 0, MD5_KEY);
assert!(!auth_packet(None).verify(&expected).unwrap());
// A bit set but no section
let mut p = auth_packet(None);
p.flags = p.flags.with_auth_present(true);
assert!(!p.verify(&expected).unwrap());
}
#[test]
fn verify_simple_password() {
let p = over_the_wire(password_auth(5, b"secret"));
assert!(p.verify(&password_auth(5, b"secret")).unwrap());
assert!(!p.verify(&password_auth(5, b"secreT")).unwrap());
assert!(!p.verify(&password_auth(5, b"secrets")).unwrap());
assert!(!p.verify(&password_auth(5, b"secre")).unwrap());
assert!(!p.verify(&password_auth(6, b"secret")).unwrap());
}
}
+177 -24
View File
@@ -3,7 +3,9 @@ use futures::future;
use rust_bfd::config::{Config, ConfigError, ResolvedConfig, ResolvedPeer};
use rust_bfd::events::{EventMessageSink, ScriptHookSink, StateChangeEvent};
use rust_bfd::{bfd::*, events::EventMessage};
use rust_bfd::{set_ttl_or_hop_limit, AtomicInstant, AtomicIntervalStats, BfdSessionStats};
use rust_bfd::{
set_ttl_or_hop_limit, AtomicInstant, AtomicIntervalStats, AuthStats, BfdSessionStats,
};
use std::{
collections::{HashMap, HashSet},
error::Error,
@@ -98,6 +100,14 @@ impl Display for BfdError {
}
impl Error for BfdError {}
/// RFC 5880 6.7.3: a received sequence number must lie in [last, last + 3 * detect_mult] (or [last + 1, ..] when
/// meticulous), treated as an unsigned 32-bit circular number space.
fn seq_in_window(last: u32, seq: u32, detect_mult: u8, meticulous: bool) -> bool {
let lo = last.wrapping_add(meticulous as u32);
let hi = last.wrapping_add(3 * detect_mult as u32);
seq.wrapping_sub(lo) <= hi.wrapping_sub(lo)
}
/// Data structure to store the state of the Bfd machine. The impl does *not*
/// implement any Bfd logic, it is merely a thread-safe data structure
#[derive(Debug)]
@@ -116,10 +126,10 @@ struct BfdSessionState {
demand_mode: Atomic<bool>, // mutable by rx thread
remote_demand_mode: Atomic<bool>, // mutable by rx thread
detect_mult: Atomic<u8>, // mutable by control thread
auth_type: Atomic<BfdAuthType>, // mutable by control thread
rcv_auth_seq: Atomic<u32>, // mutable by rx thread
xmit_auth_seq: Atomic<u32>, // mutable by tx thread?
rcv_auth_seq: Mutex<u32>, // must guard validation against parallel reception
xmit_auth_seq: Mutex<u32>, // must guard against OOO parallel transmission e.g. in rx task
auth_seq_known: Atomic<bool>, // mutable by rx thread?
auth_type: Atomic<BfdAuthType>, // immutable after construction
detection_time: Atomic<BfdInterval>, // mutable by rx thread
poll_mode: Atomic<bool>, // mutable by rx thread
@@ -193,6 +203,12 @@ impl BfdSessionState {
fn auth_type(&self) -> BfdAuthType {
self.auth_type.load(ORDERING)
}
fn auth_seq_known(&self) -> bool {
self.auth_seq_known.load(ORDERING)
}
fn set_auth_seq_known(&self, value: bool) {
self.auth_seq_known.store(value, ORDERING)
}
fn detect_mult(&self) -> u8 {
self.detect_mult.load(ORDERING)
}
@@ -265,6 +281,7 @@ struct BfdSession {
// DesiredMinTxInterval actually used for the transmit rate; lags the advertised value during a Poll Sequence
effective_desired_min_tx: BfdInterval,
event_dispatcher: EventDispatcherHandle,
auth: Option<BfdAuth>,
}
impl From<&BfdSessionState> for BfdSessionStats {
@@ -283,6 +300,7 @@ impl From<&BfdSessionState> for BfdSessionStats {
last_change: state.last_state_change(),
detect_time: state.detection_time(),
base_interval: BfdInterval::from_secs(0), // TODO: This is a hack and will be patched up in the session
auth: None, // filled in by the session
}
}
}
@@ -463,6 +481,9 @@ impl BfdSession {
// .connect(SocketAddr::new(remote_addr, CONTROL_PORT))
// .await?;
let auth: Option<BfdAuth> = peer.auth.map(Into::into);
let auth_type = auth.as_ref().map_or(BfdAuthType::None, BfdAuth::auth_type);
// Incoming packets will come over the channel from the mux, since they don't send to the reciprocal port
Self {
rx,
@@ -473,6 +494,7 @@ impl BfdSession {
configured_desired_min_tx,
effective_desired_min_tx: slow_desired_min_tx,
event_dispatcher,
auth,
state: BfdSessionState {
control_sock: Arc::new(control_sock),
local_ip: local_addr,
@@ -488,10 +510,10 @@ impl BfdSession {
demand_mode: Atomic::new(false),
remote_demand_mode: Atomic::new(false),
detect_mult: Atomic::new(peer.detect_mult),
auth_type: Atomic::new(BfdAuthType::None),
rcv_auth_seq: Atomic::new(0),
xmit_auth_seq: Atomic::new(rng.gen()),
rcv_auth_seq: Mutex::new(0),
xmit_auth_seq: Mutex::new(rng.gen()),
auth_seq_known: Atomic::new(false),
auth_type: Atomic::new(auth_type),
detection_time: Atomic::new(BfdInterval::from_secs(0)),
poll_mode: Atomic::new(false),
@@ -568,16 +590,41 @@ impl BfdSession {
self.transmit_control_packet(false).await;
}
async fn auth_stats(&self) -> Option<AuthStats> {
let auth = self.auth.as_ref()?;
let uses_seq = !matches!(auth, BfdAuth::SimplePassword(_));
let rcv_seq = if uses_seq && self.state.auth_seq_known() {
Some(*self.state.rcv_auth_seq.lock().await)
} else {
None
};
let xmit_seq = if uses_seq {
Some(*self.state.xmit_auth_seq.lock().await)
} else {
None
};
Some(AuthStats {
auth_type: self.state.auth_type(),
key_id: auth.key_id(),
rcv_seq,
xmit_seq,
})
}
async fn run(&mut self) {
while let Some(cmd) = self.rx.recv().await {
let mut missed_detect_times = 0;
match cmd {
SessionControlCommand::Quit => return,
SessionControlCommand::RxPacket(packet) => {
missed_detect_times = 0;
self.receive_control_packet(&packet).await;
}
SessionControlCommand::GetSessionStats { respond_to } => {
let stats =
BfdSessionStats::from(&self.state).with_base_interval(self.cur_interval);
let auth = self.auth_stats().await;
let stats = BfdSessionStats::from(&self.state)
.with_base_interval(self.cur_interval)
.with_auth(auth);
respond_to.send(stats).unwrap()
}
SessionControlCommand::TxControlPacket { poll_response } => {
@@ -585,15 +632,19 @@ impl BfdSession {
}
SessionControlCommand::ReceiveTimeout => {
self.state.set_local_diag(BfdDiagnostic::TimeExpired);
missed_detect_times += 1;
self.transition(BfdState::Down).await;
}
}
if missed_detect_times == 2 {
self.state.set_auth_seq_known(false);
}
}
println!("Session command channel closed");
}
async fn transmit_control_packet(&self, poll_response: bool) {
let packet = BfdPacket {
async fn transmit_control_packet(&mut self, poll_response: bool) {
let mut packet = BfdPacket {
flags: BfdFlags(0)
.with_vers(1)
.with_diag(self.state.local_diag().into())
@@ -607,18 +658,32 @@ impl BfdSession {
.with_cpi(false)
.with_demand(self.state.demand_mode())
.with_detect_mult(self.state.detect_mult())
.with_length(24),
.with_length(24 + self.auth.as_ref().map_or(0, BfdAuth::auth_len))
.with_auth_present(self.auth.is_some()),
my_disc: self.state.local_discr(),
your_disc: self.state.remote_discr(),
desired_min_tx: self.state.desired_min_tx_interval(),
required_min_rx: self.state.required_min_rx_interval(),
required_min_echo_rx: BfdInterval::from_secs(0),
auth: None,
auth: self.auth.clone(), // put configured method and key into packet
};
let socket = self.state.control_sock.clone();
let dest = self.state.peer_addr;
// Held until the packet is sent so sequence numbers hit the wire in order
let mut xmit_seq = match packet.auth {
Some(_) => Some(self.state.xmit_auth_seq.lock().await),
None => None,
};
if let (Some(auth), Some(seq)) = (&mut packet.auth, xmit_seq.as_mut()) {
**seq = seq.wrapping_add(1);
auth.set_seq(**seq);
}
packet.sign().unwrap();
self.state.record_tx();
debug!("tx packet: {:?}", packet);
if let Err(e) = socket
.send_to(
packet.serialize().unwrap().as_ref(),
@@ -755,7 +820,6 @@ impl BfdSession {
warn!("Got packet with auth enabled when we disagree, discarding");
return;
}
// If the A bit is clear and authentication is in use (bfd.AuthType is nonzero), the packet MUST be discarded.
if !p.flags.auth_present() && self.state.auth_type() != BfdAuthType::None {
warn!("Got packet without auth when we expect it, discarding");
@@ -764,8 +828,48 @@ impl BfdSession {
// If the A bit is set, the packet MUST be authenticated under the rules of section 6.7, based on the
// authentication type in use (bfd.AuthType). This may cause the packet to be discarded.
if p.flags.auth_present() {
unimplemented!("Authentication is not implemented");
if let Some(expected) = self.auth.as_ref().filter(|_| p.flags.auth_present()) {
let received_seq = p.auth.as_ref().and_then(BfdAuth::seq);
let mut rcv_auth_seq = self.state.rcv_auth_seq.lock().await;
if let Some(seq) = received_seq {
// If bfd.AuthSeqKnown is 1, examine the Sequence Number field. [...] if the sequence number lies
// outside of the valid range the received packet MUST be discarded.
if self.state.auth_seq_known() {
if !seq_in_window(
*rcv_auth_seq,
seq,
p.flags.detect_mult(),
expected.meticulous(),
) {
warn!(
"Got packet with out of window sequence number {} (last {}), discarding",
seq, *rcv_auth_seq
);
return;
}
} else {
// Otherwise (bfd.AuthSeqKnown is 0), set bfd.AuthSeqKnown to 1.
self.state.set_auth_seq_known(true);
}
// bfd.RcvAuthSeq is set to the Sequence Number field.
*rcv_auth_seq = seq;
}
match p.verify(expected) {
Ok(true) => {}
Ok(false) => {
warn!("Got packet failing authentication, discarding");
return;
}
Err(e) => {
warn!(
"Error verifying packet authentication ({:?}), discarding",
e
);
return;
}
}
}
// Set bfd.RemoteDiscr to the value of My Discriminator.
@@ -1083,7 +1187,7 @@ impl Bfdd {
}
let session = BfdSessionHandle::new(
self.local.ip(),
*peer,
peer.clone(),
local_discr,
self.event_dispatcher.clone(),
)
@@ -1164,13 +1268,22 @@ impl Bfdd {
}
}
async fn session_report(&self) -> String {
future::join_all(
let mut stats = future::join_all(
self.sessions_by_discr
.values()
.map(|session| async { session.get_stats().await.unwrap().to_string() }),
.map(|session| async { session.get_stats().await.unwrap() }),
)
.await
.join("\n\t")
.await;
stats.sort_by_key(|s| s.remote_ip);
stats
.iter()
.flat_map(|s| {
s.to_string()
.lines()
.map(|l| format!("\n {l}"))
.collect::<Vec<_>>()
})
.collect()
}
async fn handle_datagram(&self, data: &[u8], addr: SocketAddr) {
let Ok((_leftover, packet)) = BfdPacket::parse(data) else {
@@ -1189,7 +1302,7 @@ impl Bfdd {
async fn run(mut self) -> io::Result<()> {
info!("rust-bfd starting up");
self.start_peers().await?;
info!("configured sessions: {}", self.session_report().await);
info!("configured sessions:{}", self.session_report().await);
let mut sigusr1 = signal(SignalKind::user_defined1())?;
let mut buf = [0; 1024];
@@ -1204,14 +1317,14 @@ impl Bfdd {
Ok((len, addr)) => {
self.handle_datagram(&buf[..len], addr).await;
if log_enabled!(log::Level::Debug) {
debug!("configured sessions: {}", self.session_report().await);
debug!("configured sessions:{}", self.session_report().await);
}
}
Err(e) => warn!("Error receiving from socket {e:?}"),
},
_ = sigusr1.recv() => {
info!("SIGUSR1 received");
info!("configured sessions: {}", self.session_report().await);
info!("configured sessions:{}", self.session_report().await);
}
}
}
@@ -1248,6 +1361,45 @@ async fn main() -> io::Result<()> {
mod tests {
use super::*;
#[test]
fn seq_window_non_meticulous_bounds() {
// RFC 5880 6.7.3: [RcvAuthSeq, RcvAuthSeq + 3*DetectMult]
assert!(seq_in_window(100, 100, 3, false));
assert!(seq_in_window(100, 101, 3, false));
assert!(seq_in_window(100, 109, 3, false));
assert!(!seq_in_window(100, 110, 3, false));
assert!(!seq_in_window(100, 99, 3, false));
}
#[test]
fn seq_window_meticulous_bounds() {
// RFC 5880 6.7.4: [RcvAuthSeq + 1, RcvAuthSeq + 3*DetectMult]
assert!(!seq_in_window(100, 100, 3, true));
assert!(seq_in_window(100, 101, 3, true));
assert!(seq_in_window(100, 109, 3, true));
assert!(!seq_in_window(100, 110, 3, true));
assert!(!seq_in_window(100, 99, 3, true));
}
#[test]
fn seq_window_scales_with_detect_mult() {
assert!(seq_in_window(0, 3, 1, false));
assert!(!seq_in_window(0, 4, 1, false));
assert!(seq_in_window(0, 255 * 3, 255, false));
assert!(!seq_in_window(0, 255 * 3 + 1, 255, false));
}
#[test]
fn seq_window_wraps_at_u32_max() {
assert!(seq_in_window(u32::MAX, u32::MAX, 3, false));
assert!(!seq_in_window(u32::MAX, u32::MAX, 3, true));
assert!(seq_in_window(u32::MAX, 0, 3, true));
assert!(seq_in_window(u32::MAX - 2, 6, 3, true));
assert!(!seq_in_window(u32::MAX - 2, 7, 3, true));
assert!(!seq_in_window(u32::MAX - 2, u32::MAX - 3, 3, true));
assert!(!seq_in_window(5, u32::MAX, 3, true));
}
#[test]
fn jitter_is_uniform_over_rfc_range() {
// RFC 5880 6.8.7: each interval is reduced by a random 0-25% (0-10% when DetectMult is 1), so delays are
@@ -1384,6 +1536,7 @@ mod tests {
desired_min_tx: BfdInterval::from_millis(desired_ms),
required_min_rx: BfdInterval::from_millis(required_ms),
detect_mult,
auth: None,
}
}
+83 -7
View File
@@ -17,8 +17,9 @@ use std::{
};
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_valid::Validate;
use crate::bfd::BfdInterval;
use crate::bfd::{BfdAuth, BfdAuthKeyedMD5, BfdAuthKeyedSHA1, BfdAuthSimplePassword, BfdInterval};
pub const DEFAULT_CONFIG_PATH: &str = "/etc/rust-bfd/config.yaml";
pub const DEFAULT_HOOKS_DIR: &str = "/etc/rust-bfd/hooks.d";
@@ -29,6 +30,10 @@ pub const ENV_DEFAULTS_DESIRED_MIN_TX: &str = "BFD_DEFAULTS_DESIRED_MIN_TX";
pub const ENV_DEFAULTS_REQUIRED_MIN_RX: &str = "BFD_DEFAULTS_REQUIRED_MIN_RX";
pub const ENV_DEFAULTS_DETECT_MULT: &str = "BFD_DEFAULTS_DETECT_MULT";
fn default_true() -> bool {
true
}
#[derive(Debug)]
pub enum ConfigError {
Io {
@@ -94,6 +99,71 @@ impl Default for Defaults {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Validate)]
#[serde(deny_unknown_fields)]
pub struct PasswordAuthConfig {
pub key_id: u8,
#[serde(with = "serde_bytes")]
// The password is a binary string, and MUST be 1 to 16 bytes in
// length.
#[validate(min_items = 1)]
#[validate(max_items = 16)]
pub password: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Validate)]
#[serde(deny_unknown_fields)]
pub struct Md5AuthConfig {
pub key_id: u8,
#[serde(with = "serde_bytes")]
// The key is given verbatim (not derived) and must be exactly 16 bytes
pub key: [u8; 16],
#[serde(default = "default_true")]
pub meticulous: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Validate)]
#[serde(deny_unknown_fields)]
pub struct Sha1AuthConfig {
pub key_id: u8,
#[serde(with = "serde_bytes")]
// The key is given verbatim (not derived) and must be exactly 20 bytes
pub key: [u8; 20],
#[serde(default = "default_true")]
pub meticulous: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, tag = "type", rename_all = "lowercase")]
pub enum AuthConfig {
Password(PasswordAuthConfig),
Md5(Md5AuthConfig),
Sha1(Sha1AuthConfig),
}
impl From<AuthConfig> for BfdAuth {
fn from(value: AuthConfig) -> Self {
match value {
AuthConfig::Password(ac) => BfdAuth::SimplePassword(BfdAuthSimplePassword {
key_id: ac.key_id,
password: ac.password,
}),
AuthConfig::Md5(ac) => BfdAuth::KeyedMD5(BfdAuthKeyedMD5 {
key_id: ac.key_id,
meticulous: ac.meticulous,
digest: ac.key,
seq: 0, // set during signing
}),
AuthConfig::Sha1(ac) => BfdAuth::KeyedSHA1(BfdAuthKeyedSHA1 {
key_id: ac.key_id,
meticulous: ac.meticulous,
digest: ac.key,
seq: 0, // set during signing
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeerConfig {
@@ -112,6 +182,8 @@ pub struct PeerConfig {
pub required_min_rx: Option<Duration>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detect_mult: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<AuthConfig>,
}
impl PeerConfig {
@@ -121,6 +193,7 @@ impl PeerConfig {
desired_min_tx: None,
required_min_rx: None,
detect_mult: None,
auth: None,
}
}
}
@@ -253,7 +326,7 @@ impl Config {
}
/// Validate and apply defaults to each peer.
pub fn resolve(&self) -> Result<ResolvedConfig, ConfigError> {
pub fn resolve(self) -> Result<ResolvedConfig, ConfigError> {
let invalid = |message: String| ConfigError::Invalid(message);
if self.bind.is_empty() {
@@ -269,7 +342,7 @@ impl Config {
let mut seen = HashSet::new();
let peers = self
.peers
.iter()
.into_iter()
.map(|peer| {
if !seen.insert(peer.address) {
return Err(invalid(format!("duplicate peer {}", peer.address)));
@@ -304,6 +377,7 @@ impl Config {
&what("required_min_rx"),
)?,
detect_mult,
auth: peer.auth,
})
})
.collect::<Result<Vec<_>, _>>()?;
@@ -351,12 +425,13 @@ pub struct ResolvedConfig {
pub peers: Vec<ResolvedPeer>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedPeer {
pub address: IpAddr,
pub desired_min_tx: BfdInterval,
pub required_min_rx: BfdInterval,
pub detect_mult: u8,
pub auth: Option<AuthConfig>,
}
#[cfg(test)]
@@ -418,7 +493,7 @@ peers:
#[test]
fn peers_inherit_defaults_and_override() {
let resolved = Config::from_yaml(FULL).unwrap().resolve().unwrap();
let (a, b) = (resolved.peers[0], resolved.peers[1]);
let (a, b) = (&resolved.peers[0], &resolved.peers[1]);
assert_eq!(a.desired_min_tx, BfdInterval::from_millis(500));
assert_eq!(a.required_min_rx, BfdInterval::from_millis(250));
assert_eq!(a.detect_mult, 5);
@@ -500,10 +575,10 @@ peers:
#[test]
fn dist_config_is_valid() {
let config = Config::from_yaml(include_str!("../config.yaml.dist")).unwrap();
let resolved = config.resolve().unwrap();
assert_eq!(resolved.peers.len(), 1);
assert_eq!(config.defaults, Defaults::default());
assert_eq!(config.hooks_dir, PathBuf::from(DEFAULT_HOOKS_DIR));
let resolved = config.resolve().unwrap();
assert_eq!(resolved.peers.len(), 1);
}
#[test]
@@ -582,6 +657,7 @@ peers:
let mut c = base();
c.peers.push(PeerConfig::new(ip("2001:db8::2")));
assert!(c
.clone()
.resolve()
.unwrap_err()
.to_string()
+64 -2
View File
@@ -167,6 +167,28 @@ fn fmt_ago(t: Option<Instant>) -> String {
})
}
/// A snapshot of a session's authentication state. Sequence numbers are `None` when not applicable or not yet known.
#[derive(Debug, Clone, Copy)]
pub struct AuthStats {
pub auth_type: BfdAuthType,
pub key_id: u8,
pub rcv_seq: Option<u32>,
pub xmit_seq: Option<u32>,
}
impl Display for AuthStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let seq = |s: Option<u32>| s.map_or("-".to_string(), |s| s.to_string());
write!(
f,
"{} KeyId:{} RcvSeq:{} XmitSeq:{}",
self.auth_type,
self.key_id,
seq(self.rcv_seq),
seq(self.xmit_seq)
)
}
}
#[derive(Debug)]
pub struct BfdSessionStats {
pub local_ip: IpAddr,
@@ -183,8 +205,14 @@ pub struct BfdSessionStats {
pub last_change: Instant,
pub detect_time: BfdInterval,
pub base_interval: BfdInterval,
/// `None` when the session does not use authentication
pub auth: Option<AuthStats>,
}
impl BfdSessionStats {
pub fn with_auth(mut self, auth: Option<AuthStats>) -> Self {
self.auth = auth;
self
}
pub fn with_base_interval(mut self, base_interval: BfdInterval) -> Self {
self.base_interval = base_interval;
self
@@ -212,6 +240,10 @@ impl Display for BfdSessionStats {
)?;
writeln!(f, " Rx: {}", self.rx)?;
writeln!(f, " Tx: {}", self.tx)?;
match &self.auth {
Some(auth) => writeln!(f, " Auth: {auth}")?,
None => writeln!(f, " Auth: none")?,
}
write!(
f,
" Last Up: {} Last Down: {}",
@@ -360,6 +392,7 @@ mod tests {
last_change: Instant::now(),
detect_time: BfdInterval::from_millis(900),
base_interval: BfdInterval::from_secs(1),
auth: None,
}
}
@@ -367,7 +400,7 @@ mod tests {
fn session_stats_display_never() {
let out = session_stats(None, None).to_string();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 5);
assert_eq!(lines.len(), 6);
assert_eq!(
lines[0].split_whitespace().collect::<Vec<_>>(),
["192.0.2.2", "Up", "2", "1"]
@@ -381,7 +414,36 @@ mod tests {
" Rx: 2 pkts, interval min/avg/max 300/300/300 ms"
);
assert_eq!(lines[3], " Tx: 1 pkts, interval min/avg/max -/-/- ms");
assert_eq!(lines[4], " Last Up: never Last Down: never");
assert_eq!(lines[4], " Auth: none");
assert_eq!(lines[5], " Last Up: never Last Down: never");
}
#[test]
fn session_stats_display_auth() {
let out = session_stats(None, None)
.with_auth(Some(AuthStats {
auth_type: BfdAuthType::MetKeyedSHA1,
key_id: 3,
rcv_seq: Some(100),
xmit_seq: Some(7),
}))
.to_string();
assert!(
out.contains("\n Auth: meticulous-sha1 KeyId:3 RcvSeq:100 XmitSeq:7\n"),
"{out}"
);
let out = session_stats(None, None)
.with_auth(Some(AuthStats {
auth_type: BfdAuthType::KeyedMD5,
key_id: 1,
rcv_seq: None,
xmit_seq: Some(7),
}))
.to_string();
assert!(
out.contains("\n Auth: md5 KeyId:1 RcvSeq:- XmitSeq:7\n"),
"{out}"
);
}
#[test]