Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ html-escape = { version = "0.2" }
inetnum = { workspace = true }
indoc = "2.0"
layout-rs = { version = "0.1" }
libc = "0.2"
mqtt = { version = "0.23.0", package = "rumqttc", default-features = false }
memmap2 = "0.9.4"
non-empty-vec = { version = "0.2", features = ["serde"]}
Expand Down
1 change: 1 addition & 0 deletions etc/rotonda.conf
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ listen = "0.0.0.0:11019"
# [units.bgp-in.peers."10.1.0.1"]
# name = "PeerA"
# remote_asn = []
# md5_key = "shared-secret"
# protocols = ["Ipv4Unicast", "Ipv4Multicast", "Ipv6Unicast"]

# [units.bgp-in.peers."10.1.0.2"]
Expand Down
2 changes: 1 addition & 1 deletion src/common/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl TcpListenerFactory<StandardTcpListener> for StandardTcpListenerFactory {
}
}

pub struct StandardTcpListener(::tokio::net::TcpListener);
pub struct StandardTcpListener(pub(crate) ::tokio::net::TcpListener);

/// A thin wrapper around the real Tokio TcpListener bind call.
#[async_trait::async_trait]
Expand Down
18 changes: 18 additions & 0 deletions src/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,7 @@ pub mod net {
use crate::common::net::{
TcpListener, TcpListenerFactory, TcpStreamWrapper,
};
use crate::units::bgp_tcp_in::ListenerMd5Config;

/// A mock TcpListenerFactory that stores a callback supplied by the
/// unit test thereby allowing the unit test to determine if binding to
Expand Down Expand Up @@ -1701,6 +1702,23 @@ pub mod net {
}
}

impl<T, Fut> ListenerMd5Config for MockTcpListener<T, Fut>
where
T: Fn() -> Fut,
Fut: Future<
Output = std::io::Result<(MockTcpStreamWrapper, SocketAddr)>,
>,
{
fn configure_md5(
&self,
_addr: std::net::IpAddr,
_prefix_len: Option<u8>,
_key: &[u8],
) -> std::io::Result<()> {
Ok(())
}
}

/// A mock TcpStreamWraper that is not actually usable, but can be passed
/// in place of a StandardTcpStream in order to avoid needing to create a
/// real TcpStream which would interact with the actual operating system
Expand Down
14 changes: 14 additions & 0 deletions src/units/bgp_tcp_in/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,19 @@ pub(crate) mod metrics;
pub(crate) mod peer_config;
pub(crate) mod router_handler;
pub(crate) mod status_reporter;
pub(crate) mod tcp_md5;

pub mod unit;

use std::io;
use std::net::IpAddr;

pub(crate) trait ListenerMd5Config {
// Extend for TTL/GTSM or other socket options (via setsockopt) as needed.
fn configure_md5(
&self,
addr: IpAddr,
prefix_len: Option<u8>,
key: &[u8],
) -> io::Result<()>;
}
32 changes: 31 additions & 1 deletion src/units/bgp_tcp_in/peer_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//! that we want to move this configuration to Roto in the future.

use std::collections::BTreeMap;
use std::fmt;
use std::net::IpAddr;

use inetnum::addr::Prefix;
Expand Down Expand Up @@ -103,27 +104,49 @@ impl PeerConfigs {
pub fn get_exact(&self, key: &PrefixOrExact) -> Option<&PeerConfig> {
self.0.get(key)
}

pub fn iter(
&self,
) -> impl Iterator<Item = (&PrefixOrExact, &PeerConfig)> {
self.0.iter()
}
}

/// Configuration for a remote BGP peer.
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Deserialize)]
pub struct PeerConfig {
name: String,
remote_asn: OneOrManyAsns,
hold_time: Option<u16>,
#[serde(default)]
md5_key: Option<String>,
#[serde(default)]
protocols: Vec<AfiSafiType>,
#[serde(default)]
addpath: Vec<AfiSafiType>,
}

