Skip to content

Commit fc20038

Browse files
committed
Reject duplicate numbered BGP peer IPs
Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
1 parent f94b990 commit fc20038

1 file changed

Lines changed: 249 additions & 1 deletion

File tree

mgd/src/bgp_admin.rs

Lines changed: 249 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,18 @@ async fn do_bgp_apply(
12311231

12321232
// Validate originate prefixes before processing
12331233
validate_prefixes(&rq.originate)?;
1234+
helpers::ensure_unique_bgp_peers(
1235+
rq.peers
1236+
.values()
1237+
.flatten()
1238+
.map(|peer| PeerId::Ip(peer.host.ip()))
1239+
.chain(
1240+
rq.unnumbered_peers
1241+
.values()
1242+
.flatten()
1243+
.map(|peer| PeerId::Interface(peer.interface.clone())),
1244+
),
1245+
)?;
12341246

12351247
bgp_log!(log, info, "bgp apply: {rq:#?}";
12361248
"params" => format!("{rq:?}")
@@ -1293,6 +1305,40 @@ async fn do_bgp_apply(
12931305
}
12941306
}
12951307

1308+
let mut peer_ids: Vec<_> = ctx
1309+
.db
1310+
.get_bgp_neighbors()
1311+
.map_err(Error::Db)?
1312+
.into_iter()
1313+
.map(|nbr| (nbr.asn, PeerId::Ip(nbr.host.ip())))
1314+
.collect();
1315+
peer_ids.extend(
1316+
ctx.db
1317+
.get_unnumbered_bgp_neighbors()
1318+
.map_err(Error::Db)?
1319+
.into_iter()
1320+
.map(|nbr| (nbr.asn, PeerId::Interface(nbr.interface))),
1321+
);
1322+
1323+
// bgp_apply is the desired final state for this ASN. Existing peers under
1324+
// this ASN are replaced by the request below, so do not count them as
1325+
// duplicates of their requested updates.
1326+
peer_ids.retain(|(peer_asn, _)| *peer_asn != rq.asn);
1327+
helpers::ensure_unique_bgp_peers(
1328+
peer_ids.into_iter().map(|(_, peer)| peer).chain(
1329+
rq.peers
1330+
.values()
1331+
.flatten()
1332+
.map(|peer| PeerId::Ip(peer.host.ip()))
1333+
.chain(
1334+
rq.unnumbered_peers
1335+
.values()
1336+
.flatten()
1337+
.map(|peer| PeerId::Interface(peer.interface.clone())),
1338+
),
1339+
),
1340+
)?;
1341+
12961342
helpers::ensure_router(
12971343
ctx.clone(),
12981344
mg_api_types::bgp::config::Router {
@@ -1896,6 +1942,21 @@ pub(crate) mod helpers {
18961942
Ok(HttpResponseDeleted())
18971943
}
18981944

1945+
pub(crate) fn ensure_unique_bgp_peers(
1946+
peers: impl IntoIterator<Item = PeerId>,
1947+
) -> Result<(), Error> {
1948+
let mut seen = HashSet::new();
1949+
for peer in peers {
1950+
if !seen.insert(peer.clone()) {
1951+
return Err(Error::Conflict(format!(
1952+
"BGP peer {peer} is already configured",
1953+
)));
1954+
}
1955+
}
1956+
1957+
Ok(())
1958+
}
1959+
18991960
pub(crate) fn add_neighbor_v1(
19001961
ctx: Arc<HandlerContext>,
19011962
rq: v1::bgp::config::Neighbor,
@@ -1906,6 +1967,21 @@ pub(crate) mod helpers {
19061967
"params" => format!("{rq:#?}")
19071968
);
19081969

1970+
let peer = PeerId::Ip(rq.host.ip());
1971+
let mut peers: Vec<_> = ctx
1972+
.db
1973+
.get_bgp_neighbors()?
1974+
.into_iter()
1975+
.map(|nbr| (nbr.asn, PeerId::Ip(nbr.host.ip())))
1976+
.collect();
1977+
if ensure {
1978+
peers.retain(|(asn, existing)| {
1979+
!(*asn == rq.asn && existing == &peer)
1980+
});
1981+
}
1982+
peers.push((rq.asn, peer));
1983+
ensure_unique_bgp_peers(peers.into_iter().map(|(_, peer)| peer))?;
1984+
19091985
let (event_tx, event_rx) = channel();
19101986

19111987
// V1 API is IPv4-only; extract only IPv4 policies
@@ -2003,6 +2079,21 @@ pub(crate) mod helpers {
20032079
rq.validate_address_families()
20042080
.map_err(Error::InvalidRequest)?;
20052081

2082+
let peer = PeerId::Ip(rq.host.ip());
2083+
let mut peers: Vec<_> = ctx
2084+
.db
2085+
.get_bgp_neighbors()?
2086+
.into_iter()
2087+
.map(|nbr| (nbr.asn, PeerId::Ip(nbr.host.ip())))
2088+
.collect();
2089+
if ensure {
2090+
peers.retain(|(asn, existing)| {
2091+
!(*asn == rq.asn && existing == &peer)
2092+
});
2093+
}
2094+
peers.push((rq.asn, peer));
2095+
ensure_unique_bgp_peers(peers.into_iter().map(|(_, peer)| peer))?;
2096+
20062097
let (event_tx, event_rx) = channel();
20072098

20082099
let info = SessionInfo::from(&rq.parameters);
@@ -2117,6 +2208,21 @@ pub(crate) mod helpers {
21172208
rq.validate_address_families()
21182209
.map_err(Error::InvalidRequest)?;
21192210

2211+
let peer = PeerId::Interface(rq.interface.clone());
2212+
let mut peers: Vec<_> = ctx
2213+
.db
2214+
.get_unnumbered_bgp_neighbors()?
2215+
.into_iter()
2216+
.map(|nbr| (nbr.asn, PeerId::Interface(nbr.interface)))
2217+
.collect();
2218+
if ensure {
2219+
peers.retain(|(asn, existing)| {
2220+
!(*asn == rq.asn && existing == &peer)
2221+
});
2222+
}
2223+
peers.push((rq.asn, peer));
2224+
ensure_unique_bgp_peers(peers.into_iter().map(|(_, peer)| peer))?;
2225+
21202226
let (event_tx, event_rx) = channel();
21212227
let info = SessionInfo::from(&rq.parameters);
21222228

@@ -2606,7 +2712,9 @@ mod tests {
26062712
use client_common::println_nopipe;
26072713
use mg_api_types::bgp::config::{
26082714
ApplyRequest, BgpPeerConfig, BgpPeerParameters, Ipv4UnicastConfig,
2609-
Ipv6UnicastConfig, UnnumberedBgpPeerConfig,
2715+
Ipv6UnicastConfig, Neighbor as BgpNeighborConfig,
2716+
Router as BgpRouterConfig, UnnumberedBgpPeerConfig,
2717+
UnnumberedNeighbor as BgpUnnumberedNeighborConfig,
26102718
};
26112719
use mg_api_types::bgp::peer::PeerId;
26122720
use mg_api_types::bgp::policy::{ImportExportPolicy4, ImportExportPolicy6};
@@ -2742,6 +2850,15 @@ fn update(message, asn, addr) {
27422850
apply_req(asn, HashMap::default(), HashMap::default())
27432851
}
27442852

2853+
fn router(asn: u32) -> BgpRouterConfig {
2854+
BgpRouterConfig {
2855+
asn,
2856+
id: asn,
2857+
listen: super::DEFAULT_BGP_LISTEN.to_string(),
2858+
graceful_shutdown: false,
2859+
}
2860+
}
2861+
27452862
/// Apply request carrying both a numbered and an unnumbered peer under
27462863
/// `asn`, so a single test exercises both code paths at once.
27472864
fn mixed_req(asn: u32, hold_time: u64) -> ApplyRequest {
@@ -2950,6 +3067,137 @@ fn update(message, asn, addr) {
29503067
assert!(routers.contains_key(&456));
29513068
}
29523069

3070+
#[tokio::test]
3071+
async fn apply_updates_bgp_peers() {
3072+
let ctx = test_ctx("apply_updates_bgp_peers");
3073+
3074+
do_bgp_apply(&ctx, mixed_req(123, 6))
3075+
.await
3076+
.expect("apply initial peers");
3077+
assert_single_neighbor_state(&ctx, 123, 6);
3078+
3079+
do_bgp_apply(&ctx, mixed_req(123, 10))
3080+
.await
3081+
.expect("update existing peers");
3082+
assert_single_neighbor_state(&ctx, 123, 10);
3083+
}
3084+
3085+
#[tokio::test]
3086+
async fn duplicate_bgp_peer_is_rejected() {
3087+
let ctx = test_ctx("duplicate_bgp_peer_is_rejected");
3088+
let numbered_peer = numbered("203.0.113.1", "bob", 6);
3089+
let unnumbered_peer = unnumbered("tfportqsfp1_0", "alice", 6);
3090+
3091+
super::helpers::ensure_router(ctx.clone(), router(123))
3092+
.await
3093+
.expect("create asn 123");
3094+
super::helpers::ensure_router(ctx.clone(), router(456))
3095+
.await
3096+
.expect("create asn 456");
3097+
3098+
super::helpers::add_neighbor(
3099+
ctx.clone(),
3100+
BgpNeighborConfig::from_bgp_peer_config(
3101+
123,
3102+
"qsfp0".into(),
3103+
numbered_peer.clone(),
3104+
),
3105+
false,
3106+
)
3107+
.expect("add first numbered peer");
3108+
super::helpers::add_unnumbered_neighbor(
3109+
ctx.clone(),
3110+
BgpUnnumberedNeighborConfig::from_bgp_peer_config(
3111+
123,
3112+
"qsfp1".into(),
3113+
unnumbered_peer.clone(),
3114+
),
3115+
false,
3116+
)
3117+
.expect("add first unnumbered peer");
3118+
3119+
let err = super::helpers::add_neighbor(
3120+
ctx.clone(),
3121+
BgpNeighborConfig::from_bgp_peer_config(
3122+
456,
3123+
"qsfp1".into(),
3124+
numbered_peer.clone(),
3125+
),
3126+
false,
3127+
)
3128+
.expect_err("duplicate peer IP should fail");
3129+
assert!(matches!(err, crate::error::Error::Conflict(_)));
3130+
let err = super::helpers::add_unnumbered_neighbor(
3131+
ctx.clone(),
3132+
BgpUnnumberedNeighborConfig::from_bgp_peer_config(
3133+
456,
3134+
"qsfp2".into(),
3135+
unnumbered_peer.clone(),
3136+
),
3137+
false,
3138+
)
3139+
.expect_err("duplicate unnumbered peer should fail");
3140+
assert!(matches!(err, crate::error::Error::Conflict(_)));
3141+
3142+
assert_neighbor_counts(&ctx, 1, 1);
3143+
assert_eq!(
3144+
ctx.db.get_bgp_neighbors().expect("get bgp neighbors")[0].asn,
3145+
123,
3146+
);
3147+
assert_eq!(
3148+
ctx.db
3149+
.get_unnumbered_bgp_neighbors()
3150+
.expect("get unnumbered bgp neighbors")[0]
3151+
.asn,
3152+
123,
3153+
);
3154+
3155+
let result = do_bgp_apply(
3156+
&ctx,
3157+
apply_req(
3158+
789,
3159+
HashMap::from([
3160+
("qsfp0".into(), vec![numbered_peer.clone()]),
3161+
("qsfp1".into(), vec![numbered_peer]),
3162+
]),
3163+
HashMap::default(),
3164+
),
3165+
)
3166+
.await;
3167+
assert!(
3168+
result.is_err(),
3169+
"duplicate peer IP in apply request should fail",
3170+
);
3171+
let result = do_bgp_apply(
3172+
&ctx,
3173+
apply_req(
3174+
789,
3175+
HashMap::default(),
3176+
HashMap::from([
3177+
("qsfp0".into(), vec![unnumbered_peer.clone()]),
3178+
("qsfp1".into(), vec![unnumbered_peer]),
3179+
]),
3180+
),
3181+
)
3182+
.await;
3183+
assert!(
3184+
result.is_err(),
3185+
"duplicate unnumbered peer in apply request should fail",
3186+
);
3187+
assert_neighbor_counts(&ctx, 1, 1);
3188+
assert_eq!(
3189+
ctx.db.get_bgp_neighbors().expect("get bgp neighbors")[0].asn,
3190+
123,
3191+
);
3192+
assert_eq!(
3193+
ctx.db
3194+
.get_unnumbered_bgp_neighbors()
3195+
.expect("get unnumbered bgp neighbors")[0]
3196+
.asn,
3197+
123,
3198+
);
3199+
}
3200+
29533201
/// Regression test for https://github.com/oxidecomputer/maghemite/issues/772
29543202
///
29553203
/// Applying a config with empty peer maps for an ASN that previously had

0 commit comments

Comments
 (0)