Skip to content

Latest commit

 

History

History
168 lines (141 loc) · 8.51 KB

File metadata and controls

168 lines (141 loc) · 8.51 KB

Runtime rate limits

Status: implemented, pre-alpha defaults

The daemon combines hard size, queue, connection, page, timeout, and worker bounds with token-bucket throughput limits. A token bucket permits its configured burst immediately, then restores per_minute tokens continuously. The implementation uses integer time accounting rather than floating point, so the same configuration has deterministic capacity and no cumulative rounding drift.

The checked-in profiles currently use these policies:

Policy Scope Per minute Burst Covered work
Protocol Each IRC or native WebSocket connection 600 60 Every parsed command except IRC PONG, QUIT, and ERROR
Admission Whole process, across all listeners 120 64 Accepted TCP connections before TLS, WebSocket, or HTTP work
Authentication Whole process 120 64 SASL and native-control password verifications
Invitation Ephemeral native-control source bucket 5 5 Redemption and, separately, authenticated issue/revoke mutations
Messages Each IRC or native WebSocket connection 120 20 PRIVMSG, NOTICE, TAGMSG, native message.send, and automation.invoke
History Each IRC or native WebSocket connection 60 20 CHATHISTORY, MARKREAD, MARKDELIVERED, native sync.resume, and native delivery.ack
Administration Each IRC or native WebSocket connection 60 20 IRC state commands and native channel.join

These are independent budgets. Exhausting history does not prevent a user from chatting or sending PONG, and a message flood cannot consume the authority mutation budget. One multi-target command consumes one token, while existing target-count and output-queue bounds limit its fanout.

Admission remains additionally bounded by limits.max_connections and the registration timeout. Argon2 password verification and channel-key work remain separately concurrency-bounded at four workers. Invitation hashing, SQLite actor queues, outbound client queues, history pages, and command target lists retain their existing hard capacities. Rate limits supplement those bounds; they do not replace them.

Rejection behavior

  • Admission rejection closes the newly accepted socket before protocol work.
  • An IRC command rejected by a per-connection budget receives numeric 263 (RPL_TRYAGAIN). Rejected NOTICE and TAGMSG commands remain silent. PONG, QUIT, and ERROR always bypass the protocol bucket.
  • A server-wide SASL rejection is deliberately indistinguishable from another authentication failure (904). It does not disclose whether an account, device, or credential exists.
  • A native sync command receives a requester-correlated rate_limited error with retryable: true; exhausting one typed bucket does not consume another.
  • Native-control source or global authentication rejection returns HTTP 429 with Retry-After: 60. Invitation redemption otherwise retains its generic registration_unavailable response.

The HTTP onboarding edge also retains a fixed 20-token, 20-per-minute authentication-attempt bucket per ephemeral source key before it performs Basic authentication. This intentionally tighter edge policy is not a TOML setting.

After successful authentication, automated-account provisioning, owner/operator automation-policy mutations, and configured-operator capability changes use distinct ephemeral source buckets with the configured limits.administration rate and burst. Account-avatar owner set/remove and account nickname-color replacement use a dedicated identity-mutation source bucket; operator avatar clear shares the configured-operator mutation bucket. Image decoding is limited to two workers. Authenticated avatar reads use the ordinary control authentication bucket and bounded password-verification pool. Exhausting one mutation class does not consume the others.

Privacy boundary and counters

IRC, WebSocket, and global admission/authentication limiters do not retain or group client addresses. This avoids turning a transport address into a durable identity and behaves predictably behind NAT. The tradeoff is explicit: one source can temporarily consume a process-wide admission or authentication budget and delay unrelated users. For a small private node, a hard anonymous resource ceiling is preferable to retaining a server-side address history. A future deployment should change that only in response to observed abuse and a reviewed proxy/address trust model.

The native-control edge needs source isolation because it accepts bearer-token and password attempts before a session exists. It hashes each source address with a process-random key, stores at most 2,048 (hash, class) entries, expires idle entries, and rotates the key and clears the map every minute. Raw addresses and stable derivatives are never logged or persisted. A reverse proxy is seen as one source unless it uses reviewed layer-4 pass-through; untrusted forwarding headers are ignored.

External-content capability redemption has its own source-attempt class at that edge, separate from invitation and ordinary control authentication. Catalogue and capability issuance use durable one-hour counters keyed by requester and channel so reconnecting or switching devices cannot reset them. The fixed v1 allowances are 60 searches, 60 requests, 64 GiB of claimed bytes, and four outstanding capabilities for a human; 12, 12, 8 GiB, and two for ordinary automation; and 1,000, 1,000, 1 TiB, and 16 for specifically authorized bulk automation. Exact idempotent retries are not charged twice. These are policy guardrails rather than exported diagnostics dimensions; see external content references.

Operational counters contain only aggregate accepted/refused totals, the seven policy names above, and a heartbeat-timeout total. The heartbeat counter increments only when a registered connection fails to return the server's matching PONG; peer closes and ordinary reconnects do not increment it. None of these counters has an address, account, device, nickname, channel, target, message, DM relationship, or private-membership label. Rejections log only the first and power-of-two aggregate totals to resist log amplification, and a complete aggregate snapshot is logged during clean shutdown. Counters are process-local and reset on restart. GET /v1/diagnostics exposes the same snapshot only to an account with diagnostics.read; it does not add identifying dimensions or a live-presence count.

Automation guardrail accounting is a separate authorization mechanism, not a diagnostics metric. It necessarily keys one-minute counters by the durable bot account and channel whose shared allowance is being enforced, but never exports those keys through diagnostics. Repeated-content detection stores only a channel-scoped SHA-256 digest of lowercased, whitespace-collapsed text. Idle counters and digests are deleted after the window by normal maintenance.

Offline administrative commands are deliberately outside these runtime token buckets. They require local operating-system authority, acquire the exclusive database lease while the service is stopped, execute one bounded operation, and exit; an operator should use ordinary host process controls for that trust boundary.

Configuration

Each nested table accepts per_minute and burst. Both must be non-zero; per_minute is capped at 60,000 and burst at 10,000. Omitting either value retains that field's compiled default.

[limits.protocol]
per_minute = 600
burst = 60

[limits.admission]
per_minute = 120
burst = 64

[limits.authentication]
per_minute = 120
burst = 64

[limits.invitation]
per_minute = 5
burst = 5

[limits.messages]
per_minute = 120
burst = 20

[limits.history]
per_minute = 60
burst = 20

[limits.administration]
per_minute = 60
burst = 20

Tune from measurements, not merely to suppress a rejection. Large bursts move work back into the hard queues; very small bursts can break reconnects that legitimately restore several channels and cursors at once. Native automation uses these typed protocol, history, message, and administration classes and an additional durable channel policy. The latter defaults to 60 messages/32 KiB per account and 240 messages/128 KiB across the channel in a fixed one-minute window. It also enforces repeated-content and causal-loop tripwires, a 30-second account/channel cooldown, and a revisioned moderator pause. Exact idempotent retries do not spend the allowance twice. See automated participants for tuning bounds and suspension behavior.