impl fmt::Debug for PeerConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PeerConfig")
.field("name", &self.name)
.field("remote_asn", &self.remote_asn)
.field("hold_time", &self.hold_time)
.field("protocols", &self.protocols)
.field("addpath", &self.addpath)
.field("md5_key", &self.md5_key.as_ref().map(|_| "<redacted>"))
.finish()
}
}

impl PeerConfig {
#[cfg(test)]
pub fn mock() -> Self {
Self {
name: "MOCK".to_string(),
remote_asn: OneOrManyAsns::Many(vec![]),
hold_time: None,
md5_key: None,
protocols: vec![],
addpath: vec![],
}
Expand All @@ -145,12 +168,17 @@ impl PeerConfig {
}
self.remote_asn.contains(remote)
}

pub fn md5_key(&self) -> Option<&str> {
self.md5_key.as_deref().filter(|value| !value.is_empty())
}
}

impl PartialEq for PeerConfig {
fn eq(&self, other: &PeerConfig) -> bool {
self.remote_asn == other.remote_asn
&& self.hold_time == other.hold_time
&& self.md5_key == other.md5_key
}
}

Expand Down Expand Up @@ -266,6 +294,7 @@ hold_time = 10
name = "Peer-exact"
remote_asn = 100
hold_time = 10
md5_key = "s3cr3t"

[peers."2.3.4.7"]
name = "Explicit-protocols"
Expand Down Expand Up @@ -296,6 +325,7 @@ addpath = ["Ipv4Unicast", "Ipv6Unicast"]
let cfg2 = cfg.peer_configs.get(ip2).unwrap();
assert!(cfg.peer_configs.get(ip2).unwrap().1.name == "Peer-exact");
assert!(!cfg2.1.accept_remote_asn(Asn::from_u32(1234)));
assert_eq!(cfg2.1.md5_key(), Some("s3cr3t"));

let cfg3 = cfg
.peer_configs
Expand Down
218 changes: 218 additions & 0 deletions src/units/bgp_tcp_in/tcp_md5.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
use std::io;
use std::net::IpAddr;

use tokio::net::{TcpListener, TcpStream};

#[cfg(target_os = "linux")]
use std::os::unix::io::AsRawFd;

use crate::common::net::StandardTcpListener;
use super::ListenerMd5Config;

#[cfg(target_os = "linux")]
const TCP_MD5SIG_MAXKEYLEN: usize = 80;

#[cfg(target_os = "linux")]
const TCP_MD5SIG_FLAG_PREFIX: u8 = 0x1;

#[cfg(target_os = "linux")]
#[repr(C)]
struct TcpMd5Sig {
tcpm_addr: libc::sockaddr_storage,
tcpm_flags: u8,
tcpm_prefixlen: u8,
tcpm_keylen: u16,
tcpm_ifindex: i32,
tcpm_key: [u8; TCP_MD5SIG_MAXKEYLEN],
}

#[cfg(target_os = "linux")]
fn sockaddr_storage_from_ip(addr: IpAddr) -> libc::sockaddr_storage {
let mut storage: libc::sockaddr_storage =
unsafe { std::mem::zeroed() };

match addr {
IpAddr::V4(ip) => {
let v4 = libc::sockaddr_in {
sin_family: libc::AF_INET as libc::sa_family_t,
sin_port: 0,
sin_addr: libc::in_addr {
s_addr: u32::from(ip).to_be(),
},
sin_zero: [0; 8],
};
unsafe {
std::ptr::copy_nonoverlapping(
&v4 as *const _ as *const u8,
&mut storage as *mut _ as *mut u8,
std::mem::size_of::<libc::sockaddr_in>(),
);
}
}
IpAddr::V6(ip) => {
let v6 = libc::sockaddr_in6 {
sin6_family: libc::AF_INET6 as libc::sa_family_t,
sin6_port: 0,
sin6_flowinfo: 0,
sin6_addr: libc::in6_addr { s6_addr: ip.octets() },
sin6_scope_id: 0,
};
unsafe {
std::ptr::copy_nonoverlapping(
&v6 as *const _ as *const u8,
&mut storage as *mut _ as *mut u8,
std::mem::size_of::<libc::sockaddr_in6>(),
);
}
}
}

storage
}

