Refactored client binary.

This commit is contained in:
Rayhaan Jaufeerally
2024-07-07 22:34:00 +02:00
parent 75dbfc319a
commit 9be6b1d59d
34 changed files with 452 additions and 296 deletions

View File

@ -0,0 +1,37 @@
[package]
name = "route_client"
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[lints]
workspace = true
[dependencies]
async-trait.workspace = true
bgp_packet.workspace = true
byteorder = "1.4.3"
bytes.workspace = true
eyre.workspace = true
futures.workspace = true
ip_network_table-deps-treebitmap.workspace = true
log.workspace = true
netlink-packet-route.workspace = true
netlink-packet-utils.workspace = true
nom = "7.1"
prost.workspace = true
rtnetlink.workspace = true
serde.workspace = true
tokio-stream = "0.1.14"
tokio-util = { version = "0.7.10", features = ["codec"] }
tokio.workspace = true
tonic.workspace = true
tracing.workspace = true
warp.workspace = true
[build-dependencies]
tonic-build = { version = "0.5.1", features = ["compression", "prost"] }

View File

@ -0,0 +1,19 @@
// Copyright 2021 Rayhaan Jaufeerally.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
fn main() {
tonic_build::configure()
.compile(&["proto/route_service.proto"], &["proto"])
.unwrap();
}

View File

@ -0,0 +1,83 @@
// Copyright 2021 Rayhaan Jaufeerally.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package bgpd.grpc;
enum AddressFamily {
UNKNOWN = 0;
IPv4 = 1;
IPv6 = 2;
}
message Prefix {
bytes ip_prefix = 1;
int32 prefix_len = 2;
AddressFamily address_family = 3;
}
// Path represents the metadata associated with the route to a particular
// prefix.
message Path {
bytes nexthop = 1;
string peer_name = 2;
uint32 local_pref = 3;
uint32 med = 4;
repeated uint32 as_path = 5;
// TODO: Path attributes. Not yet supported because we need to generate proto
// definitions for all of them.
}
message PathSet {
uint64 epoch = 1;
Prefix prefix = 2;
repeated Path paths = 3;
}
message StreamPathsRequest { AddressFamily address_family = 1; }
message DumpPathsRequest { AddressFamily address_family = 1; };
message DumpPathsResponse {
uint64 epoch = 1;
repeated PathSet path_sets = 2;
};
service RouteService {
// DumpPaths returns all the paths currently in the RIB.
rpc DumpPaths(DumpPathsRequest) returns (DumpPathsResponse);
// StreamPaths dumps the existing routes and starts streaming updates to the
// RIB.
rpc StreamPaths(StreamPathsRequest) returns (stream PathSet);
}
message PeerStatusRequest {}
message PeerStatus {
string peer_name = 1;
string state = 2;
uint64 session_established_time = 3;
uint64 last_messaage_time = 4;
uint64 route_updates_in = 5;
uint64 route_updates_out = 6;
}
message PeerStatusResponse { repeated PeerStatus peer_status = 1; }
// BGPServerAdminService implements an administrative interface to
// view the status and control the operation of this BGP server.
service BGPServerAdminService {
rpc PeerStatus(PeerStatusRequest) returns (PeerStatusResponse);
}

View File

