You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mod_rate_limit.lua 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. -- Rate limits connection based on their ip address.
  2. -- Rate limits creating sessions (new connections),
  3. -- rate limits sent stanzas from same ip address (presence, iq, messages)
  4. -- Copyright (C) 2023-present 8x8, Inc.
  5. local cache = require"util.cache";
  6. local ceil = math.ceil;
  7. local http_server = require "net.http.server";
  8. local gettime = require "util.time".now
  9. local filters = require "util.filters";
  10. local new_throttle = require "util.throttle".create;
  11. local timer = require "util.timer";
  12. local ip_util = require "util.ip";
  13. local new_ip = ip_util.new_ip;
  14. local match_ip = ip_util.match;
  15. local parse_cidr = ip_util.parse_cidr;
  16. local get_ip = module:require "util".get_ip;
  17. local config = {};
  18. local limits_resolution = 1;
  19. local function load_config()
  20. -- Max allowed login rate in events per second.
  21. config.login_rate = module:get_option_number("rate_limit_login_rate", 3);
  22. -- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
  23. config.ip_rate = module:get_option_number("rate_limit_ip_rate", 2000);
  24. -- The rate to which sessions exceeding the stanza(iq, presence, message) rate will be limited, in bytes per second.
  25. config.session_rate = module:get_option_number("rate_limit_session_rate", 1000);
  26. -- The time in seconds, after which the limit for an IP address is lifted.
  27. config.timeout = module:get_option_number("rate_limit_timeout", 60);
  28. -- List of regular expressions for IP addresses that are not limited by this module.
  29. config.whitelist = module:get_option_set("rate_limit_whitelist", { "127.0.0.1", "::1" })._items;
  30. -- The size of the cache that saves state for IP addresses
  31. config.cache_size = module:get_option_number("rate_limit_cache_size", 10000);
  32. -- Max allowed presence rate in events per second.
  33. config.presence_rate = module:get_option_number("rate_limit_presence_rate", 4);
  34. -- Max allowed iq rate in events per second.
  35. config.iq_rate = module:get_option_number("rate_limit_iq_rate", 15);
  36. -- Max allowed message rate in events per second.
  37. config.message_rate = module:get_option_number("rate_limit_message_rate", 3);
  38. -- A list of hosts for which sessions we ignore rate limiting
  39. config.whitelist_hosts = module:get_option_set("rate_limit_whitelist_hosts", {});
  40. local wl = "";
  41. for ip in config.whitelist do wl = wl .. ip .. "," end
  42. local wl_hosts = "";
  43. for j in config.whitelist_hosts do wl_hosts = wl_hosts .. j .. "," end
  44. module:log("info", "Loaded configuration: ");
  45. module:log("info", "- ip_rate=%s bytes/sec, session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_hosts=%s",
  46. config.ip_rate, config.session_rate, config.timeout, config.cache_size, wl, wl_hosts);
  47. module:log("info", "- login_rate=%s/sec, presence_rate=%s/sec, iq_rate=%s/sec, message_rate=%s/sec",
  48. config.login_rate, config.presence_rate, config.iq_rate, config.message_rate);
  49. end
  50. load_config();
  51. -- Maps an IP address to a util.throttle which keeps the rate of login/join events from that IP.
  52. local login_rates = cache.new(config.cache_size);
  53. -- Keeps the IP addresses that have exceeded the allowed login/join rate (i.e. the IP addresses whose sessions need
  54. -- to be limited). Mapped to the last instant at which the rate was exceeded.
  55. local limited_ips = cache.new(config.cache_size);
  56. local function is_whitelisted(ip)
  57. local parsed_ip = new_ip(ip)
  58. for entry in config.whitelist do
  59. if match_ip(parsed_ip, parse_cidr(entry)) then
  60. return true;
  61. end
  62. end
  63. return false;
  64. end
  65. local function is_whitelisted_host(h)
  66. return config.whitelist_hosts:contains(h);
  67. end
  68. -- Add an IP to the set of limied IPs
  69. local function limit_ip(ip)
  70. module:log("info", "Limiting %s due to login/join rate exceeded.", ip);
  71. limited_ips:set(ip, gettime());
  72. end
  73. -- Installable as a session filter to limit the reading rate for a session. Based on mod_limits.
  74. local function limit_bytes_in(bytes, session)
  75. local sess_throttle = session.jitsi_throttle;
  76. if sess_throttle then
  77. -- if the limit timeout has elapsed let's stop the throttle
  78. if not sess_throttle.start or gettime() - sess_throttle.start > config.timeout then
  79. module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
  80. session.jitsi_throttle = nil;
  81. return bytes;
  82. end
  83. local ok, _, outstanding = sess_throttle:poll(#bytes, true);
  84. if not ok then
  85. session.log("debug",
  86. "Session over rate limit (%d) with %d (by %d), pausing", sess_throttle.max, #bytes, outstanding);
  87. outstanding = ceil(outstanding);
  88. session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
  89. local outstanding_data = bytes:sub(-outstanding);
  90. bytes = bytes:sub(1, #bytes-outstanding);
  91. timer.add_task(limits_resolution, function ()
  92. if not session.conn then return; end
  93. if sess_throttle:peek(#outstanding_data) then
  94. session.log("debug", "Resuming paused session");
  95. session.conn:resume();
  96. end
  97. -- Handle what we can of the outstanding data
  98. session.data(outstanding_data);
  99. end);
  100. end
  101. end
  102. return bytes;
  103. end
  104. -- Throttles reading from the connection of a specific session.
  105. local function throttle_session(session, rate, timeout)
  106. if not session.jitsi_throttle then
  107. if (session.conn and session.conn.setlimit) then
  108. session.jitsi_throttle_counter = session.jitsi_throttle_counter + 1;
  109. module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s, counter=%s.",
  110. rate, session.id, session.ip, session.jitsi_throttle_counter);
  111. session.conn:setlimit(rate);
  112. if timeout then
  113. if session.jitsi_throttle_timer then
  114. -- if there was a timer stop it as we will schedule a new one
  115. session.jitsi_throttle_timer:stop();
  116. session.jitsi_throttle_timer = nil;
  117. end
  118. session.jitsi_throttle_timer = module:add_timer(timeout, function()
  119. if session.conn then
  120. module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
  121. session.conn:setlimit(0);
  122. end
  123. session.jitsi_throttle_timer = nil;
  124. end);
  125. end
  126. else
  127. module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", rate, session.id, session.ip);
  128. session.jitsi_throttle = new_throttle(rate, 2);
  129. filters.add_filter(session, "bytes/in", limit_bytes_in, 1000);
  130. -- throttle.start used for stop throttling after the timeout
  131. session.jitsi_throttle.start = gettime();
  132. end
  133. else
  134. -- update the throttling start
  135. session.jitsi_throttle.start = gettime();
  136. end
  137. end
  138. -- checks different stanzas for rate limiting (per session)
  139. function filter_stanza(stanza, session)
  140. local rate = session[stanza.name.."_rate"];
  141. if rate then
  142. local ok, _, _ = rate:poll(1, true);
  143. if not ok then
  144. module:log("info", "%s rate exceeded for %s, limiting.", stanza.name, session.full_jid);
  145. throttle_session(session, config.session_rate, config.timeout);
  146. end
  147. end
  148. return stanza;
  149. end
  150. local function on_login(session, ip)
  151. local login_rate = login_rates:get(ip);
  152. if not login_rate then
  153. module:log("debug", "Create new join rate for %s", ip);
  154. login_rate = new_throttle(config.login_rate, 2);
  155. login_rates:set(ip, login_rate);
  156. end
  157. local ok, _, _ = login_rate:poll(1, true);
  158. if not ok then
  159. module:log("info", "Join rate exceeded for %s, limiting.", ip);
  160. limit_ip(ip);
  161. end
  162. end
  163. local function filter_hook(session)
  164. -- ignore outgoing sessions (s2s)
  165. if session.outgoing then
  166. return;
  167. end
  168. local ip = get_ip(session);
  169. module:log("debug", "New session from %s", ip);
  170. if is_whitelisted(ip) or is_whitelisted_host(session.host) then
  171. return;
  172. end
  173. on_login(session, ip);
  174. -- creates the stanzas rates
  175. session.jitsi_throttle_counter = 0;
  176. session.presence_rate = new_throttle(config.presence_rate, 2);
  177. session.iq_rate = new_throttle(config.iq_rate, 2);
  178. session.message_rate = new_throttle(config.message_rate, 2);
  179. filters.add_filter(session, "stanzas/in", filter_stanza);
  180. local oldt = limited_ips:get(ip);
  181. if oldt then
  182. local newt = gettime();
  183. local elapsed = newt - oldt;
  184. if elapsed < config.timeout then
  185. if elapsed < 5 then
  186. module:log("info", "IP address %s was limited %s seconds ago, refreshing.", ip, elapsed);
  187. limited_ips:set(ip, newt);
  188. end
  189. throttle_session(session, config.ip_rate);
  190. else
  191. module:log("info", "Removing the limit for %s", ip);
  192. limited_ips:set(ip, nil);
  193. end
  194. end
  195. end
  196. function module.load()
  197. filters.add_filter_hook(filter_hook);
  198. end
  199. function module.unload()
  200. filters.remove_filter_hook(filter_hook);
  201. end
  202. module:hook_global("config-reloaded", load_config);
  203. -- we calculate the stats on the configured interval (60 seconds by default)
  204. local measure_limited_ips = module:measure('limited-ips', 'amount'); -- we send stats for the total number limited ips
  205. module:hook_global('stats-update', function ()
  206. measure_limited_ips(limited_ips:count());
  207. end);