#[cfg(target_os = "linux")]
fn configure_md5_fd(
fd: libc::c_int,
addr: IpAddr,
prefix_len: Option<u8>,
key: &[u8],
) -> io::Result<()> {
let family = listener_family(fd)?;
match (family, addr) {
(val, IpAddr::V6(_))
if val == libc::AF_INET as libc::sa_family_t =>
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"TCP listener is IPv4, cannot set TCP MD5 for IPv6 peer",
));
}
(val, IpAddr::V4(_))
if val == libc::AF_INET6 as libc::sa_family_t =>
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"TCP listener is IPv6, cannot set TCP MD5 for IPv4 peer",
));
}
_ => {}
}

// RFC 2385 p3: "The MD5 digest is always 16 bytes in length, and the
// option would appear in every segment of a connection."
if key.len() > TCP_MD5SIG_MAXKEYLEN {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"TCP MD5 key length exceeds 80 bytes",
));
}

let mut md5sig = TcpMd5Sig {
tcpm_addr: sockaddr_storage_from_ip(addr),
tcpm_flags: 0,
tcpm_prefixlen: 0,
tcpm_keylen: key.len() as u16,
tcpm_ifindex: 0,
tcpm_key: [0u8; TCP_MD5SIG_MAXKEYLEN],
};

if let Some(prefix_len) = prefix_len {
md5sig.tcpm_flags = TCP_MD5SIG_FLAG_PREFIX;
md5sig.tcpm_prefixlen = prefix_len;
}

md5sig.tcpm_key[..key.len()].copy_from_slice(key);

let ret = unsafe {
libc::setsockopt(
fd,
libc::IPPROTO_TCP,
libc::TCP_MD5SIG,
&md5sig as *const _ as *const libc::c_void,
std::mem::size_of::<TcpMd5Sig>() as libc::socklen_t,
)
};

if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

#[cfg(target_os = "linux")]
fn listener_family(fd: libc::c_int) -> io::Result<libc::sa_family_t> {
let mut storage: libc::sockaddr_storage =
unsafe { std::mem::zeroed() };
let mut len = std::mem::size_of::<libc::sockaddr_storage>()
as libc::socklen_t;
let ret = unsafe {
libc::getsockname(
fd,
&mut storage as *mut _ as *mut libc::sockaddr,
&mut len,
)
};
if ret == 0 {
Ok(storage.ss_family as libc::sa_family_t)
} else {
Err(io::Error::last_os_error())
}
}

#[cfg(target_os = "linux")]
pub fn configure_tcp_md5(
stream: &TcpStream,
addr: IpAddr,
key: &[u8],
) -> io::Result<()> {
configure_md5_fd(stream.as_raw_fd(), addr, None, key)
}

#[cfg(target_os = "linux")]
pub fn configure_tcp_md5_listener(
listener: &TcpListener,
addr: IpAddr,
prefix_len: Option<u8>,
key: &[u8],
) -> io::Result<()> {
// RFC 2385 p2: "there is no negotiation for the use of this option in a
// connection... [it is] purely a matter of site policy."
configure_md5_fd(listener.as_raw_fd(), addr, prefix_len, key)
}

#[cfg(not(target_os = "linux"))]
pub fn configure_tcp_md5(
_stream: &TcpStream,
_addr: IpAddr,
_key: &[u8],
) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"TCP MD5 is only supported on Linux",
))
}

#[cfg(not(target_os = "linux"))]
pub fn configure_tcp_md5_listener(
_listener: &TcpListener,
_addr: IpAddr,
_prefix_len: Option<u8>,
_key: &[u8],
) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"TCP MD5 is only supported on Linux",
))
}

impl ListenerMd5Config for StandardTcpListener {
fn configure_md5(
&self,
addr: IpAddr,
prefix_len: Option<u8>,
key: &[u8],
) -> io::Result<()> {
configure_tcp_md5_listener(&self.0, addr, prefix_len, key)
}
}
Loading