Skip to content

Commit 541eedb

Browse files
committed
feat: Implement IP-based access control
Add IpAccessControl for whitelist/blacklist connection filtering: - Support CIDR notation for IP ranges (IPv4 and IPv6) - Whitelist mode: only allow specified IP ranges - Blacklist mode: block specific IP ranges - Blacklist takes priority over whitelist - Dynamic updates: block/unblock IPs at runtime - Thread-safe SharedIpAccessControl for shared access - Integration at connection level before handler creation Configuration: - allowed_ips: CIDR ranges for whitelist mode - blocked_ips: CIDR ranges always denied Features: - 14 comprehensive unit tests for access control - Rejected connections get minimal handler that rejects auth - Logging for blocked/allowed connections - Reloadable configuration support Closes #141
1 parent 51dc329 commit 541eedb

5 files changed

Lines changed: 786 additions & 1 deletion

File tree

src/server/config/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,20 @@ pub struct ServerConfig {
165165
/// IP addresses that are never banned (whitelist).
166166
#[serde(default)]
167167
pub whitelist_ips: Vec<String>,
168+
169+
/// Allowed IP ranges in CIDR notation for connection filtering.
170+
///
171+
/// If non-empty, only connections from these ranges are allowed.
172+
/// Empty list means all IPs are allowed (subject to blocked_ips).
173+
#[serde(default)]
174+
pub allowed_ips: Vec<String>,
175+
176+
/// Blocked IP ranges in CIDR notation for connection filtering.
177+
///
178+
/// Connections from these ranges are always denied.
179+
/// Blocked IPs take priority over allowed IPs.
180+
#[serde(default)]
181+
pub blocked_ips: Vec<String>,
168182
}
169183

170184
/// Serializable configuration for public key authentication.
@@ -260,6 +274,8 @@ impl Default for ServerConfig {
260274
auth_window_secs: default_auth_window_secs(),
261275
ban_time_secs: default_ban_time_secs(),
262276
whitelist_ips: Vec::new(),
277+
allowed_ips: Vec::new(),
278+
blocked_ips: Vec::new(),
263279
}
264280
}
265281
}
@@ -551,6 +567,8 @@ impl ServerFileConfig {
551567
auth_window_secs: self.security.auth_window,
552568
ban_time_secs: self.security.ban_time,
553569
whitelist_ips: self.security.whitelist_ips,
570+
allowed_ips: self.security.allowed_ips,
571+
blocked_ips: self.security.blocked_ips,
554572
}
555573
}
556574
}

src/server/handler.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ pub struct SshHandler {
6666

6767
/// Active channels for this connection.
6868
channels: HashMap<ChannelId, ChannelState>,
69+
70+
/// Whether this connection should be immediately rejected.
71+
/// Set when IP access control denies the connection.
72+
rejected: bool,
6973
}
7074

7175
impl SshHandler {
@@ -90,6 +94,7 @@ impl SshHandler {
9094
auth_rate_limiter: None,
9195
session_info: Some(SessionInfo::new(peer_addr)),
9296
channels: HashMap::new(),
97+
rejected: false,
9398
}
9499
}
95100

@@ -114,6 +119,7 @@ impl SshHandler {
114119
auth_rate_limiter: None,
115120
session_info: Some(SessionInfo::new(peer_addr)),
116121
channels: HashMap::new(),
122+
rejected: false,
117123
}
118124
}
119125

@@ -140,6 +146,7 @@ impl SshHandler {
140146
auth_rate_limiter: Some(auth_rate_limiter),
141147
session_info: Some(SessionInfo::new(peer_addr)),
142148
channels: HashMap::new(),
149+
rejected: false,
143150
}
144151
}
145152

@@ -163,6 +170,32 @@ impl SshHandler {
163170
auth_rate_limiter: None,
164171
session_info: Some(SessionInfo::new(peer_addr)),
165172
channels: HashMap::new(),
173+
rejected: false,
174+
}
175+
}
176+
177+
/// Create a handler for a rejected connection.
178+
///
179+
/// This handler will immediately reject all authentication attempts.
180+
/// Used when IP access control denies a connection.
181+
pub fn rejected(
182+
peer_addr: Option<SocketAddr>,
183+
config: Arc<ServerConfig>,
184+
sessions: Arc<RwLock<SessionManager>>,
185+
) -> Self {
186+
let auth_provider = config.create_auth_provider();
187+
let rate_limiter = RateLimiter::with_simple_config(1, 0.1);
188+
189+
Self {
190+
peer_addr,
191+
config,
192+
sessions,
193+
auth_provider,
194+
rate_limiter,
195+
auth_rate_limiter: None,
196+
session_info: None, // No session for rejected connections
197+
channels: HashMap::new(),
198+
rejected: true,
166199
}
167200
}
168201

