Mohammed Naser | a7f726d | 2025-02-16 13:18:14 -0500 | [diff] [blame] | 1 | extern crate ipnet; |
| 2 | |
| 3 | mod routes; |
| 4 | |
| 5 | use futures_util::stream::TryStreamExt; |
| 6 | use ipnet::IpNet; |
| 7 | use log::{error, info}; |
| 8 | use netlink_packet_route::{ |
| 9 | address::{AddressAttribute, AddressMessage}, |
| 10 | route::{RouteAttribute, RouteMessage, RouteScope}, |
| 11 | AddressFamily, |
| 12 | }; |
| 13 | use rtnetlink::{Handle, IpVersion}; |
| 14 | use std::net::IpAddr; |
| 15 | use thiserror::Error; |
| 16 | |
| 17 | #[derive(Error, Debug)] |
| 18 | pub enum InterfaceError { |
| 19 | #[error("Interface {0} not found")] |
| 20 | NotFound(String), |
| 21 | |
| 22 | #[error(transparent)] |
| 23 | NetlinkError(#[from] rtnetlink::Error), |
| 24 | |
| 25 | #[error(transparent)] |
| 26 | IpNetError(#[from] ipnet::PrefixLenError), |
| 27 | |
| 28 | #[error(transparent)] |
| 29 | RouteError(#[from] routes::RouteError), |
| 30 | } |
| 31 | |
| 32 | #[derive(Error, Debug)] |
| 33 | pub enum InterfaceMigrationError { |
| 34 | #[error(transparent)] |
| 35 | InterfaceError(#[from] InterfaceError), |
| 36 | |
| 37 | #[error("IP configuration on both interfaces")] |
| 38 | IpConflict, |
| 39 | } |
| 40 | |
| 41 | pub struct Interface { |
| 42 | name: String, |
| 43 | index: u32, |
| 44 | address_messages: Vec<AddressMessage>, |
| 45 | route_messages: Vec<RouteMessage>, |
| 46 | } |
| 47 | |
| 48 | impl Interface { |
| 49 | pub async fn new(handle: &Handle, name: String) -> Result<Self, InterfaceError> { |
| 50 | let index = handle |
| 51 | .link() |
| 52 | .get() |
| 53 | .match_name(name.clone()) |
| 54 | .execute() |
| 55 | .try_next() |
| 56 | .await |
| 57 | .map_err(|e| match e { |
| 58 | rtnetlink::Error::NetlinkError(inner) if -inner.raw_code() == libc::ENODEV => { |
| 59 | InterfaceError::NotFound(name.clone()) |
| 60 | } |
| 61 | _ => InterfaceError::NetlinkError(e), |
| 62 | })? |
| 63 | .map(|link| link.header.index) |
| 64 | .ok_or_else(|| InterfaceError::NotFound(name.clone()))?; |
| 65 | |
| 66 | let address_messages: Vec<AddressMessage> = handle |
| 67 | .address() |
| 68 | .get() |
| 69 | .set_link_index_filter(index) |
| 70 | .execute() |
| 71 | .map_err(InterfaceError::NetlinkError) |
| 72 | .try_filter(|msg| futures::future::ready(msg.header.family == AddressFamily::Inet)) |
| 73 | .try_collect() |
| 74 | .await?; |
| 75 | |
| 76 | let route_messages: Vec<RouteMessage> = handle |
| 77 | .route() |
| 78 | .get(IpVersion::V4) |
| 79 | .execute() |
| 80 | .map_err(InterfaceError::NetlinkError) |
| 81 | .try_filter(move |route_msg| { |
| 82 | let matches = route_msg |
| 83 | .attributes |
| 84 | .iter() |
| 85 | .any(|attr| matches!(attr, RouteAttribute::Oif(idx) if *idx == index)) |
| 86 | && route_msg.header.kind != netlink_packet_route::route::RouteType::Local; |
| 87 | |
| 88 | futures_util::future::ready(matches) |
| 89 | }) |
| 90 | .try_collect() |
| 91 | .await?; |
| 92 | |
| 93 | Ok(Self { |
| 94 | name, |
| 95 | index, |
| 96 | address_messages, |
| 97 | route_messages, |
| 98 | }) |
| 99 | } |
| 100 | |
| 101 | fn addresses(&self) -> Vec<IpNet> { |
| 102 | self.address_messages |
| 103 | .iter() |
| 104 | .filter_map(|msg| { |
| 105 | msg.attributes.iter().find_map(|nla| { |
| 106 | if let AddressAttribute::Address(ip) = nla { |
| 107 | IpNet::new(*ip, msg.header.prefix_len).ok() |
| 108 | } else { |
| 109 | None |
| 110 | } |
| 111 | }) |
| 112 | }) |
| 113 | .collect() |
| 114 | } |
| 115 | |
| 116 | fn routes(&self) -> Result<Vec<routes::Route>, routes::RouteError> { |
| 117 | self.route_messages |
| 118 | .iter() |
| 119 | .filter_map(|msg| { |
| 120 | if msg.header.scope == RouteScope::Link { |
| 121 | return None; |
| 122 | } |
| 123 | |
| 124 | Some(routes::Route::from_message(msg.clone())) |
| 125 | }) |
| 126 | .collect::<Result<Vec<routes::Route>, routes::RouteError>>() |
| 127 | } |
| 128 | |
| 129 | async fn up(&self, handle: &Handle) -> Result<(), InterfaceError> { |
| 130 | handle |
| 131 | .link() |
| 132 | .set(self.index) |
| 133 | .up() |
| 134 | .execute() |
| 135 | .await |
| 136 | .map_err(InterfaceError::NetlinkError) |
| 137 | } |
| 138 | |
| 139 | async fn restore(&self, handle: &Handle) -> Result<(), InterfaceError> { |
| 140 | self.migrate_addresses_from_interface(handle, self).await?; |
| 141 | self.migrate_routes_from_interface(handle, self).await?; |
| 142 | |
| 143 | Ok(()) |
| 144 | } |
| 145 | |
| 146 | async fn flush(&self, handle: &Handle) -> Result<(), InterfaceError> { |
| 147 | for msg in self.address_messages.iter() { |
| 148 | handle.address().del(msg.clone()).execute().await?; |
| 149 | } |
| 150 | |
| 151 | // NOTE(mnaser): Once the interface has no more addresses, it will |
| 152 | // automatically lose all of it's routes. |
| 153 | |
| 154 | Ok(()) |
| 155 | } |
| 156 | |
| 157 | async fn migrate_addresses_from_interface( |
| 158 | &self, |
| 159 | handle: &Handle, |
| 160 | src_interface: &Interface, |
| 161 | ) -> Result<(), InterfaceError> { |
| 162 | for msg in src_interface.address_messages.iter() { |
| 163 | let ip = msg.attributes.iter().find_map(|nla| match nla { |
| 164 | AddressAttribute::Address(ip) => Some(ip), |
| 165 | _ => None, |
| 166 | }); |
| 167 | |
| 168 | if let Some(ip) = ip { |
| 169 | handle |
| 170 | .address() |
| 171 | .add(self.index, *ip, msg.header.prefix_len) |
| 172 | .replace() |
| 173 | .execute() |
| 174 | .await?; |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | Ok(()) |
| 179 | } |
| 180 | |
| 181 | async fn migrate_routes_from_interface( |
| 182 | &self, |
| 183 | handle: &Handle, |
| 184 | src_interface: &Interface, |
| 185 | ) -> Result<(), InterfaceError> { |
| 186 | for route in src_interface.routes()?.iter() { |
| 187 | let mut request = handle.route().add(); |
| 188 | request = request.protocol(route.protocol); |
| 189 | |
| 190 | match route.destination.addr() { |
| 191 | IpAddr::V4(ipv4) => { |
| 192 | let mut request = request |
| 193 | .v4() |
| 194 | .replace() |
| 195 | .destination_prefix(ipv4, route.destination.prefix_len()); |
| 196 | |
| 197 | if let IpAddr::V4(gateway) = route.gateway { |
| 198 | request = request.gateway(gateway); |
| 199 | } |
| 200 | |
| 201 | request.execute().await?; |
| 202 | } |
| 203 | IpAddr::V6(ipv6) => { |
| 204 | let mut request = request |
| 205 | .v6() |
| 206 | .replace() |
| 207 | .destination_prefix(ipv6, route.destination.prefix_len()); |
| 208 | |
| 209 | if let IpAddr::V6(gateway) = route.gateway { |
| 210 | request = request.gateway(gateway); |
| 211 | } |
| 212 | |
| 213 | request.execute().await?; |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | Ok(()) |
| 219 | } |
| 220 | |
| 221 | pub async fn migrate_from_interface( |
| 222 | &self, |
| 223 | handle: &Handle, |
| 224 | src_interface: &Interface, |
| 225 | ) -> Result<(), InterfaceMigrationError> { |
| 226 | self.up(handle).await?; |
| 227 | |
| 228 | match ( |
| 229 | src_interface.address_messages.is_empty(), |
| 230 | self.address_messages.is_empty(), |
| 231 | ) { |
| 232 | (false, false) => { |
| 233 | // Both source and destination interfaces have IPs assigned |
| 234 | error!( |
| 235 | src_interface = src_interface.name.as_str(), |
| 236 | dst_interface = self.name.as_str(), |
| 237 | src_ip_addresses = format!("{:?}", src_interface.addresses()).as_str(), |
| 238 | dst_ip_addresses = format!("{:?}", self.addresses()).as_str(); |
| 239 | "Both source and destination interfaces have IPs assigned. This is not safe in production, please fix manually." |
| 240 | ); |
| 241 | |
| 242 | Err(InterfaceMigrationError::IpConflict) |
| 243 | } |
| 244 | (false, true) => { |
| 245 | // Source interface has IPs, destination interface has no IPs |
| 246 | info!( |
| 247 | src_interface = src_interface.name.as_str(), |
| 248 | dst_interface = self.name.as_str(), |
| 249 | ip_addresses = format!("{:?}", src_interface.addresses()).as_str(), |
| 250 | routes = format!("{:?}", src_interface.routes()).as_str(); |
| 251 | "Migrating IP addresses from interface to bridge." |
| 252 | ); |
| 253 | |
| 254 | if let Err(e) = src_interface.flush(handle).await { |
| 255 | error!( |
| 256 | src_interface = src_interface.name.as_str(), |
| 257 | error = e.to_string().as_str(); |
| 258 | "Error while flushing IPs from source interface." |
| 259 | ); |
| 260 | |
| 261 | if let Err(restore_err) = src_interface.restore(handle).await { |
| 262 | error!( |
| 263 | src_interface = src_interface.name.as_str(), |
| 264 | error = restore_err.to_string().as_str(); |
| 265 | "Error while restoring IPs to source interface." |
| 266 | ); |
| 267 | } |
| 268 | |
| 269 | return Err(InterfaceMigrationError::InterfaceError(e)); |
| 270 | } |
| 271 | |
| 272 | info!( |
| 273 | src_interface = src_interface.name.as_str(), |
| 274 | dst_interface = self.name.as_str(); |
| 275 | "Successfully flushed IP addresses from source interface." |
| 276 | ); |
| 277 | |
| 278 | if let Err(e) = self |
| 279 | .migrate_addresses_from_interface(handle, src_interface) |
| 280 | .await |
| 281 | { |
| 282 | error!( |
| 283 | dst_interface = self.name.as_str(), |
| 284 | error = e.to_string().as_str(); |
| 285 | "Error while migrating IP addresses to destination interface." |
| 286 | ); |
| 287 | |
| 288 | if let Err(restore_err) = src_interface.restore(handle).await { |
| 289 | error!( |
| 290 | src_interface = src_interface.name.as_str(), |
| 291 | error = restore_err.to_string().as_str(); |
| 292 | "Error while restoring IPs to source interface." |
| 293 | ); |
| 294 | } |
| 295 | |
| 296 | return Err(InterfaceMigrationError::InterfaceError(e)); |
| 297 | } |
| 298 | |
| 299 | info!( |
| 300 | src_interface = src_interface.name.as_str(), |
| 301 | dst_interface = self.name.as_str(); |
| 302 | "Successfully migrated IP addresseses to new interface." |
| 303 | ); |
| 304 | |
| 305 | if let Err(e) = self |
| 306 | .migrate_routes_from_interface(handle, src_interface) |
| 307 | .await |
| 308 | { |
| 309 | error!( |
| 310 | dst_interface = self.name.as_str(), |
| 311 | routes = format!("{:?}", src_interface.routes()).as_str(), |
| 312 | error = e.to_string().as_str(); |
| 313 | "Error while migrating routes to destination interface." |
| 314 | ); |
| 315 | |
| 316 | if let Err(restore_err) = src_interface.restore(handle).await { |
| 317 | error!( |
| 318 | src_interface = src_interface.name.as_str(), |
| 319 | routes = format!("{:?}", src_interface.routes()).as_str(), |
| 320 | error = restore_err.to_string().as_str(); |
| 321 | "Error while restoring source interface." |
| 322 | ); |
| 323 | } |
| 324 | |
| 325 | return Err(InterfaceMigrationError::InterfaceError(e)); |
| 326 | } |
| 327 | |
| 328 | Ok(()) |
| 329 | } |
| 330 | (true, false) => { |
| 331 | // Destination interface has IPs, source interface has no IPs |
| 332 | info!( |
| 333 | src_interface = src_interface.name.as_str(), |
| 334 | dst_interface = self.name.as_str(), |
| 335 | ip_addresses = format!("{:?}", self.addresses()).as_str(); |
| 336 | "Bridge already has IPs assigned. Skipping migration." |
| 337 | ); |
| 338 | |
| 339 | Ok(()) |
| 340 | } |
| 341 | (true, true) => { |
| 342 | // Neither interface has IPs |
| 343 | info!( |
| 344 | src_interface = src_interface.name.as_str(), |
| 345 | dst_interface = self.name.as_str(); |
| 346 | "Neither interface nor bridge have IPs assigned. Skipping migration." |
| 347 | ); |
| 348 | |
| 349 | Ok(()) |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | } |