rate_limit.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. package wire
  2. import (
  3. "iter"
  4. "time"
  5. )
  6. type (
  7. // RateLimitClassID identifies a rate limit class.
  8. RateLimitClassID uint16
  9. // RateLimitStatus represents a session's current rate limiting state.
  10. RateLimitStatus uint8
  11. )
  12. const (
  13. // RateLimitStatusLimited indicates the session is currently rate-limited
  14. // and should not send further messages in this class.
  15. RateLimitStatusLimited RateLimitStatus = 1
  16. // RateLimitStatusAlert indicates the session is approaching the rate limit threshold
  17. // and may soon be limited if activity continues.
  18. RateLimitStatusAlert RateLimitStatus = 2
  19. // RateLimitStatusClear indicates the session is under the limit and in good standing.
  20. RateLimitStatusClear RateLimitStatus = 3
  21. // RateLimitStatusDisconnect indicates the session has exceeded a critical threshold
  22. // and should be forcibly disconnected.
  23. RateLimitStatusDisconnect RateLimitStatus = 4
  24. )
  25. // NewRateLimitClasses creates a new RateLimitClasses instance from a fixed array
  26. // of 5 RateClass definitions.
  27. //
  28. // Each RateClass must have a unique ID from 1 to 5, and the array is expected
  29. // to be ordered such that classes[ID-1] corresponds to RateClass.ID == ID.
  30. // No validation is performed on the input.
  31. func NewRateLimitClasses(classes [5]RateClass) RateLimitClasses {
  32. return RateLimitClasses{
  33. classes: classes,
  34. }
  35. }
  36. // DefaultRateLimitClasses returns the default SNAC rate limit classes used at
  37. // one point by the original AIM service, as memorialized by the iserverd
  38. // project.
  39. func DefaultRateLimitClasses() RateLimitClasses {
  40. return RateLimitClasses{
  41. classes: [5]RateClass{
  42. {
  43. ID: 1,
  44. WindowSize: 80,
  45. ClearLevel: 2500,
  46. AlertLevel: 2000,
  47. LimitLevel: 1500,
  48. DisconnectLevel: 800,
  49. MaxLevel: 6000,
  50. },
  51. {
  52. ID: 2,
  53. WindowSize: 80,
  54. ClearLevel: 3000,
  55. AlertLevel: 2000,
  56. LimitLevel: 1500,
  57. DisconnectLevel: 1000,
  58. MaxLevel: 6000,
  59. },
  60. {
  61. ID: 3,
  62. WindowSize: 20,
  63. ClearLevel: 5100,
  64. AlertLevel: 5000,
  65. LimitLevel: 4000,
  66. DisconnectLevel: 3000,
  67. MaxLevel: 6000,
  68. },
  69. {
  70. ID: 4,
  71. WindowSize: 20,
  72. ClearLevel: 5500,
  73. AlertLevel: 5300,
  74. LimitLevel: 4200,
  75. DisconnectLevel: 3000,
  76. MaxLevel: 8000,
  77. },
  78. {
  79. ID: 5,
  80. WindowSize: 10,
  81. ClearLevel: 5500,
  82. AlertLevel: 5300,
  83. LimitLevel: 4200,
  84. DisconnectLevel: 3000,
  85. MaxLevel: 8000,
  86. },
  87. },
  88. }
  89. }
  90. // RateLimitClasses stores a fixed set of rate limit class definitions.
  91. //
  92. // Each RateClass defines thresholds and behavior for computing moving-average-based
  93. // rate limits. This struct provides access to individual classes by ID
  94. // or to the full set.
  95. type RateLimitClasses struct {
  96. classes [5]RateClass // Indexed by class ID - 1
  97. }
  98. // Get returns the RateClass associated with the given class ID.
  99. //
  100. // The class ID must be between 1 and 5 inclusive. Calling Get with an invalid
  101. // ID will panic.
  102. func (r RateLimitClasses) Get(ID RateLimitClassID) RateClass {
  103. return r.classes[ID-1]
  104. }
  105. // All returns all defined RateClass entries in order of their class IDs.
  106. func (r RateLimitClasses) All() [5]RateClass {
  107. return r.classes
  108. }
  109. // DefaultSNACRateLimits returns the default SNAC rate limit mapping used at
  110. // one point by the original AIM service, as memorialized by the iserverd
  111. // project.
  112. func DefaultSNACRateLimits() SNACRateLimits {
  113. return SNACRateLimits{
  114. lookup: map[uint16]map[uint16]RateLimitClassID{
  115. OService: {
  116. OServiceErr: 1,
  117. OServiceClientOnline: 1,
  118. OServiceHostOnline: 1,
  119. OServiceServiceRequest: 1,
  120. OServiceServiceResponse: 1,
  121. OServiceRateParamsQuery: 1,
  122. OServiceRateParamsReply: 1,
  123. OServiceRateParamsSubAdd: 1,
  124. OServiceRateDelParamSub: 1,
  125. OServiceRateParamChange: 1,
  126. OServicePauseReq: 1,
  127. OServicePauseAck: 1,
  128. OServiceResume: 1,
  129. OServiceUserInfoQuery: 1,
  130. OServiceUserInfoUpdate: 1,
  131. OServiceEvilNotification: 1,
  132. OServiceIdleNotification: 1,
  133. OServiceMigrateGroups: 1,
  134. OServiceMotd: 1,
  135. OServiceSetPrivacyFlags: 1,
  136. OServiceWellKnownUrls: 1,
  137. OServiceNoop: 1,
  138. OServiceClientVersions: 1,
  139. OServiceHostVersions: 1,
  140. OServiceMaxConfigQuery: 1,
  141. OServiceMaxConfigReply: 1,
  142. OServiceStoreConfig: 1,
  143. OServiceConfigQuery: 1,
  144. OServiceConfigReply: 1,
  145. OServiceSetUserInfoFields: 1,
  146. OServiceProbeReq: 1,
  147. OServiceProbeAck: 1,
  148. OServiceBartReply: 1,
  149. OServiceBartQuery2: 1,
  150. OServiceBartReply2: 1,
  151. },
  152. Locate: {
  153. LocateErr: 1,
  154. LocateRightsQuery: 1,
  155. LocateRightsReply: 1,
  156. LocateSetInfo: 1,
  157. LocateUserInfoQuery: 3,
  158. LocateUserInfoReply: 1,
  159. LocateWatcherSubRequest: 1,
  160. LocateWatcherNotification: 1,
  161. LocateSetDirInfo: 4,
  162. LocateSetDirReply: 1,
  163. LocateGetDirInfo: 4,
  164. LocateGetDirReply: 1,
  165. LocateGroupCapabilityQuery: 1,
  166. LocateGroupCapabilityReply: 1,
  167. LocateSetKeywordInfo: 1,
  168. LocateSetKeywordReply: 1,
  169. LocateGetKeywordInfo: 1,
  170. LocateGetKeywordReply: 1,
  171. LocateFindListByEmail: 1,
  172. LocateFindListReply: 1,
  173. LocateUserInfoQuery2: 1,
  174. },
  175. Buddy: {
  176. BuddyErr: 1,
  177. BuddyRightsQuery: 1,
  178. BuddyRightsReply: 1,
  179. BuddyAddBuddies: 2,
  180. BuddyDelBuddies: 2,
  181. BuddyWatcherListQuery: 1,
  182. BuddyWatcherListResponse: 1,
  183. BuddyWatcherSubRequest: 1,
  184. BuddyWatcherNotification: 1,
  185. BuddyRejectNotification: 1,
  186. BuddyArrived: 1,
  187. BuddyDeparted: 1,
  188. BuddyAddTempBuddies: 1,
  189. BuddyDelTempBuddies: 1,
  190. },
  191. ICBM: {
  192. ICBMErr: 1,
  193. ICBMAddParameters: 1,
  194. ICBMDelParameters: 1,
  195. ICBMParameterQuery: 1,
  196. ICBMParameterReply: 1,
  197. ICBMChannelMsgToHost: 3,
  198. ICBMChannelMsgToClient: 1,
  199. ICBMEvilRequest: 1,
  200. ICBMEvilReply: 1,
  201. ICBMMissedCalls: 1,
  202. ICBMClientErr: 1,
  203. ICBMHostAck: 1,
  204. ICBMSinStored: 1,
  205. ICBMSinListQuery: 1,
  206. ICBMSinListReply: 1,
  207. ICBMOfflineRetrieve: 1,
  208. ICBMSinDelete: 1,
  209. ICBMNotifyRequest: 1,
  210. ICBMNotifyReply: 1,
  211. ICBMClientEvent: 1,
  212. },
  213. Advert: {
  214. AdvertErr: 1,
  215. AdvertAdsQuery: 1,
  216. AdvertAdsReply: 1,
  217. },
  218. Invite: {
  219. InviteErr: 1,
  220. InviteRequestQuery: 1,
  221. InviteRequestReply: 1,
  222. },
  223. Admin: {
  224. AdminErr: 1,
  225. AdminInfoQuery: 1,
  226. AdminInfoReply: 1,
  227. AdminInfoChangeRequest: 1,
  228. AdminInfoChangeReply: 1,
  229. AdminAcctConfirmRequest: 1,
  230. AdminAcctConfirmReply: 1,
  231. AdminAcctDeleteRequest: 1,
  232. AdminAcctDeleteReply: 1,
  233. },
  234. Popup: {
  235. PopupErr: 1,
  236. PopupDisplay: 1,
  237. },
  238. PermitDeny: {
  239. PermitDenyErr: 1,
  240. PermitDenyRightsQuery: 1,
  241. PermitDenyRightsReply: 1,
  242. PermitDenySetGroupPermitMask: 1,
  243. PermitDenyAddPermListEntries: 2,
  244. PermitDenyDelPermListEntries: 2,
  245. PermitDenyAddDenyListEntries: 2,
  246. PermitDenyDelDenyListEntries: 2,
  247. PermitDenyBosErr: 1,
  248. PermitDenyAddTempPermitListEntries: 1,
  249. PermitDenyDelTempPermitListEntries: 1,
  250. },
  251. UserLookup: {
  252. UserLookupErr: 1,
  253. UserLookupFindByEmail: 1,
  254. UserLookupFindReply: 1,
  255. },
  256. Stats: {
  257. StatsErr: 1,
  258. StatsSetMinReportInterval: 1,
  259. StatsReportEvents: 1,
  260. StatsReportAck: 1,
  261. },
  262. Translate: {
  263. TranslateErr: 1,
  264. TranslateRequest: 1,
  265. TranslateReply: 1,
  266. },
  267. ChatNav: {
  268. ChatNavErr: 1,
  269. ChatNavRequestChatRights: 1,
  270. ChatNavRequestExchangeInfo: 1,
  271. ChatNavRequestRoomInfo: 1,
  272. ChatNavRequestMoreRoomInfo: 1,
  273. ChatNavRequestOccupantList: 1,
  274. ChatNavSearchForRoom: 1,
  275. ChatNavCreateRoom: 1,
  276. ChatNavNavInfo: 1,
  277. },
  278. Chat: {
  279. ChatErr: 1,
  280. ChatRoomInfoUpdate: 1,
  281. ChatUsersJoined: 1,
  282. ChatUsersLeft: 1,
  283. ChatChannelMsgToHost: 2,
  284. ChatChannelMsgToClient: 1,
  285. ChatEvilRequest: 1,
  286. ChatEvilReply: 1,
  287. ChatClientErr: 1,
  288. },
  289. ODir: {
  290. ODirErr: 1,
  291. ODirInfoQuery: 1,
  292. ODirInfoReply: 1,
  293. ODirKeywordListQuery: 1,
  294. },
  295. BART: {
  296. BARTErr: 1,
  297. BARTUploadQuery: 1,
  298. BARTDownloadQuery: 1,
  299. BARTDownload2Query: 1,
  300. },
  301. Feedbag: {
  302. FeedbagErr: 1,
  303. FeedbagRightsQuery: 1,
  304. FeedbagRightsReply: 1,
  305. FeedbagQuery: 1,
  306. FeedbagQueryIfModified: 1,
  307. FeedbagReply: 1,
  308. FeedbagUse: 1,
  309. FeedbagInsertItem: 1,
  310. FeedbagUpdateItem: 1,
  311. FeedbagDeleteItem: 1,
  312. FeedbagInsertClass: 1,
  313. FeedbagUpdateClass: 1,
  314. FeedbagDeleteClass: 1,
  315. FeedbagStatus: 1,
  316. FeedbagReplyNotModified: 1,
  317. FeedbagDeleteUser: 1,
  318. FeedbagStartCluster: 1,
  319. FeedbagEndCluster: 1,
  320. FeedbagAuthorizeBuddy: 1,
  321. FeedbagPreAuthorizeBuddy: 1,
  322. FeedbagPreAuthorizedBuddy: 1,
  323. FeedbagRemoveMe: 1,
  324. FeedbagRemoveMe2: 1,
  325. FeedbagRequestAuthorizeToHost: 1,
  326. FeedbagRequestAuthorizeToClient: 1,
  327. FeedbagRespondAuthorizeToHost: 1,
  328. FeedbagRespondAuthorizeToClient: 1,
  329. FeedbagBuddyAdded: 1,
  330. FeedbagRequestAuthorizeToBadog: 1,
  331. FeedbagRespondAuthorizeToBadog: 1,
  332. FeedbagBuddyAddedToBadog: 1,
  333. 0x0020: 1, // unknown
  334. FeedbagTestSnac: 1,
  335. FeedbagForwardMsg: 1,
  336. FeedbagIsAuthRequiredQuery: 1,
  337. FeedbagIsAuthRequiredReply: 1,
  338. FeedbagRecentBuddyUpdate: 1,
  339. 0x0026: 1, // unknown
  340. 0x0027: 1, // unknown
  341. 0x0028: 1, // unknown
  342. },
  343. ICQ: {
  344. ICQErr: 1,
  345. ICQDBQuery: 1,
  346. ICQDBReply: 1,
  347. },
  348. BUCP: {
  349. BUCPErr: 1,
  350. BUCPLoginRequest: 1,
  351. BUCPRegisterRequest: 1,
  352. BUCPChallengeRequest: 1,
  353. BUCPAsasnRequest: 1,
  354. BUCPSecuridRequest: 1,
  355. BUCPRegistrationImageRequest: 1,
  356. },
  357. Alert: {
  358. AlertErr: 1,
  359. AlertSetAlertRequest: 1,
  360. AlertGetSubsRequest: 1,
  361. AlertNotifyCapabilities: 1,
  362. AlertNotify: 1,
  363. AlertGetRuleRequest: 1,
  364. AlertGetFeedRequest: 1,
  365. AlertRefreshFeed: 1,
  366. AlertEvent: 1,
  367. AlertQogSnac: 1,
  368. AlertRefreshFeedStock: 1,
  369. AlertNotifyTransport: 1,
  370. AlertSetAlertRequestV2: 1,
  371. AlertNotifyAck: 1,
  372. AlertNotifyDisplayCapabilities: 1,
  373. AlertUserOnline: 1,
  374. },
  375. },
  376. }
  377. }
  378. // SNACRateLimits maps SNACs to rate limit classes.
  379. type SNACRateLimits struct {
  380. lookup map[uint16]map[uint16]RateLimitClassID
  381. }
  382. // All returns an iterator over all SNAC message types and their associated
  383. // rate limit classes.
  384. func (rg SNACRateLimits) All() iter.Seq[struct {
  385. FoodGroup uint16
  386. SubGroup uint16
  387. RateLimitClass RateLimitClassID
  388. }] {
  389. return func(yield func(struct {
  390. FoodGroup uint16
  391. SubGroup uint16
  392. RateLimitClass RateLimitClassID
  393. }) bool) {
  394. for foodGroup, subGroups := range rg.lookup {
  395. for subGroup, classID := range subGroups {
  396. match := struct {
  397. FoodGroup uint16
  398. SubGroup uint16
  399. RateLimitClass RateLimitClassID
  400. }{
  401. FoodGroup: foodGroup,
  402. SubGroup: subGroup,
  403. RateLimitClass: classID,
  404. }
  405. if !yield(match) {
  406. return
  407. }
  408. }
  409. }
  410. }
  411. }
  412. // RateClassLookup returns the RateLimitClassID associated with the given SNAC
  413. // food group and subgroup.
  414. //
  415. // If a match is found, it returns the associated rate class ID and true.
  416. // If not found, it returns false.
  417. func (rg SNACRateLimits) RateClassLookup(foodGroup uint16, subGroup uint16) (RateLimitClassID, bool) {
  418. group, ok := rg.lookup[foodGroup]
  419. if !ok {
  420. return 0, false
  421. }
  422. class, ok := group[subGroup]
  423. if !ok {
  424. return 0, false
  425. }
  426. return class, true
  427. }
  428. // RateClass defines the configuration for computing rate-limiting behavior
  429. // using an exponential moving average over time.
  430. //
  431. // Each incoming event contributes a time delta (in ms), and the average inter-event
  432. // time is calculated over a moving window of the most recent N events (`WindowSize`).
  433. // The resulting average is compared against threshold levels to determine the
  434. // current rate status (e.g., limited, alert, clear, or disconnect).
  435. type RateClass struct {
  436. ID RateLimitClassID // Unique identifier for this rate class.
  437. WindowSize int32 // Number of samples used in the moving average calculation.
  438. ClearLevel int32 // If rate-limited and average exceeds this, rate-limiting is lifted.
  439. AlertLevel int32 // If average is below this, an alert state is triggered.
  440. LimitLevel int32 // If average is below this, rate-limiting is triggered.
  441. DisconnectLevel int32 // If average is below this, the session should be disconnected.
  442. MaxLevel int32 // Maximum allowed value for the moving average.
  443. }
  444. // CheckRateLimit calculates a rate limit status and a new moving average based on
  445. // the time elapsed between the last event and the current event, a specified rate
  446. // class, and whether the system is currently limited.
  447. //
  448. // Parameters:
  449. //
  450. // lastTime: The timestamp of the previous event.
  451. // currentTime: The current timestamp.
  452. // rateClass: Configuration for rate limiting thresholds and window size.
  453. // currentAvg: The current moving average of the elapsed time between events.
  454. // limitedNow: Indicates if the system is currently under a rate limit.
  455. //
  456. // Returns:
  457. //
  458. // status: The new RateLimitStatus, which can be one of:
  459. // - RateLimitStatusDisconnect (when the moving average is smallest)
  460. // - RateLimitStatusLimited
  461. // - RateLimitStatusAlert
  462. // - RateLimitStatusClear (when the moving average is largest)
  463. // newAvg: The updated moving average of the elapsed time.
  464. //
  465. // The function updates currentAvg by combining the current interval (the difference
  466. // between currentTime and lastTime, in milliseconds) with the previous average. If
  467. // the system was already limited (limitedNow == true), the function checks whether
  468. // currentAvg has risen above the ClearLevel threshold to move the status back to
  469. // RateLimitStatusClear. If not, it keeps the status at RateLimitStatusLimited.
  470. //
  471. // If the system was not already limited, the updated currentAvg is compared against
  472. // DisconnectLevel, LimitLevel, and AlertLevel thresholds of the provided RateClass
  473. // to determine the appropriate rate limit status.
  474. func CheckRateLimit(
  475. lastTime time.Time,
  476. currentTime time.Time,
  477. rateClass RateClass,
  478. currentAvg int32,
  479. limitedNow bool,
  480. ) (RateLimitStatus, int32) {
  481. // calculate the time elapsed in milliseconds since the last event
  482. elapsedMs := int32(currentTime.Sub(lastTime).Milliseconds())
  483. // update the moving average
  484. newAvg := (currentAvg*(rateClass.WindowSize-1) + elapsedMs) / rateClass.WindowSize
  485. // clamp the moving average to the maximum allowable level
  486. if newAvg > rateClass.MaxLevel {
  487. newAvg = rateClass.MaxLevel
  488. }
  489. var status RateLimitStatus
  490. switch {
  491. case newAvg < rateClass.DisconnectLevel:
  492. status = RateLimitStatusDisconnect
  493. case limitedNow && newAvg >= rateClass.ClearLevel:
  494. status = RateLimitStatusClear
  495. case limitedNow:
  496. status = RateLimitStatusLimited
  497. case newAvg < rateClass.LimitLevel:
  498. status = RateLimitStatusLimited
  499. case newAvg < rateClass.AlertLevel:
  500. status = RateLimitStatusAlert
  501. default:
  502. status = RateLimitStatusClear
  503. }
  504. return status, newAvg
  505. }