Access control

Peer ACL, accept rate limits, and quota trackers in hopf-core. These sit on TcpListenerConfig (and related accept paths) so unwanted peers never reach ProtocolHandler::connected. Authentication (identity) is separate — see Auth.

Architecture

accept()
  │
  ├─ PeerAcl::allows(peer)?     else close + TelemetryHook::on_error
  ├─ AcceptRateLimit::…?        else close + on_error
  └─ ProtocolHandler::connected

Deny wins over allow. Empty allow list means allow-all (subject to deny). IPv4 and IPv6 CIDR supported via IpNet.

CIDR allow / deny (PeerAcl)

use hopf_core::{IpNet, PeerAcl, TcpListenerConfig};

let acl = PeerAcl {
    allow: vec![], // empty allow = allow all (unless denied)
    deny: vec![IpNet::parse("10.0.0.0/8").unwrap()],
};
let config = TcpListenerConfig::new(addr, factory).with_acl(acl);
Property Type Default Notes
allow Vec<IpNet> empty If non-empty, peer must match ≥1
deny Vec<IpNet> empty Any match → reject (deny wins)
Method Notes
PeerAcl::open() / Default Allow all
PeerAcl::allows(peer: SocketAddr) -> bool Evaluate
IpNet::parse("addr/prefix") IPv4 / IPv6

Rejection closes the accepted socket before ProtocolHandler::connected.

Accept rate limits

Token-bucket style window counters:

use std::time::Duration;
use hopf_core::AcceptRateLimit;

let config = TcpListenerConfig::new(addr, factory).with_rate_limit(
    AcceptRateLimit::new(
        /* per_source */ 100,
        Duration::from_secs(1),
        /* global */ 0, // 0 = unlimited on that axis
    ),
);

AcceptRateLimit

Property Type Default / meaning Notes
per_source u32 caller Max accepts per window from one source IP
window Duration caller Window length
global u32 caller Max accepts per window overall; 0 = unlimited

AcceptRateLimit::new(per_source, window, global). Axis value 0 disables that limit.

Quotas

Application / protocol policy skeleton — not auto-enforced on every codec datapath yet.

Type Role
QuotaTracker Trait: add_inbound / add_outbound / add_message
QuotaVerdict Allow / deny outcome
CounterQuota Atomic counters with caps
UnlimitedQuota Always allow

CounterQuota

Constructor Notes
CounterQuota::new(max_in, max_out, max_messages) -> Arc<Self> 0 disables that axis

Protocols may call the tracker as policy when ready; do not assume Hopf closes sockets for you on quota alone today.

Listener configuration

Relevant TcpListenerConfig fields / builders (see also Runtime options):

Property Type Default Notes
acl PeerAcl open (allow all) .with_acl
rate_limit Option<AcceptRateLimit> None .with_rate_limit
max_net_in usize 1 MiB (DEFAULT_MAX_NET_IN) Buffer cap → close
max_net_out usize 4 MiB (DEFAULT_MAX_NET_OUT) Buffer cap → close
idle_timeout Option<Duration> None Partially wired (Tranche 1)

Buffer caps are connection safety limits (not identity ACL), but they are part of the same listener hardening surface.

Examples

Compose ACL + rate limit on any service listener:

use std::sync::Arc;
use std::time::Duration;
use hopf_core::{
    AcceptRateLimit, IpNet, PeerAcl, Runtime, RuntimeConfig, TcpListenerConfig,
};

let rt = Arc::new(Runtime::start(RuntimeConfig::default())?);

let acl = PeerAcl {
    allow: vec![IpNet::parse("192.168.0.0/16").unwrap()],
    deny: vec![],
};

let listener = TcpListenerConfig::new(addr, factory)
    .with_acl(acl)
    .with_rate_limit(AcceptRateLimit::new(50, Duration::from_secs(1), 500));

rt.add_tcp_listener(listener)?;

FTP/SMTP services build a TcpListenerConfig internally — prefer their config builders where available, or obtain control_listener and layer ACL before Runtime::add_tcp_listener when the service exposes that path (FTP, SMTP).

Pair with Telemetry to observe on_error for denials.

Limitations