1use anyhow::{bail, ensure, Context, Result};
5use md5::{Digest as _, Md5};
6use sha1::Sha1;
7
8pub const SINGLE_HOP_PORT: u16 = 3784;
10pub const MULTIHOP_PORT: u16 = 4784;
11pub const SOURCE_PORTS: std::ops::RangeInclusive<u16> = 49152..=65535;
13pub const MANDATORY_LEN: usize = 24;
15pub const MAX_PACKET: usize = MANDATORY_LEN + 28;
18pub 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
48pub 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 pub fn meticulous(self) -> bool {
134 matches!(
135 self,
136 AuthType::MeticulousKeyedMd5 | AuthType::MeticulousKeyedSha1
137 )
138 }
139}
140
141#[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#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct AuthSection {
174 pub kind: AuthType,
175 pub key_id: u8,
176 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 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); 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
261pub 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
364pub 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, ©) == bytes[start..length],
397 "the {} digest does not match",
398 kind.name()
399 );
400 }
401 }
402 Ok(())
403}