Passive peers: configured, and left to connect

A session could only be configured for a peer we would then dial, every
few seconds to two minutes, for as long as it was not up. A new peer
whose side was not ready yet filled the log with failed attempts, and a
peer that opens its sessions itself was dialled for nothing.

A peer with `passive: true` is not dialled. A connection from its address
is taken as for any configured peer, and the session comes up with all
that is configured for it: role, import policy, export limits,
hold-downs. Until then the status page and /api/peers show it as
`Passive`, waiting for the peer to connect; after a session, with how
that session ended, which nothing else would say since nobody dials it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Opus 5.5 committed 2026-10-06 00:05:54 +02:00
1 parent 910dd258fb
commit c9751883c4
22 files changed
+178 -8

No files matched your search

@@ -43,6 +43,7 @@ async fn server_offering(
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "route-server-client".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
@@ -33,6 +33,7 @@ fn peer_at(endpoint: SocketAddr) -> PeerConfig {
require_role: false,
export: Default::default(),
export_limits: None,
passive: false,
}
}
+1
View File
@@ -60,6 +60,7 @@ async fn server_with(
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -39,6 +39,7 @@ async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -170,6 +170,7 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "refused-demo".into(),
endpoint: endpoint.into(),
+69
View File
@@ -23,13 +23,25 @@ const TIMEOUT: Duration = Duration::from_secs(5);
/// Starts a server configured to dial `listener`'s address as peer "r1".
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
server_with_peer(listener, remote_asn, false).await
}
/// Starts a server with peer "r1" at `listener`'s address, which it dials
/// unless the peer is `passive`.
async fn server_with_peer(
listener: &TcpListener,
remote_asn: u32,
passive: bool,
) -> Result<TestServer> {
let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.accept_unconfigured_peers = !passive;
c.peers = vec![PeerConfig {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -192,6 +204,7 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
@@ -248,6 +261,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -273,6 +287,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -350,3 +365,57 @@ async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> R
);
Ok(())
}
/// A passive peer is not dialled: its session comes up when it connects,
/// and until then, and after, it is shown as waiting, with how its last
/// session ended.
#[tokio::test]
async fn test_a_passive_peer_is_not_dialled_and_comes_up_when_it_connects() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_with_peer(&listener, PEER_ASN, true).await?;
let state = || server.registry.configured_peers()[0].1.clone();
// Many retry intervals pass, and nothing connects.
assert!(
tokio::time::timeout(Duration::from_secs(1), listener.accept())
.await
.is_err(),
"a passive peer was dialled"
);
assert_eq!(state(), DialState::Passive { last_error: None });
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let listed = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(session) = server.registry.list().first().cloned() {
return session;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
assert_eq!(listed.name.as_deref(), Some("r1"));
drop(peer);
tokio::time::timeout(TIMEOUT, async {
while !matches!(
state(),
DialState::Passive {
last_error: Some(_)
}
) {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
assert!(server.registry.list().is_empty());
assert!(
tokio::time::timeout(Duration::from_millis(600), listener.accept())
.await
.is_err(),
"a passive peer was dialled after its session ended"
);
Ok(())
}
@@ -54,6 +54,7 @@ async fn server_with(originated: &[&str], limits: ExportLimits) -> Result<Setup>
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: Some(Arc::new(limits)),
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -45,6 +45,7 @@ async fn server_with(import: ImportPolicy) -> Result<(TestServer, TcpListener)>
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
@@ -186,6 +186,7 @@ async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "v6-only".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
+1
View File
@@ -51,6 +51,7 @@ async fn server_with(role: Option<PeerRole>, require_role: bool) -> Result<Setup
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
+1
View File
@@ -70,6 +70,7 @@ async fn server_with(
graceful_restart: Default::default(),
groups: Vec::new(),
export_limits: None,
passive: false,
reflector_client: false,
name: "r1".into(),
endpoint: endpoint.into(),
+7
View File
@@ -359,6 +359,13 @@ message Peer {
// one whose role is PROVIDER, PEER or ROUTE_SERVER, or that is in
// another AS and has no role.
ExportLimits export_limits = 15;
// Do not open connections to the peer: wait for it to connect. The
// session is configured in full, and comes up when the peer connects
// from `endpoint`'s address; the port of `endpoint` is not used. For a
// new peer whose side is not ready yet, or one that is to open the
// connections itself.
bool passive = 16;
}
// What is announced to a peer.
+16
View File
@@ -108,6 +108,8 @@ pub struct PeerConfig {
/// Pass on to this peer, in our own AS, the routes learned from our
/// other routers, and its routes to them (RFC 4456).
pub reflector_client: bool,
/// Do not dial the peer: wait for it to connect.
pub passive: bool,
}
impl Config {
@@ -456,6 +458,7 @@ fn parse_peer(
},
groups: peer.groups,
reflector_client: peer.route_reflector_client,
passive: peer.passive,
})
}
@@ -1400,4 +1403,17 @@ mod tests {
}
));
}
/// A peer is dialled unless it is passive.
#[test]
fn a_peer_can_be_left_to_connect() {
let config = parse(&format!(
"{BASE}peers {{ name: \"a\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 }}
peers {{ name: \"b\" endpoint {{ address: \"192.0.2.3\" }} remote_asn: 2
passive: true }}"
))
.unwrap();
let passive: Vec<bool> = config.peers.iter().map(|peer| peer.passive).collect();
assert_eq!(passive, [false, true]);
}
}
+4
View File
@@ -124,6 +124,8 @@ pub struct PeerSettings {
pub reflector_client: bool,
/// Graceful restart with the peer.
pub graceful_restart: GracefulRestartConfig,
/// Whether the peer is left to connect to us, and not dialled.
pub passive: bool,
}
impl PeerSettings {
@@ -150,6 +152,7 @@ impl PeerSettings {
groups: Arc::default(),
reflector_client: false,
graceful_restart,
passive: false,
}
}
}
@@ -170,6 +173,7 @@ impl From<&PeerConfig> for PeerSettings {
groups: peer.groups.as_slice().into(),
reflector_client: peer.reflector_client,
graceful_restart: peer.graceful_restart,
passive: peer.passive,
}
}
}
+2
View File
@@ -32,6 +32,8 @@ pub fn spawn_dialers(
) -> Vec<JoinHandle<()>> {
peers
.iter()
// A passive peer is left to connect to us.
.filter(|peer| !peer.passive)
.map(|peer| tokio::spawn(dial_loop(peer.clone(), Arc::clone(server), connect_retry)))
.collect()
}
+20 -2
View File
@@ -32,7 +32,7 @@ use crate::reach;
use crate::refusals::RefusalLog;
use crate::restart::{Leave, Restarts};
use crate::rpki::spawn_revalidator;
use crate::session::{ConfiguredPeer, SessionRegistry};
use crate::session::{ConfiguredPeer, DialState, SessionRegistry};
use crate::sync::IgnorePoison;
use crate::throttle::Throttle;
@@ -125,6 +125,7 @@ impl BgpServer {
endpoint: p.endpoint.clone(),
remote_asn: p.remote_asn,
role: p.role,
passive: p.passive,
})
.collect();
// The client starts now: with luck the data is there before the
@@ -304,6 +305,9 @@ impl BgpServer {
reach::spawn_revalidator(interfaces, Arc::clone(&self.rib), self.registry.clone()),
));
}
for peer in self.config.peers.iter().filter(|peer| peer.passive) {
info!(peer = %peer.name, endpoint = %peer.endpoint, "Waiting for the peer to connect (passive)");
}
for dialer in spawn_dialers(&self.config.peers, &context, self.config.connect_retry) {
tasks.push(named("dialer", dialer));
}
@@ -476,10 +480,24 @@ async fn serve(
peer: PeerSettings,
_slot: ConnectionSlot,
) {
// Nobody dials a passive peer, so its state is said here.
let passive = peer.name.clone().filter(|_| peer.passive);
let registry = context.registry.clone();
let end = run_session(stream, peer_addr, Direction::Inbound, context, peer).await;
if let Err(e) = end.result {
if let Err(e) = &end.result {
warn!(peer_addr = %socket_text(peer_addr), err = %e, "BGP session terminated with error");
}
if let Some(name) = passive {
registry.set_dial_state(
&name,
DialState::Passive {
last_error: Some(match end.result {
Ok(()) => "session closed at our request".to_string(),
Err(e) => e.to_string(),
}),
},
);
}
}
/// Why an incoming connection was closed without a session.
+16 -1
View File
@@ -54,6 +54,12 @@ pub enum DialState {
/// How the last attempt or session ended.
last_error: String,
},
/// The peer is passive: not dialled, waiting for it to connect.
Passive {
/// How its last session, or the last connection from it, ended,
/// if one did.
last_error: Option<String>,
},
}
/// A peer from the configuration file, whether or not it is currently up.
@@ -64,6 +70,8 @@ pub struct ConfiguredPeer {
pub remote_asn: u32,
/// What the peer is to us, if that was said.
pub role: Option<PeerRole>,
/// Whether it is left to the peer to connect.
pub passive: bool,
}
/// Counts kept by a session as it goes, for whoever wants to look.
@@ -321,7 +329,14 @@ impl SessionRegistry {
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let dial_states = configured
.iter()
.map(|p| (p.name.clone(), DialState::Idle))
.map(|p| {
let state = if p.passive {
DialState::Passive { last_error: None }
} else {
DialState::Idle
};
(p.name.clone(), state)
})
.collect();
for peer in &configured {
PEER_UP.with_label_values(&[&peer.name]).set(0);
+5 -2
View File
@@ -63,7 +63,8 @@ pub struct PeerView {
pub name: Option<String>,
pub peer_addr: String,
pub peer_asn: u32,
/// `Established`, `Connecting`, `Backoff` or `Idle`.
/// `Established`, `Connecting`, `Backoff`, `Idle`, or `Passive` for a
/// peer that is not dialled and has not connected.
pub state: String,
/// `inbound` or `outbound`: which side opened the live TCP connection.
pub direction: Option<String>,
@@ -95,7 +96,8 @@ pub struct PeerView {
/// Whether the peer's OPEN said the same (RFC 9234). `false` for a
/// peer that does not say; one that says otherwise gets no session.
pub role_confirmed: Option<bool>,
/// Why the last connection attempt or session ended, while in Backoff.
/// Why the last connection attempt or session ended, while in Backoff;
/// for a passive peer, why its last session ended, if one did.
pub last_error: Option<String>,
pub retry_at_unix: Option<u64>,
/// Graceful restart (RFC 4724), if both sides offered it.
@@ -172,6 +174,7 @@ impl PeerView {
DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
DialState::Passive { last_error } => ("Passive", last_error.clone(), None),
};
Self {
name: Some(peer.name.clone()),
+4 -2
View File
@@ -242,7 +242,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
tick();
}
const STATE_CLASS = { Established: 'ok', Connecting: 'warn', Backoff: 'bad', Idle: 'dim' };
const STATE_CLASS = { Established: 'ok', Connecting: 'warn', Backoff: 'bad', Idle: 'dim', Passive: 'dim' };
// Where several routes for a prefix are exchanged (ADD-PATH), and which way.
function addPath(p) {
const rx = p.add_path_receive || [], tx = p.add_path_send || [];
@@ -260,7 +260,9 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
const up = p.state === 'Established';
const restarting = !up && p.stale_until_unix
? `<span class="warn" title="Graceful restart: the peer's routes are kept, and used, until it is back and has sent them again">restarting: ${p.stale_routes} routes kept for ${until(p.stale_until_unix)}</span>` : '';
const detail = [restarting, p.state === 'Backoff' ? `${esc(p.last_error || '')}; retry in ${until(p.retry_at_unix)}` : '']
const detail = [restarting,
p.state === 'Backoff' ? `${esc(p.last_error || '')}; retry in ${until(p.retry_at_unix)}` : '',
p.state === 'Passive' ? `waiting for the peer to connect${p.last_error ? `; last session: ${esc(p.last_error)}` : ''}` : '']
.filter(Boolean).join('; ');
const gr = p.graceful_restart;
const graceful = gr ? ` &middot; <span title="Graceful restart (RFC 4724): the peer says a restart takes ${gr.restart_time_secs}s${gr.notification ? ', and NOTIFICATIONs other than Hard Reset count as restarts (RFC 8538)' : ''}">graceful restart</span>` : '';