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:
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 ? ` · <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>` : '';
|
||||
|
||||
Reference in new issue
Block a user