@ -0,0 +1,180 @@
// Copyright 2021 Rayhaan Jaufeerally.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use futures::lock::Mutex;
use ip_network_table_deps_treebitmap::address::Address;
use ip_network_table_deps_treebitmap::IpLookupTable;
use std::convert::{TryFrom, TryInto};
use std::fmt::Formatter;
use std::net::Ipv6Addr;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use tracing::{trace, warn};
use bgp_packet::constants::AddressFamilyIdentifier;
use bgp_packet::nlri::NLRI;
use crate::southbound_interface::SouthboundInterface;
/// fib_state implements the logic to maintain forwarding routes in the FIB.
/// This for now means the Linux Kernel via Netlink, but in the future can
/// be extended to include other targets such as OpenFlow or even program
/// a router using BGP.
#[derive(Debug)]
pub struct FibEntry {
nexthop: IpAddr,
}
pub struct FibState<A: Address, S: SouthboundInterface> {
pub fib: IpLookupTable<A, Arc<Mutex<FibEntry>>>,
pub southbound: S,
pub af: AddressFamilyIdentifier,
pub table: u32,
}
impl<A: Address, S: SouthboundInterface> std::fmt::Debug for FibState<A, S> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(f, "FibState af: {:?}, table: {}", self.af, self.table)
}
}
/// to_octets provides an interface for accessing an address as a vector of bytes.
/// This is implemented for IPv4Addr and IPv6Addr to be able to use them interchangably
/// to send updates to the kernel.
pub trait ToOctets {
fn octets(&self) -> Vec<u8>;
}
impl ToOctets for Ipv4Addr {
fn octets(&self) -> Vec<u8> {
self.octets().into()
}
}
impl ToOctets for Ipv6Addr {
fn octets(&self) -> Vec<u8> {
self.octets().into()
}
}
impl<
A: Address
+ std::convert::TryFrom<NLRI>
+ ToOctets
+ std::cmp::PartialEq
+ std::fmt::Display
+ std::fmt::Debug,
S: SouthboundInterface,
> FibState<A, S>
where
String: From<<A as TryFrom<NLRI>>::Error>,
{
/// route_add requests updating the nexthop to a particular path if it is not already
/// the best path.
pub async fn route_add(&mut self, nlri: &NLRI, nexthop: IpAddr) -> Result<(), String> {
// Lookup the path in the Fib, there are three possible outcomes:
// 1. The route is not yet known, we add it to the FibState and inject it into the kernel,
// 2. The route is known and has a prior nexthop that needs to be updated
// 3. The route is known and has the same nexthop: no-op.
let prefix_addr: A = nlri.clone().try_into()?;
match self
.fib
.exact_match(prefix_addr, nlri.prefixlen.into())
.as_mut()
{
Some(entry_wrapped) => {
let mut entry = entry_wrapped.lock().await;
if entry.nexthop == nexthop {
// Nothing to do, route already in kernel.
trace!("Skipping route that already exists in kernel");
} else {
// Remove old route
trace!("Remove old route: {:?}", entry);
if let Err(e) = self.southbound.route_del(nlri.clone(), entry.nexthop).await {
warn!(
"Southbound interface returned error when trying to remove route: {} via {}, error: {}",
nlri, entry.nexthop, e
);
return Err("Netlink remove error".to_string());
}
// Add new route
trace!(
"Add new route: prefix: {:?}, nexthop: {}",
nlri.prefix,
nexthop
);
if let Err(e) = self
.southbound
.route_add(self.af, nlri.clone(), nexthop)
.await
{
warn!(
"Netlink returned error when trying to add route: {} via {}, error: {}",
nlri, nexthop, e
);
return Err("Netlink add error".to_string());
}
entry.nexthop = nexthop;
}
}
None => {
// Need to insert a new entry for this route
let entry = FibEntry {
nexthop: nexthop.clone(),
};
if let Err(e) = self
.southbound
.route_add(self.af, nlri.clone(), nexthop)
.await
{
warn!(
"Netlink returned error when trying to add route: {} via {}, error: {}",
nlri, nexthop, e
);
return Err("Netlink add error".to_string());
}
let addr: A = nlri.clone().try_into()?;
self.fib
.insert(addr, nlri.prefixlen.into(), Arc::new(Mutex::new(entry)));
}
};
Ok(())
}
/// route_del removes a route from the FibState and kernel.
pub async fn route_del(&mut self, nlri: NLRI) -> Result<(), String> {
let prefix_addr: A = nlri.clone().try_into()?;
if let Some(entry_wrapped) = self.fib.exact_match(prefix_addr, nlri.prefixlen.into()) {
{
let entry = entry_wrapped.lock().await;
if let Err(e) = self.southbound.route_del(nlri.clone(), entry.nexthop).await {
warn!(
"Failed to apply route mutation to remove NLRI: {}, error: {}",
nlri, e
);
}
}
self.fib.remove(prefix_addr, nlri.prefixlen.into());
} else {
warn!("Failed to find prefix to remove from FIB: {}", nlri);
}
Ok(())
}
}

View File

