Skip to content

Implement basic SSH server handler with russh #125

@inureyes

Description

@inureyes

Summary

Implement the core SSH server handler using the russh library's server API. This is the foundation for all SSH server functionality including authentication, command execution, and subsystems.

Parent Epic

Background

The russh library (v0.56.0) provides full SSH server support through the russh::server module. We need to implement:

  1. russh::server::Server trait - Creates handler instances for connections
  2. russh::server::Handler trait - Handles SSH protocol events

Implementation Details

1. Create Server Module Structure

src/server/
├── mod.rs              # Module exports
├── handler.rs          # SSH handler implementation
└── session.rs          # Session state management

2. Implement Server Struct

// src/server/mod.rs
use std::sync::Arc;
use tokio::sync::RwLock;

pub struct BsshServer {
    config: Arc<ServerConfig>,
    sessions: Arc<RwLock<SessionManager>>,
}

impl BsshServer {
    pub fn new(config: ServerConfig) -> Self {
        Self {
            config: Arc::new(config),
            sessions: Arc::new(RwLock::new(SessionManager::new())),
        }
    }

    pub async fn run(&self, addr: impl ToSocketAddrs) -> Result<()> {
        let russh_config = self.build_russh_config()?;
        self.run_on_address(Arc::new(russh_config), addr).await
    }

    fn build_russh_config(&self) -> Result<russh::server::Config> {
        let mut keys = Vec::new();
        for key_path in &self.config.host_keys {
            let key = russh::keys::load_secret_key(key_path, None)
                .context("Failed to load host key")?;
            keys.push(key);
        }

        Ok(russh::server::Config {
            keys,
            auth_rejection_time: Duration::from_secs(3),
            auth_rejection_time_initial: Some(Duration::from_secs(0)),
            ..Default::default()
        })
    }
}

3. Implement russh::server::Server Trait

impl russh::server::Server for BsshServer {
    type Handler = SshHandler;

    fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler {
        SshHandler::new(
            peer_addr,
            Arc::clone(&self.config),
            Arc::clone(&self.sessions),
        )
    }
}

4. Implement SSH Handler

// src/server/handler.rs
use russh::server::{Auth, Handler, Session};
use russh::{Channel, ChannelId};

pub struct SshHandler {
    peer_addr: Option<SocketAddr>,
    config: Arc<ServerConfig>,
    sessions: Arc<RwLock<SessionManager>>,
    user: Option<String>,
    channels: HashMap<ChannelId, ChannelState>,
}

#[async_trait]
impl Handler for SshHandler {
    type Error = anyhow::Error;

    /// Called when client requests authentication
    async fn auth_none(&mut self, user: &str) -> Result<Auth, Self::Error> {
        tracing::debug!("auth_none attempt for user: {}", user);
        Ok(Auth::Reject {
            proceed_with_methods: Some(MethodSet::PUBLICKEY | MethodSet::PASSWORD),
        })
    }

    /// Public key authentication - to be implemented in #126
    async fn auth_publickey(
        &mut self,
        user: &str,
        public_key: &russh::keys::ssh_key::PublicKey,
    ) -> Result<Auth, Self::Error> {
        // Placeholder - will be implemented in #126
        Ok(Auth::Reject {
            proceed_with_methods: Some(MethodSet::PASSWORD),
        })
    }

    /// Password authentication - to be implemented in #127
    async fn auth_password(
        &mut self,
        user: &str,
        password: &str,
    ) -> Result<Auth, Self::Error> {
        // Placeholder - will be implemented in #127
        Ok(Auth::Reject {
            proceed_with_methods: None,
        })
    }

    /// Called when a channel is opened
    async fn channel_open_session(
        &mut self,
        channel: Channel<Msg>,
        session: &mut Session,
    ) -> Result<bool, Self::Error> {
        let channel_id = channel.id();
        self.channels.insert(channel_id, ChannelState::new(channel));
        Ok(true)
    }

    /// Handle exec requests - placeholder for #128
    async fn exec_request(
        &mut self,
        channel_id: ChannelId,
        data: &[u8],
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        // Placeholder - will be implemented in #128
        session.channel_failure(channel_id)?;
        Ok(())
    }

    /// Handle shell requests - placeholder for #129
    async fn shell_request(
        &mut self,
        channel_id: ChannelId,
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        // Placeholder - will be implemented in #129
        session.channel_failure(channel_id)?;
        Ok(())
    }

    /// Handle PTY requests - placeholder for #129
    async fn pty_request(
        &mut self,
        channel_id: ChannelId,
        term: &str,
        col_width: u32,
        row_height: u32,
        pix_width: u32,
        pix_height: u32,
        modes: &[(russh::Pty, u32)],
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        // Placeholder - will be implemented in #129
        session.channel_success(channel_id)?;
        Ok(())
    }

    /// Handle subsystem requests (sftp) - placeholder for #132
    async fn subsystem_request(
        &mut self,
        channel_id: ChannelId,
        name: &str,
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        // Placeholder - will be implemented in #132
        session.channel_failure(channel_id)?;
        Ok(())
    }

    /// Handle data from client
    async fn data(
        &mut self,
        channel_id: ChannelId,
        data: &[u8],
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        // Forward to appropriate handler based on channel state
        if let Some(channel_state) = self.channels.get_mut(&channel_id) {
            channel_state.handle_data(data, session).await?;
        }
        Ok(())
    }

    /// Handle channel close
    async fn channel_close(
        &mut self,
        channel_id: ChannelId,
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        self.channels.remove(&channel_id);
        Ok(())
    }

    /// Handle channel EOF
    async fn channel_eof(
        &mut self,
        channel_id: ChannelId,
        session: &mut Session,
    ) -> Result<(), Self::Error> {
        if let Some(channel_state) = self.channels.get_mut(&channel_id) {
            channel_state.handle_eof(session).await?;
        }
        Ok(())
    }
}

5. Session State Management

// src/server/session.rs
pub struct SessionManager {
    sessions: HashMap<SessionId, SessionInfo>,
    max_sessions: usize,
}

pub struct SessionInfo {
    pub id: SessionId,
    pub user: String,
    pub peer_addr: SocketAddr,
    pub started_at: Instant,
    pub authenticated: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionId(u64);

pub struct ChannelState {
    channel: Channel<Msg>,
    state: ChannelMode,
}

pub enum ChannelMode {
    Idle,
    Exec { command: String },
    Shell,
    Sftp,
}

Reference Implementation

See russh examples:

Files to Create/Modify

File Action
src/server/mod.rs Create - Module exports and BsshServer struct
src/server/handler.rs Create - SshHandler implementation
src/server/session.rs Create - Session management
src/lib.rs Modify - Add server module

Testing Requirements

  1. Unit tests for session management
  2. Integration test: Connect with OpenSSH client, verify handshake completes
  3. Integration test: Verify auth rejection returns proper methods
# Test basic connection (should fail auth but complete handshake)
ssh -v -o StrictHostKeyChecking=no -p 2222 testuser@localhost

Acceptance Criteria

  • BsshServer struct created with russh configuration
  • russh::server::Server trait implemented
  • russh::server::Handler trait implemented with all required methods (placeholders OK)
  • Host key loading from file
  • Session management infrastructure in place
  • Channel state tracking
  • Server can accept connections and complete SSH handshake
  • Authentication rejection returns available methods
  • Logging for connection events

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions