Merge branch 'main' of https://github.com/net-control-plane/bgp
This commit is contained in:
37
crates/route_client/Cargo.toml
Normal file
37
crates/route_client/Cargo.toml
Normal 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"] }
|
||||
19
crates/route_client/build.rs
Normal file
19
crates/route_client/build.rs
Normal 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();
|
||||
}
|
||||
83
crates/route_client/proto/route_service.proto
Normal file
83
crates/route_client/proto/route_service.proto
Normal 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);
|
||||
}
|
||||
180
crates/route_client/src/fib_state.rs
Normal file
180
crates/route_client/src/fib_state.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
205
crates/route_client/src/lib.rs
Normal file
205
crates/route_client/src/lib.rs
Normal 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!()
|
||||
}
|
||||
160
crates/route_client/src/netlink.rs
Normal file
160
crates/route_client/src/netlink.rs
Normal file
@ -0,0 +1,160 @@
|
||||
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 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
|
||||
}
|
||||
}
|
||||
102
crates/route_client/src/southbound_interface.rs
Normal file
102
crates/route_client/src/southbound_interface.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user