@@ -246,6 +279,19 @@ impl russh::server::Handler for SshHandler {
246279
"Auth none attempt"
247280
);
248281

282+
// If connection was rejected by IP access control, immediately reject
283+
if self.rejected {
284+
tracing::debug!(
285+
peer = ?self.peer_addr,
286+
"Rejecting auth for IP-blocked connection"
287+
);
288+
return std::future::ready(Ok(Auth::Reject {
289+
proceed_with_methods: None,
290+
partial_success: false,
291+
}))
292+
.left_future();
293+
}
294+
249295
// Create session info if not already created
250296
let peer_addr = self.peer_addr;
251297
let sessions = Arc::clone(&self.sessions);
@@ -287,6 +333,7 @@ impl russh::server::Handler for SshHandler {
287333
partial_success: false,
288334
})
289335
}
336+
.right_future()
290337
}
291338

292339
/// Handle public key authentication.

src/server/mod.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ pub use self::config::{ServerConfig, ServerConfigBuilder};
7070
pub use self::exec::{CommandExecutor, ExecConfig};
7171
pub use self::handler::SshHandler;
7272
pub use self::pty::{PtyConfig as PtyMasterConfig, PtyMaster};
73-
pub use self::security::{AuthRateLimitConfig, AuthRateLimiter};
73+
pub use self::security::{
74+
AccessPolicy, AuthRateLimitConfig, AuthRateLimiter, IpAccessControl, SharedIpAccessControl,
75+
};
7476
pub use self::session::{
7577
ChannelMode, ChannelState, PtyConfig, SessionId, SessionInfo, SessionManager,
7678
};
@@ -247,6 +249,15 @@ impl BsshServer {
247249
"Auth rate limiter configured"
248250
);
249251

252+
// Create IP access control from configuration
253+
let ip_access_control = IpAccessControl::from_config(
254+
&self.config.allowed_ips,
255+
&self.config.blocked_ips,
256+
)
257+
.context("Failed to configure IP access control")?;
258+
259+
let shared_ip_access = SharedIpAccessControl::new(ip_access_control);
260+
250261
// Start background cleanup task for auth rate limiter
251262
let cleanup_limiter = auth_rate_limiter.clone();
252263
tokio::spawn(async move {
@@ -262,6 +273,7 @@ impl BsshServer {
262273
sessions: Arc::clone(&self.sessions),
263274
rate_limiter,
264275
auth_rate_limiter,
276+
ip_access_control: shared_ip_access,
265277
};
266278

267279
// Use run_on_socket which handles the server loop
@@ -294,12 +306,53 @@ struct BsshServerRunner {
294306
rate_limiter: RateLimiter<String>,
295307
/// Auth rate limiter with ban support (fail2ban-like)
296308
auth_rate_limiter: AuthRateLimiter,
309+
/// IP-based access control
310+
ip_access_control: SharedIpAccessControl,
297311
}
298312

299313
impl russh::server::Server for BsshServerRunner {
300314
type Handler = SshHandler;
301315

302316
fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler {
317+
// Check IP access control before creating handler
318+
if let Some(addr) = peer_addr {
319+
let ip = addr.ip();
320+
321+
// Check IP access control (synchronous to avoid blocking)
322+
if self.ip_access_control.check_sync(&ip) == AccessPolicy::Deny {
323+
tracing::info!(
324+
ip = %ip,
325+
"Connection rejected by IP access control"
326+
);
327+
// Return a handler that will immediately reject
328+
// We can't return None here due to trait constraints,
329+
// so we'll mark it for rejection in the handler
330+
return SshHandler::rejected(
331+
peer_addr,
332+
Arc::clone(&self.config),
333+
Arc::clone(&self.sessions),
334+
);
335+
}
336+
337+
// Check if banned by auth rate limiter
338+
// Use try_read to avoid blocking in sync context
339+
if let Ok(is_banned) = tokio::runtime::Handle::try_current()
340+
.map(|h| h.block_on(self.auth_rate_limiter.is_banned(&ip)))
341+
{
342+
if is_banned {
343+
tracing::info!(
344+
ip = %ip,
345+
"Connection rejected from banned IP"
346+
);
347+
return SshHandler::rejected(
348+
peer_addr,
349+
Arc::clone(&self.config),
350+
Arc::clone(&self.sessions),
351+
);
352+
}
353+
}
354+
}
355+
303356
tracing::info!(
304357
peer = ?peer_addr,
305358
"New client connection"

0 commit comments

Comments
 (0)