@ -0,0 +1,205 @@
// Copyright 2021 Rayhaan Jaufeerally.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod fib_state;
pub mod netlink;
pub mod southbound_interface;
use log::trace;
use std::convert::TryInto;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::str::FromStr;
use std::time::Duration;
use bgp_packet::constants::AddressFamilyIdentifier;
use bgp_packet::nlri::NLRI;
use eyre::{anyhow, Result};
use ip_network_table_deps_treebitmap::IpLookupTable;
use tonic::transport::Endpoint;
use tonic::transport::Uri;
use tracing::{info, warn};
use crate::fib_state::FibState;
use crate::netlink::NetlinkConnector;
use crate::proto::route_service_client::RouteServiceClient;
use crate::southbound_interface::SouthboundInterface;
pub mod proto {
tonic::include_proto!("bgpd.grpc");
}
fn vec_to_array<T, const N: usize>(v: Vec<T>) -> Result<[T; N]> {
v.try_into()
.map_err(|_| eyre::Error::msg("Wrong size of Vec".to_string()))
}
/// Temporary hack to select the route to install to the FIB.
/// TODO: Implement proper route selection logic.
fn select_best_route(ps: &proto::PathSet) -> Option<proto::Path> {
let mut selected: Option<proto::Path> = None;
for path in &ps.paths {
if let Some(current) = selected.as_ref() {
if path.local_pref < current.local_pref {
selected = Some(path.clone());
}
} else {
selected = Some(path.clone());
}
}
selected
}
pub async fn run_connector_v4<S: SouthboundInterface>(
route_server: String,
rt_table: u32,
dry_run: bool,
southbound: S,
) -> Result<()> {
// Create netlink socket.
let mut fib_state = FibState::<Ipv4Addr, S> {
fib: IpLookupTable::new(),
southbound,
af: AddressFamilyIdentifier::Ipv4,
table: rt_table,
};
let uri = Uri::from_str(route_server.as_str()).unwrap();
let endpoint = Endpoint::from(uri).keep_alive_timeout(Duration::from_secs(10));
let mut client = RouteServiceClient::connect(endpoint).await?;
let request = proto::StreamPathsRequest {
address_family: proto::AddressFamily::IPv4.into(),
};
let mut stream = client.stream_paths(request).await?.into_inner();
let mut msg_ctr: u64 = 0;
while let Some(route) = stream.message().await? {
let nlri = NLRI {
afi: AddressFamilyIdentifier::Ipv4,
prefixlen: route.prefix.as_ref().unwrap().prefix_len as u8,
prefix: route.prefix.as_ref().unwrap().ip_prefix.clone(),
};
trace!("IPv4 Update {} for: {} ", msg_ctr, nlri);
msg_ctr += 1;
if !dry_run {
if !route.paths.is_empty() {
if let Some(best) = select_best_route(&route) {
// Hack to convert the nexthop into a v4 addr
let nh_bytes: [u8; 4] = vec_to_array(best.nexthop.clone())?;
let nh_addr: Ipv4Addr = Ipv4Addr::from(nh_bytes);
if let Err(e) = fib_state.route_add(&nlri, IpAddr::V4(nh_addr)).await {
return Err(anyhow!("Failed to add route {}: {}", nlri, e));
}
}
} else {
// No more paths, delete
if let Err(e) = fib_state.route_del(nlri).await {
return Err(anyhow!("Failed to delete route: {}", e));
}
}
}
trace!("Number of paths: {}", route.paths.len());
for path in &route.paths {
// TODO: have a proper error here not unwrap.
let nexthop_bytes: [u8; 4] = path.nexthop.clone().try_into().unwrap();
let nexthop: Ipv4Addr = nexthop_bytes.into();
trace!(
"nexthop: {}, peer: {}, local_pref: {}, med: {}, as_path: {:?}",
nexthop,
path.peer_name,
path.local_pref,
path.med,
path.as_path
);
}
}
unreachable!()
}
pub async fn run_connector_v6<S: SouthboundInterface>(
route_server: String,
rt_table: u32,
dry_run: bool,
southbound: S,
) -> Result<()> {
let mut fib_state = FibState::<Ipv6Addr, S> {
fib: IpLookupTable::new(),
southbound,
af: AddressFamilyIdentifier::Ipv6,
table: rt_table,
};
let uri = Uri::from_str(route_server.as_str()).unwrap();
let endpoint = Endpoint::from(uri).keep_alive_timeout(Duration::from_secs(10));
let mut client = RouteServiceClient::connect(endpoint).await?;
let request = proto::StreamPathsRequest {
address_family: proto::AddressFamily::IPv6.into(),
};
info!("Request: {:?}", request);
let mut stream = client.stream_paths(request).await?.into_inner();
let mut msg_ctr: u64 = 0;
while let Some(route) = stream.message().await? {
let nlri = NLRI {
afi: AddressFamilyIdentifier::Ipv6,
prefixlen: route.prefix.as_ref().unwrap().prefix_len as u8,
prefix: route.prefix.as_ref().unwrap().ip_prefix.clone(),
};
trace!("IPv6 Update {} for: {} ", msg_ctr, nlri);
msg_ctr += 1;
if !dry_run {
if !route.paths.is_empty() {
if let Some(best) = select_best_route(&route) {
// Hack to convert the nexthop into a v6 addr
let nh_bytes: [u8; 16] = vec_to_array(best.nexthop.clone())?;
let nh_addr: Ipv6Addr = Ipv6Addr::from(nh_bytes);
if let Err(e) = fib_state.route_add(&nlri, IpAddr::V6(nh_addr)).await {
return Err(anyhow!("Failed to add route {}: {}", nlri, e));
}
}
} else {
// No more paths, delete
if let Err(e) = fib_state.route_del(nlri).await {
return Err(anyhow!("Failed to delete route: {}", e));
}
}
}
trace!("Number of paths: {}", route.paths.len());
for path in &route.paths {
// TODO: have a proper error here not unwrap.
let nexthop_bytes: [u8; 16] = path.nexthop.clone().try_into().unwrap();
let nexthop: Ipv6Addr = nexthop_bytes.into();
trace!(
"nexthop: {}, peer: {}, local_pref: {}, med: {}, as_path: {:?}",
nexthop,
path.peer_name,
path.local_pref,
path.med,
path.as_path
);
}
}
unreachable!()
}

