Skip to content

Commit f2e1121

Browse files
authored
Start unifying un/numbered bgp peer internals (#790)
* Add mgd-unnumbered falcon-lab topology and test Add simple p2p toplogy with two mgd nodes running BGP unnumbered. This test captures the issue where outbound connect() attempts fail due to an invalid destination TCP port being used (0), as observed in maghemite#782. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * mgd: use BGP_PORT in unnumbered dummy sockaddr Fixes: #782 Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * Convert BGP_PORT from u16 to NonZeroU16 Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * Unify internals for un/numbered peers Instead of using a placeholder SocketAddr to represent the (peer, port), use a PeerId and a NonZeruU16. This removes the need for unnumbered peers to store a placeholder SocketAddr (which was only disambiguated by scope_id anyway), since we have a proper way to represent them. This provides the basis for further consolidation of un/numbered peers: - merge new_session/new_unnumbered_session and ensure_session/ensure_unnumbered_session into single functions taking unnumbered_manager: Option<...>; drop the redundant peer_id arg from new_session_locked - fold update_unnumbered_session into update_session - widen Error::UnknownPeer from IpAddr to PeerId - route reset_unnumbered_neighbor through the unified get_session, deleting UnnumberedManagerNdp::get_neighbor_session and its now-dead routers table Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * bgp: fix PeerId variant in unnumbered tests Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * bgp: bind unnumbered peers to ifindex We had been binding unnumbered interfaces to a source SocketAddr with the scope_id set, but this is not always sufficient for socket lookups to ensure link-local traffic is delivered consistently (See #780). This adds a setsockopt call to bind the socket to the unnumbered interface (via a socket2 wrapper). There haven't been any issues observed/reported with BgpConnection sockets so far, but it's better to be proactive. Also refactors try_resolve_connect_addr to centralize logging in the caller and give better context around the resolution failure by returning a Result with a new Error type ResolvePeerError. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * buildomat: run mgd-unnumbered falcon-lab test Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * falcon-lab: add juniper, promote trio to quartet Add support for JunOS in falcon-lab, integrating it into existing BFD and BGP unnumbered tests (renaming trio to quartet). Adds a JuniperNode type with full diagnostic collection. JunOS images are expected to be prebuilt (this one was build using the experimental voxel-image process) with a handful of prerequisites: 1) installed packages: docker, jq 2) crpd docker container installed 3) systemd service to manage creation of cargo-bay mount 4) systemd service to apply license and config to crpd This allows falcon-lab to use cargo-bay as a transparent mount point so it doesn't have to be in charge of managing its items. In particular, the JunOS license must be staged there but falcon-lab is not responsible for populating it. This is done either by the user or by CI. Also refactors the linux diagnostics to be centralized so any other node riding atop a linux base can make use of them (e.g. frr, eos, juniper). Also updates the diagnostic collection logic to unpause nodes before trying to grab diag info... can't really query a paused node, can you? Also adds a README for falcon-lab. Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * falcon-lab: cleanup old state, per protocol diag Adds logic to cleanup old guest configs on startup. Adds logic to CI to call cleanup on test topologies after each run. Splits diag collection by protocol (e.g. BGP/BFD info only collected in tests when those protocols are in use). Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> * PR feedback Signed-off-by: Trey Aspelund <trey@oxidecomputer.com> --------- Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
1 parent 3b64226 commit f2e1121

20 files changed

Lines changed: 1654 additions & 460 deletions

.github/buildomat/jobs/falcon-lab.sh

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ mkdir -p cargo-bay
5454
mv mgd cargo-bay/
5555
mv ddmd cargo-bay/
5656

57+
# Juniper/cRPD images require a runtime license. Fetch it on the CI runner,
58+
# which has catacomb access, and pass it to the guest by file via cargo-bay.
59+
# The license contents must never be printed or committed.
60+
curl -sSfL --retry 10 --retry-all-errors \
61+
-o cargo-bay/falcon-juniper-license.key \
62+
http://catacomb.eng.oxide.computer:12346/falcon/jl
63+
chmod 0600 cargo-bay/falcon-juniper-license.key
64+
5765
export EXT_INTERFACE=${EXT_INTERFACE:-igb0}
5866

5967
first=$(bmat address ls -f extra -Ho first)
@@ -62,8 +70,15 @@ gw=$(bmat address ls -f extra -Ho gateway)
6270
server=$(ipadm show-addr "${EXT_INTERFACE}"/dhcp -po ADDR | sed 's#/.*##g')
6371
pfexec ./dhcp-server "${first}" "${last}" "${gw}" "${server}" &> /work/dhcp-server.log &
6472

65-
RUST_LOG=debug pfexec ./falcon-lab run \
66-
trio-unnumbered
73+
run_test() {
74+
local test=$1
75+
local status=0
76+
77+
RUST_LOG=debug pfexec ./falcon-lab run "${test}" || status=$?
78+
pfexec ./falcon-lab cleanup "${test}" || true
79+
return "${status}"
80+
}
6781

68-
RUST_LOG=debug pfexec ./falcon-lab run \
69-
trio-bfd-static-routing
82+
run_test mgd-unnumbered
83+
run_test quartet-unnumbered
84+
run_test quartet-bfd-static-routing

bgp/src/config.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5+
use crate::BGP_PORT;
56
use mg_api_types::bgp::config::{
67
BgpPeerParameters, Neighbor, UnnumberedNeighbor,
78
};
9+
use mg_api_types::bgp::peer::PeerId;
810
use mg_api_types_versions::v1;
911
use rdb::Asn;
1012
use schemars::JsonSchema;
1113
use serde::{Deserialize, Serialize};
12-
use std::net::{SocketAddr, SocketAddrV6};
14+
use std::num::NonZeroU16;
1315

1416
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1517
pub struct PeerConfig {
1618
pub name: String,
1719
pub group: String,
18-
pub host: SocketAddr,
20+
pub id: PeerId,
21+
pub port: NonZeroU16,
1922
pub hold_time: u64,
2023
pub idle_hold_time: u64,
2124
pub delay_open: u64,
@@ -77,7 +80,8 @@ impl From<Neighbor> for PeerConfig {
7780
Self {
7881
name,
7982
group,
80-
host: *host,
83+
id: PeerId::Ip(host.ip()),
84+
port: NonZeroU16::new(host.port()).unwrap_or(BGP_PORT),
8185
hold_time,
8286
idle_hold_time,
8387
delay_open,
@@ -119,7 +123,8 @@ impl From<v1::bgp::config::Neighbor> for PeerConfig {
119123
Self {
120124
name,
121125
group,
122-
host,
126+
id: PeerId::Ip(host.ip()),
127+
port: NonZeroU16::new(host.port()).unwrap_or(BGP_PORT),
123128
hold_time,
124129
idle_hold_time,
125130
delay_open,
@@ -131,17 +136,15 @@ impl From<v1::bgp::config::Neighbor> for PeerConfig {
131136
}
132137

133138
impl PeerConfig {
134-
/// Construct a `PeerConfig` from an `UnnumberedNeighbor` (uses the supplied
135-
/// IPv6 link-local socket address as the connection target).
136-
pub fn from_unnumbered_neighbor(
137-
n: &UnnumberedNeighbor,
138-
addr: SocketAddrV6,
139-
) -> Self {
139+
/// Construct a `PeerConfig` from an `UnnumberedNeighbor`. The peer is
140+
/// identified by its interface; the connection target is resolved via NDP
141+
/// at connect time, and the port is always the well-known BGP port.
142+
pub fn from_unnumbered_neighbor(n: &UnnumberedNeighbor) -> Self {
140143
let UnnumberedNeighbor {
141144
asn: _,
142145
name,
143146
group,
144-
interface: _,
147+
interface,
145148
act_as_a_default_ipv6_router: _,
146149
parameters,
147150
} = n.clone();
@@ -171,8 +174,9 @@ impl PeerConfig {
171174
} = parameters;
172175
Self {
173176
name,
174-
host: addr.into(),
175177
group,
178+
id: PeerId::Interface(interface),
179+
port: BGP_PORT,
176180
hold_time,
177181
idle_hold_time,
178182
delay_open,

bgp/src/connection_channel.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ impl BgpListener<BgpConnectionChannel> for BgpListenerChannel {
256256
let runner = lock!(sessions)
257257
.get(&key)
258258
.cloned()
259-
.ok_or(Error::UnknownPeer(peer.ip()))?;
259+
.ok_or_else(|| Error::UnknownPeer(key.clone()))?;
260260

261261
let config = lock!(runner.session);
262262
Ok(BgpConnectionChannel::with_conn(

bgp/src/connection_tcp.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use std::{
3030
io::Read,
3131
io::Write,
3232
net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs},
33+
num::NonZeroU32,
3334
sync::atomic::AtomicBool,
3435
sync::{Arc, Mutex, atomic::Ordering, mpsc::Sender},
3536
thread::{JoinHandle, sleep},
@@ -172,7 +173,7 @@ impl BgpListener<BgpConnectionTcp> for BgpListenerTcp {
172173
let runner = lock!(sessions)
173174
.get(&key)
174175
.cloned()
175-
.ok_or(Error::UnknownPeer(ip))?;
176+
.ok_or_else(|| Error::UnknownPeer(key.clone()))?;
176177

177178
let config = lock!(runner.session);
178179
return BgpConnectionTcp::with_conn(
@@ -316,6 +317,22 @@ impl BgpConnector<BgpConnectionTcp> for BgpConnectorTcp {
316317
"timeout" => timeout.as_millis()
317318
);
318319

320+
// Bind the socket to the destination's interface for scoped
321+
// (link-local / unnumbered) peers.
322+
if let SocketAddr::V6(v6) = peer
323+
&& let Some(idx) = NonZeroU32::new(v6.scope_id())
324+
&& let Err(e) = s.bind_device_by_index_v6(Some(idx))
325+
{
326+
connection_log_lite!(log,
327+
warn,
328+
"failed to bind device index {idx} for {peer}: {e}";
329+
"direction" => ConnectionDirection::Outbound,
330+
"peer" => format!("{peer}"),
331+
"error" => format!("{e}")
332+
);
333+
return;
334+
}
335+
319336
// Bind to source address/port if specified
320337
if let Some(src) = config.bind_addr {
321338
let ba: socket2::SockAddr = src.into();
@@ -1382,7 +1399,7 @@ fn get_md5_source_addrs(peer_ip: IpAddr) -> Result<Vec<SocketAddr>, Error> {
13821399

13831400
Ok(sources
13841401
.iter()
1385-
.map(|x| SocketAddr::new(*x, crate::BGP_PORT))
1402+
.map(|x| SocketAddr::new(*x, crate::BGP_PORT.get()))
13861403
.collect())
13871404
}
13881405

@@ -1520,7 +1537,7 @@ fn setup_outbound_md5(
15201537

15211538
let local: Vec<SocketAddr> = sources
15221539
.iter()
1523-
.map(|x| SocketAddr::new(*x, crate::BGP_PORT))
1540+
.map(|x| SocketAddr::new(*x, crate::BGP_PORT.get()))
15241541
.collect();
15251542

15261543
init_md5_associations(fd, key, local.clone(), peer)?;

bgp/src/error.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
net::{IpAddr, Ipv4Addr},
88
};
99

10+
use mg_api_types::bgp::peer::PeerId;
1011
use num_enum::TryFromPrimitiveError;
1112

1213
#[derive(thiserror::Error, Debug)]
@@ -122,7 +123,7 @@ pub enum Error {
122123
NotConnected,
123124

124125
#[error("Connection attempt from unknown peer: {0}")]
125-
UnknownPeer(IpAddr),
126+
UnknownPeer(PeerId),
126127

127128
#[error("Session for peer already exists")]
128129
PeerExists,

bgp/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub mod router;
1616
pub mod session;
1717
pub mod unnumbered;
1818

19+
use std::num::NonZeroU16;
20+
1921
mod rhai_integration;
2022

2123
#[cfg(test)]
@@ -34,7 +36,7 @@ pub mod connection_channel;
3436
#[cfg(test)]
3537
pub mod unnumbered_mock;
3638

37-
pub const BGP_PORT: u16 = 179;
39+
pub const BGP_PORT: NonZeroU16 = NonZeroU16::new(179).unwrap();
3840
pub const BGP_VERSION: u8 = 4;
3941
pub const COMPONENT_BGP: &str = "bgp";
4042
pub const MOD_ROUTER: &str = "router";

bgp/src/router.rs

Lines changed: 12 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -305,104 +305,57 @@ impl<Cnx: BgpConnection + 'static> Router<Cnx> {
305305
}
306306
}
307307

308+
#[allow(clippy::too_many_arguments)]
308309
pub fn ensure_session(
309310
self: &Arc<Self>,
310311
peer: PeerConfig,
311312
bind_addr: Option<SocketAddr>,
312313
event_tx: Sender<FsmEvent<Cnx>>,
313314
event_rx: Receiver<FsmEvent<Cnx>>,
314315
info: SessionInfo,
316+
unnumbered_manager: Option<Arc<dyn UnnumberedManager>>,
315317
) -> Result<EnsureSessionResult<Cnx>, Error> {
316318
let sessions = lock!(self.sessions);
317-
let key = PeerId::Ip(peer.host.ip());
318-
if sessions.contains_key(&key) {
319+
if sessions.contains_key(&peer.id) {
319320
drop(sessions);
320321
Ok(EnsureSessionResult::Updated(
321322
self.update_session(peer, info)?,
322323
))
323-
} else {
324-
Ok(EnsureSessionResult::New(self.new_session_locked(
325-
sessions, key, peer, bind_addr, event_tx, event_rx, info, None,
326-
)?))
327-
}
328-
}
329-
330-
#[allow(clippy::too_many_arguments)]
331-
pub fn ensure_unnumbered_session(
332-
self: &Arc<Self>,
333-
interface: String,
334-
peer: PeerConfig,
335-
bind_addr: Option<SocketAddr>,
336-
event_tx: Sender<FsmEvent<Cnx>>,
337-
event_rx: Receiver<FsmEvent<Cnx>>,
338-
info: SessionInfo,
339-
unnumbered_manager: Arc<dyn UnnumberedManager>,
340-
) -> Result<EnsureSessionResult<Cnx>, Error> {
341-
let sessions = lock!(self.sessions);
342-
let key = PeerId::Interface(interface.clone());
343-
if sessions.contains_key(&key) {
344-
drop(sessions);
345-
Ok(EnsureSessionResult::Updated(
346-
self.update_unnumbered_session(&interface, peer, info)?,
347-
))
348324
} else {
349325
Ok(EnsureSessionResult::New(self.new_session_locked(
350326
sessions,
351-
key,
352327
peer,
353328
bind_addr,
354329
event_tx,
355330
event_rx,
356331
info,
357-
Some(unnumbered_manager),
332+
unnumbered_manager,
358333
)?))
359334
}
360335
}
361336

362-
pub fn new_session(
363-
self: &Arc<Self>,
364-
peer: PeerConfig,
365-
bind_addr: Option<SocketAddr>,
366-
event_tx: Sender<FsmEvent<Cnx>>,
367-
event_rx: Receiver<FsmEvent<Cnx>>,
368-
info: SessionInfo,
369-
) -> Result<Arc<SessionRunner<Cnx>>, Error> {
370-
let sessions = lock!(self.sessions);
371-
let key = PeerId::Ip(peer.host.ip());
372-
if sessions.contains_key(&key) {
373-
Err(Error::PeerExists)
374-
} else {
375-
self.new_session_locked(
376-
sessions, key, peer, bind_addr, event_tx, event_rx, info, None,
377-
)
378-
}
379-
}
380-
381337
#[allow(clippy::too_many_arguments)]
382-
pub fn new_unnumbered_session(
338+
pub fn new_session(
383339
self: &Arc<Self>,
384-
interface: String,
385340
peer: PeerConfig,
386341
bind_addr: Option<SocketAddr>,
387342
event_tx: Sender<FsmEvent<Cnx>>,
388343
event_rx: Receiver<FsmEvent<Cnx>>,
389344
info: SessionInfo,
390-
unnumbered_manager: Arc<dyn UnnumberedManager>,
345+
unnumbered_manager: Option<Arc<dyn UnnumberedManager>>,
391346
) -> Result<Arc<SessionRunner<Cnx>>, Error> {
392347
let sessions = lock!(self.sessions);
393-
let key = PeerId::Interface(interface);
394-
if sessions.contains_key(&key) {
348+
if sessions.contains_key(&peer.id) {
395349
Err(Error::PeerExists)
396350
} else {
397351
self.new_session_locked(
398352
sessions,
399-
key,
400353
peer,
401354
bind_addr,
402355
event_tx,
403356
event_rx,
404357
info,
405-
Some(unnumbered_manager),
358+
unnumbered_manager,
406359
)
407360
}
408361
}
@@ -411,7 +364,6 @@ impl<Cnx: BgpConnection + 'static> Router<Cnx> {
411364
fn new_session_locked(
412365
self: &Arc<Self>,
413366
mut sessions: MutexGuard<SessionMap<Cnx>>,
414-
peer_id: PeerId,
415367
peer: PeerConfig,
416368
bind_addr: Option<SocketAddr>,
417369
event_tx: Sender<FsmEvent<Cnx>>,
@@ -434,8 +386,8 @@ impl<Cnx: BgpConnection + 'static> Router<Cnx> {
434386
let neighbor = NeighborInfo {
435387
name: Arc::new(Mutex::new(peer.name.clone())),
436388
peer_group: peer.group.clone(),
437-
peer: peer_id,
438-
port: peer.host.port(),
389+
peer: peer.id.clone(),
390+
port: peer.port,
439391
};
440392

441393
let runner = Arc::new(SessionRunner::new(
@@ -460,33 +412,9 @@ impl<Cnx: BgpConnection + 'static> Router<Cnx> {
460412
peer: PeerConfig,
461413
info: SessionInfo,
462414
) -> Result<Arc<SessionRunner<Cnx>>, Error> {
463-
// Use PeerId::Ip for numbered sessions
464-
let key = PeerId::Ip(peer.host.ip());
465-
let session = match lock!(self.sessions).get(&key) {
466-
None => return Err(Error::UnknownPeer(peer.host.ip())),
467-
Some(s) => s.clone(),
468-
};
469-
470-
session.update_session_parameters(peer, info)?;
471-
472-
Ok(session)
473-
}
474-
475-
pub fn update_unnumbered_session(
476-
self: &Arc<Self>,
477-
interface: &str,
478-
peer: PeerConfig,
479-
info: SessionInfo,
480-
) -> Result<Arc<SessionRunner<Cnx>>, Error> {
481-
// Use PeerId::Interface for unnumbered sessions
482-
let key = PeerId::Interface(interface.to_string());
415+
let key = peer.id.clone();
483416
let session = match lock!(self.sessions).get(&key) {
484-
None => {
485-
return Err(Error::InternalCommunication(format!(
486-
"unnumbered session not found for interface: {}",
487-
interface
488-
)));
489-
}
417+
None => return Err(Error::UnknownPeer(key)),
490418
Some(s) => s.clone(),
491419
};
492420

0 commit comments

Comments
 (0)