@@ -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