on master help sign in
Improve server.knot security and performance: add federation pair rate limiting, peer server cache to reduce O(n*k) scans, writeAllPresence single-pass optimization, constant-time IP check, verify endpoint rate limiting, and enhanced stats observability
commit7076ef9bc49d5d2a1dc5d8dcd509b40a032c3115 authoriko <mail@iko.soy> dateSat 25 Apr 2026, 08:47 (6 months ago) parent0703ad8
1 file changed+80−30
server.knot+80−30
server.knot+80−30
@@ -76,6 +76,11 @@ maxFederationRatePerServer = 50 -- max federation requests per server per window
7676federationRateWindowMs : Int<Ms>
7777federationRateWindowMs = 1000
7878maxFederationRateEntries = 100
79+-- Per-server-per-recipient rate limiting for federation forwards
80+maxFederationPairRatePerPair = 10 -- max forwards per fromServer-recipient pair per window
81+federationPairRateWindowMs : Int<Ms>
82+federationPairRateWindowMs = 60000 -- 1 minute
83+maxFederationPairRateEntries = 5000
7984-- Global request rate limiting
8085maxGlobalRequestRate = 1000 -- max total requests per window
8186globalRateWindowMs : Int<Ms>
@@ -256,6 +261,7 @@ type PairRateEntry = {sender: PubkeyHex, recipient: PubkeyHex, windowStart: Time
256261type ChallengeRateEntry = {pubkey: PubkeyHex, windowStart: Timestamp, count: Int}
257262-- Sliding-window rate limit state per federation peer server
258263type FederationRateEntry = {server: Text, windowStart: Timestamp, count: Int}
264+type FederationPairRateEntry = {server: Text, recipient: PubkeyHex, windowStart: Timestamp, count: Int}
259265-- Tracks active long-poll connections to enforce per-key concurrency limits
260266type ActivePoll = {pubkey: PubkeyHex, startedAt: Timestamp}
261267data AuthResult = Authorized {pubkey: PubkeyHex, ip: Text} | Unauthorized {}
@@ -342,6 +348,9 @@ type RecentFederationRequest = {server: Text, timestamp: Timestamp, seenAt: Time
342348
343349*totalPresenceCount : Int
344350
351+-- Cached set of known peer server names (updated on prune and gossip, avoids full presence scans)
352+*knownPeerServers : [Text]
353+
345354-- Monotonic counter ensuring unique receivedAt ordering even within the same millisecond
346355*seqCounter : Timestamp
347356
@@ -393,6 +402,8 @@ type RecentFederationRequest = {server: Text, timestamp: Timestamp, seenAt: Time
393402
394403*federationRates : [FederationRateEntry]
395404
405+*federationPairRates : [FederationPairRateEntry]
406+
396407*globalRateCount : Int
397408
398409*globalRateWindowStart : Timestamp
@@ -558,7 +569,7 @@ route HealthApi where
558569-- Returns counts of in-memory collections (requires maintenance secret)
559570route StatsApi where
560571 GET /stats headers {authorization: Text}
561− -> {sessions: Int, sessionsMax: Int, messages: Int, messagesMax: Int, presence: Int, presenceMax: Int, forwards: Int, forwardsMax: Int, challenges: Int, challengesMax: Int, ackedIds: Int, ackedIdsMax: Int, ackOverflow: Int, ackOverflowMax: Int, deliveredIds: Int, deliveredIdsMax: Int, forwardFailures: Int, activePolls: Int, activePollsMax: Int, sendRates: Int, challengeRates: Int, federationRates: Int, globalRateCount: Int, ipRates: Int, lookupRates: Int, recvRates: Int, pruneCount: Int, pruneRunning: Timestamp, heartbeatCount: Int, uptime: Int, lastPruneAt: Timestamp, lastHeartbeatAt: Timestamp, draining: Bool, federationEnabled: Bool, recentGossip: Int, totalMessageCount: Int, recipientCounts: Int, federationInFlight: Int, messagesPct: Int, presencePct: Int, forwardsPct: Int, sessionsPct: Int, federationAvgLatencyMs: Int, federationMaxLatencyMs: Int, federationCallCount: Int, drainCompletedAt: Timestamp, ok: Bool}
572+ -> {sessions: Int, sessionsMax: Int, messages: Int, messagesMax: Int, presence: Int, presenceMax: Int, forwards: Int, forwardsMax: Int, challenges: Int, challengesMax: Int, ackedIds: Int, ackedIdsMax: Int, ackOverflow: Int, ackOverflowMax: Int, deliveredIds: Int, deliveredIdsMax: Int, forwardFailures: Int, activePolls: Int, activePollsMax: Int, sendRates: Int, challengeRates: Int, federationRates: Int, globalRateCount: Int, ipRates: Int, lookupRates: Int, recvRates: Int, pairRates: Int, federationPairRates: Int, pruneCount: Int, pruneRunning: Timestamp, heartbeatCount: Int, uptime: Int, lastPruneAt: Timestamp, lastHeartbeatAt: Timestamp, draining: Bool, federationEnabled: Bool, recentGossip: Int, totalMessageCount: Int, recipientCounts: Int, federationInFlight: Int, messagesPct: Int, presencePct: Int, forwardsPct: Int, sessionsPct: Int, federationAvgLatencyMs: Int, federationMaxLatencyMs: Int, federationCallCount: Int, drainCompletedAt: Timestamp, ok: Bool}
562573 = Stats
563574 GET /stats/peers headers {authorization: Text}
564575 -> {peers: [PeerStatus], ok: Bool}
@@ -1066,25 +1077,44 @@ countAllPresence = do
10661077 p15 <- *presenceBucket15
10671078 yield (count p0 + count p1 + count p2 + count p3 + count p4 + count p5 + count p6 + count p7 + count p8 + count p9 + count p10 + count p11 + count p12 + count p13 + count p14 + count p15)
10681079
1080+-- Single-pass partitioning: each entry's bucket is computed once (vs 16x in the list-comprehension approach)
10691081writeAllPresence : [PresenceEntry] -> IO ()
10701082writeAllPresence = \entries -> do
1071− let bFor = \b -> do p <- entries; where pubkeyBucket p.pubkey == b; yield p
1072− writePresenceBucket 0 (bFor 0)
1073− writePresenceBucket 1 (bFor 1)
1074− writePresenceBucket 2 (bFor 2)
1075− writePresenceBucket 3 (bFor 3)
1076− writePresenceBucket 4 (bFor 4)
1077− writePresenceBucket 5 (bFor 5)
1078− writePresenceBucket 6 (bFor 6)
1079− writePresenceBucket 7 (bFor 7)
1080− writePresenceBucket 8 (bFor 8)
1081− writePresenceBucket 9 (bFor 9)
1082− writePresenceBucket 10 (bFor 10)
1083− writePresenceBucket 11 (bFor 11)
1084− writePresenceBucket 12 (bFor 12)
1085− writePresenceBucket 13 (bFor 13)
1086− writePresenceBucket 14 (bFor 14)
1087− writePresenceBucket 15 (bFor 15)
1083+ let partitioned = fold (\acc p ->
1084+ let b = pubkeyBucket p.pubkey in
1085+ {b0: if b == 0 then union [p] acc.b0 else acc.b0,
1086+ b1: if b == 1 then union [p] acc.b1 else acc.b1,
1087+ b2: if b == 2 then union [p] acc.b2 else acc.b2,
1088+ b3: if b == 3 then union [p] acc.b3 else acc.b3,
1089+ b4: if b == 4 then union [p] acc.b4 else acc.b4,
1090+ b5: if b == 5 then union [p] acc.b5 else acc.b5,
1091+ b6: if b == 6 then union [p] acc.b6 else acc.b6,
1092+ b7: if b == 7 then union [p] acc.b7 else acc.b7,
1093+ b8: if b == 8 then union [p] acc.b8 else acc.b8,
1094+ b9: if b == 9 then union [p] acc.b9 else acc.b9,
1095+ b10: if b == 10 then union [p] acc.b10 else acc.b10,
1096+ b11: if b == 11 then union [p] acc.b11 else acc.b11,
1097+ b12: if b == 12 then union [p] acc.b12 else acc.b12,
1098+ b13: if b == 13 then union [p] acc.b13 else acc.b13,
1099+ b14: if b == 14 then union [p] acc.b14 else acc.b14,
1100+ b15: if b == 15 then union [p] acc.b15 else acc.b15})
1101+ {b0: [], b1: [], b2: [], b3: [], b4: [], b5: [], b6: [], b7: [], b8: [], b9: [], b10: [], b11: [], b12: [], b13: [], b14: [], b15: []} entries
1102+ writePresenceBucket 0 partitioned.b0
1103+ writePresenceBucket 1 partitioned.b1
1104+ writePresenceBucket 2 partitioned.b2
1105+ writePresenceBucket 3 partitioned.b3
1106+ writePresenceBucket 4 partitioned.b4
1107+ writePresenceBucket 5 partitioned.b5
1108+ writePresenceBucket 6 partitioned.b6
1109+ writePresenceBucket 7 partitioned.b7
1110+ writePresenceBucket 8 partitioned.b8
1111+ writePresenceBucket 9 partitioned.b9
1112+ writePresenceBucket 10 partitioned.b10
1113+ writePresenceBucket 11 partitioned.b11
1114+ writePresenceBucket 12 partitioned.b12
1115+ writePresenceBucket 13 partitioned.b13
1116+ writePresenceBucket 14 partitioned.b14
1117+ writePresenceBucket 15 partitioned.b15
10881118
10891119transformAllPresenceBuckets : ([PresenceEntry] -> [PresenceEntry]) -> IO ()
10901120transformAllPresenceBuckets = \f -> do
@@ -1540,6 +1570,7 @@ pruneExpired = \t -> do
15401570 let presenceAfter = do x <- presenceBefore; where x.expiresAt > t; yield x
15411571 writeAllPresence presenceAfter
15421572 set *totalPresenceCount = count presenceAfter
1573+ set *knownPeerServers = collectPeerServers presenceAfter (\p -> p.server)
15431574 yield {expiredLocal: expiredLocalPresence presenceBefore presenceAfter t, peerServers: peerServersBefore}
15441575 -- Prune auth state
15451576 atomic do
@@ -1601,6 +1632,8 @@ pruneExpired = \t -> do
16011632 atomic do
16021633 federationRates <- *federationRates
16031634 set *federationRates = do r <- federationRates; where t - r.windowStart < federationRateWindowMs; yield r
1635+ federationPairRates <- *federationPairRates
1636+ set *federationPairRates = do r <- federationPairRates; where t - r.windowStart < federationPairRateWindowMs; yield r
16041637 ipRates <- *ipRates
16051638 set *ipRates = do r <- ipRates; where t - r.windowStart < ipRateWindowMs; yield r
16061639 lookupRates <- *lookupRates
@@ -2159,7 +2192,8 @@ pruneAndRetry = \t -> do
21592192 when (count toExtend > 0) (do
21602193 let updatedPresence = do pp <- allPresence; yield (if any toExtend (\e -> e.pubkey == pp.pubkey && isLocalServer pp.server) then {pubkey: pp.pubkey, server: pp.server, expiresAt: t + sessionExpiry, timestamp: t} else pp)
21612194 writeAllPresence updatedPresence)
2162− let refreshPeers = if shouldRefresh then collectPeerServers allPresence (\p -> p.server) else []
2195+ knownPeers <- *knownPeerServers
2196+ let refreshPeers = if shouldRefresh then knownPeers else []
21632197 when shouldRefresh (set *lastPresenceRefresh = t)
21642198 yield {extendedActive: toExtend, nearExpiryActive: notExtended, refreshPeers: refreshPeers, shouldRefresh: shouldRefresh}
21652199 -- Reset federation latency metrics periodically for windowed averages (prevents
@@ -2251,7 +2285,7 @@ ackPolledMessages = \pubkey cursor t -> do
22512285 then do
22522286 writeMessageBucket b (diff bucket toAck)
22532287 totalCount <- *totalMessageCount
2254− set *totalMessageCount = totalCount - ackCount
2288+ set *totalMessageCount = max 0 (totalCount - ackCount)
22552289 recipientCounts <- *recipientMsgCounts
22562290 set *recipientMsgCounts = adjustRecipientCount recipientCounts pubkey (0 - ackCount)
22572291 let ackedMsgIds = do m <- toAck; yield m.messageId
@@ -2580,8 +2614,8 @@ verifyAuthInner = \pubkey challenge signature revokeOthers clientIp respond t ->
25802614 else do
25812615 -- Read all session buckets for per-key enforcement (revokeOthers, capPerKey)
25822616 allSessions <- readAllSessions
2583− allPresence <- readAllPresence
25842617 tpc <- *totalPresenceCount
2618+ knownPeers <- *knownPeerServers
25852619 let sessionsForKey = count (do s <- allSessions; where s.pubkey == pubkey; yield s)
25862620 let totalSessions = count allSessions
25872621 let newSession = {token: token, pubkey: pubkey, expiresAt: expiry, ip: clientIp}
@@ -2595,7 +2629,7 @@ verifyAuthInner = \pubkey challenge signature revokeOthers clientIp respond t ->
25952629 writePresenceBucket pb (union bucketEvicted [newPresenceEntry])
25962630 let presenceDelta = count (union bucketEvicted [newPresenceEntry]) - count presenceBkt
25972631 set *totalPresenceCount = tpc + presenceDelta
2598− let peerServers = if federationEnabled then collectPeerServers allPresence (\p -> p.server) else []
2632+ let peerServers = if federationEnabled then knownPeers else []
25992633 -- Fast path: no revocation, under per-key and global limits — only write one bucket
26002634 if not revokeOthers && sessionsForKey < maxSessionsPerKey && totalSessions < maxSessions
26012635 then do
@@ -2927,7 +2961,7 @@ handleLogout = \authorization clientIp respond -> do
29272961 case authenticateSession sessions token t of
29282962 Unauthorized {} -> yield (Nothing {})
29292963 Authorized {pubkey, ip} ->
2930− if clientIp != "" && ip != "" && clientIp != ip
2964+ if not (matchIp clientIp ip)
29312965 then yield (Nothing {})
29322966 else do
29332967 -- Remove the caller's session
@@ -2948,9 +2982,7 @@ handleLogout = \authorization clientIp respond -> do
29482982 let removedCount = count presenceBkt - count (do p <- presenceBkt; where p.pubkey != pubkey || not (isLocalServer p.server); yield p)
29492983 tpc <- *totalPresenceCount
29502984 set *totalPresenceCount = max 0 (tpc - removedCount)
2951− -- Capture peer servers inside atomic block (fix: was read outside)
2952− allPresence <- readAllPresence
2953− let peerServers = collectPeerServers allPresence (\p -> p.server)
2985+ peerServers <- *knownPeerServers
29542986 yield (Just {value: {pubkey: pubkey, gossipOffline: True {}, peerServers: peerServers}})
29552987 else yield (Just {value: {pubkey: pubkey, gossipOffline: False {}, peerServers: []}})
29562988 case result of
@@ -2967,6 +2999,9 @@ handleLogout = \authorization clientIp respond -> do
29672999-- Tokens are single-use and expire after verificationTokenExpiry.
29683000handleVerifyToken = \token scope respond -> do
29693001 t <- now
3002+ globalRateLimited <- checkGlobalRate t
3003+ if globalRateLimited then yield (respond {ok: False {}})
3004+ else do
29703005 found <- atomic do
29713006 pending <- *pendingVerifications
29723007 -- Empty scope accepts any (backward compat with peers that don't send scope yet)
@@ -2998,6 +3033,7 @@ handleRecvGossip = \events hopsRemaining fromServer requestTimestamp authorizati
29983033 let gm = mergeGossipPresence allPresence deduped trustedOffline fromServer t
29993034 writeAllPresence gm.finalPresence
30003035 set *totalPresenceCount = count gm.finalPresence
3036+ set *knownPeerServers = gm.gossipPeerServers
30013037 yield {gm: gm}
30023038 let effectiveEvents = union (effectiveGossipEvents filteredOnline gossipResult.gm.appliedKeys (GossipOnline {})) (effectiveGossipEvents filteredOffline gossipResult.gm.appliedKeys (GossipOffline {}))
30033039 when (not gossipResult.gm.withinCapacity)
@@ -3066,6 +3102,15 @@ handleForwardMessage = \toKey encryptedBlob timestamp originServer messageId fro
30663102 then yield {forward: ForwardRejected {}, globalCapReached: False {}, recvRateLimited: True {}}
30673103 else do
30683104 set *recvRates = recvResult.entries
3105+ federationPairRates <- *federationPairRates
3106+ let fedPairResult = checkRateWindow federationPairRates (\r -> r.server == fromServer && r.recipient == toKey)
3107+ (\ts -> {server: fromServer, recipient: toKey, windowStart: ts, count: 1})
3108+ (\r -> {server: r.server, recipient: r.recipient, windowStart: r.windowStart, count: r.count + 1})
3109+ maxFederationPairRatePerPair federationPairRateWindowMs maxFederationPairRateEntries t
3110+ if fedPairResult.limited
3111+ then yield {forward: ForwardRejected {}, globalCapReached: False {}, recvRateLimited: False {}}
3112+ else do
3113+ set *federationPairRates = fedPairResult.entries
30693114 seqCounter <- *seqCounter
30703115 let seqTs = nextSeqTs t seqCounter
30713116 set *seqCounter = seqTs
@@ -3225,7 +3270,7 @@ handleHealth = \respond -> do
32253270 capacityHealthy: result.capacityOk,
32263271 federationHealthy: result.federationOk})
32273272
3228−zeroStats = {sessions: 0, sessionsMax: 0, messages: 0, messagesMax: 0, presence: 0, presenceMax: 0, forwards: 0, forwardsMax: 0, challenges: 0, challengesMax: 0, ackedIds: 0, ackedIdsMax: 0, ackOverflow: 0, ackOverflowMax: 0, deliveredIds: 0, deliveredIdsMax: 0, forwardFailures: 0, activePolls: 0, activePollsMax: 0, sendRates: 0, challengeRates: 0, federationRates: 0, globalRateCount: 0, ipRates: 0, lookupRates: 0, recvRates: 0, pruneCount: 0, pruneRunning: 0, heartbeatCount: 0, uptime: 0, lastPruneAt: 0, lastHeartbeatAt: 0, draining: False {}, federationEnabled: False {}, recentGossip: 0, totalMessageCount: 0, recipientCounts: 0, federationInFlight: 0, messagesPct: 0, presencePct: 0, forwardsPct: 0, sessionsPct: 0, federationAvgLatencyMs: 0, federationMaxLatencyMs: 0, federationCallCount: 0, drainCompletedAt: 0, ok: False {}}
3273+zeroStats = {sessions: 0, sessionsMax: 0, messages: 0, messagesMax: 0, presence: 0, presenceMax: 0, forwards: 0, forwardsMax: 0, challenges: 0, challengesMax: 0, ackedIds: 0, ackedIdsMax: 0, ackOverflow: 0, ackOverflowMax: 0, deliveredIds: 0, deliveredIdsMax: 0, forwardFailures: 0, activePolls: 0, activePollsMax: 0, sendRates: 0, challengeRates: 0, federationRates: 0, globalRateCount: 0, ipRates: 0, lookupRates: 0, recvRates: 0, pairRates: 0, federationPairRates: 0, pruneCount: 0, pruneRunning: 0, heartbeatCount: 0, uptime: 0, lastPruneAt: 0, lastHeartbeatAt: 0, draining: False {}, federationEnabled: False {}, recentGossip: 0, totalMessageCount: 0, recipientCounts: 0, federationInFlight: 0, messagesPct: 0, presencePct: 0, forwardsPct: 0, sessionsPct: 0, federationAvgLatencyMs: 0, federationMaxLatencyMs: 0, federationCallCount: 0, drainCompletedAt: 0, ok: False {}}
32293274
32303275handleStats = \authorization respond -> do
32313276 t <- now
@@ -3256,10 +3301,12 @@ handleStats = \authorization respond -> do
32563301 sr <- *sendRates
32573302 cr <- *challengeRates
32583303 fr <- *federationRates
3304+ fpr <- *federationPairRates
32593305 grc <- *globalRateCount
32603306 ir <- *ipRates
32613307 lr <- *lookupRates
32623308 rr <- *recvRates
3309+ prr <- *pairRates
32633310 pc <- *pruneCount
32643311 pr <- *pruneRunning
32653312 hc <- *heartbeatCount
@@ -3271,14 +3318,14 @@ handleStats = \authorization respond -> do
32713318 flc <- *federationLatencyCount
32723319 flm <- *federationLatencyMax
32733320 dca <- *drainCompletedAt
3274− yield {sr: sr, cr: cr, fr: fr, grc: grc, ir: ir, lr: lr, rr: rr, pc: pc, pr: pr, hc: hc, sa: sa, lp: lp, lh: lh, dr: dr, flt: flt, flc: flc, flm: flm, dca: dca}
3275− let result = {sessions: core.sc, messages: core.tmc, presence: core.pc, forwards: count core.f, challenges: count core.c, ackedIds: count core.a, ackOverflow: count core.ao, deliveredIds: count core.d, forwardFailures: count core.ff, activePolls: count core.ap, sendRates: count counters.sr, challengeRates: count counters.cr, federationRates: count counters.fr, globalRateCount: counters.grc, ipRates: count counters.ir, lookupRates: count counters.lr, recvRates: count counters.rr, pruneCount: counters.pc, pruneRunning: counters.pr, heartbeatCount: counters.hc, uptime: t - counters.sa, lastPruneAt: counters.lp, lastHeartbeatAt: counters.lh, draining: counters.dr, recentGossip: count core.rg, totalMessageCount: core.tmc, recipientCounts: count core.rc, federationInFlight: count core.fi, federationLatencyTotal: counters.flt, federationLatencyCount: counters.flc, federationLatencyMax: counters.flm, drainCompletedAt: counters.dca}
3321+ yield {sr: sr, cr: cr, fr: fr, fpr: fpr, grc: grc, ir: ir, lr: lr, rr: rr, prr: prr, pc: pc, pr: pr, hc: hc, sa: sa, lp: lp, lh: lh, dr: dr, flt: flt, flc: flc, flm: flm, dca: dca}
3322+ let result = {sessions: core.sc, messages: core.tmc, presence: core.pc, forwards: count core.f, challenges: count core.c, ackedIds: count core.a, ackOverflow: count core.ao, deliveredIds: count core.d, forwardFailures: count core.ff, activePolls: count core.ap, sendRates: count counters.sr, challengeRates: count counters.cr, federationRates: count counters.fr, globalRateCount: counters.grc, ipRates: count counters.ir, lookupRates: count counters.lr, recvRates: count counters.rr, pairRates: count counters.prr, federationPairRates: count counters.fpr, pruneCount: counters.pc, pruneRunning: counters.pr, heartbeatCount: counters.hc, uptime: t - counters.sa, lastPruneAt: counters.lp, lastHeartbeatAt: counters.lh, draining: counters.dr, recentGossip: count core.rg, totalMessageCount: core.tmc, recipientCounts: count core.rc, federationInFlight: count core.fi, federationLatencyTotal: counters.flt, federationLatencyCount: counters.flc, federationLatencyMax: counters.flm, drainCompletedAt: counters.dca}
32763323 let msgPct = if maxMessages > 0 then result.messages * 100 / maxMessages else 0
32773324 let presPct = if maxPresenceEntries > 0 then result.presence * 100 / maxPresenceEntries else 0
32783325 let fwdPct = if maxForwardsEntries > 0 then result.forwards * 100 / maxForwardsEntries else 0
32793326 let sessPct = if maxSessions > 0 then result.sessions * 100 / maxSessions else 0
32803327 let fedAvg = if result.federationLatencyCount > 0 then result.federationLatencyTotal / result.federationLatencyCount else 0
3281− yield (respond {sessions: result.sessions, sessionsMax: maxSessions, messages: result.messages, messagesMax: maxMessages, presence: result.presence, presenceMax: maxPresenceEntries, forwards: result.forwards, forwardsMax: maxForwardsEntries, challenges: result.challenges, challengesMax: maxChallenges, ackedIds: result.ackedIds, ackedIdsMax: maxAckedIds, ackOverflow: result.ackOverflow, ackOverflowMax: maxAckOverflow, deliveredIds: result.deliveredIds, deliveredIdsMax: maxDeliveredIds, forwardFailures: result.forwardFailures, activePolls: result.activePolls, activePollsMax: maxActivePolls, sendRates: result.sendRates, challengeRates: result.challengeRates, federationRates: result.federationRates, globalRateCount: result.globalRateCount, ipRates: result.ipRates, lookupRates: result.lookupRates, recvRates: result.recvRates, pruneCount: result.pruneCount, pruneRunning: result.pruneRunning, heartbeatCount: result.heartbeatCount, uptime: result.uptime, lastPruneAt: result.lastPruneAt, lastHeartbeatAt: result.lastHeartbeatAt, draining: result.draining, federationEnabled: federationEnabled, recentGossip: result.recentGossip, totalMessageCount: result.totalMessageCount, recipientCounts: result.recipientCounts, federationInFlight: result.federationInFlight, messagesPct: msgPct, presencePct: presPct, forwardsPct: fwdPct, sessionsPct: sessPct, federationAvgLatencyMs: fedAvg, federationMaxLatencyMs: result.federationLatencyMax, federationCallCount: result.federationLatencyCount, drainCompletedAt: result.drainCompletedAt, ok: True {}})
3328+ yield (respond {sessions: result.sessions, sessionsMax: maxSessions, messages: result.messages, messagesMax: maxMessages, presence: result.presence, presenceMax: maxPresenceEntries, forwards: result.forwards, forwardsMax: maxForwardsEntries, challenges: result.challenges, challengesMax: maxChallenges, ackedIds: result.ackedIds, ackedIdsMax: maxAckedIds, ackOverflow: result.ackOverflow, ackOverflowMax: maxAckOverflow, deliveredIds: result.deliveredIds, deliveredIdsMax: maxDeliveredIds, forwardFailures: result.forwardFailures, activePolls: result.activePolls, activePollsMax: maxActivePolls, sendRates: result.sendRates, challengeRates: result.challengeRates, federationRates: result.federationRates, globalRateCount: result.globalRateCount, ipRates: result.ipRates, lookupRates: result.lookupRates, recvRates: result.recvRates, pairRates: result.pairRates, federationPairRates: result.federationPairRates, pruneCount: result.pruneCount, pruneRunning: result.pruneRunning, heartbeatCount: result.heartbeatCount, uptime: result.uptime, lastPruneAt: result.lastPruneAt, lastHeartbeatAt: result.lastHeartbeatAt, draining: result.draining, federationEnabled: federationEnabled, recentGossip: result.recentGossip, totalMessageCount: result.totalMessageCount, recipientCounts: result.recipientCounts, federationInFlight: result.federationInFlight, messagesPct: msgPct, presencePct: presPct, forwardsPct: fwdPct, sessionsPct: sessPct, federationAvgLatencyMs: fedAvg, federationMaxLatencyMs: result.federationLatencyMax, federationCallCount: result.federationLatencyCount, drainCompletedAt: result.drainCompletedAt, ok: True {}})
32823329 else yield (respond zeroStats)
32833330
32843331handleStatsPeers = \authorization respond -> do
@@ -3359,6 +3406,7 @@ handleRestore = \presenceData ackedIdsData ackOverflowData deliveredIdsData forw
33593406 let cappedPresence = takeRelation maxPresenceEntries mergedPresence
33603407 writeAllPresence cappedPresence
33613408 set *totalPresenceCount = count cappedPresence
3409+ set *knownPeerServers = collectPeerServers cappedPresence (\p -> p.server)
33623410 -- Union dedup state, respecting capacity limits
33633411 ackedIds <- *ackedIds
33643412 let mergedAcked = union ackedIds ackedIdsData
@@ -3512,6 +3560,7 @@ main = do
35123560 && gossipOriginHops > 0
35133561 && verificationTokenExpiry > 0
35143562 && maxAckRetries > 0 && maxPairRatePerPair > 0 && pairRateWindowMs > 0
3563+ && maxFederationPairRatePerPair > 0 && federationPairRateWindowMs > 0
35153564 -- Relationship invariants: these misconfigurations cause subtle bugs
35163565 -- (e.g., messages pruned before their stated max age, backoff that never increases)
35173566 let configRelationsOk = unackedMessageMaxAge <= messageMaxAge
@@ -3552,6 +3601,7 @@ main = do
35523601 println (" global rate limit: " ++ show maxGlobalRequestRate ++ "/" ++ show globalRateWindowMs ++ "ms")
35533602 println (" auth rate limit: " ++ show maxAuthGlobalRate ++ "/" ++ show authGlobalRateWindowMs ++ "ms")
35543603 println (" pair rate limit: " ++ show maxPairRatePerPair ++ "/" ++ show pairRateWindowMs ++ "ms per sender-recipient")
3604+ println (" federation pair rate limit: " ++ show maxFederationPairRatePerPair ++ "/" ++ show federationPairRateWindowMs ++ "ms per server-recipient")
35553605 println (" ack retry limit: " ++ show maxAckRetries ++ " per entry")
35563606 when (federationCircuitBreakerThreshold > 0)
35573607 (println (" circuit breaker: " ++ show federationCircuitBreakerThreshold ++ " failing peers"))