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 ports443, 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¶
- Use default_policy = "block" (whitelist approach)
- More secure than blacklist
-
Explicit allow rules are easier to audit
-
Set rule priorities
- BLOCK rules implicitly higher priority than ALLOW
- Use numeric priorities for same-action rules
-
Higher number = evaluated first
-
Use group inheritance
- Create groups for common rule sets
- Users inherit group rules
-
Easier to manage permissions
-
Specific before general
- Put specific BLOCK rules before general ALLOW rules
-
Example: Block admin.example.com before allowing *.example.com
-
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.