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(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "route-server-client".into(),
|
name: "route-server-client".into(),
|
||||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
|
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).into(),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ fn peer_at(endpoint: SocketAddr) -> PeerConfig {
|
|||||||
require_role: false,
|
require_role: false,
|
||||||
export: Default::default(),
|
export: Default::default(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ async fn server_with(
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> {
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "refused-demo".into(),
|
name: "refused-demo".into(),
|
||||||
endpoint: endpoint.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".
|
/// Starts a server configured to dial `listener`'s address as peer "r1".
|
||||||
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
|
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()?;
|
let endpoint: SocketAddr = listener.local_addr()?;
|
||||||
TestServer::start_with(LOCAL_ASN, |c| {
|
TestServer::start_with(LOCAL_ASN, |c| {
|
||||||
c.connect_retry = Duration::from_millis(200);
|
c.connect_retry = Duration::from_millis(200);
|
||||||
|
c.accept_unconfigured_peers = !passive;
|
||||||
c.peers = vec![PeerConfig {
|
c.peers = vec![PeerConfig {
|
||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
@@ -192,6 +204,7 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).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(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
@@ -273,6 +287,7 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
@@ -350,3 +365,57 @@ async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> R
|
|||||||
);
|
);
|
||||||
Ok(())
|
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(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: Some(Arc::new(limits)),
|
export_limits: Some(Arc::new(limits)),
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ async fn server_with(import: ImportPolicy) -> Result<(TestServer, TcpListener)>
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "v6-only".into(),
|
name: "v6-only".into(),
|
||||||
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1).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(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ async fn server_with(
|
|||||||
graceful_restart: Default::default(),
|
graceful_restart: Default::default(),
|
||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
export_limits: None,
|
export_limits: None,
|
||||||
|
passive: false,
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
name: "r1".into(),
|
name: "r1".into(),
|
||||||
endpoint: endpoint.into(),
|
endpoint: endpoint.into(),
|
||||||
|
|||||||
@@ -359,6 +359,13 @@ message Peer {
|
|||||||
// one whose role is PROVIDER, PEER or ROUTE_SERVER, or that is in
|
// one whose role is PROVIDER, PEER or ROUTE_SERVER, or that is in
|
||||||
// another AS and has no role.
|
// another AS and has no role.
|
||||||
ExportLimits export_limits = 15;
|
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.
|
// 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
|
/// Pass on to this peer, in our own AS, the routes learned from our
|
||||||
/// other routers, and its routes to them (RFC 4456).
|
/// other routers, and its routes to them (RFC 4456).
|
||||||
pub reflector_client: bool,
|
pub reflector_client: bool,
|
||||||
|
/// Do not dial the peer: wait for it to connect.
|
||||||
|
pub passive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -456,6 +458,7 @@ fn parse_peer(
|
|||||||
},
|
},
|
||||||
groups: peer.groups,
|
groups: peer.groups,
|
||||||
reflector_client: peer.route_reflector_client,
|
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,
|
pub reflector_client: bool,
|
||||||
/// Graceful restart with the peer.
|
/// Graceful restart with the peer.
|
||||||
pub graceful_restart: GracefulRestartConfig,
|
pub graceful_restart: GracefulRestartConfig,
|
||||||
|
/// Whether the peer is left to connect to us, and not dialled.
|
||||||
|
pub passive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PeerSettings {
|
impl PeerSettings {
|
||||||
@@ -150,6 +152,7 @@ impl PeerSettings {
|
|||||||
groups: Arc::default(),
|
groups: Arc::default(),
|
||||||
reflector_client: false,
|
reflector_client: false,
|
||||||
graceful_restart,
|
graceful_restart,
|
||||||
|
passive: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,6 +173,7 @@ impl From<&PeerConfig> for PeerSettings {
|
|||||||
groups: peer.groups.as_slice().into(),
|
groups: peer.groups.as_slice().into(),
|
||||||
reflector_client: peer.reflector_client,
|
reflector_client: peer.reflector_client,
|
||||||
graceful_restart: peer.graceful_restart,
|
graceful_restart: peer.graceful_restart,
|
||||||
|
passive: peer.passive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ pub fn spawn_dialers(
|
|||||||
) -> Vec<JoinHandle<()>> {
|
) -> Vec<JoinHandle<()>> {
|
||||||
peers
|
peers
|
||||||
.iter()
|
.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)))
|
.map(|peer| tokio::spawn(dial_loop(peer.clone(), Arc::clone(server), connect_retry)))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ use crate::reach;
|
|||||||
use crate::refusals::RefusalLog;
|
use crate::refusals::RefusalLog;
|
||||||
use crate::restart::{Leave, Restarts};
|
use crate::restart::{Leave, Restarts};
|
||||||
use crate::rpki::spawn_revalidator;
|
use crate::rpki::spawn_revalidator;
|
||||||
use crate::session::{ConfiguredPeer, SessionRegistry};
|
use crate::session::{ConfiguredPeer, DialState, SessionRegistry};
|
||||||
use crate::sync::IgnorePoison;
|
use crate::sync::IgnorePoison;
|
||||||
use crate::throttle::Throttle;
|
use crate::throttle::Throttle;
|
||||||
|
|
||||||
@@ -125,6 +125,7 @@ impl BgpServer {
|
|||||||
endpoint: p.endpoint.clone(),
|
endpoint: p.endpoint.clone(),
|
||||||
remote_asn: p.remote_asn,
|
remote_asn: p.remote_asn,
|
||||||
role: p.role,
|
role: p.role,
|
||||||
|
passive: p.passive,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// The client starts now: with luck the data is there before the
|
// 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()),
|
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) {
|
for dialer in spawn_dialers(&self.config.peers, &context, self.config.connect_retry) {
|
||||||
tasks.push(named("dialer", dialer));
|
tasks.push(named("dialer", dialer));
|
||||||
}
|
}
|
||||||
@@ -476,10 +480,24 @@ async fn serve(
|
|||||||
peer: PeerSettings,
|
peer: PeerSettings,
|
||||||
_slot: ConnectionSlot,
|
_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;
|
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");
|
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.
|
/// Why an incoming connection was closed without a session.
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ pub enum DialState {
|
|||||||
/// How the last attempt or session ended.
|
/// How the last attempt or session ended.
|
||||||
last_error: String,
|
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.
|
/// A peer from the configuration file, whether or not it is currently up.
|
||||||
@@ -64,6 +70,8 @@ pub struct ConfiguredPeer {
|
|||||||
pub remote_asn: u32,
|
pub remote_asn: u32,
|
||||||
/// What the peer is to us, if that was said.
|
/// What the peer is to us, if that was said.
|
||||||
pub role: Option<PeerRole>,
|
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.
|
/// 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 (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
|
||||||
let dial_states = configured
|
let dial_states = configured
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
for peer in &configured {
|
for peer in &configured {
|
||||||
PEER_UP.with_label_values(&[&peer.name]).set(0);
|
PEER_UP.with_label_values(&[&peer.name]).set(0);
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ pub struct PeerView {
|
|||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub peer_addr: String,
|
pub peer_addr: String,
|
||||||
pub peer_asn: u32,
|
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,
|
pub state: String,
|
||||||
/// `inbound` or `outbound`: which side opened the live TCP connection.
|
/// `inbound` or `outbound`: which side opened the live TCP connection.
|
||||||
pub direction: Option<String>,
|
pub direction: Option<String>,
|
||||||
@@ -95,7 +96,8 @@ pub struct PeerView {
|
|||||||
/// Whether the peer's OPEN said the same (RFC 9234). `false` for a
|
/// 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.
|
/// peer that does not say; one that says otherwise gets no session.
|
||||||
pub role_confirmed: Option<bool>,
|
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 last_error: Option<String>,
|
||||||
pub retry_at_unix: Option<u64>,
|
pub retry_at_unix: Option<u64>,
|
||||||
/// Graceful restart (RFC 4724), if both sides offered it.
|
/// Graceful restart (RFC 4724), if both sides offered it.
|
||||||
@@ -172,6 +174,7 @@ impl PeerView {
|
|||||||
DialState::Backoff { until, last_error } => {
|
DialState::Backoff { until, last_error } => {
|
||||||
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
|
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
|
||||||
}
|
}
|
||||||
|
DialState::Passive { last_error } => ("Passive", last_error.clone(), None),
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
name: Some(peer.name.clone()),
|
name: Some(peer.name.clone()),
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
|
|||||||
tick();
|
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.
|
// Where several routes for a prefix are exchanged (ADD-PATH), and which way.
|
||||||
function addPath(p) {
|
function addPath(p) {
|
||||||
const rx = p.add_path_receive || [], tx = p.add_path_send || [];
|
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 up = p.state === 'Established';
|
||||||
const restarting = !up && p.stale_until_unix
|
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>` : '';
|
? `<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('; ');
|
.filter(Boolean).join('; ');
|
||||||
const gr = p.graceful_restart;
|
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>` : '';
|
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>` : '';
|
||||||
|
|||||||
@@ -57,6 +57,29 @@ Ferrix rejects a neighbor whose OPEN message contains an AS number other
|
|||||||
than `remote_asn`, with the NOTIFICATION message OPEN Message Error / Bad
|
than `remote_asn`, with the NOTIFICATION message OPEN Message Error / Bad
|
||||||
Peer AS.
|
Peer AS.
|
||||||
|
|
||||||
|
### Passive Neighbors
|
||||||
|
|
||||||
|
A passive neighbor is a neighbor to which Ferrix does not initiate
|
||||||
|
connections. Ferrix accepts connections from the neighbor's address and
|
||||||
|
establishes the session with the configured settings when the neighbor
|
||||||
|
connects. Use a passive neighbor to configure a session before the
|
||||||
|
neighbor's side is ready, or when the neighbor initiates the connections:
|
||||||
|
|
||||||
|
```textproto
|
||||||
|
peers {
|
||||||
|
name: "new-peer"
|
||||||
|
endpoint { address: "192.0.2.10" }
|
||||||
|
remote_asn: 64510
|
||||||
|
role: PEER
|
||||||
|
passive: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Until the neighbor connects, the status page and `/api/peers` show the
|
||||||
|
state `Passive`. After a session ends, they show the reason in the
|
||||||
|
neighbor's last error. Ferrix does not log connection attempts to a
|
||||||
|
passive neighbor, because it makes none.
|
||||||
|
|
||||||
## Inbound Connection Filtering
|
## Inbound Connection Filtering
|
||||||
|
|
||||||
Ferrix accepts inbound connections only from the addresses of configured
|
Ferrix accepts inbound connections only from the addresses of configured
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ Statements for a BGP neighbor.
|
|||||||
| `import` <span class="type">[ImportPolicy](#importpolicy)</span> | Default checks | Specifies the import policy for routes received from the neighbor. |
|
| `import` <span class="type">[ImportPolicy](#importpolicy)</span> | Default checks | Specifies the import policy for routes received from the neighbor. |
|
||||||
| `role` <span class="type">[Role](#role)</span> | None | Specifies the BGP role of the neighbor (RFC 9234). Not permitted for IBGP neighbors. Required for every EBGP neighbor if any `export` statement enables `learned_routes`. |
|
| `role` <span class="type">[Role](#role)</span> | None | Specifies the BGP role of the neighbor (RFC 9234). Not permitted for IBGP neighbors. Required for every EBGP neighbor if any `export` statement enables `learned_routes`. |
|
||||||
| `require_role` <span class="type">bool</span> | `false` | Rejects the session if the neighbor does not advertise a BGP role. Requires `role`. |
|
| `require_role` <span class="type">bool</span> | `false` | Rejects the session if the neighbor does not advertise a BGP role. Requires `role`. |
|
||||||
|
| `passive` <span class="type">bool</span> | `false` | Prevents Ferrix from initiating connections to the neighbor. Ferrix accepts connections from the address of `endpoint`; the port of `endpoint` is not used. See [Passive Neighbors](@/docs/guides/peers.md#passive-neighbors). |
|
||||||
| `export` <span class="type">[ExportPolicy](#exportpolicy)</span> | Top-level `export` | Specifies the export policy for the neighbor. Replaces the top-level `export` statement. |
|
| `export` <span class="type">[ExportPolicy](#exportpolicy)</span> | Top-level `export` | Specifies the export policy for the neighbor. Replaces the top-level `export` statement. |
|
||||||
| `graceful_restart` <span class="type">[GracefulRestart](#gracefulrestart)</span> | Top-level `graceful_restart` | Specifies graceful restart for the neighbor. Each statement that is set replaces the corresponding top-level statement. |
|
| `graceful_restart` <span class="type">[GracefulRestart](#gracefulrestart)</span> | Top-level `graceful_restart` | Specifies graceful restart for the neighbor. Each statement that is set replaces the corresponding top-level statement. |
|
||||||
| `route_reflector_client` <span class="type">bool</span> | `false` | Makes the IBGP neighbor a route reflector client: Ferrix advertises to it the routes learned from other IBGP neighbors, and advertises its routes to them. Permitted only for IBGP neighbors. See [Route Reflection](@/docs/guides/passing-routes-on.md#route-reflection). |
|
| `route_reflector_client` <span class="type">bool</span> | `false` | Makes the IBGP neighbor a route reflector client: Ferrix advertises to it the routes learned from other IBGP neighbors, and advertises its routes to them. Permitted only for IBGP neighbors. See [Route Reflection](@/docs/guides/passing-routes-on.md#route-reflection). |
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ neighbor:
|
|||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `state` | Session state: `Established`, `Connecting`, `Backoff`, or `Idle`. |
|
| `state` | Session state: `Established`, `Connecting`, `Backoff`, `Idle`, or `Passive` (a passive neighbor that has no session). |
|
||||||
| `direction` | `inbound` if the neighbor initiated the connection, `outbound` otherwise. |
|
| `direction` | `inbound` if the neighbor initiated the connection, `outbound` otherwise. |
|
||||||
| `routes` | Number of the neighbor's routes in the routing table. |
|
| `routes` | Number of the neighbor's routes in the routing table. |
|
||||||
| `rejected_routes` | Number of the neighbor's routes rejected by its import policy. |
|
| `rejected_routes` | Number of the neighbor's routes rejected by its import policy. |
|
||||||
|
|||||||
Reference in new issue
Block a user