Skip to content

Commit 1f2ac4f

Browse files
authored
feat: Implement basic SSH server handler with russh (#146)
* feat: Implement basic SSH server handler with russh Add the core SSH server implementation using the russh library's server API. This provides the foundation for all SSH server functionality including authentication, command execution, and subsystems. Implementation includes: - BsshServer struct with russh configuration and session management - russh::server::Server trait implementation for creating handlers - russh::server::Handler trait with all required methods: - auth_none, auth_publickey, auth_password (placeholder rejections) - channel_open_session, channel_close, channel_eof - pty_request, exec_request, shell_request, subsystem_request - data handling - ServerConfig with builder pattern for configuration - SessionManager for tracking active sessions with capacity limits - SessionInfo and SessionId for session metadata - ChannelState and ChannelMode for channel tracking - PtyConfig for terminal configuration storage - Host key loading from OpenSSH format files - Comprehensive logging for connection events All authentication methods are placeholder implementations that reject and advertise available methods. These will be implemented in follow-up issues (#126 for publickey, #127 for password). The implementation follows russh 0.56.0 API patterns and includes 25 unit tests covering session management, configuration, and handler creation. Closes #125 * fix: Resolve HIGH severity issues in SSH server implementation Fixed two HIGH severity issues found in PR #146 review: 1. Session resource leak: SshHandler Drop now properly removes sessions from SessionManager using try_write(). Previously, sessions were only logged on drop but never removed, causing session slots to accumulate. The implementation uses try_write() to avoid blocking in Drop and logs a warning if lock acquisition fails. 2. Inefficient authenticated_count(): Replaced the inefficient .collect::<Vec<_>>().len() pattern with direct .count() method. This eliminates unnecessary heap allocation and improves performance. Both changes maintain all existing tests and pass clippy checks. * chore: Finalize PR with additional tests, docs, and lint fixes - Add 17 new unit tests for comprehensive server module coverage (42 total) - Update ARCHITECTURE.md with SSH server module documentation - Update docs/architecture/README.md with server component references - Fix code formatting issues in server module - All tests passing, clippy clean, format verified
1 parent e3180cf commit 1f2ac4f

7 files changed

Lines changed: 1864 additions & 1 deletion

File tree

ARCHITECTURE.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ MPI-compatible exit code handling:
180180

181181
### Shared Module
182182

183-
Common utilities for code reuse between bssh client and potential server implementations:
183+
Common utilities for code reuse between bssh client and server implementations:
184184

185185
- **Validation**: Input validation for usernames, hostnames, paths with security checks
186186
- **Rate Limiting**: Generic token bucket rate limiter for connection/auth throttling
@@ -189,6 +189,40 @@ Common utilities for code reuse between bssh client and potential server impleme
189189

190190
The `security` and `jump::rate_limiter` modules re-export from shared for backward compatibility.
191191

192+
### SSH Server Module
193+
194+
SSH server implementation using the russh library for accepting incoming connections:
195+
196+
**Structure** (`src/server/`):
197+
- `mod.rs` - `BsshServer` struct and `russh::server::Server` trait implementation
198+
- `config.rs` - `ServerConfig` with builder pattern for server settings
199+
- `handler.rs` - `SshHandler` implementing `russh::server::Handler` trait
200+
- `session.rs` - Session state management (`SessionManager`, `SessionInfo`, `ChannelState`)
201+
202+
**Key Components**:
203+
204+
- **BsshServer**: Main server struct managing the SSH server lifecycle
205+
- Accepts connections on configured address
206+
- Loads host keys from OpenSSH format files
207+
- Configures russh with authentication settings
208+
209+
- **ServerConfig**: Configuration options with builder pattern
210+
- Host key paths and listen address
211+
- Connection limits and timeouts
212+
- Authentication method toggles (password, publickey, keyboard-interactive)
213+
214+
- **SshHandler**: Per-connection handler for SSH protocol events
215+
- Authentication handling (placeholder implementations)
216+
- Channel operations (open, close, EOF, data)
217+
- PTY, exec, shell, and subsystem request handling
218+
219+
- **SessionManager**: Tracks active sessions with configurable capacity
220+
- Session creation and cleanup
221+
- Idle session management
222+
- Authentication state tracking
223+
224+
**Current Status**: Foundation implementation with placeholder authentication. Actual authentication and command execution will be implemented in follow-up issues (#126-#132).
225+
192226
## Data Flow
193227

194228
### Command Execution Flow

docs/architecture/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ bssh is a high-performance parallel SSH command execution tool with SSH-compatib
3030

3131
- **[Exit Code Strategy](./exit-code-strategy.md)** - Main rank detection, exit code strategies, MPI compatibility
3232

33+
### Server Components
34+
35+
- **SSH Server Module** - SSH server implementation using russh (see main ARCHITECTURE.md)
36+
3337
## Navigation
3438

3539
- [Main Architecture Documentation](../../ARCHITECTURE.md)
@@ -71,6 +75,7 @@ src/
7175
├── interactive/ → Interactive Mode
7276
├── jump/ → Jump Host Support
7377
├── forward/ → Port Forwarding
78+
├── server/ → SSH Server (handler, session, config)
7479
├── shared/ → Shared utilities (validation, rate limiting, auth types, errors)
7580
├── security/ → Security utilities (re-exports from shared for compatibility)
7681
└── commands/ → Command Implementations

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod jump;
2222
pub mod node;
2323
pub mod pty;
2424
pub mod security;
25+
pub mod server;
2526
pub mod shared;
2627
pub mod ssh;
2728
pub mod ui;

src/server/config.rs

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
// Copyright 2025 Lablup Inc. and Jeongkyu Shin
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Server configuration types.
16+
//!
17+
//! This module defines configuration options for the SSH server.
18+
19+
use std::path::PathBuf;
20+
use std::time::Duration;
21+
22+
use serde::{Deserialize, Serialize};
23+
24+
/// Configuration for the SSH server.
25+
///
26+
/// Contains all settings needed to initialize and run the SSH server.
27+
#[derive(Debug, Clone, Serialize, Deserialize)]
28+
pub struct ServerConfig {
29+
/// Paths to host key files (e.g., SSH private keys).
30+
#[serde(default)]
31+
pub host_keys: Vec<PathBuf>,
32+
33+
/// Address to listen on (e.g., "0.0.0.0:2222").
34+
#[serde(default = "default_listen_address")]
35+
pub listen_address: String,
36+
37+
/// Maximum number of concurrent connections.
38+
#[serde(default = "default_max_connections")]
39+
pub max_connections: usize,
40+
41+
/// Maximum number of authentication attempts per connection.
42+
#[serde(default = "default_max_auth_attempts")]
43+
pub max_auth_attempts: u32,
44+
45+
/// Timeout for authentication in seconds.
46+
#[serde(default = "default_auth_timeout_secs")]
47+
pub auth_timeout_secs: u64,
48+
49+
/// Connection idle timeout in seconds.
50+
#[serde(default = "default_idle_timeout_secs")]
51+
pub idle_timeout_secs: u64,
52+
53+
/// Enable password authentication.
54+
#[serde(default)]
55+
pub allow_password_auth: bool,
56+
57+
/// Enable public key authentication.
58+
#[serde(default = "default_true")]
59+
pub allow_publickey_auth: bool,
60+
61+
/// Enable keyboard-interactive authentication.
62+
#[serde(default)]
63+
pub allow_keyboard_interactive: bool,
64+
65+
/// Banner message displayed to clients before authentication.
66+
#[serde(default)]
67+
pub banner: Option<String>,
68+
}
69+
70+
fn default_listen_address() -> String {
71+
"0.0.0.0:2222".to_string()
72+
}
73+
74+
fn default_max_connections() -> usize {
75+
100
76+
}
77+
78+
fn default_max_auth_attempts() -> u32 {
79+
6
80+
}
81+
82+
fn default_auth_timeout_secs() -> u64 {
83+
120
84+
}
85+
86+
fn default_idle_timeout_secs() -> u64 {
87+
0 // 0 means no timeout
88+
}
89+
90+
fn default_true() -> bool {
91+
true
92+
}
93+
94+
impl Default for ServerConfig {
95+
fn default() -> Self {
96+
Self {
97+
host_keys: Vec::new(),
98+
listen_address: default_listen_address(),
99+
max_connections: default_max_connections(),
100+
max_auth_attempts: default_max_auth_attempts(),
101+
auth_timeout_secs: default_auth_timeout_secs(),
102+
idle_timeout_secs: default_idle_timeout_secs(),
103+
allow_password_auth: false,
104+
allow_publickey_auth: true,
105+
allow_keyboard_interactive: false,
106+
banner: None,
107+
}
108+
}
109+
}
110+
111+
impl ServerConfig {
112+
/// Create a new server configuration with default values.
113+
pub fn new() -> Self {
114+
Self::default()
115+
}
116+
117+
/// Create a builder for constructing server configuration.
118+
pub fn builder() -> ServerConfigBuilder {
119+
ServerConfigBuilder::default()
120+
}
121+
122+
/// Get the authentication timeout as a Duration.
123+
pub fn auth_timeout(&self) -> Duration {
124+
Duration::from_secs(self.auth_timeout_secs)
125+
}
126+
127+
/// Get the idle timeout as a Duration.
128+
///
129+
/// Returns `None` if idle timeout is disabled (set to 0).
130+
pub fn idle_timeout(&self) -> Option<Duration> {
131+
if self.idle_timeout_secs == 0 {
132+
None
133+
} else {
134+
Some(Duration::from_secs(self.idle_timeout_secs))
135+
}
136+
}
137+
138+
/// Check if any host keys are configured.
139+
pub fn has_host_keys(&self) -> bool {
140+
!self.host_keys.is_empty()
141+
}
142+
143+
/// Add a host key path.
144+
pub fn add_host_key(&mut self, path: impl Into<PathBuf>) {
145+
self.host_keys.push(path.into());
146+
}
147+
}
148+
149+
/// Builder for constructing ServerConfig.
150+
#[derive(Debug, Default)]
151+
pub struct ServerConfigBuilder {
152+
config: ServerConfig,
153+
}
154+
155+
impl ServerConfigBuilder {
156+
/// Set the host key paths.
157+
pub fn host_keys(mut self, keys: Vec<PathBuf>) -> Self {
158+
self.config.host_keys = keys;
159+
self
160+
}
161+
162+
/// Add a host key path.
163+
pub fn host_key(mut self, key: impl Into<PathBuf>) -> Self {
164+
self.config.host_keys.push(key.into());
165+
self
166+
}
167+
168+
/// Set the listen address.
169+
pub fn listen_address(mut self, addr: impl Into<String>) -> Self {
170+
self.config.listen_address = addr.into();
171+
self
172+
}
173+
174+
/// Set the maximum number of connections.
175+
pub fn max_connections(mut self, max: usize) -> Self {
176+
self.config.max_connections = max;
177+
self
178+
}
179+
180+
/// Set the maximum authentication attempts.
181+
pub fn max_auth_attempts(mut self, max: u32) -> Self {
182+
self.config.max_auth_attempts = max;
183+
self
184+
}
185+
186+
/// Set the authentication timeout in seconds.
187+
pub fn auth_timeout_secs(mut self, secs: u64) -> Self {
188+
self.config.auth_timeout_secs = secs;
189+
self
190+
}
191+
192+
/// Set the idle timeout in seconds.
193+
pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
194+
self.config.idle_timeout_secs = secs;
195+
self
196+
}
197+
198+
/// Enable or disable password authentication.
199+
pub fn allow_password_auth(mut self, allow: bool) -> Self {
200+
self.config.allow_password_auth = allow;
201+
self
202+
}
203+
204+
/// Enable or disable public key authentication.
205+
pub fn allow_publickey_auth(mut self, allow: bool) -> Self {
206+
self.config.allow_publickey_auth = allow;
207+
self
208+
}
209+
210+
/// Enable or disable keyboard-interactive authentication.
211+
pub fn allow_keyboard_interactive(mut self, allow: bool) -> Self {
212+
self.config.allow_keyboard_interactive = allow;
213+
self
214+
}
215+
216+
/// Set the banner message.
217+
pub fn banner(mut self, banner: impl Into<String>) -> Self {
218+
self.config.banner = Some(banner.into());
219+
self
220+
}
221+
222+
/// Build the ServerConfig.
223+
pub fn build(self) -> ServerConfig {
224+
self.config
225+
}
226+
}
227+
228+
#[cfg(test)]
229+
mod tests {
230+
use super::*;
231+
232+
#[test]
233+
fn test_default_config() {
234+
let config = ServerConfig::default();
235+
assert!(config.host_keys.is_empty());
236+
assert_eq!(config.listen_address, "0.0.0.0:2222");
237+
assert_eq!(config.max_connections, 100);
238+
assert_eq!(config.max_auth_attempts, 6);
239+
assert!(!config.allow_password_auth);
240+
assert!(config.allow_publickey_auth);
241+
}
242+
243+
#[test]
244+
fn test_config_builder() {
245+
let config = ServerConfig::builder()
246+
.host_key("/etc/ssh/ssh_host_ed25519_key")
247+
.listen_address("127.0.0.1:22")
248+
.max_connections(50)
249+
.max_auth_attempts(3)
250+
.allow_password_auth(true)
251+
.banner("Welcome to bssh server!")
252+
.build();
253+
254+
assert_eq!(config.host_keys.len(), 1);
255+
assert_eq!(config.listen_address, "127.0.0.1:22");
256+
assert_eq!(config.max_connections, 50);
257+
assert_eq!(config.max_auth_attempts, 3);
258+
assert!(config.allow_password_auth);
259+
assert_eq!(config.banner, Some("Welcome to bssh server!".to_string()));
260+
}
261+
262+
#[test]
263+
fn test_auth_timeout() {
264+
let config = ServerConfig::default();
265+
assert_eq!(config.auth_timeout(), Duration::from_secs(120));
266+
}
267+
268+
#[test]
269+
fn test_idle_timeout() {
270+
let mut config = ServerConfig::default();
271+
assert!(config.idle_timeout().is_none());
272+
273+
config.idle_timeout_secs = 300;
274+
assert_eq!(config.idle_timeout(), Some(Duration::from_secs(300)));
275+
}
276+
277+
#[test]
278+
fn test_has_host_keys() {
279+
let mut config = ServerConfig::default();
280+
assert!(!config.has_host_keys());
281+
282+
config.add_host_key("/path/to/key");
283+
assert!(config.has_host_keys());
284+
}
285+
286+
#[test]
287+
fn test_config_new() {
288+
let config = ServerConfig::new();
289+
assert!(config.host_keys.is_empty());
290+
assert_eq!(config.listen_address, "0.0.0.0:2222");
291+
}
292+
293+
#[test]
294+
fn test_builder_host_keys_vec() {
295+
let config = ServerConfig::builder()
296+
.host_keys(vec!["/path/to/key1".into(), "/path/to/key2".into()])
297+
.build();
298+
299+
assert_eq!(config.host_keys.len(), 2);
300+
}
301+
302+
#[test]
303+
fn test_builder_auth_timeout() {
304+
let config = ServerConfig::builder().auth_timeout_secs(60).build();
305+
306+
assert_eq!(config.auth_timeout_secs, 60);
307+
assert_eq!(config.auth_timeout(), Duration::from_secs(60));
308+
}
309+
310+
#[test]
311+
fn test_builder_idle_timeout() {
312+
let config = ServerConfig::builder().idle_timeout_secs(600).build();
313+
314+
assert_eq!(config.idle_timeout_secs, 600);
315+
assert_eq!(config.idle_timeout(), Some(Duration::from_secs(600)));
316+
}
317+
}

0 commit comments

Comments
 (0)