Skip to content

Commit e0c25b6

Browse files
committed
More fixes for bugs found by AI review
- Validate inbound unnumbered BGP peers against the NDP-discovered neighbor before resolving them to an interface session. - Drop unknown inbound BGP peers in the dispatcher accept loop instead of treating them as listener errors and rebinding. - Fix multicast Router Solicitation checksum generation to use the all-routers destination (ff02::2) in the ICMPv6 pseudo-header. - Add regression tests for unnumbered inbound peer resolution: - discovered link-local peer uses the interface key - undiscovered/mismatched/inactive/error cases fall back to IP key - Add NDP checksum regression tests for RS all-routers multicast and RA all-nodes multicast behavior. - Delete unnumbered neighbor DB state before tearing down runtime BGP/NDP state, matching numbered neighbor deletion ordering. - Report unnumbered peer expiry using the same reachable-time semantics as runtime neighbor usability, rather than router-lifetime. Fixes: #802 Signed-off-by: Trey Aspelund <trey@oxidecomputer.com>
1 parent bb4d604 commit e0c25b6

5 files changed

Lines changed: 137 additions & 36 deletions

File tree

bgp/src/connection.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
router::SessionMap,
1010
session::{FsmEvent, PeerId, SessionInfo, SessionRunner},
1111
};
12-
use slog::{Logger, error};
12+
use slog::{Logger, debug, error};
1313
use std::{
1414
net::{SocketAddr, ToSocketAddrs},
1515
sync::{Arc, Mutex, mpsc::Sender},
@@ -52,7 +52,35 @@ pub fn resolve_session_key(
5252
&& v6_addr.ip().is_unicast_link_local()
5353
{
5454
match mgr.get_active_interface_by_scope(v6_addr.scope_id()) {
55-
Ok(Some(interface)) => return PeerId::Interface(interface),
55+
Ok(Some(interface)) => {
56+
match mgr.get_discovered_ndp_neighbor(&interface) {
57+
Ok(Some(neighbor)) if neighbor.addr == *v6_addr.ip() => {
58+
return PeerId::Interface(interface);
59+
}
60+
Ok(Some(neighbor)) => {
61+
debug!(log,
62+
"incoming unnumbered peer does not match discovered NDP neighbor";
63+
"interface" => interface.as_str(),
64+
"peer" => peer_addr,
65+
"discovered" => neighbor.addr.to_string(),
66+
);
67+
}
68+
Ok(None) => {
69+
debug!(log,
70+
"incoming unnumbered peer has no discovered NDP neighbor";
71+
"interface" => interface.as_str(),
72+
"peer" => peer_addr,
73+
);
74+
}
75+
Err(e) => {
76+
error!(log,
77+
"discovered unnumbered neighbor query error: {e}";
78+
"interface" => interface.as_str(),
79+
"error" => format!("{e}"),
80+
);
81+
}
82+
}
83+
}
5684
// Fallback to IP-based key on failure/missing interface
5785
Ok(None) => {}
5886
Err(e) => {

bgp/src/dispatcher.rs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use crate::{
66
IO_TIMEOUT,
77
connection::{BgpConnection, BgpListener},
8+
error::Error,
89
router::SessionMap,
910
session::{FsmEvent, SessionEvent},
1011
};
@@ -123,7 +124,14 @@ impl<Cnx: BgpConnection + 'static> Dispatcher<Cnx> {
123124
);
124125
accepted
125126
}
126-
Err(crate::error::Error::Timeout) => {
127+
Err(Error::Timeout) => {
128+
continue 'accept;
129+
}
130+
Err(Error::UnknownPeer(peer)) => {
131+
debug!(log,
132+
"no session found for peer, dropping connection";
133+
"peer" => peer.to_string(),
134+
);
127135
continue 'accept;
128136
}
129137
Err(e) => {
@@ -193,6 +201,7 @@ mod tests {
193201

194202
struct TestUnnumbered {
195203
scope_result: Result<Option<String>, UnnumberedError>,
204+
neighbor_result: Result<Option<NdpNeighbor>, UnnumberedError>,
196205
}
197206

198207
impl BgpUnnumbered for TestUnnumbered {
@@ -207,7 +216,7 @@ mod tests {
207216
&self,
208217
_interface: &str,
209218
) -> Result<Option<NdpNeighbor>, UnnumberedError> {
210-
Ok(None)
219+
self.neighbor_result.clone()
211220
}
212221
}
213222

@@ -218,12 +227,31 @@ mod tests {
218227
fn unnumbered_manager(
219228
scope_result: Result<Option<String>, UnnumberedError>,
220229
) -> Arc<dyn BgpUnnumbered> {
221-
Arc::new(TestUnnumbered { scope_result })
230+
Arc::new(TestUnnumbered {
231+
scope_result,
232+
neighbor_result: Ok(None),
233+
})
234+
}
235+
236+
fn unnumbered_manager_with_neighbor(
237+
scope_result: Result<Option<String>, UnnumberedError>,
238+
neighbor_result: Result<Option<NdpNeighbor>, UnnumberedError>,
239+
) -> Arc<dyn BgpUnnumbered> {
240+
Arc::new(TestUnnumbered {
241+
scope_result,
242+
neighbor_result,
243+
})
222244
}
223245

224246
#[test]
225-
fn link_local_scope_resolves_to_interface_session_key() {
226-
let manager = unnumbered_manager(Ok(Some("eth0".into())));
247+
fn discovered_link_local_uses_interface_key() {
248+
let manager = unnumbered_manager_with_neighbor(
249+
Ok(Some("eth0".into())),
250+
Ok(Some(NdpNeighbor {
251+
addr: "fe80::1".parse().unwrap(),
252+
scope_id: 7,
253+
})),
254+
);
227255
let peer = "[fe80::1%7]:179".parse().unwrap();
228256

229257
let key = resolve_session_key(peer, Some(&manager), &log());
@@ -232,7 +260,33 @@ mod tests {
232260
}
233261

234262
#[test]
235-
fn link_local_without_active_scope_falls_back_to_ip_session_key() {
263+
fn undiscovered_link_local_uses_ip_key() {
264+
let manager = unnumbered_manager(Ok(Some("eth0".into())));
265+
let peer = "[fe80::1%7]:179".parse().unwrap();
266+
267+
let key = resolve_session_key(peer, Some(&manager), &log());
268+
269+
assert_eq!(key, PeerId::Ip(peer.ip()));
270+
}
271+
272+
#[test]
273+
fn mismatched_link_local_uses_ip_key() {
274+
let manager = unnumbered_manager_with_neighbor(
275+
Ok(Some("eth0".into())),
276+
Ok(Some(NdpNeighbor {
277+
addr: "fe80::2".parse().unwrap(),
278+
scope_id: 7,
279+
})),
280+
);
281+
let peer = "[fe80::1%7]:179".parse().unwrap();
282+
283+
let key = resolve_session_key(peer, Some(&manager), &log());
284+
285+
assert_eq!(key, PeerId::Ip(peer.ip()));
286+
}
287+
288+
#[test]
289+
fn inactive_link_local_scope_uses_ip_key() {
236290
let manager = unnumbered_manager(Ok(None));
237291
let peer = "[fe80::1%7]:179".parse().unwrap();
238292

@@ -242,7 +296,7 @@ mod tests {
242296
}
243297

244298
#[test]
245-
fn link_local_resolution_error_falls_back_to_ip_session_key() {
299+
fn link_local_lookup_error_uses_ip_key() {
246300
let manager =
247301
unnumbered_manager(Err(UnnumberedError::ResolutionFailed {
248302
interface: "eth0".into(),

mgd/src/bgp_admin.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -544,9 +544,13 @@ fn convert_discovered_router_to_api(
544544
let discovered_at = instant_to_iso8601(state.first_seen);
545545
let last_advertisement = instant_to_iso8601(state.when);
546546

547-
// Calculate time until expiry
548-
let effective_lifetime =
549-
Duration::from_secs(u64::from(state.router_lifetime));
547+
// Keep this in sync with ndp::ReceivedRouterAdvertisement::expired(): BGP
548+
// treats reachability, not router-lifetime, as the peer usability timer.
549+
let effective_lifetime = if state.reachable_time == 0 {
550+
Duration::from_secs(10)
551+
} else {
552+
Duration::from_millis(u64::from(state.reachable_time))
553+
};
550554
let time_until_expiry = if state.expired {
551555
// Calculate time since expiry
552556
let time_since_expiry = elapsed_since_when
@@ -1928,6 +1932,12 @@ pub(crate) mod helpers {
19281932
let peer = PeerId::Interface(interface.to_string());
19291933
ctx.db.remove_bgp_prefixes_from_peer(&peer);
19301934

1935+
// Match numbered neighbor deletion: remove persistent intent before
1936+
// tearing down runtime state. If the DB removal fails, the live
1937+
// session/NDP runtime remain consistent with the still-configured peer.
1938+
ctx.db
1939+
.remove_unnumbered_bgp_neighbor(asn.into(), interface)?;
1940+
19311941
// Delete the BGP session for this unnumbered neighbor.
19321942
// Unnumbered sessions are keyed by interface name, not IP address.
19331943
get_router!(&ctx, asn)?.delete_session(peer);
@@ -1937,10 +1947,6 @@ pub(crate) mod helpers {
19371947
.unnumbered_manager
19381948
.unconfigure_interface(interface)?;
19391949

1940-
// And now clear out the top level database entry
1941-
ctx.db
1942-
.remove_unnumbered_bgp_neighbor(asn.into(), interface)?;
1943-
19441950
Ok(HttpResponseDeleted())
19451951
}
19461952

ndp/src/manager.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,33 @@ impl RouterDiscoveryState {
347347
#[cfg(test)]
348348
mod test {
349349
use crate::packet::{Icmp6RouterAdvertisement, Icmp6RouterSolicitation};
350+
use crate::util::{ALL_NODES_MCAST, ALL_ROUTERS_MCAST, cksum};
351+
use std::net::Ipv6Addr;
352+
353+
#[test]
354+
fn router_solicitation_checksum_uses_all_routers_multicast() {
355+
let src = "fe80::1".parse().unwrap();
356+
let mut data = ispf::to_bytes_be(&Icmp6RouterSolicitation::default())
357+
.expect("serialize router solicitation");
358+
359+
cksum(src, Some(ALL_ROUTERS_MCAST), 8, &mut data);
360+
361+
assert_eq!(&data[2..4], &[0x7d, 0x36]);
362+
}
363+
364+
#[test]
365+
fn router_advertisement_checksum_defaults_to_all_nodes_multicast() {
366+
let src: Ipv6Addr = "fe80::1".parse().unwrap();
367+
let mut default_dst =
368+
ispf::to_bytes_be(&Icmp6RouterAdvertisement::default())
369+
.expect("serialize router advertisement");
370+
let mut all_nodes_dst = default_dst.clone();
371+
372+
cksum(src, None, 16, &mut default_dst);
373+
cksum(src, Some(ALL_NODES_MCAST), 16, &mut all_nodes_dst);
374+
375+
assert_eq!(default_dst[2..4], all_nodes_dst[2..4]);
376+
}
350377

351378
#[test]
352379
fn router_solicitation_with_link_layer_addr() {

ndp/src/util.rs

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,10 @@ pub fn send_ra(
9696
return;
9797
}
9898
};
99-
cksum(src, dst, ICMP6_RA_ULP_LEN, &mut out);
99+
let dst_ip = dst.unwrap_or(ALL_NODES_MCAST);
100+
cksum(src, Some(dst_ip), ICMP6_RA_ULP_LEN, &mut out);
100101

101-
let dst = SocketAddrV6::new(
102-
match dst {
103-
Some(d) => d,
104-
None => ALL_NODES_MCAST,
105-
},
106-
0,
107-
0,
108-
ifindex.get(),
109-
);
102+
let dst = SocketAddrV6::new(dst_ip, 0, 0, ifindex.get());
110103
if let Err(e) = s.send_to(&out, &dst.into()) {
111104
error!(log, "send_ra: send: {e}");
112105
}
@@ -127,17 +120,10 @@ pub fn send_rs(
127120
return;
128121
}
129122
};
130-
cksum(src, dst, ICMP6_RS_ULP_LEN, &mut out);
123+
let dst_ip = dst.unwrap_or(ALL_ROUTERS_MCAST);
124+
cksum(src, Some(dst_ip), ICMP6_RS_ULP_LEN, &mut out);
131125

132-
let dst = SocketAddrV6::new(
133-
match dst {
134-
Some(d) => d,
135-
None => ALL_ROUTERS_MCAST,
136-
},
137-
0,
138-
0,
139-
ifindex.get(),
140-
);
126+
let dst = SocketAddrV6::new(dst_ip, 0, 0, ifindex.get());
141127
if let Err(e) = s.send_to(&out, &dst.into()) {
142128
error!(log, "send_rs: send: {e}");
143129
}

0 commit comments

Comments
 (0)