diff --git a/radicle-cli/examples/rad-config.md b/radicle-cli/examples/rad-config.md index 3a60c80a..8fcbf4d9 100644 --- a/radicle-cli/examples/rad-config.md +++ b/radicle-cli/examples/rad-config.md @@ -43,6 +43,10 @@ $ rad config "fillRate": 1.0, "capacity": 64 } + }, + "connection": { + "inbound": 128, + "outbound": 16 } }, "policy": "block", diff --git a/radicle-httpd/src/api/v1/profile.rs b/radicle-httpd/src/api/v1/profile.rs index e8286eee..67b859c4 100644 --- a/radicle-httpd/src/api/v1/profile.rs +++ b/radicle-httpd/src/api/v1/profile.rs @@ -105,6 +105,10 @@ mod routes { "fillRate": 1.0, "capacity": 64 } + }, + "connection": { + "inbound": 128, + "outbound": 16 } }, "policy": "block", diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index 22f6c38b..0df45c66 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -1059,10 +1059,15 @@ where /// Inbound connection attempt. pub fn accepted(&mut self, addr: Address) -> bool { - // Always accept trusted connections. + // Always accept trusted connections, even if we already reached + // our inbound connection limit. if addr.is_trusted() { return true; } + // Check for inbound connection limit. + if self.sessions.inbound().count() >= self.config.limits.connection.inbound { + return false; + } let host: HostName = addr.into(); if self @@ -1912,6 +1917,10 @@ where error!(target: "service", "Attempted connection to self"); return false; } + if self.sessions.outbound().count() >= self.config.limits.connection.outbound { + error!(target: "service", "Outbound connection limit reached when attempting {nid} ({addr})"); + return false; + } let persistent = self.config.is_persistent(&nid); let time = self.time(); @@ -2345,6 +2354,16 @@ impl Sessions { }) } + /// Iterator over connected inbound peers. + pub fn inbound(&self) -> impl Iterator + Clone { + self.connected().filter(|(_, s)| s.link.is_inbound()) + } + + /// Iterator over outbound peers. + pub fn outbound(&self) -> impl Iterator + Clone { + self.connected().filter(|(_, s)| s.link.is_outbound()) + } + /// Iterator over mutable fully connected peers. pub fn connected_mut(&mut self) -> impl Iterator { self.0.iter_mut().filter(move |(_, s)| s.is_connected()) diff --git a/radicle/src/node/config.rs b/radicle/src/node/config.rs index da28ab50..6c235340 100644 --- a/radicle/src/node/config.rs +++ b/radicle/src/node/config.rs @@ -93,6 +93,9 @@ pub struct Limits { /// Rate limitter settings. #[serde(default)] pub rate: RateLimits, + /// Connection limits. + #[serde(default)] + pub connection: ConnectionLimits, } impl Default for Limits { @@ -104,6 +107,27 @@ impl Default for Limits { fetch_concurrency: 1, max_open_files: 4096, rate: RateLimits::default(), + connection: ConnectionLimits::default(), + } + } +} + +/// Connection limits. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionLimits { + /// Max inbound connections. + pub inbound: usize, + /// Max outbound connections. Note that this is higher than the *target* number + /// in [`TARGET_OUTBOUND_PEERS`]. + pub outbound: usize, +} + +impl Default for ConnectionLimits { + fn default() -> Self { + Self { + inbound: 128, + outbound: 16, } } }