diff --git a/Cargo.lock b/Cargo.lock index 6ca68e5c..b06b5fa7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2738,6 +2738,7 @@ dependencies = [ "inetnum", "insta", "layout-rs", + "libc", "log", "log-reroute", "memmap2", diff --git a/Cargo.toml b/Cargo.toml index a9a77379..6e4e43e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"]} diff --git a/etc/rotonda.conf b/etc/rotonda.conf index 20aed6f7..1723c2a2 100644 --- a/etc/rotonda.conf +++ b/etc/rotonda.conf @@ -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"] diff --git a/src/common/net.rs b/src/common/net.rs index 416ada33..ae46a546 100644 --- a/src/common/net.rs +++ b/src/common/net.rs @@ -36,7 +36,7 @@ impl TcpListenerFactory 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] diff --git a/src/tests/util.rs b/src/tests/util.rs index 3ec55e77..593f635d 100644 --- a/src/tests/util.rs +++ b/src/tests/util.rs @@ -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 @@ -1701,6 +1702,23 @@ pub mod net { } } + impl ListenerMd5Config for MockTcpListener + where + T: Fn() -> Fut, + Fut: Future< + Output = std::io::Result<(MockTcpStreamWrapper, SocketAddr)>, + >, + { + fn configure_md5( + &self, + _addr: std::net::IpAddr, + _prefix_len: Option, + _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 diff --git a/src/units/bgp_tcp_in/mod.rs b/src/units/bgp_tcp_in/mod.rs index 32ff84e1..616383ff 100644 --- a/src/units/bgp_tcp_in/mod.rs +++ b/src/units/bgp_tcp_in/mod.rs @@ -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, + key: &[u8], + ) -> io::Result<()>; +} diff --git a/src/units/bgp_tcp_in/peer_config.rs b/src/units/bgp_tcp_in/peer_config.rs index da6195e4..6a777967 100644 --- a/src/units/bgp_tcp_in/peer_config.rs +++ b/src/units/bgp_tcp_in/peer_config.rs @@ -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; @@ -103,20 +104,41 @@ impl PeerConfigs { pub fn get_exact(&self, key: &PrefixOrExact) -> Option<&PeerConfig> { self.0.get(key) } + + pub fn iter( + &self, + ) -> impl Iterator { + 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, #[serde(default)] + md5_key: Option, + #[serde(default)] protocols: Vec, #[serde(default)] addpath: Vec, } +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(|_| "")) + .finish() + } +} + impl PeerConfig { #[cfg(test)] pub fn mock() -> Self { @@ -124,6 +146,7 @@ impl PeerConfig { name: "MOCK".to_string(), remote_asn: OneOrManyAsns::Many(vec![]), hold_time: None, + md5_key: None, protocols: vec![], addpath: vec![], } @@ -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 } } @@ -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" @@ -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 diff --git a/src/units/bgp_tcp_in/tcp_md5.rs b/src/units/bgp_tcp_in/tcp_md5.rs new file mode 100644 index 00000000..e842a02d --- /dev/null +++ b/src/units/bgp_tcp_in/tcp_md5.rs @@ -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::(), + ); + } + } + 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::(), + ); + } + } + } + + storage +} + +#[cfg(target_os = "linux")] +fn configure_md5_fd( + fd: libc::c_int, + addr: IpAddr, + prefix_len: Option, + 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::() 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 { + let mut storage: libc::sockaddr_storage = + unsafe { std::mem::zeroed() }; + let mut len = std::mem::size_of::() + 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, + 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, + _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, + key: &[u8], + ) -> io::Result<()> { + configure_tcp_md5_listener(&self.0, addr, prefix_len, key) + } +} diff --git a/src/units/bgp_tcp_in/unit.rs b/src/units/bgp_tcp_in/unit.rs index 423d3d29..0a2ebefe 100644 --- a/src/units/bgp_tcp_in/unit.rs +++ b/src/units/bgp_tcp_in/unit.rs @@ -49,7 +49,9 @@ use super::metrics::BgpTcpInMetrics; use super::router_handler::handle_connection; use super::status_reporter::BgpTcpInStatusReporter; -use super::peer_config::{CombinedConfig, PeerConfigs}; +use super::peer_config::{CombinedConfig, PeerConfigs, PrefixOrExact}; +use super::tcp_md5; +use super::ListenerMd5Config; //----------- BgpTcpIn ------------------------------------------------------- @@ -260,7 +262,7 @@ impl BgpTcpInRunner { ) -> Result<(), Terminated> where T: TcpListenerFactory, - U: TcpListener, + U: TcpListener + ListenerMd5Config, V: TcpStreamWrapper, F: ConfigAcceptor, { @@ -324,6 +326,7 @@ impl BgpTcpInRunner { }; status_reporter.listener_listening(&listen_addr); + arc_self.configure_listener_md5(&listener); 'inner: loop { match arc_self.process_until(listener.accept()).await { @@ -458,6 +461,32 @@ impl BgpTcpInRunner { } } } + + fn configure_listener_md5(&self, listener: &U) + where + U: ListenerMd5Config, + { + let cfg = self.bgp.load(); + for (remote, peer_cfg) in cfg.peer_configs.iter() { + let Some(md5_key) = peer_cfg.md5_key() else { + continue; + }; + let (addr, prefix_len) = match remote { + PrefixOrExact::Exact(addr) => (*addr, None), + PrefixOrExact::Prefix(prefix) => (prefix.addr(), Some(prefix.len())), + }; + if let Err(err) = listener.configure_md5( + addr, + prefix_len, + md5_key.as_bytes(), + ) { + error!( + "Failed to configure TCP MD5 on listener for {}: {}", + addr, err + ); + } + } + } } #[async_trait] @@ -590,6 +619,28 @@ impl ConfigAcceptor for BgpTcpInRunner { let (cmds_tx, cmds_rx) = mpsc::channel(10 * 10); //XXX this is limiting and //causes loss let tcp_stream = tcp_stream.into_inner().unwrap(); // SAFETY: StandardTcpStream::into_inner() always returns Ok(...) + if let Some(md5_key) = cfg.md5_key() { + match tcp_stream.peer_addr() { + Ok(peer_addr) => { + if let Err(err) = tcp_md5::configure_tcp_md5( + &tcp_stream, + peer_addr.ip(), + md5_key.as_bytes(), + ) { + error!( + "Failed to configure TCP MD5 for {}: {}", + peer_addr.ip(), + err + ); + return; + } + } + Err(err) => { + error!("Failed to read peer address for MD5: {}", err); + return; + } + } + } crate::tokio::spawn( &child_name, handle_connection(