Skip to content

Latest commit

 

History

History
456 lines (369 loc) · 11.9 KB

File metadata and controls

456 lines (369 loc) · 11.9 KB

ACL Engine - Implementation Guide

This document explains the ACL (Access Control List) engine implementation in RustSocks for fine-grained access control based on users, groups, IP addresses, domains, and ports.

Core Structures

// src/acl/types.rs

use ipnet::IpNet;
use std::net::IpAddr;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Action {
    Allow,
    Block,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Protocol {
    Tcp,
    Udp,
    Both,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AclRule {
    pub action: Action,
    pub description: String,
    pub destinations: Vec<DestinationMatcher>,
    pub ports: Vec<PortMatcher>,
    pub protocols: Vec<Protocol>,
    pub priority: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DestinationMatcher {
    Ip(IpAddr),
    Cidr(IpNet),
    Domain(String),
    DomainWildcard(String), // Format: "*.example.com"
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PortMatcher {
    Single(u16),
    Range(u16, u16),
    Multiple(Vec<u16>),
    Any, // "*"
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAcl {
    pub username: String,
    pub groups: Vec<String>,
    pub rules: Vec<AclRule>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupAcl {
    pub name: String,
    pub rules: Vec<AclRule>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AclConfig {
    pub global: GlobalAclConfig,
    pub users: Vec<UserAcl>,
    pub groups: Vec<GroupAcl>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalAclConfig {
    pub default_policy: Action,
}

#[derive(Debug, Clone, PartialEq)]
pub enum AclDecision {
    Allow,
    Block,
}

Matching Logic

The ACL engine supports multiple matching strategies:

  • IP exact match: Match specific IPv4 or IPv6 address
  • CIDR ranges: Match address blocks (e.g., 10.0.0.0/8)
  • Domain exact match: Case-insensitive domain matching
  • Wildcard domains: Patterns like *.example.com, api.*.com
  • Port ranges: 8000-9000, single ports 443, or any port *
  • Protocol filtering: TCP, UDP, or both

Example ACL configuration (config/acl.toml):

[global]
default_policy = "block"  # Block by default (whitelist approach)

[[users]]
username = "alice"
groups = ["developers"]

  [[users.rules]]
  action = "block"
  description = "Block admin panel"
  destinations = ["admin.company.com"]
  ports = ["*"]
  protocols = ["tcp"]
  priority = 1000

  [[users.rules]]
  action = "allow"
  description = "Allow HTTPS anywhere"
  destinations = ["0.0.0.0/0"]
  ports = ["443"]
  protocols = ["tcp"]
  priority = 100

[[groups]]
name = "developers"

  [[groups.rules]]
  action = "allow"
  description = "Dev servers"
  destinations = ["*.dev.company.com"]
  ports = ["*"]
  protocols = ["both"]
  priority = 50

ACL Engine

The ACL engine evaluates rules in priority order with BLOCK rules taking precedence:

// src/acl/engine.rs

use super::types::*;
use std::sync::Arc;
use tokio::sync::RwLock;

pub struct AclEngine {
    config: Arc<RwLock<AclConfig>>,
}

impl AclEngine {
    pub fn new(config: AclConfig) -> Self {
        Self {
            config: Arc::new(RwLock::new(config)),
        }
    }

    /// Evaluate ACL for a connection attempt
    /// Returns (Decision, matched_rule_description)
    pub async fn evaluate(
        &self,
        user: &str,
        dest: &Address,
        port: u16,
        protocol: &Protocol,
    ) -> (AclDecision, Option<String>) {
        let config = self.config.read().await;

        // 1. Collect all rules for this user (user rules + group rules)
        let mut all_rules = Vec::new();

        if let Some(user_acl) = config.users.iter().find(|u| u.username == user) {
            // Add user's direct rules
            all_rules.extend(user_acl.rules.iter().cloned());

            // Add rules from user's groups
            for group_name in &user_acl.groups {
                if let Some(group) = config.groups.iter().find(|g| g.name == group_name) {
                    all_rules.extend(group.rules.iter().cloned());
                }
            }
        }

        // 2. Sort rules by priority
        // BLOCK rules are evaluated before ALLOW rules (security first)
        all_rules.sort_by(|a, b| {
            match (&a.action, &b.action) {
                (Action::Block, Action::Allow) => std::cmp::Ordering::Less,
                (Action::Allow, Action::Block) => std::cmp::Ordering::Greater,
                _ => b.priority.cmp(&a.priority),
            }
        });

        // 3. Evaluate rules in order until first match
        for rule in &all_rules {
            if rule.matches(dest, port, protocol) {
                let decision = match rule.action {
                    Action::Allow => AclDecision::Allow,
                    Action::Block => AclDecision::Block,
                };
                return (decision, Some(rule.description.clone()));
            }
        }

        // 4. No rule matched - apply default policy
        let decision = match config.global.default_policy {
            Action::Allow => AclDecision::Allow,
            Action::Block => AclDecision::Block,
        };

        (decision, None)
    }

    /// Hot reload ACL configuration
    pub async fn reload(&self, new_config: AclConfig) -> Result<(), String> {
        // Validate config first
        new_config.validate()?;

        // Atomic swap
        let mut config = self.config.write().await;
        *config = new_config;

        Ok(())
    }

    /// Get current config
    pub async fn get_config(&self) -> AclConfig {
        self.config.read().await.clone()
    }
}

impl AclConfig {
    /// Validate configuration
    pub fn validate(&self) -> Result<(), String> {
        // Check for duplicate users
        let mut seen_users = std::collections::HashSet::new();
        for user in &self.users {
            if !seen_users.insert(&user.username) {
                return Err(format!("Duplicate user: {}", user.username));
            }
        }

        // Check for duplicate groups
        let mut seen_groups = std::collections::HashSet::new();
        for group in &self.groups {
            if !seen_groups.insert(&group.name) {
                return Err(format!("Duplicate group: {}", group.name));
            }
        }

        // Check that user groups exist
        for user in &self.users {
            for group_name in &user.groups {
                if !self.groups.iter().any(|g| &g.name == group_name) {
                    return Err(format!(
                        "User '{}' references non-existent group '{}'",
                        user.username, group_name
                    ));
                }
            }
        }

        Ok(())
    }
}

Hot Reload Mechanism

The ACL engine supports zero-downtime configuration reloading via file watching:

// src/acl/watcher.rs

use super::engine::AclEngine;
use notify::{Watcher, RecursiveMode, Event, EventKind};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::mpsc;

pub struct AclWatcher {
    acl_engine: Arc<AclEngine>,
    config_path: PathBuf,
}

impl AclWatcher {
    pub fn new(acl_engine: Arc<AclEngine>, config_path: PathBuf) -> Self {
        Self { acl_engine, config_path }
    }

    pub async fn start(self) -> Result<(), Box<dyn std::error::Error>> {
        let (tx, mut rx) = mpsc::channel(10);
        let config_path = self.config_path.clone();

        // Create file watcher
        let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| {
            if let Ok(event) = res {
                if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
                    let _ = tx.blocking_send(());
                }
            }
        })?;

        watcher.watch(&config_path, RecursiveMode::NonRecursive)?;

        // Event loop
        tokio::spawn(async move {
            let _watcher = watcher;

            while rx.recv().await.is_some() {
                // Small delay to ensure file is fully written
                tokio::time::sleep(std::time::Duration::from_millis(100)).await;

                match load_acl_config_sync(&config_path) {
                    Ok(new_config) => {
                        match self.acl_engine.reload(new_config).await {
                            Ok(_) => {
                                // Reload successful
                            }
                            Err(e) => {
                                // Keep previous config on validation error
                                eprintln!("Failed to reload ACL config: {}", e);
                            }
                        }
                    }
                    Err(e) => {
                        eprintln!("Failed to load ACL config: {}", e);
                    }
                }
            }
        });

        Ok(())
    }
}

Integration with Connection Handler

// Example from src/server/handler.rs

use crate::acl::engine::AclEngine;
use crate::acl::types::{AclDecision, Protocol};
use std::sync::Arc;

pub async fn handle_socks_connection(
    user: &str,
    dest_addr: &Address,
    dest_port: u16,
    protocol: &Protocol,
    acl_engine: Arc<AclEngine>,
) -> Result<()> {
    // Evaluate ACL
    let (decision, matched_rule) = acl_engine.evaluate(
        user,
        dest_addr,
        dest_port,
        protocol,
    ).await;

    match decision {
        AclDecision::Block => {
            // Send SOCKS5 error response
            send_socks5_error(&mut stream, ErrorCode::ConnectionNotAllowed).await?;
            return Ok(());
        }
        AclDecision::Allow => {
            // Proceed with connection
        }
    }

    Ok(())
}

REST API Endpoints

The ACL engine provides REST endpoints for management:

# Get all users
curl http://127.0.0.1:9090/api/acl/users

# Get all groups
curl http://127.0.0.1:9090/api/acl/groups

# Get user details
curl http://127.0.0.1:9090/api/acl/users/alice

# Get group details
curl http://127.0.0.1:9090/api/acl/groups/developers

# Reload ACL config
curl -X POST http://127.0.0.1:9090/api/admin/reload-acl

Performance Characteristics

Evaluation Performance:

  • Depends on rule count, matchers, and destination mix
  • Typically fast enough to stay well below network latency
  • Hot reload does not block rule evaluation (read-locked config)

Configuration Best Practices

  1. Use default_policy = "block" (whitelist approach)

    • More secure than blacklist
    • Explicit allow rules are easier to audit
  2. Set rule priorities

    • BLOCK rules implicitly higher priority than ALLOW
    • Use numeric priorities for same-action rules
    • Higher number = evaluated first
  3. Use group inheritance

    • Create groups for common rule sets
    • Users inherit group rules
    • Easier to manage permissions
  4. Specific before general

    • Put specific BLOCK rules before general ALLOW rules
    • Example: Block admin.example.com before allowing *.example.com
  5. Watch your ACL file

    [acl]
    enabled = true
    config_file = "config/acl.toml"
    watch = true  # Enable hot reload

Monitoring & Observability

ACL configuration and decisions can be inspected via the API:

GET /api/acl/groups   # Groups and their rules
GET /api/acl/users    # Users and their rules
GET /api/acl/global   # Default policy
POST /api/acl/test    # Evaluate a rule decision

Summary

The RustSocks ACL engine provides:

Granular control - per-user and per-group rules ✅ Security first - BLOCK rules have priority ✅ Zero-downtime - hot reload support ✅ High performance - microsecond evaluation ✅ Flexible matching - IP, CIDR, domains, wildcards, ports ✅ Auditable - every decision is logged ✅ Production-ready - comprehensive validation

The implementation leverages Rust's type system for safety and Tokio for async operations, making it both fast and maintainable.