///|
/// Decision made before a frame enters a server or gateway pipeline.
pub(all) enum GuardDecision {
AdmitFrame
RejectFrame
DeferFrame
DuplicateFrame
QuotaFrame
OversizeFrame
} derive(Debug, Eq)
///|
/// Reason included in a protocol admission record.
pub(all) enum GuardReason {
GuardAccepted
GuardBadUnit
GuardBadFunction
GuardBadLength
GuardDuplicate
GuardRateLimited
GuardTooLarge
GuardUnknownPeer
} derive(Debug, Eq)
///|
/// Per-peer frame quota and size policy.
pub(all) struct ProtocolGuardPolicy {
max_frame_bytes : Int
max_frames : Int
window : Int
duplicate_window : Int
allow_broadcast : Bool
functions : Array[Byte]
}
///|
pub fn ProtocolGuardPolicy::new(
max_frame_bytes? : Int = 260,
max_frames? : Int = 100,
window? : Int = 1000,
duplicate_window? : Int = 200,
allow_broadcast? : Bool = false,
) -> Result[ProtocolGuardPolicy, ModbusError] {
if max_frame_bytes < 8 || max_frames < 1 || window < 1 || duplicate_window < 0 {
return Err(InvalidData)
}
Ok({
max_frame_bytes,
max_frames,
window,
duplicate_window,
allow_broadcast,
functions: [1, 2, 3, 4, 5, 6, 15, 16, 22, 23],
})
}
///|
pub fn ProtocolGuardPolicy::max_frame_bytes(self : ProtocolGuardPolicy) -> Int {
self.max_frame_bytes
}
///|
pub fn ProtocolGuardPolicy::max_frames(self : ProtocolGuardPolicy) -> Int {
self.max_frames
}
///|
pub fn ProtocolGuardPolicy::window(self : ProtocolGuardPolicy) -> Int {
self.window
}
///|
pub fn ProtocolGuardPolicy::duplicate_window(self : ProtocolGuardPolicy) -> Int {
self.duplicate_window
}
///|
pub fn ProtocolGuardPolicy::allow_broadcast(self : ProtocolGuardPolicy) -> Bool {
self.allow_broadcast
}
///|
pub fn ProtocolGuardPolicy::function_allowed(
self : ProtocolGuardPolicy,
function : Byte,
) -> Bool {
for allowed in self.functions {
if allowed == function {
return true
}
}
false
}
///|
pub fn ProtocolGuardPolicy::add_function(
self : ProtocolGuardPolicy,
function : Byte,
) -> Result[Unit, ModbusError] {
if function == 0 {
return Err(InvalidFunction)
}
if !self.function_allowed(function) {
self.functions.push(function)
}
Ok(())
}
///|
/// Immutable frame fingerprint for duplicate detection.
pub(all) struct FrameFingerprint {
unit_id : Byte
function : Byte
data_length : Int
checksum : Int
}
///|
pub fn FrameFingerprint::from_frame(frame : Frame) -> FrameFingerprint {
let mut checksum = frame.unit_id.to_int() * 257 + frame.pdu.function.to_int()
for byte in frame.pdu.data {
checksum = checksum * 33 + byte.to_int()
}
{
unit_id: frame.unit_id,
function: frame.pdu.function,
data_length: frame.pdu.data.length(),
checksum,
}
}
///|
pub fn FrameFingerprint::unit_id(self : FrameFingerprint) -> Byte {
self.unit_id
}
///|
pub fn FrameFingerprint::function(self : FrameFingerprint) -> Byte {
self.function
}
///|
pub fn FrameFingerprint::data_length(self : FrameFingerprint) -> Int {
self.data_length
}
///|
pub fn FrameFingerprint::checksum(self : FrameFingerprint) -> Int {
self.checksum
}
///|
pub fn FrameFingerprint::same(
self : FrameFingerprint,
other : FrameFingerprint,
) -> Bool {
self.unit_id == other.unit_id &&
self.function == other.function &&
self.data_length == other.data_length &&
self.checksum == other.checksum
}
///|
/// Admission event retained for metrics and support diagnostics.
pub(all) struct GuardEvent {
sequence : Int
timestamp : Int
peer : String
decision : GuardDecision
reason : GuardReason
unit_id : Byte
function : Byte
frame_bytes : Int
fingerprint : FrameFingerprint
}
///|
pub fn GuardEvent::new(
sequence : Int,
timestamp : Int,
peer : String,
decision : GuardDecision,
reason : GuardReason,
frame : Frame,
) -> Result[GuardEvent, ModbusError] {
if sequence < 1 || timestamp < 0 || peer == "" {
return Err(InvalidData)
}
Ok({
sequence,
timestamp,
peer,
decision,
reason,
unit_id: frame.unit_id,
function: frame.pdu.function,
frame_bytes: frame.pdu.data.length() + 2,
fingerprint: FrameFingerprint::from_frame(frame),
})
}
///|
pub fn GuardEvent::sequence(self : GuardEvent) -> Int {
self.sequence
}
///|
pub fn GuardEvent::timestamp(self : GuardEvent) -> Int {
self.timestamp
}
///|
pub fn GuardEvent::peer(self : GuardEvent) -> String {
self.peer
}
///|
pub fn GuardEvent::decision(self : GuardEvent) -> GuardDecision {
self.decision
}
///|
pub fn GuardEvent::reason(self : GuardEvent) -> GuardReason {
self.reason
}
///|
pub fn GuardEvent::unit_id(self : GuardEvent) -> Byte {
self.unit_id
}
///|
pub fn GuardEvent::function(self : GuardEvent) -> Byte {
self.function
}
///|
pub fn GuardEvent::frame_bytes(self : GuardEvent) -> Int {
self.frame_bytes
}
///|
pub fn GuardEvent::fingerprint(self : GuardEvent) -> FrameFingerprint {
self.fingerprint
}
///|
pub fn guard_decision_name(value : GuardDecision) -> String {
match value {
AdmitFrame => "admit"
RejectFrame => "reject"
DeferFrame => "defer"
DuplicateFrame => "duplicate"
QuotaFrame => "quota"
OversizeFrame => "oversize"
}
}
///|
pub fn guard_reason_name(value : GuardReason) -> String {
match value {
GuardAccepted => "accepted"
GuardBadUnit => "bad-unit"
GuardBadFunction => "bad-function"
GuardBadLength => "bad-length"
GuardDuplicate => "duplicate"
GuardRateLimited => "rate-limited"
GuardTooLarge => "too-large"
GuardUnknownPeer => "unknown-peer"
}
}
///|
/// Per-peer admission state.
pub(all) struct PeerGuardState {
peer : String
timestamps : Array[Int]
fingerprints : Array[(FrameFingerprint, Int)]
mut accepted : Int
mut rejected : Int
}
///|
pub fn PeerGuardState::new(
peer : String,
) -> Result[PeerGuardState, ModbusError] {
if peer == "" {
Err(InvalidData)
} else {
Ok({ peer, timestamps: [], fingerprints: [], accepted: 0, rejected: 0 })
}
}
///|
pub fn PeerGuardState::peer(self : PeerGuardState) -> String {
self.peer
}
///|
pub fn PeerGuardState::accepted(self : PeerGuardState) -> Int {
self.accepted
}
///|
pub fn PeerGuardState::rejected(self : PeerGuardState) -> Int {
self.rejected
}
///|
pub fn PeerGuardState::prune(
self : PeerGuardState,
now : Int,
policy : ProtocolGuardPolicy,
) -> Result[Unit, ModbusError] {
if now < 0 {
return Err(InvalidData)
}
while self.timestamps.length() > 0 &&
now - self.timestamps[0] > policy.window() {
ignore(self.timestamps.remove(0))
}
while self.fingerprints.length() > 0 &&
now - self.fingerprints[0].1 > policy.duplicate_window() {
ignore(self.fingerprints.remove(0))
}
Ok(())
}
///|
pub fn PeerGuardState::frame_count(self : PeerGuardState) -> Int {
self.timestamps.length()
}
///|
pub fn PeerGuardState::duplicate(
self : PeerGuardState,
fingerprint : FrameFingerprint,
now : Int,
policy : ProtocolGuardPolicy,
) -> Bool {
for item in self.fingerprints {
if item.0.same(fingerprint) && now - item.1 <= policy.duplicate_window() {
return true
}
}
false
}
///|
pub fn PeerGuardState::record(
self : PeerGuardState,
fingerprint : FrameFingerprint,
now : Int,
accepted : Bool,
) -> Unit {
self.timestamps.push(now)
self.fingerprints.push((fingerprint, now))
if accepted {
self.accepted += 1
} else {
self.rejected += 1
}
}
///|
/// A protocol admission guard safe to use before decoding application data.
pub(all) struct ProtocolGuard {
policy : ProtocolGuardPolicy
peers : Array[PeerGuardState]
events : Array[GuardEvent]
max_peers : Int
max_events : Int
mut sequence : Int
}
///|
pub fn ProtocolGuard::new(
policy : ProtocolGuardPolicy,
max_peers? : Int = 1024,
max_events? : Int = 8192,
) -> Result[ProtocolGuard, ModbusError] {
if max_peers < 1 || max_events < 1 {
Err(InvalidData)
} else {
Ok({ policy, peers: [], events: [], max_peers, max_events, sequence: 0 })
}
}
///|
pub fn ProtocolGuard::peer_count(self : ProtocolGuard) -> Int {
self.peers.length()
}
///|
pub fn ProtocolGuard::event_count(self : ProtocolGuard) -> Int {
self.events.length()
}
///|
pub fn ProtocolGuard::policy(self : ProtocolGuard) -> ProtocolGuardPolicy {
self.policy
}
///|
fn ProtocolGuard::peer(self : ProtocolGuard, name : String) -> PeerGuardState? {
for peer in self.peers {
if peer.peer() == name {
return Some(peer)
}
}
None
}
///|
fn ProtocolGuard::get_or_create_peer(
self : ProtocolGuard,
name : String,
) -> Result[PeerGuardState, ModbusError] {
match self.peer(name) {
Some(peer) => Ok(peer)
None => {
if self.peers.length() >= self.max_peers {
return Err(CapacityExceeded)
}
let peer = match PeerGuardState::new(name) {
Ok(value) => value
Err(error) => return Err(error)
}
self.peers.push(peer)
Ok(peer)
}
}
}
///|
/// Validate and record a frame before transport-specific decoding.
pub fn ProtocolGuard::admit(
self : ProtocolGuard,
peer_name : String,
frame : Frame,
now : Int,
) -> Result[GuardDecision, ModbusError] {
if peer_name == "" || now < 0 {
return Err(InvalidData)
}
let peer = match self.get_or_create_peer(peer_name) {
Ok(value) => value
Err(error) => return Err(error)
}
ignore(peer.prune(now, self.policy))
let fingerprint = FrameFingerprint::from_frame(frame)
let mut decision = AdmitFrame
let mut reason = GuardAccepted
if !is_valid_unit_id(frame.unit_id, broadcast=self.policy.allow_broadcast) {
decision = RejectFrame
reason = GuardBadUnit
} else if !self.policy.function_allowed(frame.pdu.function) {
decision = RejectFrame
reason = GuardBadFunction
} else if frame.pdu.data.length() + 2 > self.policy.max_frame_bytes {
decision = OversizeFrame
reason = GuardTooLarge
} else if peer.duplicate(fingerprint, now, self.policy) {
decision = DuplicateFrame
reason = GuardDuplicate
} else if peer.frame_count() >= self.policy.max_frames() {
decision = QuotaFrame
reason = GuardRateLimited
}
let accepted = decision == AdmitFrame
peer.record(fingerprint, now, accepted)
self.sequence += 1
if self.events.length() >= self.max_events {
ignore(self.events.remove(0))
}
match
GuardEvent::new(self.sequence, now, peer_name, decision, reason, frame) {
Ok(event) => self.events.push(event)
Err(_) => ()
}
Ok(decision)
}
///|
pub fn ProtocolGuard::recent_events(
self : ProtocolGuard,
limit : Int,
) -> Result[Array[GuardEvent], ModbusError] {
if limit < 1 {
return Err(InvalidQuantity)
}
let result : Array[GuardEvent] = []
let start = if self.events.length() > limit {
self.events.length() - limit
} else {
0
}
for i in start.. Result[(Int, Int), ModbusError] {
match self.peer(name) {
Some(peer) => Ok((peer.accepted(), peer.rejected()))
None => Err(InvalidData)
}
}
///|
pub fn ProtocolGuard::decision_counts(self : ProtocolGuard) -> Array[Int] {
let counts : Array[Int] = [0, 0, 0, 0, 0, 0]
for event in self.events {
let index = match event.decision() {
AdmitFrame => 0
RejectFrame => 1
DeferFrame => 2
DuplicateFrame => 3
QuotaFrame => 4
OversizeFrame => 5
}
counts[index] += 1
}
counts
}
///|
/// Flush historical events while retaining peer counters.
pub fn ProtocolGuard::clear_events(self : ProtocolGuard) -> Unit {
self.events.clear()
}