Skip to main content

netget/server/bfd/
packet.rs

1//! The BFD Control packet (RFC 5880 §4) and its authentication section (§4.2–4.4, §6.7).
2//! Shared by the server and the client (`src/client/bfd/`). Everything here is pure: the
3//! session state machine is in `session.rs`.
4use anyhow::{bail, ensure, Context, Result};
5use md5::{Digest as _, Md5};
6use sha1::Sha1;
7
8/// Single-hop BFD (RFC 5881) and multihop BFD (RFC 5883) destination ports.
9pub const SINGLE_HOP_PORT: u16 = 3784;
10pub const MULTIHOP_PORT: u16 = 4784;
11/// RFC 5881 §4: the source port MUST be in this range.
12pub const SOURCE_PORTS: std::ops::RangeInclusive<u16> = 49152..=65535;
13/// A Control packet with no authentication section.
14pub const MANDATORY_LEN: usize = 24;
15/// The largest Control packet: the mandatory section plus a keyed SHA1 section (28 bytes).
16/// Anything longer is refused before it is parsed.
17pub const MAX_PACKET: usize = MANDATORY_LEN + 28;
18/// Simple Password: 1 to 16 bytes (§4.2).
19pub const MAX_PASSWORD: usize = 16;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum State {
23    AdminDown = 0,
24    Down = 1,
25    Init = 2,
26    Up = 3,
27}
28
29impl State {
30    pub fn from_bits(b: u8) -> Self {
31        match b & 3 {
32            0 => State::AdminDown,
33            1 => State::Down,
34            2 => State::Init,
35            _ => State::Up,
36        }
37    }
38    pub fn name(self) -> &'static str {
39        match self {
40            State::AdminDown => "AdminDown",
41            State::Down => "Down",
42            State::Init => "Init",
43            State::Up => "Up",
44        }
45    }
46}
47
48/// The diagnostic codes of §4.1.
49pub const DIAGNOSTICS: &[(u8, &str)] = &[
50    (0, "no_diagnostic"),
51    (1, "control_detection_time_expired"),
52    (2, "echo_function_failed"),
53    (3, "neighbor_signaled_session_down"),
54    (4, "forwarding_plane_reset"),
55    (5, "path_down"),
56    (6, "concatenated_path_down"),
57    (7, "administratively_down"),
58    (8, "reverse_concatenated_path_down"),
59];
60
61pub fn diag_name(code: u8) -> String {
62    DIAGNOSTICS
63        .iter()
64        .find(|(c, _)| *c == code)
65        .map(|(_, n)| n.to_string())
66        .unwrap_or_else(|| format!("reserved_{code}"))
67}
68
69pub fn diag_code(name: &str) -> Result<u8> {
70    DIAGNOSTICS
71        .iter()
72        .find(|(_, n)| *n == name)
73        .map(|(c, _)| *c)
74        .with_context(|| {
75            format!(
76                "diag {name:?} is not one of {}",
77                DIAGNOSTICS
78                    .iter()
79                    .map(|(_, n)| *n)
80                    .collect::<Vec<_>>()
81                    .join(", ")
82            )
83        })
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum AuthType {
88    SimplePassword = 1,
89    KeyedMd5 = 2,
90    MeticulousKeyedMd5 = 3,
91    KeyedSha1 = 4,
92    MeticulousKeyedSha1 = 5,
93}
94
95impl AuthType {
96    pub fn from_code(c: u8) -> Result<Self> {
97        Ok(match c {
98            1 => AuthType::SimplePassword,
99            2 => AuthType::KeyedMd5,
100            3 => AuthType::MeticulousKeyedMd5,
101            4 => AuthType::KeyedSha1,
102            5 => AuthType::MeticulousKeyedSha1,
103            other => bail!("authentication type {other} is reserved"),
104        })
105    }
106    pub fn parse(name: &str) -> Result<Self> {
107        Ok(match name {
108            "simple" => AuthType::SimplePassword,
109            "keyed_md5" => AuthType::KeyedMd5,
110            "meticulous_keyed_md5" => AuthType::MeticulousKeyedMd5,
111            "keyed_sha1" => AuthType::KeyedSha1,
112            "meticulous_keyed_sha1" => AuthType::MeticulousKeyedSha1,
113            other => bail!("auth_type {other:?} is not simple, keyed_md5, meticulous_keyed_md5, keyed_sha1 or meticulous_keyed_sha1"),
114        })
115    }
116    pub fn name(self) -> &'static str {
117        match self {
118            AuthType::SimplePassword => "simple",
119            AuthType::KeyedMd5 => "keyed_md5",
120            AuthType::MeticulousKeyedMd5 => "meticulous_keyed_md5",
121            AuthType::KeyedSha1 => "keyed_sha1",
122            AuthType::MeticulousKeyedSha1 => "meticulous_keyed_sha1",
123        }
124    }
125    fn digest_len(self) -> usize {
126        match self {
127            AuthType::SimplePassword => 0,
128            AuthType::KeyedMd5 | AuthType::MeticulousKeyedMd5 => 16,
129            AuthType::KeyedSha1 | AuthType::MeticulousKeyedSha1 => 20,
130        }
131    }
132    /// The sequence number must increase with every packet, not merely not decrease.
133    pub fn meticulous(self) -> bool {
134        matches!(
135            self,
136            AuthType::MeticulousKeyedMd5 | AuthType::MeticulousKeyedSha1
137        )
138    }
139}
140
141/// A session's authentication: one key, as BIRD and FRR configure it.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct AuthConfig {
144    pub kind: AuthType,
145    pub key_id: u8,
146    pub password: Vec<u8>,
147}
148
149impl AuthConfig {
150    pub fn new(kind: AuthType, key_id: u8, password: &str) -> Result<Self> {
151        let password = password.as_bytes().to_vec();
152        ensure!(!password.is_empty(), "the BFD password is empty");
153        let max = match kind {
154            AuthType::SimplePassword => MAX_PASSWORD,
155            other => other.digest_len(),
156        };
157        ensure!(
158            password.len() <= max,
159            "a {} password is at most {max} bytes",
160            kind.name()
161        );
162        Ok(Self {
163            kind,
164            key_id,
165            password,
166        })
167    }
168}
169
170/// The authentication section of a received packet, as far as it can be read before the key
171/// is known.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct AuthSection {
174    pub kind: AuthType,
175    pub key_id: u8,
176    /// Keyed types only.
177    pub sequence: Option<u32>,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct ControlPacket {
182    pub diag: u8,
183    pub state: State,
184    pub poll: bool,
185    pub final_: bool,
186    pub control_plane_independent: bool,
187    pub demand: bool,
188    pub detect_mult: u8,
189    pub my_discriminator: u32,
190    pub your_discriminator: u32,
191    pub desired_min_tx_us: u32,
192    pub required_min_rx_us: u32,
193    pub required_min_echo_rx_us: u32,
194    pub auth: Option<AuthSection>,
195}
196
197impl ControlPacket {
198    /// Encode, signing with `auth` (and `sequence`, for the keyed types) when given.
199    pub fn encode(&self, auth: Option<(&AuthConfig, u32)>) -> Vec<u8> {
200        let mut out = Vec::with_capacity(MAX_PACKET);
201        out.push((1 << 5) | (self.diag & 0x1f));
202        out.push(
203            ((self.state as u8) << 6)
204                | (u8::from(self.poll) << 5)
205                | (u8::from(self.final_) << 4)
206                | (u8::from(self.control_plane_independent) << 3)
207                | (u8::from(auth.is_some()) << 2)
208                | (u8::from(self.demand) << 1),
209        );
210        out.push(self.detect_mult);
211        out.push(0); // length, below
212        for v in [
213            self.my_discriminator,
214            self.your_discriminator,
215            self.desired_min_tx_us,
216            self.required_min_rx_us,
217            self.required_min_echo_rx_us,
218        ] {
219            out.extend_from_slice(&v.to_be_bytes());
220        }
221        if let Some((a, sequence)) = auth {
222            out.push(a.kind as u8);
223            match a.kind {
224                AuthType::SimplePassword => {
225                    out.push(3 + a.password.len() as u8);
226                    out.push(a.key_id);
227                    out.extend_from_slice(&a.password);
228                }
229                kind => {
230                    let n = kind.digest_len();
231                    out.push(8 + n as u8);
232                    out.push(a.key_id);
233                    out.push(0);
234                    out.extend_from_slice(&sequence.to_be_bytes());
235                    let start = out.len();
236                    out.extend_from_slice(&padded(&a.password, n));
237                    out[3] = out.len() as u8;
238                    let digest = digest(kind, &out);
239                    out[start..].copy_from_slice(&digest);
240                }
241            }
242        }
243        out[3] = out.len() as u8;
244        out
245    }
246}
247
248fn padded(key: &[u8], n: usize) -> Vec<u8> {
249    let mut k = key.to_vec();
250    k.resize(n, 0);
251    k
252}
253
254fn digest(kind: AuthType, bytes: &[u8]) -> Vec<u8> {
255    match kind.digest_len() {
256        16 => Md5::digest(bytes).to_vec(),
257        _ => Sha1::digest(bytes).to_vec(),
258    }
259}
260
261/// Decode and check everything §6.8.6 lets a receiver check without session state: version 1,
262/// the length against the datagram, a non-zero Detect Mult and My Discriminator, no
263/// Multipoint, and a well-formed authentication section. Authentication itself is
264/// [`verify`]; discriminator lookups are the session table's.
265pub fn decode(bytes: &[u8]) -> Result<ControlPacket> {
266    ensure!(
267        bytes.len() <= MAX_PACKET,
268        "a {}-byte datagram is longer than any BFD Control packet ({MAX_PACKET})",
269        bytes.len()
270    );
271    ensure!(
272        bytes.len() >= MANDATORY_LEN,
273        "a {}-byte datagram is shorter than a BFD Control packet",
274        bytes.len()
275    );
276    let version = bytes[0] >> 5;
277    ensure!(version == 1, "BFD version {version}, not 1");
278    let flags = bytes[1];
279    let auth_present = flags & 0x04 != 0;
280    let length = bytes[3] as usize;
281    ensure!(
282        length
283            >= if auth_present {
284                MANDATORY_LEN + 2
285            } else {
286                MANDATORY_LEN
287            },
288        "Length {length} is too short"
289    );
290    ensure!(
291        length <= bytes.len(),
292        "Length {length} is longer than the {}-byte datagram",
293        bytes.len()
294    );
295    ensure!(flags & 0x01 == 0, "the Multipoint bit is set");
296    let detect_mult = bytes[2];
297    ensure!(detect_mult != 0, "Detect Mult is zero");
298    let word = |i: usize| u32::from_be_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
299    let my_discriminator = word(4);
300    ensure!(my_discriminator != 0, "My Discriminator is zero");
301    let state = State::from_bits(flags >> 6);
302    let your_discriminator = word(8);
303    ensure!(
304        your_discriminator != 0 || matches!(state, State::Down | State::AdminDown),
305        "Your Discriminator is zero in state {}",
306        state.name()
307    );
308    let auth = if auth_present {
309        let kind = AuthType::from_code(bytes[24])?;
310        let auth_len = bytes[25] as usize;
311        ensure!(
312            MANDATORY_LEN + auth_len == length,
313            "Auth Len {auth_len} does not end the packet at Length {length}"
314        );
315        match kind {
316            AuthType::SimplePassword => {
317                ensure!(
318                    (4..=3 + MAX_PASSWORD).contains(&auth_len),
319                    "a Simple Password section of {auth_len} bytes"
320                );
321                Some(AuthSection {
322                    kind,
323                    key_id: bytes[26],
324                    sequence: None,
325                })
326            }
327            kind => {
328                ensure!(
329                    auth_len == 8 + kind.digest_len(),
330                    "a {} section of {auth_len} bytes",
331                    kind.name()
332                );
333                Some(AuthSection {
334                    kind,
335                    key_id: bytes[26],
336                    sequence: Some(word(28)),
337                })
338            }
339        }
340    } else {
341        ensure!(
342            length == MANDATORY_LEN,
343            "Length {length} with no authentication section"
344        );
345        None
346    };
347    Ok(ControlPacket {
348        diag: bytes[0] & 0x1f,
349        state,
350        poll: flags & 0x20 != 0,
351        final_: flags & 0x10 != 0,
352        control_plane_independent: flags & 0x08 != 0,
353        demand: flags & 0x02 != 0,
354        detect_mult,
355        my_discriminator,
356        your_discriminator,
357        desired_min_tx_us: word(12),
358        required_min_rx_us: word(16),
359        required_min_echo_rx_us: word(20),
360        auth,
361    })
362}
363
364/// Check a received packet's authentication against the session's configuration: the same
365/// type and key, the password or the digest. The sequence-number window is the session's.
366pub fn verify(bytes: &[u8], packet: &ControlPacket, config: Option<&AuthConfig>) -> Result<()> {
367    let (section, config) = match (&packet.auth, config) {
368        (None, None) => return Ok(()),
369        (Some(_), None) => bail!("the packet is authenticated and this session is not"),
370        (None, Some(_)) => bail!("the packet is not authenticated and this session is"),
371        (Some(s), Some(c)) => (s, c),
372    };
373    ensure!(
374        section.kind == config.kind,
375        "authentication type {} where {} is configured",
376        section.kind.name(),
377        config.kind.name()
378    );
379    ensure!(
380        section.key_id == config.key_id,
381        "key id {} where {} is configured",
382        section.key_id,
383        config.key_id
384    );
385    let length = bytes[3] as usize;
386    match config.kind {
387        AuthType::SimplePassword => {
388            ensure!(bytes[27..length] == config.password[..], "wrong password");
389        }
390        kind => {
391            let n = kind.digest_len();
392            let start = length - n;
393            let mut copy = bytes[..length].to_vec();
394            copy[start..].copy_from_slice(&padded(&config.password, n));
395            ensure!(
396                digest(kind, &copy) == bytes[start..length],
397                "the {} digest does not match",
398                kind.name()
399            );
400        }
401    }
402    Ok(())
403}