Skip to content

Implement IP-based access control #141

@inureyes

Description

@inureyes

Summary

Implement IP-based access control to allow or deny connections from specific IP addresses or CIDR ranges.

Parent Epic

Implementation Details

1. IP Access Control

// src/server/security/access.rs
use ipnetwork::IpNetwork;
use std::net::IpAddr;

/// IP-based access control
pub struct IpAccessControl {
    /// Allowed IP ranges (whitelist mode)
    allowed: Vec<IpNetwork>,
    /// Blocked IP ranges (blacklist)
    blocked: Vec<IpNetwork>,
    /// Default policy when no rules match
    default_policy: AccessPolicy,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AccessPolicy {
    Allow,
    Deny,
}

impl IpAccessControl {
    pub fn new() -> Self {
        Self {
            allowed: Vec::new(),
            blocked: Vec::new(),
            default_policy: AccessPolicy::Allow,
        }
    }

    pub fn from_config(config: &SecurityConfig) -> Result<Self> {
        let mut ctrl = Self::new();

        for cidr in &config.allowed_ips {
            let network: IpNetwork = cidr.parse()
                .context(format!("Invalid allowed_ips CIDR: {}", cidr))?;
            ctrl.allowed.push(network);
        }

        for cidr in &config.blocked_ips {
            let network: IpNetwork = cidr.parse()
                .context(format!("Invalid blocked_ips CIDR: {}", cidr))?;
            ctrl.blocked.push(network);
        }

        // If allowed list is specified, default to deny
        if !ctrl.allowed.is_empty() {
            ctrl.default_policy = AccessPolicy::Deny;
        }

        Ok(ctrl)
    }

    /// Check if IP is allowed to connect
    pub fn check(&self, ip: &IpAddr) -> AccessPolicy {
        // Check blocked list first (blacklist takes priority)
        for network in &self.blocked {
            if network.contains(*ip) {
                tracing::debug!("IP {} blocked by rule {}", ip, network);
                return AccessPolicy::Deny;
            }
        }

        // Check allowed list (whitelist)
        if !self.allowed.is_empty() {
            for network in &self.allowed {
                if network.contains(*ip) {
                    return AccessPolicy::Allow;
                }
            }
            // Not in whitelist
            tracing::debug!("IP {} not in allowed list", ip);
            return AccessPolicy::Deny;
        }

        self.default_policy
    }

    /// Add allowed range
    pub fn allow(&mut self, network: IpNetwork) {
        self.allowed.push(network);
    }

    /// Add blocked range
    pub fn block(&mut self, network: IpNetwork) {
        self.blocked.push(network);
    }
}

2. Integrate at Connection Level

// Update src/server/mod.rs
impl russh::server::Server for BsshServer {
    fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Option<Self::Handler> {
        // Check IP access control before creating handler
        if let Some(addr) = peer_addr {
            let ip = addr.ip();

            // Check IP access control
            if self.config.security.ip_access.check(&ip) == AccessPolicy::Deny {
                tracing::info!("Connection rejected from {} (IP access control)", ip);
                return None;
            }

            // Check rate limit ban
            if self.rate_limiter.is_banned_sync(&ip) {
                tracing::info!("Connection rejected from {} (banned)", ip);
                return None;
            }
        }

        Some(SshHandler::new(peer_addr, ...))
    }
}

3. Dynamic Updates

impl IpAccessControl {
    /// Add IP to block list at runtime
    pub fn block_ip(&mut self, ip: IpAddr) {
        let network = IpNetwork::from(ip);
        if !self.blocked.contains(&network) {
            self.blocked.push(network);
            tracing::info!("Blocked IP: {}", ip);
        }
    }

    /// Remove IP from block list
    pub fn unblock_ip(&mut self, ip: IpAddr) {
        let network = IpNetwork::from(ip);
        self.blocked.retain(|n| n != &network);
        tracing::info!("Unblocked IP: {}", ip);
    }

    /// Reload from config
    pub fn reload(&mut self, config: &SecurityConfig) -> Result<()> {
        *self = Self::from_config(config)?;
        Ok(())
    }
}

Configuration

security:
  # Whitelist mode: only allow these IPs
  allowed_ips:
    - 10.0.0.0/8
    - 192.168.0.0/16
    - 172.16.0.0/12

  # Blacklist: always deny these IPs
  blocked_ips:
    - 192.168.100.0/24
    - 10.10.10.10/32

Files to Create/Modify

File Action
src/server/security/access.rs Create - IP access control
src/server/security/mod.rs Modify - Add access module
src/server/mod.rs Modify - Integrate at connection level

Testing Requirements

  1. Unit test: CIDR matching
  2. Unit test: Whitelist mode (default deny)
  3. Unit test: Blacklist takes priority
  4. Unit test: Single IP blocking
  5. Integration test: Connection rejection

Acceptance Criteria

  • IpAccessControl implementation
  • CIDR range support
  • Whitelist mode (allowed_ips)
  • Blacklist mode (blocked_ips)
  • Blacklist takes priority over whitelist
  • Connection-level rejection
  • Runtime updates support
  • Tests passing

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