View File

@ -0,0 +1,161 @@
use async_trait::async_trait;
use bgp_packet::{constants::AddressFamilyIdentifier, nlri::NLRI};
use eyre::{eyre, Result};
use futures::TryStreamExt;
use netlink_packet_route::route::RouteAddress;
use netlink_packet_route::route::RouteAttribute;
use netlink_packet_route::route::RouteHeader;
use netlink_packet_route::route::RouteMessage;
use netlink_packet_route::route::RouteProtocol;
use netlink_packet_route::route::RouteType;
use netlink_packet_route::AddressFamily as NetlinkAddressFamily;
use netlink_packet_utils::nla::Nla;
use rtnetlink::IpVersion;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::{convert::TryInto, io::ErrorKind};
use super::southbound_interface::SouthboundInterface;
/// NetlinkConnector implements methods to read/update Linux networking stuff including
/// routes and link level info.
pub struct NetlinkConnector {
handle: rtnetlink::Handle,
table: Option<u32>,
}
#[async_trait]
impl SouthboundInterface for NetlinkConnector {
async fn route_add(
&mut self,
address_family: AddressFamilyIdentifier,
prefix: NLRI,
nexthop: IpAddr,
) -> Result<()> {
let route = self.handle.route();
match address_family {
AddressFamilyIdentifier::Ipv6 => {
let prefix_len = prefix.prefixlen;
let addr: Ipv6Addr = match prefix.try_into()? {
IpAddr::V6(addr) => addr,
_ => {
return Err(eyre::Error::from(std::io::Error::new(
ErrorKind::InvalidInput,
"Got non-IPv6 address from NLRI",
)))
}
};
let gw_addr: Ipv6Addr = match nexthop.clone().try_into()? {
IpAddr::V6(addr) => addr,
_ => {
return Err(eyre::Error::from(std::io::Error::new(
ErrorKind::InvalidInput,
"Got non-IPv6 gateway for IPv6 NLRI",
)))
}
};
let mut mutation = route
.add()
.v6()
.destination_prefix(addr, prefix_len)
.gateway(gw_addr);
if let Some(table_id) = self.table {
mutation = mutation.table(table_id.try_into().unwrap());
}
mutation.execute().await.map_err(|e| eyre::Error::from(e))
}
AddressFamilyIdentifier::Ipv4 => {
let prefix_len = prefix.prefixlen;
let addr: Ipv4Addr = match prefix.clone().try_into()? {
IpAddr::V4(addr) => addr,
_ => {
return Err(eyre::Error::from(std::io::Error::new(
ErrorKind::InvalidInput,
"Got non-IPv4 address from NLRI",
)))
}
};
let gw_addr = match nexthop.clone().try_into()? {
IpAddr::V4(addr) => addr,
_ => {
return Err(eyre::Error::from(std::io::Error::new(
ErrorKind::InvalidInput,
"Got non-IPv4 gateway for IPv4 NLRI",
)))
}
};
let mut mutation = route
.add()
.v4()
.destination_prefix(addr, prefix_len)
.gateway(gw_addr);
if let Some(table_id) = self.table {
mutation = mutation.table(table_id.try_into().unwrap());
}
mutation.execute().await.map_err(|e| eyre::Error::from(e))
}
}
}
async fn route_del(&mut self, prefix: NLRI, nexthop: IpAddr) -> Result<()> {
let rt_handle = self.handle.route();
let destination = match prefix.afi {
AddressFamilyIdentifier::Ipv4 => {
RouteAddress::Inet(prefix.clone().try_into().map_err(|e: String| eyre!(e))?)
}
AddressFamilyIdentifier::Ipv6 => {
RouteAddress::Inet6(prefix.clone().try_into().map_err(|e: String| eyre!(e))?)
}
};
let nexthop = match nexthop {
IpAddr::V4(ipv4) => RouteAddress::Inet(ipv4),
IpAddr::V6(ipv6) => RouteAddress::Inet6(ipv6),
};
let header = RouteHeader {
address_family: match prefix.afi {
AddressFamilyIdentifier::Ipv4 => NetlinkAddressFamily::Inet,
AddressFamilyIdentifier::Ipv6 => NetlinkAddressFamily::Inet6,
},
destination_prefix_length: prefix.prefixlen,
table: self.table.unwrap_or(0) as u8,
protocol: RouteProtocol::Bgp,
kind: RouteType::Unicast,
..Default::default()
};
let mut rt_msg: RouteMessage = Default::default();
rt_msg.header = header;
rt_msg.attributes = vec![
RouteAttribute::Destination(destination),
RouteAttribute::Gateway(nexthop),
];
rt_handle
.del(rt_msg)
.execute()
.await
.map_err(|e| eyre::Error::from(e))
}
}
impl NetlinkConnector {
pub async fn new(table: Option<u32>) -> Result<Self> {
let (connection, handle, _) = rtnetlink::new_connection()?;
tokio::spawn(connection);
Ok(NetlinkConnector { handle, table })
}
pub async fn dump_routes(
&mut self,
address_family: AddressFamilyIdentifier,
table: Option<u32>,
) -> Result<Vec<RouteMessage>, rtnetlink::Error> {
let mut req = self.handle.route().get(match address_family {
AddressFamilyIdentifier::Ipv4 => IpVersion::V4,
AddressFamilyIdentifier::Ipv6 => IpVersion::V6,
});
if let Some(table_id) = table {
req.message_mut()
.attributes
.push(RouteAttribute::Table(table_id));
}
req.execute().try_collect().await
}
}

View File

@ -0,0 +1,102 @@
// Copyright 2021 Rayhaan Jaufeerally.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, net::IpAddr};
use async_trait::async_trait;
use eyre::{eyre, Result};
use log::info;
use bgp_packet::{constants::AddressFamilyIdentifier, nlri::NLRI};
/// SouthboundInterface provides a uniform API to network forwarding elements
/// These are devices or targets that perform packet routing and are the end
/// consumers of packet routing data.
#[async_trait]
pub trait SouthboundInterface {
/// route_add adds a route towards a particular address_family/NLRI via the given nexthop.
async fn route_add(
&mut self,
address_family: AddressFamilyIdentifier,
prefix: NLRI,
nexthop: IpAddr,
) -> Result<()>;
/// route_del removes the route towards a particular prefix via a given nexthop.
async fn route_del(&mut self, prefix: NLRI, nexthop: IpAddr) -> Result<()>;
}
/// DummyVerifier is a SouthboundInterface that checks that routes are not added more than
/// once and not removed when there are none.
pub struct DummyVerifier {
route_state: HashMap<NLRI, IpAddr>,
}
impl std::default::Default for DummyVerifier {
fn default() -> DummyVerifier {
DummyVerifier {
route_state: HashMap::default(),
}
}
}
#[async_trait]
impl SouthboundInterface for DummyVerifier {
async fn route_add(
&mut self,
_: AddressFamilyIdentifier,
prefix: NLRI,
nexthop: IpAddr,
) -> Result<()> {
// Check that the route is not already present.
match self.route_state.get(&prefix) {
Some(value) => {
return Err(eyre!(
"Prefix {} with nexthop {} already contained in route_state! when trying to add {} -> {}",
prefix, value, prefix, nexthop,
));
}
_ => {}
}
if self.route_state.get(&prefix).is_some() {}
// Insert route into in memory state.
self.route_state.insert(prefix, nexthop);
info!("Route add ok in verifier ({})", self.route_state.len());
Ok(())
}
async fn route_del(&mut self, prefix: NLRI, nexthop: IpAddr) -> Result<()> {
match self.route_state.remove(&prefix) {
Some(entry) => {
if entry != nexthop {
return Err(eyre!(
"Removed entry's nexthop did not match: {} vs requested {}",
entry,
nexthop
));
}
}
None => {
return Err(eyre!(
"Requested removal of route {} that was not in route_state",
prefix
));
}
}
info!("Route del ok in verifier ({})", self.route_state.len());
Ok(())
}
}