From 5ae8f6e249dbfbe74a140d1573f067efc534fef6 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sat, 18 Apr 2026 12:58:51 +0200 Subject: [PATCH] Cleanup and implementation of Update / Notification parser / serializers. --- Cargo.lock | 583 +++++++++++++++++- Cargo.toml | 8 + README.md | 21 +- crates/packet/Cargo.toml | 25 +- crates/packet/src/errors.rs | 208 ++++++- crates/packet/src/lib.rs | 3 - crates/packet/src/message.rs | 448 +++++++++++++- crates/packet/src/open.rs | 7 +- crates/primitives/Cargo.toml | 19 + .../{packet => primitives}/src/constants.rs | 0 .../{packet => primitives}/src/ip_prefix.rs | 0 crates/primitives/src/lib.rs | 4 + crates/{packet => primitives}/src/parser.rs | 0 crates/primitives/src/poptrie/mod.rs | 176 ++++++ crates/server/Cargo.toml | 15 + crates/server/build.rs | 9 + crates/server/proto/config.proto | 32 + crates/server/src/lib.rs | 5 + crates/server/src/main.rs | 26 + crates/server/src/tracing.rs | 30 + docs/RFCs.md | 9 + testdata/server.textpb | 10 + 22 files changed, 1573 insertions(+), 65 deletions(-) create mode 100644 crates/primitives/Cargo.toml rename crates/{packet => primitives}/src/constants.rs (100%) rename crates/{packet => primitives}/src/ip_prefix.rs (100%) create mode 100644 crates/primitives/src/lib.rs rename crates/{packet => primitives}/src/parser.rs (100%) create mode 100644 crates/primitives/src/poptrie/mod.rs create mode 100644 crates/server/Cargo.toml create mode 100644 crates/server/build.rs create mode 100644 crates/server/proto/config.proto create mode 100644 crates/server/src/lib.rs create mode 100644 crates/server/src/main.rs create mode 100644 crates/server/src/tracing.rs create mode 100644 docs/RFCs.md create mode 100644 testdata/server.textpb diff --git a/Cargo.lock b/Cargo.lock index 76a8f55..b3f3785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,71 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "autocfg" version = "1.4.0" @@ -50,6 +115,7 @@ dependencies = [ name = "bgp-packet" version = "0.0.2" dependencies = [ + "bgp-primitives", "bitfield", "bytes", "eyre", @@ -57,7 +123,35 @@ dependencies = [ "serde", "serde_repr", "strum", - "thiserror", + "thiserror 2.0.12", +] + +[[package]] +name = "bgp-primitives" +version = "0.0.2" +dependencies = [ + "bitfield", + "bytes", + "eyre", + "nom", + "serde", + "serde_repr", + "strum", + "thiserror 2.0.12", +] + +[[package]] +name = "bgp-server" +version = "0.0.0" +dependencies = [ + "clap", + "eyre", + "prometheus", + "protobuf", + "protobuf-codegen", + "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -98,6 +192,74 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "eyre" version = "0.6.12" @@ -108,30 +270,103 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "indenter" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "lock_api" version = "0.4.12" @@ -142,6 +377,21 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.4" @@ -165,7 +415,7 @@ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -177,6 +427,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "object" version = "0.36.7" @@ -192,6 +451,12 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.3" @@ -230,6 +495,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 2.0.12", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "quote" version = "1.0.40" @@ -239,6 +570,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "redox_syscall" version = "0.5.10" @@ -248,12 +585,67 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -297,6 +689,15 @@ dependencies = [ "syn", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -319,9 +720,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.27.1" @@ -355,13 +762,46 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.61.2", +] + +[[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.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[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", ] [[package]] @@ -375,6 +815,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tokio" version = "1.44.2" @@ -390,7 +839,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -404,18 +853,118 @@ dependencies = [ "syn", ] +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -425,6 +974,24 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -488,3 +1055,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml index 4ca67ba..290451b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ version = "0.0.2" [workspace.dependencies] # --- Our crates --- bgp-packet = { path = "crates/packet" } +bgp-primitives = { path = "crates/primitives" } +bgp-server = { path = "crates/server" } # --- General --- bitfield = "0.19.0" @@ -28,3 +30,9 @@ serde_repr = "0.1.20" thiserror = "2.0.12" tokio = { version = "1.44.2", features = ["full"] } strum = { version = "0.27.1", features = ["derive"] } +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +prometheus = "0.14.0" +protobuf = { package = "protobuf", version = "3.7.2" } +protobuf-codegen = { package = "protobuf-codegen", version = "3.7.2" } +clap = { version = "4.5.53", features = ["derive"] } diff --git a/README.md b/README.md index 397b043..46968ef 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,14 @@ The project is currently being rewritten from scratch to be more modular and mai We aim for the 1.0 release to be functional and stable to operate peering sessions, and to install routes in the Linux Kernel. -* ☐ Implement basic parsers for the fundamental types required - * ✅ Path Attributes from RFC4271 - * ✅ Open message - * ✅ Capabilities from RFC4271 - * ✅ Update message - * ☐ Notification message -* ☐ Implement connection logic to establish sessions with peers -* ☐ Implement an efficient data structure for storing prefixes (e.g. Poptrie) -* ☐ Filtering for peer RIB in / out -* ☐ Main / auxillary RIBs for exchanging routes between peers +- ☐ Implement basic parsers for the fundamental types required + - ✅ Path Attributes from RFC4271 + - ✅ Open message + - ✅ Capabilities from RFC4271 + - ✅ Update message + - ☐ Notification message +- ☐ Implement scaffolding required to start and configure a server +- ☐ Implement connection logic to establish sessions with peers +- ☐ Implement an efficient data structure for storing prefixes (e.g. Poptrie) +- ☐ Filtering for peer RIB in / out +- ☐ Main / auxillary RIBs for exchanging routes between peers diff --git a/crates/packet/Cargo.toml b/crates/packet/Cargo.toml index f069a6f..0faf785 100644 --- a/crates/packet/Cargo.toml +++ b/crates/packet/Cargo.toml @@ -2,18 +2,19 @@ name = "bgp-packet" description.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -repository.workspace = true -version.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true [dependencies] -bitfield.workspace = true -bytes.workspace = true -eyre.workspace = true -nom.workspace = true -serde.workspace = true +bitfield.workspace = true +bytes.workspace = true +eyre.workspace = true +nom.workspace = true +serde.workspace = true serde_repr.workspace = true -thiserror.workspace = true -strum.workspace = true +thiserror.workspace = true +strum.workspace = true +bgp-primitives.workspace = true diff --git a/crates/packet/src/errors.rs b/crates/packet/src/errors.rs index 0f50550..5f2dc7f 100644 --- a/crates/packet/src/errors.rs +++ b/crates/packet/src/errors.rs @@ -1,6 +1,7 @@ +use serde::{Deserialize, Serialize}; use thiserror::Error; -#[derive(Debug, Error)] +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] #[repr(u8)] pub enum BgpError { #[error("Message header error")] @@ -28,7 +29,36 @@ pub enum BgpError { SendHoldTimer = 8, } -#[derive(Debug, Error)] +impl BgpError { + pub fn from_code_subcode(code: u8, subcode: u8) -> Result { + match code { + 1 => Ok(Self::MsgHeader(MsgHeaderSubcode::try_from(subcode)?)), + 2 => Ok(Self::OpenMsg(OpenMsgSubcode::try_from(subcode)?)), + 3 => Ok(Self::UpdateMsg(UpdateMsgSubcode::try_from(subcode)?)), + 4 => Ok(Self::HoldTimer), + 5 => Ok(Self::FsmError(FsmSubcode::try_from(subcode)?)), + 6 => Ok(Self::Cease(CeaseSubcode::try_from(subcode)?)), + 7 => Ok(Self::RouteRefresh(RouteRefreshSubcode::try_from(subcode)?)), + 8 => Ok(Self::SendHoldTimer), + _ => Err(()), + } + } + + pub fn code_subcode(&self) -> (u8, u8) { + match self { + Self::MsgHeader(s) => (1, u8::from(s)), + Self::OpenMsg(s) => (2, u8::from(s)), + Self::UpdateMsg(s) => (3, u8::from(s)), + Self::HoldTimer => (4, 0), + Self::FsmError(s) => (5, u8::from(s)), + Self::Cease(s) => (6, u8::from(s)), + Self::RouteRefresh(s) => (7, u8::from(s)), + Self::SendHoldTimer => (8, 0), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] pub enum MsgHeaderSubcode { #[error("Connection not synchronized")] ConnNotSynchronized = 1, @@ -40,7 +70,29 @@ pub enum MsgHeaderSubcode { BadMessagType = 3, } -#[derive(Debug, Error)] +impl TryFrom for MsgHeaderSubcode { + type Error = (); + fn try_from(v: u8) -> Result { + match v { + 1 => Ok(Self::ConnNotSynchronized), + 2 => Ok(Self::BadMessageLength), + 3 => Ok(Self::BadMessagType), + _ => Err(()), + } + } +} + +impl From<&MsgHeaderSubcode> for u8 { + fn from(s: &MsgHeaderSubcode) -> u8 { + match s { + MsgHeaderSubcode::ConnNotSynchronized => 1, + MsgHeaderSubcode::BadMessageLength => 2, + MsgHeaderSubcode::BadMessagType => 3, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] pub enum OpenMsgSubcode { #[error("Unsupported BGP version number")] UnsupportedVersion = 1, @@ -64,7 +116,37 @@ pub enum OpenMsgSubcode { RoleMismatch = 8, } -#[derive(Debug, Error)] +impl TryFrom for OpenMsgSubcode { + type Error = (); + fn try_from(v: u8) -> Result { + match v { + 1 => Ok(Self::UnsupportedVersion), + 2 => Ok(Self::BadPeerAs), + 3 => Ok(Self::BadBgpId), + 4 => Ok(Self::UnsupportedOptionalParam), + 6 => Ok(Self::UnacceptableHoldTime), + 7 => Ok(Self::UnsupportedCapability), + 8 => Ok(Self::RoleMismatch), + _ => Err(()), + } + } +} + +impl From<&OpenMsgSubcode> for u8 { + fn from(s: &OpenMsgSubcode) -> u8 { + match s { + OpenMsgSubcode::UnsupportedVersion => 1, + OpenMsgSubcode::BadPeerAs => 2, + OpenMsgSubcode::BadBgpId => 3, + OpenMsgSubcode::UnsupportedOptionalParam => 4, + OpenMsgSubcode::UnacceptableHoldTime => 6, + OpenMsgSubcode::UnsupportedCapability => 7, + OpenMsgSubcode::RoleMismatch => 8, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] pub enum UpdateMsgSubcode { #[error("Malformed attribute list")] MalforedAttrs = 1, @@ -97,7 +179,43 @@ pub enum UpdateMsgSubcode { MalformedAsPath = 11, } -#[derive(Debug, Error)] +impl TryFrom for UpdateMsgSubcode { + type Error = (); + fn try_from(v: u8) -> Result { + match v { + 1 => Ok(Self::MalforedAttrs), + 2 => Ok(Self::UnrecognizedWellKnownAttr), + 3 => Ok(Self::MissingWellKnown), + 4 => Ok(Self::AttributeFlags), + 5 => Ok(Self::AttributeLength), + 6 => Ok(Self::InvalidOrigin), + 8 => Ok(Self::InvalidNextHop), + 9 => Ok(Self::OptionalAttribute), + 10 => Ok(Self::InvalidNetworkField), + 11 => Ok(Self::MalformedAsPath), + _ => Err(()), + } + } +} + +impl From<&UpdateMsgSubcode> for u8 { + fn from(s: &UpdateMsgSubcode) -> u8 { + match s { + UpdateMsgSubcode::MalforedAttrs => 1, + UpdateMsgSubcode::UnrecognizedWellKnownAttr => 2, + UpdateMsgSubcode::MissingWellKnown => 3, + UpdateMsgSubcode::AttributeFlags => 4, + UpdateMsgSubcode::AttributeLength => 5, + UpdateMsgSubcode::InvalidOrigin => 6, + UpdateMsgSubcode::InvalidNextHop => 8, + UpdateMsgSubcode::OptionalAttribute => 9, + UpdateMsgSubcode::InvalidNetworkField => 10, + UpdateMsgSubcode::MalformedAsPath => 11, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] pub enum FsmSubcode { #[error("Received an unexpected message in OpenSent state")] UnexpectedOpenSent = 1, @@ -109,7 +227,29 @@ pub enum FsmSubcode { UnexpectedEstablished = 3, } -#[derive(Debug, Error)] +impl TryFrom for FsmSubcode { + type Error = (); + fn try_from(v: u8) -> Result { + match v { + 1 => Ok(Self::UnexpectedOpenSent), + 2 => Ok(Self::UnexpectedOpenConfirm), + 3 => Ok(Self::UnexpectedEstablished), + _ => Err(()), + } + } +} + +impl From<&FsmSubcode> for u8 { + fn from(s: &FsmSubcode) -> u8 { + match s { + FsmSubcode::UnexpectedOpenSent => 1, + FsmSubcode::UnexpectedOpenConfirm => 2, + FsmSubcode::UnexpectedEstablished => 3, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] pub enum CeaseSubcode { #[error("Maximum number of prefixes reached")] MaxPrefixes = 1, @@ -142,8 +282,62 @@ pub enum CeaseSubcode { BfdDown = 10, } -#[derive(Debug, Error)] +impl TryFrom for CeaseSubcode { + type Error = (); + fn try_from(v: u8) -> Result { + match v { + 1 => Ok(Self::MaxPrefixes), + 2 => Ok(Self::AdminShutdown), + 3 => Ok(Self::PeerDeconf), + 4 => Ok(Self::AdminReset), + 5 => Ok(Self::ConnRejected), + 6 => Ok(Self::ConfChange), + 7 => Ok(Self::ConnCollisionResolution), + 8 => Ok(Self::OutOfResources), + 9 => Ok(Self::HardReset), + 10 => Ok(Self::BfdDown), + _ => Err(()), + } + } +} + +impl From<&CeaseSubcode> for u8 { + fn from(s: &CeaseSubcode) -> u8 { + match s { + CeaseSubcode::MaxPrefixes => 1, + CeaseSubcode::AdminShutdown => 2, + CeaseSubcode::PeerDeconf => 3, + CeaseSubcode::AdminReset => 4, + CeaseSubcode::ConnRejected => 5, + CeaseSubcode::ConfChange => 6, + CeaseSubcode::ConnCollisionResolution => 7, + CeaseSubcode::OutOfResources => 8, + CeaseSubcode::HardReset => 9, + CeaseSubcode::BfdDown => 10, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)] pub enum RouteRefreshSubcode { #[error("Invalid message length")] InvalidLength = 1, } + +impl TryFrom for RouteRefreshSubcode { + type Error = (); + fn try_from(v: u8) -> Result { + match v { + 1 => Ok(Self::InvalidLength), + _ => Err(()), + } + } +} + +impl From<&RouteRefreshSubcode> for u8 { + fn from(s: &RouteRefreshSubcode) -> u8 { + match s { + RouteRefreshSubcode::InvalidLength => 1, + } + } +} diff --git a/crates/packet/src/lib.rs b/crates/packet/src/lib.rs index 145ce53..3fd7cf1 100644 --- a/crates/packet/src/lib.rs +++ b/crates/packet/src/lib.rs @@ -1,6 +1,3 @@ -pub mod constants; pub mod errors; -pub mod ip_prefix; pub mod message; pub mod open; -pub mod parser; diff --git a/crates/packet/src/message.rs b/crates/packet/src/message.rs index e655a5b..16f8df3 100644 --- a/crates/packet/src/message.rs +++ b/crates/packet/src/message.rs @@ -18,12 +18,14 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use strum::EnumDiscriminants; -use crate::constants::AddressFamilyId; -use crate::constants::SubsequentAfi; -use crate::ip_prefix::IpPrefix; +use bgp_primitives::constants::AddressFamilyId; +use bgp_primitives::constants::SubsequentAfi; +use bgp_primitives::ip_prefix::IpPrefix; +use bgp_primitives::parser::BgpParserError; +use bgp_primitives::parser::ParserContext; + +use crate::errors::BgpError; use crate::open::OpenMessage; -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. @@ -47,7 +49,7 @@ pub enum Message { impl Message {} /// Represents a BGP Update message. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct UpdateMessage { pub withdrawn_routes: Vec, pub path_attributes: Vec, @@ -55,22 +57,120 @@ pub struct UpdateMessage { } impl UpdateMessage { + /// Parses a BGP UPDATE message body per RFC 4271 §4.3. The buffer passed in + /// must contain exactly the UPDATE payload (i.e. without the 19 byte BGP + /// message header). The NLRI section has no explicit length and is inferred + /// as the remainder of the buffer, so passing extra trailing bytes will + /// cause them to be (mis-)parsed as NLRI. pub fn from_wire<'a>( - _ctx: &ParserContext, - _buf: &'a [u8], + ctx: &ParserContext, + buf: &'a [u8], ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { - todo!(); + // Withdrawn routes. Per RFC 4271 these are always IPv4 prefixes; other + // address families are carried in the MP_UNREACH_NLRI path attribute. + let (buf, withdrawn_len) = be_u16(buf)?; + let (buf, withdrawn_buf) = nom::bytes::take(withdrawn_len as usize).parse(buf)?; + let (withdrawn_rem, withdrawn_routes) = + nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf)) + .parse(withdrawn_buf)?; + if !withdrawn_rem.is_empty() { + return Err(Failure(BgpParserError::Eyre(eyre!( + "Leftover bytes {:x?} after parsing withdrawn routes", + withdrawn_rem + )))); + } + + // Path attributes. + let (buf, total_pa_len) = be_u16(buf)?; + let (buf, pa_buf) = nom::bytes::take(total_pa_len as usize).parse(buf)?; + let (pa_rem, path_attributes) = + nom::multi::many0(|buf| PathAttribute::from_wire(ctx, buf)).parse(pa_buf)?; + if !pa_rem.is_empty() { + return Err(Failure(BgpParserError::Eyre(eyre!( + "Leftover bytes {:x?} after parsing path attributes", + pa_rem + )))); + } + + // NLRI. As with withdrawn routes, these are IPv4 prefixes per RFC 4271. + // They occupy the remainder of the UPDATE body; if the caller has + // delimited `buf` correctly then `many0` will consume it entirely. + let (buf, announced_routes) = + nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf)).parse(buf)?; + if !buf.is_empty() { + return Err(Failure(BgpParserError::Eyre(eyre!( + "Leftover bytes {:x?} after parsing NLRI", + buf + )))); + } + + Ok(( + buf, + Self { + withdrawn_routes, + path_attributes, + announced_routes, + }, + )) } - pub fn to_wire(&self, _ctx: &ParserContext, _out: &mut BytesMut) -> Result<()> { - todo!() + pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { + // Withdrawn routes. + let withdrawn_len = prefix_list_wire_len(&self.withdrawn_routes) + .wrap_err("Withdrawn routes length exceeds u16::MAX")?; + out.put_u16(withdrawn_len); + for prefix in &self.withdrawn_routes { + out.put_u8(prefix.length); + out.put(prefix.prefix.as_slice()); + } + + // Path attributes. + let pa_total: u32 = self + .path_attributes + .iter() + .map(|p| p.wire_len(ctx).map(|v| v as u32)) + .sum::>()?; + let pa_total = u16::try_from(pa_total) + .map_err(|_| eyre!("Total path attribute length exceeds u16::MAX"))?; + out.put_u16(pa_total); + for pa in &self.path_attributes { + pa.to_wire(ctx, out)?; + } + + // NLRI (consumes the remainder of the message). + for prefix in &self.announced_routes { + out.put_u8(prefix.length); + out.put(prefix.prefix.as_slice()); + } + + Ok(()) } - pub fn wire_len(&self, _ctx: &ParserContext) -> Result { - todo!() + pub fn wire_len(&self, ctx: &ParserContext) -> Result { + // 2 bytes for the withdrawn length field + 2 bytes for the total path + // attribute length field. + let mut total: u32 = 4; + for prefix in &self.withdrawn_routes { + total += 1 + prefix.prefix.len() as u32; + } + for pa in &self.path_attributes { + total += pa.wire_len(ctx)? as u32; + } + for prefix in &self.announced_routes { + total += 1 + prefix.prefix.len() as u32; + } + u16::try_from(total).map_err(|_| eyre!("UPDATE message length exceeds u16::MAX")) } } +/// Computes the total wire length of a list of IPv4/IPv6 NLRI prefixes as they +/// appear in the withdrawn or NLRI sections of an UPDATE message (each prefix +/// is encoded as a 1-byte length followed by `ceil(length/8)` prefix bytes). +fn prefix_list_wire_len(prefixes: &[IpPrefix]) -> Result { + let total: u32 = prefixes.iter().map(|p| 1 + p.prefix.len() as u32).sum(); + u16::try_from(total).map_err(|_| eyre!("Prefix list length exceeds u16::MAX")) +} + bitfield! { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PathAttributeFlags(u8); @@ -207,6 +307,20 @@ impl PathAttribute { }; let discriminant = PathAttributeDiscriminants::from(type_code); + if matches!( + discriminant, + PathAttributeDiscriminants::UnknownPathAttribute + ) { + let (buf, pa_buf) = nom::bytes::take(length).parse(buf)?; + return Ok(( + buf, + PathAttribute::UnknownPathAttribute { + flags: attr_flags, + type_code, + payload: pa_buf.to_vec(), + }, + )); + } Self::parse_known_path_attribute(ctx, buf, discriminant, length) } @@ -332,18 +446,48 @@ impl PathAttribute { payload, } => { out.put_u8(flags.0); + out.put_u8(*type_code); if flags.extended_length() { out.put_u16(payload.len() as u16); } else { out.put_u8(payload.len() as u8); } - out.put_u8(*type_code); out.put(payload.as_slice()); } } Ok(()) } + + /// Returns the total on-wire length of this path attribute, including the + /// attribute header (flags + type + length) and the payload. + pub fn wire_len(&self, ctx: &ParserContext) -> Result { + let payload_len: u16 = match self { + PathAttribute::Origin(a) => a.wire_len(ctx)?, + PathAttribute::AsPath(a) => a.wire_len(ctx)?, + PathAttribute::NextHop(a) => a.wire_len(ctx)?, + PathAttribute::MultiExitDisc(a) => a.wire_len(ctx)?, + PathAttribute::LocalPref(a) => a.wire_len(ctx)?, + PathAttribute::AtomicAggregate(a) => a.wire_len(ctx)?, + PathAttribute::Aggregator(a) => a.wire_len(ctx)?, + PathAttribute::Communitites(a) => a.wire_len(ctx)?, + PathAttribute::MpReachNlri(a) => a.wire_len(ctx)?, + PathAttribute::MpUnreachNlri(a) => a.wire_len(ctx)?, + PathAttribute::ExtendedCommunities(a) => a.wire_len(ctx)?, + PathAttribute::LargeCommunities(a) => a.wire_len(ctx)?, + PathAttribute::UnknownPathAttribute { flags, payload, .. } => { + // An unknown attribute preserves its original extended_length + // flag on re-serialization, so use it verbatim here rather than + // re-deriving it from the payload length. + let payload_len = u16::try_from(payload.len()) + .map_err(|_| eyre!("UnknownPathAttribute payload exceeds u16::MAX"))?; + let header_len: u16 = if flags.extended_length() { 4 } else { 3 }; + return Ok(header_len + payload_len); + } + }; + let header_len: u16 = if payload_len > u8::MAX as u16 { 4 } else { 3 }; + Ok(header_len + payload_len) + } } /// Origin path attribute is a mandatory attribute defined in RFC4271. @@ -535,7 +679,7 @@ impl NextHopPathAttribute { Ok(()) } - pub fn wire_len(&self, _: &ParserContext) -> Result { + pub fn wire_len(&self, _: &ParserContext) -> Result { Ok(4) } } @@ -1168,8 +1312,47 @@ impl MpUnreachNlriPathAttribute { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NotificationMessage {} +/// Represents a BGP NOTIFICATION message per RFC 4271 §4.5. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NotificationMessage { + pub error: BgpError, + /// Optional diagnostic data; may be empty. + pub data: Vec, +} + +impl NotificationMessage { + /// Parses a NOTIFICATION message body (without the 19-byte BGP header). + /// Consumes the entire buffer; any remaining bytes become `data`. + pub fn from_wire<'a>( + _ctx: &ParserContext, + buf: &'a [u8], + ) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> { + let (buf, error_code) = be_u8(buf)?; + let (buf, error_subcode) = be_u8(buf)?; + + let error = BgpError::from_code_subcode(error_code, error_subcode).map_err(|_| { + Failure(BgpParserError::Eyre(eyre!( + "Unknown NOTIFICATION error code {} subcode {}", + error_code, + error_subcode + ))) + })?; + + Ok((&[], Self { error, data: buf.to_vec() })) + } + + pub fn to_wire(&self, _ctx: &ParserContext, buf: &mut BytesMut) -> Result<()> { + let (code, subcode) = self.error.code_subcode(); + buf.put_u8(code); + buf.put_u8(subcode); + buf.put_slice(&self.data); + Ok(()) + } + + pub fn wire_len(&self) -> u16 { + 2 + self.data.len() as u16 + } +} #[cfg(test)] mod tests { @@ -1181,16 +1364,18 @@ mod tests { use bytes::BytesMut; use eyre::{Result, eyre}; - use crate::{ + use crate::message::{ + AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute, + CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute, ExtendedCommunity, + LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute, + MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute, + NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute, + PathAttributeFlags, UpdateMessage, + }; + + use bgp_primitives::{ constants::{AddressFamilyId, SubsequentAfi}, ip_prefix::IpPrefix, - message::{ - AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute, - CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute, ExtendedCommunity, - LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute, - MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute, - NextHopPathAttribute, NlriNextHop, OriginPathAttribute, PathAttribute, - }, parser::ParserContext, }; @@ -1415,4 +1600,217 @@ mod tests { }] }) ); + + // Unknown Path Attribute. Ensures we both parse an unrecognized type code + // into the catch-all variant and re-serialize it with the correct field + // ordering (flags, type, length, payload) per RFC 4271 §4.3. + test_path_attribute_roundtrip!( + test_unknown_path_attribute, + &[0x80, 0x63, 0x04, 0xde, 0xad, 0xbe, 0xef], + PathAttribute::UnknownPathAttribute { + flags: PathAttributeFlags::new( + /*optional=*/ true, /*transitive=*/ false, /*partial=*/ false, + /*extended_length=*/ false, + ), + type_code: 0x63, + payload: vec![0xde, 0xad, 0xbe, 0xef], + } + ); + + // -- UPDATE message roundtrip tests -- + + /// Asserts that an UPDATE message roundtrips byte-for-byte and that + /// wire_len agrees with the length of the encoded output. + macro_rules! test_update_roundtrip { + ($name:ident, $input_bytes:expr, $expected:expr) => { + #[test] + fn $name() -> Result<()> { + let ctx = &default_v6_context(); + let (rem, parsed) = UpdateMessage::from_wire(ctx, $input_bytes)?; + assert!(rem.is_empty(), "Expected no trailing bytes, got {:x?}", rem); + assert_eq!(parsed, $expected); + + let mut out = BytesMut::with_capacity(u16::MAX as usize); + parsed.to_wire(ctx, &mut out)?; + assert_eq!(out.to_vec(), $input_bytes); + assert_eq!(parsed.wire_len(ctx)?, $input_bytes.len() as u16); + Ok(()) + } + }; + } + + // Wrapper to generate a IPv4 prefix used in tests below. + fn v4_prefix(length: u8, bytes: &[u8]) -> IpPrefix { + IpPrefix { + address_family: AddressFamilyId::Ipv4, + prefix: bytes.to_vec(), + length, + } + } + + /// A small set of path attributes that commonly accompany an IPv4 unicast + /// announcement: Origin=IGP, AS_PATH=[65000], NEXT_HOP=192.0.2.1. + fn sample_ipv4_path_attributes() -> Vec { + vec![ + PathAttribute::Origin(OriginPathAttribute::IGP), + PathAttribute::AsPath(AsPathAttribute { + segments: vec![AsPathSegment { + ordered: true, + path: vec![65000], + }], + }), + PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))), + ] + } + + // Empty UPDATE (equivalent to a keepalive-shaped body): no withdrawn + // routes, no path attributes, no NLRI. + test_update_roundtrip!( + test_update_empty, + &[0x00, 0x00, 0x00, 0x00], + UpdateMessage { + withdrawn_routes: vec![], + path_attributes: vec![], + announced_routes: vec![], + } + ); + + // Pure announcement: no withdrawn routes, standard v4 path attributes, one + // NLRI prefix (10.0.0.0/8). + test_update_roundtrip!( + test_update_announcement, + &[ + // Withdrawn routes length = 0. + 0x00, 0x00, // Total path attribute length = 20 bytes. + 0x00, 0x14, // ORIGIN = IGP. + 0x40, 0x01, 0x01, 0x00, // AS_PATH = AS_SEQUENCE [65000] (4-octet ASNs). + 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe8, + // NEXT_HOP = 192.0.2.1. + 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // NLRI: 10.0.0.0/8. + 0x08, 0x0a, + ], + UpdateMessage { + withdrawn_routes: vec![], + path_attributes: sample_ipv4_path_attributes(), + announced_routes: vec![v4_prefix(8, &[0x0a])], + } + ); + + // Pure withdrawal: one withdrawn route, no path attributes, no NLRI. RFC + // 4271 allows (and indeed requires) this shape for unreach updates. + test_update_roundtrip!( + test_update_withdrawal, + &[ + // Withdrawn routes length = 2. + 0x00, 0x02, // 10.0.0.0/8. + 0x08, 0x0a, // Total path attribute length = 0. + 0x00, 0x00, + ], + UpdateMessage { + withdrawn_routes: vec![v4_prefix(8, &[0x0a])], + path_attributes: vec![], + announced_routes: vec![], + } + ); + + // Mixed withdrawal + announcement with multiple NLRI entries at different + // prefix lengths, exercising the variable-length NLRI parser tail. + test_update_roundtrip!( + test_update_mixed, + &[ + // Withdrawn routes length = 2. + 0x00, 0x02, // 10.0.0.0/8 withdrawn. + 0x08, 0x0a, // Total path attribute length = 20. + 0x00, 0x14, // ORIGIN = IGP. + 0x40, 0x01, 0x01, 0x00, // AS_PATH = AS_SEQUENCE [65000]. + 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe8, + // NEXT_HOP = 192.0.2.1. + 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // NLRI: 172.16.0.0/16. + 0x10, 0xac, 0x10, // NLRI: 192.168.1.0/24. + 0x18, 0xc0, 0xa8, 0x01, + ], + UpdateMessage { + withdrawn_routes: vec![v4_prefix(8, &[0x0a])], + path_attributes: sample_ipv4_path_attributes(), + announced_routes: vec![ + v4_prefix(16, &[0xac, 0x10]), + v4_prefix(24, &[0xc0, 0xa8, 0x01]), + ], + } + ); + + // An UPDATE carrying an unrecognized path attribute. + test_update_roundtrip!( + test_update_with_unknown_path_attribute, + &[ + // Withdrawn routes length = 0. + 0x00, 0x00, // Total path attribute length = 7. + 0x00, 0x07, // Unknown attribute: flags=0x80 (optional), type=0x63, len=4. + 0x80, 0x63, 0x04, 0xde, 0xad, 0xbe, 0xef, + ], + UpdateMessage { + withdrawn_routes: vec![], + path_attributes: vec![PathAttribute::UnknownPathAttribute { + flags: PathAttributeFlags::new(true, false, false, false), + type_code: 0x63, + payload: vec![0xde, 0xad, 0xbe, 0xef], + }], + announced_routes: vec![], + } + ); + + // Rejects trailing garbage after the NLRI section (a partial prefix). + #[test] + fn test_update_rejects_trailing_garbage() { + let ctx = default_v6_context(); + // Well-formed withdrawn_len=0, pa_len=0, followed by a single byte that + // declares a prefix length of 8 but provides no prefix bytes. + let buf = &[0x00, 0x00, 0x00, 0x00, 0x08]; + let result = UpdateMessage::from_wire(&ctx, buf); + assert!( + result.is_err(), + "Expected a parse error for trailing garbage, got {:?}", + result + ); + } + + #[test] + fn test_notification_roundtrip() -> Result<()> { + use crate::errors::{BgpError, CeaseSubcode}; + + let ctx = &default_v6_context(); + let msg = NotificationMessage { + error: BgpError::Cease(CeaseSubcode::AdminShutdown), + data: vec![0xde, 0xad], + }; + + let mut buf = BytesMut::new(); + msg.to_wire(ctx, &mut buf)?; + assert_eq!(buf.as_ref(), &[6, 2, 0xde, 0xad]); + + let (rem, parsed) = NotificationMessage::from_wire(ctx, &buf).unwrap(); + assert!(rem.is_empty()); + assert_eq!(parsed, msg); + Ok(()) + } + + #[test] + fn test_notification_no_data() -> Result<()> { + use crate::errors::{BgpError, FsmSubcode}; + + let ctx = &default_v6_context(); + let msg = NotificationMessage { + error: BgpError::FsmError(FsmSubcode::UnexpectedEstablished), + data: vec![], + }; + + let mut buf = BytesMut::new(); + msg.to_wire(ctx, &mut buf)?; + assert_eq!(buf.as_ref(), &[5, 3]); + + let (rem, parsed) = NotificationMessage::from_wire(ctx, &buf).unwrap(); + assert!(rem.is_empty()); + assert_eq!(parsed, msg); + Ok(()) + } } diff --git a/crates/packet/src/open.rs b/crates/packet/src/open.rs index 002abca..64e1a46 100644 --- a/crates/packet/src/open.rs +++ b/crates/packet/src/open.rs @@ -12,7 +12,7 @@ use nom::{ use serde::{Deserialize, Serialize}; use strum::EnumDiscriminants; -use crate::{ +use bgp_primitives::{ constants::{AddressFamilyId, SubsequentAfi}, parser::{BgpParserError, ParserContext, ProtocolErrorValues}, }; @@ -814,9 +814,10 @@ mod tests { use bytes::BytesMut; use eyre::Result; - use crate::{ + use crate::open::{Capability, ExtendedNextHop, GracefulRestartEntry, OpenMessage, OpenOption}; + + use bgp_primitives::{ constants::{AddressFamilyId, SubsequentAfi}, - open::{Capability, ExtendedNextHop, GracefulRestartEntry, OpenMessage, OpenOption}, parser::ParserContext, }; diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml new file mode 100644 index 0000000..20a6ec9 --- /dev/null +++ b/crates/primitives/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bgp-primitives" + +description = "Primitive types for working with routing information" +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +bitfield.workspace = true +bytes.workspace = true +eyre.workspace = true +nom.workspace = true +serde.workspace = true +serde_repr.workspace = true +thiserror.workspace = true +strum.workspace = true diff --git a/crates/packet/src/constants.rs b/crates/primitives/src/constants.rs similarity index 100% rename from crates/packet/src/constants.rs rename to crates/primitives/src/constants.rs diff --git a/crates/packet/src/ip_prefix.rs b/crates/primitives/src/ip_prefix.rs similarity index 100% rename from crates/packet/src/ip_prefix.rs rename to crates/primitives/src/ip_prefix.rs diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs new file mode 100644 index 0000000..675c788 --- /dev/null +++ b/crates/primitives/src/lib.rs @@ -0,0 +1,4 @@ +pub mod constants; +pub mod ip_prefix; +pub mod parser; +pub mod poptrie; diff --git a/crates/packet/src/parser.rs b/crates/primitives/src/parser.rs similarity index 100% rename from crates/packet/src/parser.rs rename to crates/primitives/src/parser.rs diff --git a/crates/primitives/src/poptrie/mod.rs b/crates/primitives/src/poptrie/mod.rs new file mode 100644 index 0000000..e8cead5 --- /dev/null +++ b/crates/primitives/src/poptrie/mod.rs @@ -0,0 +1,176 @@ +use std::collections::BTreeMap; + +#[derive(Debug)] +pub struct Poptrie { + pub inners: Vec, + pub leaves: Vec>, +} + +#[derive(Debug)] +pub struct Inner { + /// A bit vector that describes the child internal nodes. + pub internal_v: u64, + /// A bit vector that describes the child leaf nodes. + pub leaf_v: u64, + /// Starting offset where the child interior nodes of this node begins. + pub inner_offset: u64, + /// Starting offset where the child leaf nodes of this node begins. + pub leaf_offset: u64, +} + +#[derive(Debug)] +pub struct Leaf { + pub data: T, +} + +#[inline] +fn extract(width: u8, offset: u8, v: u32, bits: u8) -> u8 { + let shift = bits.saturating_sub(width * (offset + 1)); + let mask = 0b111111 << shift; + ((v & mask) >> shift) as u8 +} + +fn extract_32(width: u8, offset: u8, v: u32) -> u8 { + extract(width, offset, v, 32) +} + +fn extract_128(width: u8, offset: u8, v: u32) -> u8 { + extract(width, offset, v, 128) +} + +impl Poptrie { + pub fn match_u32(&self, addr: u32) -> Option<&T> { + let mut i = 0_u64; + let mut v = self.inners.get(i as usize)?.internal_v; + let mut offset = 0; + let mut n = extract(6, offset, addr, 32); + + let mut result = None; + + while (v & (1 << n)) != 0 { + let base = self.inners.get(i as usize)?.inner_offset; + let arg = v & ((2 << n) - 1); + let bc = arg.count_ones() as u64; + i = base + bc - 1; + v = self.inners.get(i as usize)?.internal_v; + + offset += 1; + n = extract(6, offset, addr, 32); + + let base = self.inners.get(i as usize)?.leaf_offset; + let v = self.inners.get(i as usize)?.leaf_v; + if (v & (1 << n)) != 0 { + let i = base - 1; + result = Some(&self.leaves.get(i as usize)?.data); + } + } + + let base = self.inners.get(i as usize)?.leaf_offset; + let v = self.inners.get(i as usize)?.leaf_v; + if (v & (1 << n)) != 0 { + i = base - 1; + result = Some(&self.leaves.get(i as usize)?.data); + } + + result + } + + fn new(&mut self, rt: BTreeMap<(u32, u8), T>) { + const DEPTH: u8 = 6; + let mut forest = vec![(0, rt)]; + + let mut i_offset = 1; + for _ in 0..DEPTH { + let mut subforest = Vec::<(u8, BTreeMap<(u32, u8), T>)>::new(); + let mut children = 0; + let mut siblings = 0; + for (_, tree) in &forest { + let mut iv = 0_u64; + let mut lv = 0_u64; + + let mut subsubforest = Vec::<(u8, BTreeMap<(u32, u8), T>)>::new(); + for (r, e) in tree { + let k = extract(6, DEPTH, r.0, 32); + let consumed = core::cmp::min((DEPTH + 1) * 6, 32); + if r.1 < consumed { + if ((1 << k) & iv) == 0 { + lv |= 1 << k; + self.leaves.push(Leaf { data: e.clone() }); + } + + // If the prefix of the route entry is less than but not equal + // to the consumed number of bits, we need to add those bits + // to the bitvec. + if r.1 != consumed { + // Shift the extra bits and add to the bitvec for this + // internal node. + let extra = 1_u32 << (consumed - r.1); + for i in 1..(extra).min(64 - k as u32) { + lv |= 1_u64 << (k as u32 + i); + self.leaves.push(Leaf { data: e.clone() }); + } + } + continue; + } + + iv |= 1 << k; + match subsubforest.iter_mut().find(|x| x.0 == k) { + Some(ref mut entry) => { + entry.1.insert(*r, e.clone()); + } + None => { + let mut tbl = BTreeMap::new(); + tbl.insert(*r, e.clone()); + subsubforest.push((k, tbl)); + if iv > 0 { + children += 1; + } + } + } + } + + if iv > 0 || lv > 0 { + self.inners.push(Inner { + internal_v: iv, + leaf_v: lv, + inner_offset: if iv > 0 { i_offset + siblings } else { 0 }, + leaf_offset: self.leaves.len() as u64, + }); + if iv > 0 { + siblings += 1; + } + } + + subforest.extend_from_slice(subsubforest.as_slice()); + } + i_offset += children; + forest = subforest; + } + } +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, net::Ipv4Addr}; + + use crate::poptrie::Poptrie; + + #[test] + fn test_poptrie() { + let mut map: BTreeMap<(u32, u8), Ipv4Addr> = Default::default(); + map.insert( + (Ipv4Addr::new(127, 0, 0, 0).into(), 8), + Ipv4Addr::new(10, 0, 0, 1), + ); + + let mut poptrie: Poptrie = Poptrie { + inners: vec![], + leaves: vec![], + }; + + poptrie.new(map); + println!("Success"); + + dbg!(poptrie); + } +} diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml new file mode 100644 index 0000000..1852472 --- /dev/null +++ b/crates/server/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "bgp-server" +edition.workspace = true + +[dependencies] +clap.workspace = true +eyre.workspace = true +prometheus.workspace = true +tokio.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true +protobuf.workspace = true + +[build-dependencies] +protobuf-codegen.workspace = true diff --git a/crates/server/build.rs b/crates/server/build.rs new file mode 100644 index 0000000..f941313 --- /dev/null +++ b/crates/server/build.rs @@ -0,0 +1,9 @@ +use protobuf_codegen::Codegen; + +fn main() { + Codegen::new() + .input("proto/config.proto") + .includes(&["proto"]) + .cargo_out_dir("protos") + .run_from_script(); +} diff --git a/crates/server/proto/config.proto b/crates/server/proto/config.proto new file mode 100644 index 0000000..25c46e3 --- /dev/null +++ b/crates/server/proto/config.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +package bgp.server; + +// Top level BGP Server configuration. +message BgpConfig { + // The IPv4 address used as the router ID in BGP. + string router_id = 1; + // The local AS Number to use when communicating with peers. + uint32 local_asn = 2; + + // Endpoints to listen on. + repeated TcpEndpoint listen_endpoints = 3; + + // Peers configured on this server. + repeated Peer peers = 4; +} + +// Represents an IP address and port that can be used for listening or connecting. +message TcpEndpoint { + // Address to listen on + string address = 1; + // Port to listen on. + uint32 port = 2; +} + +// Configuration of a BGP peer. +message Peer { + string name = 1; + TcpEndpoint address = 2; + uint32 remote_asn = 3; +} diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs new file mode 100644 index 0000000..62b1359 --- /dev/null +++ b/crates/server/src/lib.rs @@ -0,0 +1,5 @@ +pub mod tracing; + +pub mod proto { + include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs new file mode 100644 index 0000000..7e5500e --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,26 @@ +use bgp_server::proto; +use clap::Parser; +use eyre::{Context, Result}; +use tracing::info; + +use crate::proto::config::BgpConfig; + +#[derive(Parser, Debug)] +struct ServerArgs { + /// Path to the configuration file. + config: String, +} + +#[tokio::main] +async fn main() -> Result<()> { + info!("Starting bgp server"); + + let args = ServerArgs::parse(); + let config_string = + std::fs::read_to_string(args.config).wrap_err("Failed to open config file")?; + let config = protobuf::text_format::parse_from_str::(&config_string)?; + + info!("Read config file: {}", config); + + Ok(()) +} diff --git a/crates/server/src/tracing.rs b/crates/server/src/tracing.rs new file mode 100644 index 0000000..ad6dbfb --- /dev/null +++ b/crates/server/src/tracing.rs @@ -0,0 +1,30 @@ +use clap::Args; +use eyre::Result; +use tracing::Level; +use tracing_subscriber::EnvFilter; + +/// Default targets that we enable logging for. +const DEFAULT_TARGETS: [&str; 1] = ["bgp_server"]; + +#[derive(Debug, Args)] +pub struct TracingArgs { + /// Set the minimum log level. + #[arg(short, long, global = true, default_value_t = Level::TRACE, help_heading = "Tracing")] + level: Level, + /// Disable logging to stdout. + #[arg(long, global = true, default_value_t = false, help_heading = "Tracing")] + quiet: bool, +} + +impl TracingArgs { + pub fn install(&self) -> Result<()> { + let filter = DEFAULT_TARGETS.into_iter().try_fold( + EnvFilter::builder().from_env_lossy(), + |filter, target| -> Result<_> { + Ok(filter.add_directive(format!("{}={}", target, self.level).parse()?)) + }, + )?; + + Ok(()) + } +} diff --git a/docs/RFCs.md b/docs/RFCs.md new file mode 100644 index 0000000..8c8f5ad --- /dev/null +++ b/docs/RFCs.md @@ -0,0 +1,9 @@ +# RFCs + +This document refers to the relevant RFCs for BGP and associated networking protocol semantics that we implement in this project. + +## RFC 4271 - Border Gateway protocol + +https://datatracker.ietf.org/doc/html/rfc4271 + +This is the main specification document for the Border Gateway Protocol, and defines the core protocol semantics and wire format. diff --git a/testdata/server.textpb b/testdata/server.textpb new file mode 100644 index 0000000..1985ed9 --- /dev/null +++ b/testdata/server.textpb @@ -0,0 +1,10 @@ +router_id: "10.0.0.1" +local_asn: 65000 +listen_endpoints { + address: "127.0.0.1" + port: 179 +} +listen_endpoints { + address: "[::1]" + port: 179 +}