Craton Shield

vs-modbus-monitor-emb

vs-modbus-monitor-emb

Modbus RTU/TCP intrusion detection for embedded IoT gateways.

Overview

Lightweight Modbus monitor for constrained IoT gateways and edge devices. This crate shares detection mechanisms with vs-modbus-monitor-ind (industrial variant) but integrates with the embedded runtime (vs-runtime-embedded) and uses vs-types-embedded types. Use this crate for resource-constrained IoT deployments; use vs-modbus-monitor-ind for SCADA/DCS/PLC environments with IEC 62443 zone/conduit requirements.

All state is stack-allocated with fixed-size arrays. No heap required.

Detection Mechanisms

MechanismDescriptionDefault
Unit ID filteringPer-unit-ID allowlist/blocklist. First-match-wins.Allow all
Function code enforcementPer-unit policy: Any, ReadOnly, or WriteOnly.Any
Write protectionBlock write operations to specific units via ReadOnly policy.Disabled
Register range enforcementPer-unit minimum and maximum register address (inclusive).Full range
Rate limitingPer-unit token bucket with automatic refill. Buckets expire after 5 minutes of inactivity.Unlimited
Invalid unit ID detectionFlags reserved Modbus unit IDs (248-255).Enabled
TCP source IP filteringAllow/block by source IP prefix (CIDR-style). TCP transport only.Allow all
Exception response trackingPer-unit exception count in a 60-second window. Alerts when threshold exceeded.10 per 60s
Timestamp validationDetects clock manipulation via monotonicity and gap checks.Enabled

Configuration

use vs_modbus_monitor::{ModbusMonitor, UnitAction, FunctionPolicy};
use vs_types_embedded::IpAction;

let mut monitor = ModbusMonitor::new();               // allow-by-default
// let mut monitor = ModbusMonitor::new_deny_default(); // deny-by-default

// Unit ID rules.
monitor.add_rule(
    1,                        // unit ID
    UnitAction::Allow,        // action
    FunctionPolicy::ReadOnly, // only reads allowed
    0,                        // register min
    999,                      // register max
    50,                       // 50 requests/sec
).unwrap();

monitor.add_rule(
    247,
    UnitAction::Block,
    FunctionPolicy::Any,
    0,
    u16::MAX,
    0,
).unwrap();

// TCP IP filters (CIDR prefix matching).
monitor.add_ip_filter([192, 168, 1, 0], 24, IpAction::Allow).unwrap();
monitor.add_ip_filter([10, 0, 0, 0], 8, IpAction::Block).unwrap();

Inspection

// RTU messages:
let result = monitor.inspect_rtu(&rtu_msg);
// result.allowed     — whether the message should be forwarded
// result.alert_count — number of alerts (0-4)
// result.alerts      — array of SecurityAlert structs

// TCP messages (includes IP filter checks):
let result = monitor.inspect_tcp(&tcp_msg);

// Exception tracking (call separately when a response contains an exception):
if let Some(alert) = monitor.record_exception(unit_id, exception, timestamp_us, source_type) {
    // exception flood alert
}

Alert Source IDs

IDMeaning
1Invalid / reserved unit ID (248-255)
2Unknown function code
3Unit ID blocked by rule
4Function code policy violation
5Register address out of range
6Rate limit exceeded
7TCP source IP blocked
8Exception response flood
9Timestamp anomaly

Limits

  • 32 unit ID rules max
  • 16 rate-limit buckets (5-minute expiry)
  • 16 TCP IP filter entries
  • 32 exception counters (per unit)

Errors

  • VsError::ResourceExhausted — rule, IP filter, or bucket capacity full
  • VsError::InvalidInput — invalid index on removal

Changelog

See the workspace CHANGELOG for version history.

Feature Flags

See core/docs/feature-flags.md for the full workspace feature reference.

License

Apache-2.0. See LICENSE.