From bea504ea8bae4030b96e338eb4442c07e7624686 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sat, 19 Apr 2025 17:13:04 +0200 Subject: [PATCH 1/2] Initial commit --- .gitignore | 1 + Cargo.lock | 455 +++++++++++++++++ Cargo.toml | 26 + README.md | 11 + bin/bgp/Cargo.toml | 8 + bin/bgp/src/main.rs | 6 + crates/packet/Cargo.toml | 13 + crates/packet/src/constants.rs | 70 +++ crates/packet/src/errors.rs | 149 ++++++ crates/packet/src/ip_prefix.rs | 215 ++++++++ crates/packet/src/lib.rs | 5 + crates/packet/src/message.rs | 905 +++++++++++++++++++++++++++++++++ crates/packet/src/parser.rs | 49 ++ 13 files changed, 1913 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 bin/bgp/Cargo.toml create mode 100644 bin/bgp/src/main.rs create mode 100644 crates/packet/Cargo.toml create mode 100644 crates/packet/src/constants.rs create mode 100644 crates/packet/src/errors.rs create mode 100644 crates/packet/src/ip_prefix.rs create mode 100644 crates/packet/src/lib.rs create mode 100644 crates/packet/src/message.rs create mode 100644 crates/packet/src/parser.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0bb55b0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,455 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bgp" +version = "0.0.1" +dependencies = [ + "eyre", + "tokio", +] + +[[package]] +name = "bitfield" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786e53b0c071573a28956cec19a92653e42de34c683e2f6e86c197a349fba318" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07805405d3f1f3a55aab895718b488821d40458f9188059909091ae0935c344a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "ferrix-bgp-packet" +version = "0.1.0" +dependencies = [ + "bitfield", + "bytes", + "eyre", + "nom", + "serde", + "serde_repr", + "thiserror", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6bbec7d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[workspace] +default-members = ["bin/bgp"] +members = ["bin/*", "crates/*"] + +[workspace.package] +authors = ["Rayhaan Jaufeerally "] +description = "A Border Gateway Protocol implementation" +edition = "2024" +license = "Apache 2" +name = "bgp" +version = "0.0.1" + +[workspace.dependencies] +# --- Our crates --- +bgp-packet = { path = "crates/packet" } + +# --- General --- +bitfield = "0.19.0" +bytes = "1.10.1" +eyre = "0.6.12" +nom = "8.0.0" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +serde_repr = "0.1.20" +thiserror = "2.0.12" +tokio = { version = "1.44.2", features = ["full"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..12ee6db --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# BGP + +This project is an implementation of the Border Gateway Protocol and associated functionality required to run an Autonomous System. + +The aim of this project is to provide a modern, high performance, secure and programatic and customizable software package that can be useful to network operators, researchers, and people interested in Internet. + +## Current status. + +The project is currently being rewritten from scratch to be more modular and maintainable, as well as to fix deeper issues with the previous implementation. + +Warning: This is currently a work in progress and not ready for use. \ No newline at end of file diff --git a/bin/bgp/Cargo.toml b/bin/bgp/Cargo.toml new file mode 100644 index 0000000..ed93e3e --- /dev/null +++ b/bin/bgp/Cargo.toml @@ -0,0 +1,8 @@ +[package] +edition.workspace = true +name = "bgp" +version.workspace = true + +[dependencies] +eyre.workspace = true +tokio.workspace = true diff --git a/bin/bgp/src/main.rs b/bin/bgp/src/main.rs new file mode 100644 index 0000000..d65921b --- /dev/null +++ b/bin/bgp/src/main.rs @@ -0,0 +1,6 @@ +use eyre::Result; + +#[tokio::main] +pub async fn main() -> Result<()> { + Ok(()) +} diff --git a/crates/packet/Cargo.toml b/crates/packet/Cargo.toml new file mode 100644 index 0000000..1177e38 --- /dev/null +++ b/crates/packet/Cargo.toml @@ -0,0 +1,13 @@ +[package] +edition = "2024" +name = "ferrix-bgp-packet" +version = "0.1.0" + +[dependencies] +bitfield.workspace = true +bytes.workspace = true +eyre.workspace = true +nom.workspace = true +serde.workspace = true +serde_repr.workspace = true +thiserror.workspace = true diff --git a/crates/packet/src/constants.rs b/crates/packet/src/constants.rs new file mode 100644 index 0000000..abe0cf5 --- /dev/null +++ b/crates/packet/src/constants.rs @@ -0,0 +1,70 @@ +use std::fmt::Display; + +use eyre::bail; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum AddressFamilyId { + Ipv4 = 1, + Ipv6 = 2, +} + +impl TryFrom for AddressFamilyId { + type Error = eyre::Error; + + fn try_from(value: u16) -> Result { + Ok(match value { + x if x == Self::Ipv4 as u16 => Self::Ipv4, + x if x == Self::Ipv6 as u16 => Self::Ipv6, + other => bail!("Unknown AddressFamily: {}", other), + }) + } +} + +impl Display for AddressFamilyId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AddressFamilyId::Ipv4 => write!(f, "Ipv4"), + AddressFamilyId::Ipv6 => write!(f, "Ipv6"), + } + } +} + +/// Represents a Subsequent Address Family Identifier. +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum SubsequentAfi { + Unicast = 1, + Multicast = 2, + NlriWithMpls = 4, + MplsLabelledVpn = 128, + MulticastMplsVpn = 129, +} + +impl TryFrom for SubsequentAfi { + type Error = eyre::Error; + + fn try_from(value: u8) -> Result { + Ok(match value { + x if x == Self::Unicast as u8 => Self::Unicast, + x if x == Self::Multicast as u8 => Self::Multicast, + x if x == Self::NlriWithMpls as u8 => Self::NlriWithMpls, + x if x == Self::MplsLabelledVpn as u8 => Self::MplsLabelledVpn, + x if x == Self::MulticastMplsVpn as u8 => Self::MulticastMplsVpn, + other => bail!("Unknown SubsequentAfi: {}", other), + }) + } +} + +impl Display for SubsequentAfi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SubsequentAfi::Unicast => write!(f, "Unicast"), + SubsequentAfi::Multicast => write!(f, "Multicast"), + SubsequentAfi::NlriWithMpls => write!(f, "NlriWithMpls"), + SubsequentAfi::MplsLabelledVpn => write!(f, "MplsLabelledVpn"), + SubsequentAfi::MulticastMplsVpn => write!(f, "MulticastMplsVpn"), + } + } +} diff --git a/crates/packet/src/errors.rs b/crates/packet/src/errors.rs new file mode 100644 index 0000000..0f50550 --- /dev/null +++ b/crates/packet/src/errors.rs @@ -0,0 +1,149 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +#[repr(u8)] +pub enum BgpError { + #[error("Message header error")] + MsgHeader(MsgHeaderSubcode) = 1, + + #[error("Open message error")] + OpenMsg(OpenMsgSubcode) = 2, + + #[error("Update message error")] + UpdateMsg(UpdateMsgSubcode) = 3, + + #[error("Hold timer expired")] + HoldTimer = 4, + + #[error("Finite state machine error")] + FsmError(FsmSubcode) = 5, + + #[error("Cease")] + Cease(CeaseSubcode) = 6, + + #[error("Route refresh message error")] + RouteRefresh(RouteRefreshSubcode) = 7, + + #[error("Send hold timer expired")] + SendHoldTimer = 8, +} + +#[derive(Debug, Error)] +pub enum MsgHeaderSubcode { + #[error("Connection not synchronized")] + ConnNotSynchronized = 1, + + #[error("Bad message length")] + BadMessageLength = 2, + + #[error("Bad message type")] + BadMessagType = 3, +} + +#[derive(Debug, Error)] +pub enum OpenMsgSubcode { + #[error("Unsupported BGP version number")] + UnsupportedVersion = 1, + + #[error("Bad peer AS number")] + BadPeerAs = 2, + + #[error("Bad BGP identifier")] + BadBgpId = 3, + + #[error("Unsupported optional parameter")] + UnsupportedOptionalParam = 4, + + #[error("Unacceptable hold time")] + UnacceptableHoldTime = 6, + + #[error("Unsupported capability")] + UnsupportedCapability = 7, + + #[error("Role mismatch")] + RoleMismatch = 8, +} + +#[derive(Debug, Error)] +pub enum UpdateMsgSubcode { + #[error("Malformed attribute list")] + MalforedAttrs = 1, + + #[error("Unrecognized well known attribute")] + UnrecognizedWellKnownAttr = 2, + + #[error("Missing well known attribute")] + MissingWellKnown = 3, + + #[error("Attribute flags error")] + AttributeFlags = 4, + + #[error("Attribute length error")] + AttributeLength = 5, + + #[error("Invalid origin")] + InvalidOrigin = 6, + + #[error("Invalid next hop")] + InvalidNextHop = 8, + + #[error("Optional attribute error")] + OptionalAttribute = 9, + + #[error("Invalid network field")] + InvalidNetworkField = 10, + + #[error("Malformed AS path")] + MalformedAsPath = 11, +} + +#[derive(Debug, Error)] +pub enum FsmSubcode { + #[error("Received an unexpected message in OpenSent state")] + UnexpectedOpenSent = 1, + + #[error("Received an unexpected message in OpenConfirm state")] + UnexpectedOpenConfirm = 2, + + #[error("Received an unexpected message in Established state")] + UnexpectedEstablished = 3, +} + +#[derive(Debug, Error)] +pub enum CeaseSubcode { + #[error("Maximum number of prefixes reached")] + MaxPrefixes = 1, + + #[error("Administrative shutdown")] + AdminShutdown = 2, + + #[error("Peer deconfigured")] + PeerDeconf = 3, + + #[error("Administrative reset")] + AdminReset = 4, + + #[error("Connection rejected")] + ConnRejected = 5, + + #[error("Configuration change")] + ConfChange = 6, + + #[error("Connection collision resolution")] + ConnCollisionResolution = 7, + + #[error("Out of resources")] + OutOfResources = 8, + + #[error("Hard reset")] + HardReset = 9, + + #[error("BFD down")] + BfdDown = 10, +} + +#[derive(Debug, Error)] +pub enum RouteRefreshSubcode { + #[error("Invalid message length")] + InvalidLength = 1, +} diff --git a/crates/packet/src/ip_prefix.rs b/crates/packet/src/ip_prefix.rs new file mode 100644 index 0000000..8e8dc09 --- /dev/null +++ b/crates/packet/src/ip_prefix.rs @@ -0,0 +1,215 @@ +use std::fmt::Display; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::str::FromStr; + +use bytes::{Buf, BufMut, BytesMut}; +use eyre::{Result, bail}; +use serde::{Deserialize, Serialize}; + +use crate::constants::AddressFamilyId; +use crate::parser::{ParserContext, ToWireError}; + +/// IpPrefix represents some IP address prefix, for a specific AddressFamilyId. +#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)] +pub struct IpPrefix { + pub address_family: AddressFamilyId, + pub prefix: Vec, + pub length: u8, +} + +impl IpPrefix { + pub fn new(address_family: AddressFamilyId, prefix: Vec, length: u8) -> Result { + // Ensure that the prefix we are given contains the right number of bytes corresponding to the prefix length. + if prefix.len() < ((length + 7) / 8).into() { + bail!( + "Mismatched prefix {:?} for given prefix length: {}", + prefix, + length + ); + } + Ok(Self { + address_family, + prefix, + length, + }) + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<(), ToWireError> { + // Verify that there is enough space to write the IpPrefix. + if out.remaining() < (self.prefix.len() + 1) { + Err(ToWireError::OutBufferOverflow)?; + } + + // Write length and prefix. + out.put_u8(self.length); + out.put(self.prefix.as_slice()); + + Ok(()) + } +} + +impl TryFrom<&str> for IpPrefix { + type Error = eyre::Error; + + fn try_from(value: &str) -> Result { + let parts: Vec<&str> = value.split("/").collect(); + + if parts.len() != 2 { + bail!( + "Expected IpPrefix in format prefix/length but got: {}", + value + ); + } + + let length: u8 = u8::from_str_radix(parts[1], 10).map_err(eyre::Error::from)?; + let mut octets; + let afi: AddressFamilyId; + + if parts[0].contains(":") { + afi = AddressFamilyId::Ipv6; + let addr: Ipv6Addr = Ipv6Addr::from_str(parts[0]).map_err(eyre::Error::from)?; + octets = addr.octets().to_vec(); + } else if parts[0].contains(".") { + afi = AddressFamilyId::Ipv4; + let addr: Ipv4Addr = Ipv4Addr::from_str(parts[0]).map_err(eyre::Error::from)?; + octets = addr.octets().to_vec(); + } else { + bail!("Could not figure out address type") + } + + // Truncate the octets we have to the right number of bytes to match the prefix length.. + if length % 8 == 0 { + // We can cleanly truncate the number of bytes since we are at a byte boundary. + octets.truncate((length / 8).into()); + } else { + // We need to keep length % 8 bits of the last byte. + let num_bytes = (length / 8) + 1; + let mask = u8::MAX << (8 - (length % 8)); + octets.truncate(num_bytes.into()); + // Fix up the last byte. + let last_pos = octets.len() - 1; + octets[last_pos] &= mask; + } + + Ok(IpPrefix { + address_family: afi, + prefix: octets, + length, + }) + } +} + +impl Display for IpPrefix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.address_family { + AddressFamilyId::Ipv4 => { + // Pad the prefix with 0 bytes if it's less than 4 bytes long. + let bytes = &mut self.prefix.clone(); + if bytes.len() < 4 { + bytes.extend(std::iter::repeat(0).take(4 - bytes.len())); + } + let four_bytes: [u8; 4] = bytes + .as_slice() + .try_into() + .map_err(|_| std::fmt::Error {})?; + let ipv4_addr = Ipv4Addr::from(four_bytes); + write!(f, "{}/{}", ipv4_addr, self.length) + } + AddressFamilyId::Ipv6 => { + let bytes = &mut self.prefix.clone(); + if bytes.len() < 16 { + bytes.extend(std::iter::repeat(0).take(16 - bytes.len())); + } + let sixteen_bytes: [u8; 16] = bytes + .as_slice() + .try_into() + .map_err(|_| std::fmt::Error {})?; + let ipv6_addr = Ipv6Addr::from(sixteen_bytes); + write!(f, "{}/{}", ipv6_addr, self.length) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::constants::AddressFamilyId; + + use super::IpPrefix; + + macro_rules! verify_roundtrip { + ($name:ident, $prefix_str:expr, $afi:expr, $bytes:expr, $length:expr) => { + #[test] + fn $name() { + let ip_prefix = IpPrefix::try_from($prefix_str).unwrap(); + + assert_eq!(ip_prefix.address_family, $afi); + assert_eq!(ip_prefix.prefix, $bytes); + assert_eq!(ip_prefix.length, $length); + + let to_str: &str = &ip_prefix.to_string(); + assert_eq!(IpPrefix::try_from(to_str).unwrap(), ip_prefix); + } + }; + } + + verify_roundtrip!( + verify_roundtrip_ipv4_24, + "10.1.2.0/24", + AddressFamilyId::Ipv4, + vec![10, 1, 2], + 24 + ); + + // Verify truncation. + verify_roundtrip!( + verify_roundtrip_ipv4_19, + "10.245.123.0/19", + AddressFamilyId::Ipv4, + vec![10, 245, 96], + 19 + ); + + // Verify truncation. + verify_roundtrip!( + verify_roundtrip_ipv4_3, + "192.168.1.0/3", + AddressFamilyId::Ipv4, + vec![192], + 3 + ); + + // Verify default address. + verify_roundtrip!( + verify_roundtrip_ipv4_0, + "0.0.0.0/0", + AddressFamilyId::Ipv4, + vec![], + 0 + ); + + verify_roundtrip!( + verify_roundtrip_ipv6_48, + "2001:db8:cafe::/48", + AddressFamilyId::Ipv6, + vec![32, 1, 13, 184, 202, 254], + 48 + ); + + // Verify truncation. + verify_roundtrip!( + verify_roundtrip_ipv6_32, + "2001:db8:cafe::/32", + AddressFamilyId::Ipv6, + vec![32, 1, 13, 184], + 32 + ); + + verify_roundtrip!( + verify_roundtrip_ipv6_0, + "::/0", + AddressFamilyId::Ipv6, + vec![], + 0 + ); +} diff --git a/crates/packet/src/lib.rs b/crates/packet/src/lib.rs new file mode 100644 index 0000000..28d54bf --- /dev/null +++ b/crates/packet/src/lib.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod errors; +pub mod ip_prefix; +pub mod message; +pub mod parser; diff --git a/crates/packet/src/message.rs b/crates/packet/src/message.rs new file mode 100644 index 0000000..ee39510 --- /dev/null +++ b/crates/packet/src/message.rs @@ -0,0 +1,905 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use bitfield::bitfield; +use bytes::BufMut; +use bytes::BytesMut; +use eyre::Context; +use eyre::Result; +use eyre::bail; +use eyre::eyre; +use nom::Err::Failure; +use nom::IResult; +use nom::Parser; +use nom::number::complete::be_u8; +use nom::number::complete::be_u16; +use nom::number::complete::be_u32; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::constants::AddressFamilyId; +use crate::constants::SubsequentAfi; +use crate::ip_prefix::IpPrefix; +use crate::parser::BgpParserError; +use crate::parser::ParserContext; + +/// A sentinel AS number used to denote that the actual AS number is provided as +/// a 4 byte value in the capabilities instead. +pub const AS_TRANS: u16 = 23456; + +/// Version number used in the BGP4 protocol. +pub const BGP4_VERSION: u8 = 4; + +/// Message represents the top-level messages in the BGP protocol. +#[derive(Debug, Serialize, Deserialize)] +pub enum Message { + Open(OpenMessage), + Update(UpdateMessage), + Notification(NotificationMessage), + KeepAlive, + // RouteRefresh(RouteRefreshMessage), +} + +impl Message {} + +#[derive(Debug, Serialize, Deserialize)] +pub enum MessageType { + /// BGP Open message (RFC 4271). + Open = 1, + /// BGP Update message (RFC 4271). + Update = 2, + /// BGP Notification message (RFC 4275). + Notification = 3, + /// BGP KeepAlive message (RFC 4271). + KeepAlive = 4, + /// BGP Route Refresh message (RFC 2918). + RouteRefresh = 5, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OpenMessage { + /// Version of the BGP protocol in use. + pub version: u8, + /// AS Number of the BGP speaker, or AS_TRANS if using 4 byte ASN. + pub asn: u16, + /// Hold time parameter in seconds. + pub hold_time: u16, + /// Global identifier of the BGP speaker. + pub identifier: Ipv4Addr, + /// Options. + pub options: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum OpenOption { + Capabilities(Vec), + /// BGP extended open options length (RFC 9072). + ExtendedLength(u32), +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Capability { + /// MultiProtocol Extension (RFC 2858). + MultiProtocol { afi: u16, safi: u8 }, + /// Route Refresh capability (RFC 2918). + RouteRefresh {}, + /// Outbound Route Filtering (RFC 5291). + /// https://datatracker.ietf.org/doc/html/rfc5291 + OutboundRouteFilter {}, + /// Extended Next Hop encoding (RFC 8950). + ExtendedNextHop {}, + /// Extended Message (RFC 8654). + ExtendedMessage {}, + /// BGPSec (RFC 8205). + BgpSec {}, + /// Multiple labels compatibility (RFC 8277). + MultiLabelCompat {}, + /// Graceful restart capability (RFC 4724). + GracefulRestart {}, + /// Four Byte ASN (RFC 4274). + FourByteAsn { asn: u32 }, + /// Additional Path (RFC 7911). + AddPath {}, + /// Enhanced Route Refresh (RFC 7313). + EnhancedRouteRefresh {}, +} + +/// Represents a BGP Update message. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateMessage {} + +bitfield! { + pub struct PathAttributeFlags(u8); + impl Debug; + u8; + optional, set_optional: 0; + transitive, set_transitive: 1; + partial, set_partial: 2; + extended_length, set_extended_length: 3; +} + +#[repr(u8)] +pub enum PathAttribute { + Origin(OriginPathAttribute) = 1, + ASPath(AsPathAttribute) = 2, + NextHop(NextHopPathAttribute) = 3, + MultiExitDisc(MultiExitDiscPathAttribute) = 4, + LocalPref(LocalPrefPathAttribute) = 5, + AtomicAggregate(AtomicAggregatePathAttribute) = 6, + Aggregator(AggregatorPathAttribute) = 7, + Communitites(CommunitiesPathAttribute) = 8, + MpReachNlri(MpReachNlriPathAttribute) = 14, + MpUnreachNlri(MpUnreachNlriPathAttribute) = 15, + ExtendedCommunities(ExtendedCommunitiesPathAttribute) = 16, + LargeCommunities(LargeCommunitiesPathAttribute) = 32, + UnknownPathAttribute { + flags: PathAttributeFlags, + type_code: u8, + payload: Vec, + }, +} + +impl PathAttribute { + /// The from_wire parser for `PathAttribute` consumes type and length which it uses to + /// determine how many bytes to take and pass down to the corresponding sub-parser. + pub fn from_wire<'a>( + ctx: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, attr_flags) = be_u8(buf).map(|(buf, b)| (buf, PathAttributeFlags(b)))?; + let (buf, type_code) = be_u8(buf)?; + + let (buf, length): (_, u16) = if attr_flags.extended_length() { + be_u16(buf)? + } else { + be_u8(buf).map(|(buf, b)| (buf, b as u16))? + }; + + todo!(); + } +} + +/// Origin path attribute is a mandatory attribute defined in RFC4271. +#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum OriginPathAttribute { + IGP = 0, + EGP = 1, + INCOMPLETE = 2, +} + +impl TryFrom for OriginPathAttribute { + type Error = eyre::Error; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::IGP), + 1 => Ok(Self::EGP), + 2 => Ok(Self::INCOMPLETE), + other => bail!("Unexpected origin code {}", other), + } + } +} + +/// ASPathAttribute is a well-known mandatory attribute that contains a list of TLV encoded path +/// segments. Type is either 1 for AS_SET or 2 for AS_SEQUENCE, length is a 1 octet field +/// containing the number of ASNS and the value contains the ASNs. This is defined in Section 4.3 +/// of RFC4271. +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct AsPathAttribute { + pub segments: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct AsPathSegment { + /// ordered is true when representing an AS_SEQUENCE, andd false when + /// representing an AS_SET. + pub ordered: bool, + /// Path is the list of ASNs. + pub path: Vec, +} + +impl AsPathAttribute { + pub fn from_asns(asns: Vec) -> PathAttribute { + let segment = AsPathSegment { + ordered: true, + path: asns, + }; + PathAttribute::ASPath(AsPathAttribute { + segments: vec![segment], + }) + } + + pub fn from_wire<'a>( + ctx: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let parse_segment = |ctx: &ParserContext, + buf: &'a [u8]| + -> IResult<&'a [u8], AsPathSegment, BgpParserError<&'a [u8]>> { + let (buf, typ) = be_u8(buf)?; + let (buf, len) = be_u8(buf)?; + let (buf, asns): (_, Vec) = match ctx.four_octet_asn { + Some(true) => { + nom::multi::many_m_n(len as usize, len as usize, be_u32).parse(buf)? + } + Some(false) => nom::multi::many_m_n(len as usize, len as usize, be_u16) + .parse(buf) + .map(|(buf, asns)| (buf, asns.iter().map(|asn| *asn as u32).collect()))?, + None => { + return Err(nom::Err::Failure(BgpParserError::CustomText( + "Context must set four_octet_asn before being used", + ))); + } + }; + + Ok(( + buf, + AsPathSegment { + ordered: typ == 2, + path: asns, + }, + )) + }; + + let (buf, segments) = + nom::multi::many0(|buf: &'a [u8]| parse_segment(ctx, buf)).parse(buf)?; + + Ok((buf, Self { segments })) + } + + pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { + if ctx.four_octet_asn.is_none_or(|val| !val) { + bail!("AsPathAttribute can only be sent for four_octet_asn enabled peers"); + } + + for segment in &self.segments { + // Segment type. + out.put_u8(if segment.ordered { 2 } else { 1 }); + // Segment AS length. + out.put_u16( + segment + .path + .len() + .try_into() + .wrap_err("AS Path length too long")?, + ); + // AS numbers. + for asn in &segment.path { + out.put_u32(*asn); + } + } + + Ok(()) + } + + pub fn wire_len(&self, ctx: &ParserContext) -> Result { + let mut counter = 0; + for segment in &self.segments { + counter += match ctx.four_octet_asn { + Some(true) => 2 + (4 * segment.path.len()), + Some(false) => 2 + (2 * segment.path.len()), + None => bail!("ParserContext needs four_octet_asn set"), + }; + counter += 2 + (4 * segment.path.len()); + } + Ok(counter as u16) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct NextHopPathAttribute(pub Ipv4Addr); + +impl NextHopPathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, ip_u32) = be_u32(buf)?; + Ok((buf, Self(Ipv4Addr::from(ip_u32)))) + } + + pub fn to_wire(&self, out: &mut BytesMut) -> Result<()> { + out.put_u32(self.0.into()); + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(4) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct MultiExitDiscPathAttribute(pub u32); + +impl MultiExitDiscPathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, val) = be_u32(buf)?; + Ok((buf, Self(val))) + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + out.put_u32(self.0); + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(4) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct LocalPrefPathAttribute(pub u32); + +impl LocalPrefPathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, val) = be_u32(buf)?; + Ok((buf, Self(val))) + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + out.put_u32(self.0); + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(4) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct AtomicAggregatePathAttribute {} + +impl AtomicAggregatePathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + Ok((buf, Self {})) + } + + pub fn to_wire(&self, _: &ParserContext, _: &mut BytesMut) -> Result<()> { + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(0) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct AggregatorPathAttribute { + pub asn: u32, + pub ip: Ipv4Addr, +} + +impl AggregatorPathAttribute { + pub fn from_wire<'a>( + ctx: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + if ctx.four_octet_asn.is_none_or(|val| !val) { + return Err(nom::Err::Failure(BgpParserError::CustomText( + "AggregatorPathAttribute can only be parsed for four_octet_asn enabled peers", + ))); + } + let (buf, asn) = be_u32(buf)?; + let (buf, ip) = be_u32(buf)?; + Ok(( + buf, + Self { + asn, + ip: Ipv4Addr::from(ip), + }, + )) + } + + pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { + if ctx.four_octet_asn.is_none_or(|val| !val) { + bail!("AggregatorPathAttribute can only be sent for four_octet_asn enabled peers"); + } + out.put_u32(self.asn); + out.put_u32(self.ip.into()); + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(8) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct CommunitiesPathAttribute(Vec<(u16, u16)>); + +impl CommunitiesPathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, values) = nom::multi::many0(|i| (be_u16, be_u16).parse(i)).parse(buf)?; + Ok((buf, CommunitiesPathAttribute(values))) + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + for value in &self.0 { + out.put_u16(value.0); + out.put_u16(value.1); + } + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok((self.0.len() * 4) as u16) + } +} + +/// Extended Communities as defined in https://www.rfc-editor.org/rfc/rfc4360.html. +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct ExtendedCommunitiesPathAttribute { + pub extended_communities: Vec, +} + +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub enum ExtendedCommunity { + /// AS Specific Extended Community as specified in Section 3.1 of RFC4360. + AsSpecific { + typ: u8, + sub_typ: u8, + global_admin: u16, + local_admin: u32, + }, + /// Ipv4 Address Specific Extended Community as specified in Section 3.2 of RFC4360. + Ipv4AddrSpecific { + typ: u8, + sub_typ: u8, + global_admin: u32, + local_admin: u16, + }, + /// Opaque Extended Community as specified in Section 3.3 of RFC4360. + Opaque { + typ: u8, + sub_typ: u8, + value: [u8; 5], + }, + /// Route Target Community as specified in Section 4 of RFC4360. + RouteTarget { + typ: u8, + sub_typ: u8, + global_admin: u32, + local_admin: u16, + }, + /// Route Origin Community as specified in Section 5 of RFC4360. + RouteOrigin { + typ: u8, + sub_typ: u8, + global_admin: u16, + local_admin: u32, + }, +} + +impl ExtendedCommunity { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, typ) = be_u8(buf)?; + let (buf, sub_typ) = be_u8(buf)?; + let (buf, parsed) = match (typ, sub_typ) { + // Route Target Extended Community. + (0x00 | 0x01 | 0x02, 0x02) => { + let (buf, global_admin) = be_u32(buf)?; + let (buf, local_admin) = be_u16(buf)?; + ( + buf, + Self::RouteTarget { + typ, + sub_typ, + global_admin, + local_admin, + }, + ) + } + // Route Origin Extended Community. + (0x00 | 0x01 | 0x02, 0x03) => { + let (buf, global_admin) = be_u16(buf)?; + let (buf, local_admin) = be_u32(buf)?; + ( + buf, + Self::RouteOrigin { + typ, + sub_typ, + global_admin, + local_admin, + }, + ) + } + // AS specific Extended Community. + (0x00 | 0x40, _) => { + let (buf, global_admin) = be_u16(buf)?; + let (buf, local_admin) = be_u32(buf)?; + ( + buf, + Self::AsSpecific { + typ, + sub_typ, + global_admin, + local_admin, + }, + ) + } + // IPv4 Address Specific Extended Community. + (0x01 | 0x41, _) => { + let (buf, global_admin) = be_u32(buf)?; + let (buf, local_admin) = be_u16(buf)?; + ( + buf, + Self::Ipv4AddrSpecific { + typ, + sub_typ, + global_admin, + local_admin, + }, + ) + } + _ => { + let (buf, payload) = nom::bytes::take(5_usize).parse(buf)?; + let value: [u8; 5] = payload.try_into().map_err(|_| { + Failure(BgpParserError::CustomText( + "Expected exactly 5 bytes from the parser", + )) + })?; + ( + buf, + Self::Opaque { + typ, + sub_typ, + value, + }, + ) + } + }; + + return Ok((buf, parsed)); + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + match self { + ExtendedCommunity::AsSpecific { + typ, + sub_typ, + global_admin, + local_admin, + } => { + out.put_u8(*typ); + out.put_u8(*sub_typ); + out.put_u16(*global_admin); + out.put_u32(*local_admin); + } + ExtendedCommunity::Ipv4AddrSpecific { + typ, + sub_typ, + global_admin, + local_admin, + } => { + out.put_u8(*typ); + out.put_u8(*sub_typ); + out.put_u32(*global_admin); + out.put_u16(*local_admin); + } + ExtendedCommunity::Opaque { + typ, + sub_typ, + value, + } => { + out.put_u8(*typ); + out.put_u8(*sub_typ); + out.put(&value[..]); + } + ExtendedCommunity::RouteTarget { + typ, + sub_typ, + global_admin, + local_admin, + } => { + out.put_u8(*typ); + out.put_u8(*sub_typ); + out.put_u32(*global_admin); + out.put_u16(*local_admin); + } + ExtendedCommunity::RouteOrigin { + typ, + sub_typ, + global_admin, + local_admin, + } => { + out.put_u8(*typ); + out.put_u8(*sub_typ); + out.put_u16(*global_admin); + out.put_u32(*local_admin); + } + } + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(8) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LargeCommunitiesPathAttribute { + pub communities: Vec, +} + +impl LargeCommunitiesPathAttribute { + pub fn from_wire<'a>( + ctx: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, communities) = + nom::multi::many1(|buf| LargeCommunity::from_wire(ctx, buf)).parse(buf)?; + Ok((buf, Self { communities })) + } + + pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { + for community in &self.communities { + community.to_wire(ctx, out)?; + } + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(12_u16 * self.communities.len() as u16) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LargeCommunity { + pub global_admin: u32, + pub data_1: u32, + pub data_2: u32, +} + +impl LargeCommunity { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, global_admin) = be_u32(buf)?; + let (buf, data_1) = be_u32(buf)?; + let (buf, data_2) = be_u32(buf)?; + Ok(( + buf, + Self { + global_admin, + data_1, + data_2, + }, + )) + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + out.put_u32(self.global_admin); + out.put_u32(self.data_1); + out.put_u32(self.data_2); + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(12) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum NlriNextHop { + /// Represents an IPv4 address nexthop. + Ipv4(Ipv4Addr), + /// Represents a global IPv6 nexthop. + Ipv6(Ipv6Addr), + /// Represents a IPv6 Link Local and global address pair nexthop. + Ipv6WithLl { + global: Ipv6Addr, + link_local: Ipv6Addr, + }, +} + +impl NlriNextHop { + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + match self { + NlriNextHop::Ipv4(ipv4_addr) => out.put(&ipv4_addr.octets()[..]), + NlriNextHop::Ipv6(ipv6_addr) => out.put(&ipv6_addr.octets()[..]), + NlriNextHop::Ipv6WithLl { global, link_local } => { + out.put(&global.octets()[..]); + out.put(&link_local.octets()[..]) + } + } + + Ok(()) + } + + pub fn wire_len(&self) -> u8 { + match self { + NlriNextHop::Ipv4(_) => 4, + NlriNextHop::Ipv6(_) => 16, + NlriNextHop::Ipv6WithLl { .. } => 32, + } + } +} + +// parse_prefix is a helper function that implements an NLRI parser for the given AFI. +fn parse_prefix<'a>( + afi: AddressFamilyId, + buf: &'a [u8], +) -> IResult<&'a [u8], IpPrefix, BgpParserError<&'a [u8]>> { + let (buf, prefix_len) = be_u8(buf)?; + let byte_len = (prefix_len + 7) / 8; + let (buf, prefix_bytes) = nom::bytes::take(byte_len as usize).parse(buf)?; + let prefix = IpPrefix::new(afi, prefix_bytes.to_vec(), byte_len) + .map_err(|e| Failure(BgpParserError::Eyre(e)))?; + Ok((buf, prefix)) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MpReachNlriPathAttribute { + pub afi: AddressFamilyId, + pub safi: SubsequentAfi, + /// Next hop address (either IPv4 or IPv6 for now). + pub next_hop: NlriNextHop, + pub prefixes: Vec, +} + +impl MpReachNlriPathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, raw_afi) = be_u16(buf)?; + let afi = + AddressFamilyId::try_from(raw_afi).map_err(|e| Failure(BgpParserError::Eyre(e)))?; + + let (buf, raw_safi) = be_u8(buf)?; + let safi = + SubsequentAfi::try_from(raw_safi).map_err(|e| Failure(BgpParserError::Eyre(e)))?; + + let (buf, nh_len) = be_u8(buf)?; + + let (buf, next_hop, prefixes) = match afi { + AddressFamilyId::Ipv4 => { + // Read the length of the nexthop which should equal 4. + if nh_len != 4 { + return Err(Failure(BgpParserError::Eyre(eyre!( + "Got nexthop address length {} when expected 4 for IPv4 AFI", + nh_len + )))); + } + // Read the nexthop address which should now be an IPv4 address. + let (buf, nh_bytes) = be_u32(buf)?; + let next_hop = NlriNextHop::Ipv4(Ipv4Addr::from(nh_bytes)); + + let (buf, prefixes) = + nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf)).parse(buf)?; + (buf, next_hop, prefixes) + } + AddressFamilyId::Ipv6 => { + // https://datatracker.ietf.org/doc/html/rfc2545 defines that the nexthop address may be 16 or 32 bytes long. + let (buf, nh_bytes) = nom::bytes::take(nh_len as usize).parse(buf)?; + let nexthop = match nh_bytes.len() { + 16 => { + // unwrap should never fire since we have explicitly checked the length. + let slice: [u8; 16] = nh_bytes.try_into().unwrap(); + NlriNextHop::Ipv6(Ipv6Addr::from(slice)) + } + 32 => { + // unwrap should never fire since we have explicitly checked the length. + let slice: [u8; 32] = nh_bytes.try_into().unwrap(); + let link_local_bytes: [u8; 16] = slice[0..16].try_into().unwrap(); + let link_local = Ipv6Addr::from(link_local_bytes); + let global_bytes: [u8; 16] = slice[16..32].try_into().unwrap(); + let global = Ipv6Addr::from(global_bytes); + NlriNextHop::Ipv6WithLl { global, link_local } + } + _ => { + return Err(Failure(BgpParserError::Eyre(eyre!( + "Mismatched IPv6 nexthop length, got {}, want 16 or 32", + nh_bytes.len() + )))); + } + }; + let (buf, prefixes) = + nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv6, buf)).parse(buf)?; + (buf, nexthop, prefixes) + } + }; + + Ok(( + buf, + Self { + afi, + safi, + next_hop, + prefixes, + }, + )) + } + + pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { + out.put_u16(self.afi as u16); + out.put_u8(self.safi as u8); + out.put_u8(self.next_hop.wire_len()); + self.next_hop.to_wire(ctx, out)?; + for prefix in &self.prefixes { + out.put_u8(prefix.length); + out.put(&prefix.prefix[..]); + } + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(4_u16 + + self.next_hop.wire_len() as u16 + + self + .prefixes + .iter() + .map(|p| 1 + p.prefix.len() as u16) + .sum::()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MpUnreachNlriPathAttribute { + pub afi: AddressFamilyId, + pub safi: SubsequentAfi, + pub prefixes: Vec, +} + +impl MpUnreachNlriPathAttribute { + pub fn from_wire<'a>( + _: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, raw_afi) = be_u16(buf)?; + let afi = + AddressFamilyId::try_from(raw_afi).map_err(|e| Failure(BgpParserError::Eyre(e)))?; + + let (buf, raw_safi) = be_u8(buf)?; + let safi = + SubsequentAfi::try_from(raw_safi).map_err(|e| Failure(BgpParserError::Eyre(e)))?; + + let (buf, prefixes) = nom::multi::many0(|buf| parse_prefix(afi, buf)).parse(buf)?; + + Ok(( + buf, + MpUnreachNlriPathAttribute { + afi, + safi, + prefixes, + }, + )) + } + + pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> { + out.put_u16(self.afi as u16); + out.put_u8(self.safi as u8); + for prefix in &self.prefixes { + out.put_u8(prefix.length); + out.put(&prefix.prefix[..]); + } + Ok(()) + } + + pub fn wire_len(&self, _: &ParserContext) -> Result { + Ok(3 + self + .prefixes + .iter() + .map(|p| 1 + p.prefix.len() as u16) + .sum::()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationMessage {} diff --git a/crates/packet/src/parser.rs b/crates/packet/src/parser.rs new file mode 100644 index 0000000..46e6422 --- /dev/null +++ b/crates/packet/src/parser.rs @@ -0,0 +1,49 @@ +use std::fmt::Display; + +use nom::error::{ErrorKind, ParseError}; + +use crate::constants::AddressFamilyId; + +#[derive(Debug, Default)] +pub struct ParserContext { + /// Whether thi parser is being run with a peer that is RFC6793 compliant. + pub four_octet_asn: Option, + /// Which address family should be parsed by default with this parser. + pub address_family: Option, +} + +#[derive(Debug)] +pub enum ToWireError { + /// There was not enough space in the output buffer to serialize the data into. + OutBufferOverflow, + /// Another error. + Other(eyre::Error), +} + +impl Display for ToWireError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ToWireError::OutBufferOverflow => write!(f, "OutBufferOverflow"), + ToWireError::Other(report) => report.fmt(f), + } + } +} + +impl std::error::Error for ToWireError {} + +// Custom error type for the parser. +#[derive(Debug)] +pub enum BgpParserError { + CustomText(&'static str), + Eyre(eyre::ErrReport), + Nom(I, ErrorKind), +} + +impl ParseError for BgpParserError { + fn from_error_kind(input: I, kind: ErrorKind) -> Self { + BgpParserError::Nom(input, kind) + } + fn append(_: I, _: ErrorKind, other: Self) -> Self { + other + } +} From 4b130985dab3d11a6298a60b99cd7a017ed97d27 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sat, 19 Apr 2025 17:19:48 +0200 Subject: [PATCH 2/2] Fix cargo.toml package info --- Cargo.lock | 26 +++++++++++++------------- Cargo.toml | 5 ++++- bin/bgp/Cargo.toml | 9 ++++++--- crates/packet/Cargo.toml | 11 ++++++++--- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bb55b0..6b56452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "bgp-packet" +version = "0.0.1" +dependencies = [ + "bitfield", + "bytes", + "eyre", + "nom", + "serde", + "serde_repr", + "thiserror", +] + [[package]] name = "bitfield" version = "0.19.0" @@ -94,19 +107,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "ferrix-bgp-packet" -version = "0.1.0" -dependencies = [ - "bitfield", - "bytes", - "eyre", - "nom", - "serde", - "serde_repr", - "thiserror", -] - [[package]] name = "gimli" version = "0.31.1" diff --git a/Cargo.toml b/Cargo.toml index 6bbec7d..7743a42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,16 @@ [workspace] default-members = ["bin/bgp"] members = ["bin/*", "crates/*"] +resolver = "3" [workspace.package] authors = ["Rayhaan Jaufeerally "] description = "A Border Gateway Protocol implementation" edition = "2024" -license = "Apache 2" +homepage = "https://rayhaan.ch" +license = "Apache-2.0" name = "bgp" +repository = "https://github.com/net-control-plane/bgp" version = "0.0.1" [workspace.dependencies] diff --git a/bin/bgp/Cargo.toml b/bin/bgp/Cargo.toml index ed93e3e..b149682 100644 --- a/bin/bgp/Cargo.toml +++ b/bin/bgp/Cargo.toml @@ -1,7 +1,10 @@ [package] -edition.workspace = true -name = "bgp" -version.workspace = true +description = "A Border Gateway Protocol implementation" +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "bgp" +version.workspace = true [dependencies] eyre.workspace = true diff --git a/crates/packet/Cargo.toml b/crates/packet/Cargo.toml index 1177e38..3cbb42c 100644 --- a/crates/packet/Cargo.toml +++ b/crates/packet/Cargo.toml @@ -1,7 +1,12 @@ [package] -edition = "2024" -name = "ferrix-bgp-packet" -version = "0.1.0" +name = "bgp-packet" + +description.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true [dependencies] bitfield.workspace = true