A review of the session driver, the server loop, the dialer, the advertiser and the HTTP API, with the fixes. Bugs: - A peer that stopped reading held its session, and our routes, for as long as TCP kept retrying: a blocked write was awaited with no timer running. Writes now give up after the Send Hold Time of RFC 9687. - Routes we originate were sent to a peer in our own AS with our AS number on the path and no LOCAL_PREF, which such a peer refuses. They now go with an empty path and a LOCAL_PREF. - Collision detection knew a peer by its BGP Identifier alone. A neighbour with one session over IPv4 and one over IPv6 had the second closed as a collision, and any host that could connect could displace a peer's connection by claiming its identifier. A peer is now its address and identifier together. - The dialer reset its backoff whenever TCP connected, so a peer that accepts the connection and refuses the session was dialled every few seconds for ever. It resets only after a session was established. - A failure on our side (routes that cannot be stored) dropped the connection without a NOTIFICATION. It goes through the state machine and the peer is sent Cease. A queued message that cannot be encoded is logged and skipped instead of ending the session. - The accept loop retried at once after an error, spinning when the process was out of file descriptors. - Attribute sets no route used any more were dropped only when a peer went away. A task now does it every minute. - The advertiser subscribed to session events inside its task, after the dialers were started, and could miss the first session. - Shutdown waited without limit on a session whose queue was full. - /api/routes/received took any limit and built the reply on a runtime thread. The limit is capped and the work is done off those threads. - The default log level was TRACE, which logs every UPDATE. - IPv6 listeners are IPv6-only: listening on 0.0.0.0 and [::] fails on systems where the second would take IPv4 too, and IPv4 peers arriving under mapped addresses matched no configured peer. - A connection closed after a NOTIFICATION is kept for a moment so that the peer can read it. - The configuration accepted two peers with one name or one address, and a router id of 0.0.0.0. - An HTTP address that cannot be bound now stops the daemon at startup. Simplifications: - SessionConfig mixed what a server's sessions share with what is configured for one peer, and was cloned and patched field by field in two places. It is ServerContext, shared, and PeerSettings, made from a PeerConfig in one place. - The attributes of an UPDATE as they were on the wire travelled from the reader to the table through a field of the session, around the state machine. They go through it, with the message. - run_session returns how the session ended and whether it had been established; Ok no longer reads as "closed by peer". - Session is not generic over its reader. - BgpServerBuilder had nothing to build: BgpServer::bind takes the configuration. - One shutdown path, close_sessions, for the signal handler and for a failed server. - The advertiser's three copies of "send every route" are one. - PeerId moved from the rib crate, which did not use it, to the server. - Unused API removed or made private to the crate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
149 lines
5.5 KiB
Rust
149 lines
5.5 KiB
Rust
//! Interoperability with what real routers put on the wire.
|
|
|
|
use std::net::Ipv4Addr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::message::{Message, PathAttribute};
|
|
use bgp_packet::open::{Capability, OpenOption};
|
|
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
|
use eyre::Result;
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
|
|
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
|
|
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
|
|
/// route refresh and long-lived graceful restart (aware only).
|
|
#[rustfmt::skip]
|
|
const BIRD_OPEN: &[u8] = &[
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
0x00, 0x37, 0x01, // length 55, type OPEN
|
|
0x04, // version
|
|
0x5b, 0xa0, // AS_TRANS
|
|
0x00, 0xf0, // hold time 240
|
|
0x0a, 0xff, 0x00, 0x01, // router id 10.255.0.1
|
|
0x1a, // optional parameters length 26
|
|
0x02, 0x18, // capabilities, 24 bytes
|
|
0x01, 0x04, 0x00, 0x02, 0x00, 0x01, // MP: IPv6 unicast
|
|
0x02, 0x00, // route refresh
|
|
0x06, 0x00, // extended message
|
|
0x40, 0x02, 0x00, 0x00, // graceful restart, no address families
|
|
0x41, 0x04, 0x00, 0x03, 0x34, 0x74, // 4-octet AS 210036
|
|
0x46, 0x00, // enhanced route refresh
|
|
0x47, 0x00, // long-lived graceful restart, none
|
|
];
|
|
|
|
/// IPv6 End-of-RIB: an UPDATE whose only attribute is an empty
|
|
/// MP_UNREACH_NLRI for IPv6 unicast (RFC 4724 §2).
|
|
#[rustfmt::skip]
|
|
const IPV6_END_OF_RIB: &[u8] = &[
|
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
|
0x00, 0x1e, 0x02, // length 30, type UPDATE
|
|
0x00, 0x00, // no withdrawn routes
|
|
0x00, 0x07, // 7 bytes of path attributes
|
|
0x90, 0x0f, 0x00, 0x03, // optional + extended length, MP_UNREACH_NLRI, len 3
|
|
0x00, 0x02, 0x01, // IPv6 unicast, no prefixes
|
|
];
|
|
|
|
#[tokio::test]
|
|
async fn test_server_advertises_ipv4_and_ipv6_unicast() -> Result<()> {
|
|
let server = TestServer::start(65000).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
let open = peer.recv_open().await?;
|
|
let caps: Vec<&Capability> = open
|
|
.options
|
|
.iter()
|
|
.flat_map(|o| match o {
|
|
OpenOption::Capabilities(c) => c.iter(),
|
|
})
|
|
.collect();
|
|
for afi in [Afi::IPV4, Afi::IPV6] {
|
|
assert!(
|
|
caps.iter().any(|c| matches!(c, Capability::MultiProtocol { afi: a, safi: Safi::UNICAST } if *a == afi)),
|
|
"missing MP capability for {afi:?}: {caps:?}"
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
|
|
let server = TestServer::start(4200000001).await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
|
|
peer.send_raw(BIRD_OPEN).await?;
|
|
peer.recv_open().await?;
|
|
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
|
|
Message::KeepAlive => {}
|
|
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
|
|
}
|
|
peer.send(&Message::KeepAlive).await?;
|
|
|
|
tokio::time::timeout(TIMEOUT, async {
|
|
while server.registry.list().is_empty() {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
}
|
|
})
|
|
.await?;
|
|
let session = server.registry.list().remove(0);
|
|
assert_eq!(
|
|
session.peer.asn, 210036,
|
|
"4-octet ASN must come from the capability, not AS_TRANS"
|
|
);
|
|
assert!(session.four_octet_asn);
|
|
|
|
// End-of-RIB must neither break the session nor create a route.
|
|
peer.send_raw(IPV6_END_OF_RIB).await?;
|
|
peer.send(&Message::KeepAlive).await?;
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
assert_eq!(
|
|
server.registry.list().len(),
|
|
1,
|
|
"session dropped after End-of-RIB"
|
|
);
|
|
assert!(server.received_routes().is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// Over an IPv4 test transport an IPv6 route cannot be sent (no IPv6 next
|
|
/// hop), so this only checks the builder the advertiser uses: the UPDATE for
|
|
/// an IPv6 prefix carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
|
|
#[tokio::test]
|
|
async fn test_ipv6_prefix_update_shape() -> Result<()> {
|
|
let route = LocalRoute::new(
|
|
IpPrefix::try_from("2001:db8:1::/48")?,
|
|
LocalRouteAttrs::default(),
|
|
);
|
|
let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?;
|
|
let update = bgp_server::advertise::build_announce(
|
|
&route,
|
|
&bgp_server::advertise::Towards {
|
|
local_asn: 4200000001,
|
|
next_hop: nh.into(),
|
|
four_octet_asn: true,
|
|
ibgp: false,
|
|
},
|
|
)
|
|
.expect("ipv6 over ipv6");
|
|
assert!(update.announced_routes.is_empty());
|
|
assert!(
|
|
!update
|
|
.path_attributes
|
|
.iter()
|
|
.any(|a| matches!(a, PathAttribute::NextHop(_)))
|
|
);
|
|
assert!(
|
|
update
|
|
.path_attributes
|
|
.iter()
|
|
.any(|a| matches!(a, PathAttribute::MpReachNlri(m)
|
|
if m.family == AfiSafi::IPV6_UNICAST && m.prefixes[0].to_string() == "2001:db8:1::/48"))
|
|
);
|
|
let _ = Ipv4Addr::LOCALHOST;
|
|
Ok(())
|
|
}
|