comparison plugins/mod_limits.lua @ 14133:4087c5b600b1 13.0

mod_limits: Support limiting total number of connections by IP address/block
author Matthew Wild <mwild1@gmail.com>
date Thu, 16 Apr 2026 18:54:56 +0100
parents f65302ea37b0
children e38916e22f59
comparison
equal deleted inserted replaced
14132:1e005ba71f0d 14133:4087c5b600b1
1 -- Because we deal with pre-authed sessions and streams we can't be host-specific 1 -- Because we deal with pre-authed sessions and streams we can't be host-specific
2 module:set_global(); 2 module:set_global();
3 3
4 local it = require "prosody.util.iterators";
4 local filters = require "prosody.util.filters"; 5 local filters = require "prosody.util.filters";
5 local throttle = require "prosody.util.throttle"; 6 local throttle = require "prosody.util.throttle";
6 local timer = require "prosody.util.timer"; 7 local timer = require "prosody.util.timer";
8
9 local pton = require "prosody.util.net".pton;
10
7 local ceil = math.ceil; 11 local ceil = math.ceil;
8 12
9 local limits_cfg = module:get_option("limits", {}); 13 local limits_cfg = module:get_option("limits", {});
10 local limits_resolution = module:get_option_period("limits_resolution", 1); 14 local limits_resolution = module:get_option_period("limits_resolution", 1);
11 15
48 bytes_per_second = 30 * 1024; 52 bytes_per_second = 30 * 1024;
49 burst_seconds = 2; 53 burst_seconds = 2;
50 }; 54 };
51 }; 55 };
52 56
57 local function get_truncated_ip(conn)
58 local session_ip = pton(conn:ip());
59 if #session_ip == 128 then
60 -- Use /64 for IPv6
61 session_ip = session_ip:sub(1, 8);
62 end
63 return session_ip;
64 end
65
66 local function limit_ip_sessions(session_type, max_sessions)
67 -- [packed_truncated_ip] = count
68 local current_ips = module:shared("ips."..session_type);
69 module:hook(session_type.."-connected", function (event)
70 local session_ip = get_truncated_ip(event.conn);
71 local new_count = (current_ips[session_ip] or 0) + 1;
72 current_ips[session_ip] = new_count;
73 if new_count > max_sessions then
74 module:log("warn", "Rejecting stream from %s (%d of %d current connections)", event.conn:ip(), new_count, max_sessions);
75 event.session:close({ condition = "policy-violation", text = "too many connections" });
76 end
77 end);
78
79 module:hook(session_type.."-closed", function (event)
80 local session_ip = get_truncated_ip(event.conn);
81 local new_count = (current_ips[session_ip] or 0) - 1;
82 if new_count < 0 then
83 module:log("warn", "IP counting failure");
84 new_count = 0;
85 end
86 if new_count == 0 then
87 current_ips[session_ip] = nil;
88 else
89 current_ips[session_ip] = new_count;
90 end
91 end);
92 end
93
53 for sess_type, sess_limits in pairs(limits_cfg) do 94 for sess_type, sess_limits in pairs(limits_cfg) do
54 limits[sess_type] = { 95 limits[sess_type] = {
55 bytes_per_second = parse_rate(sess_limits.rate, sess_type); 96 bytes_per_second = parse_rate(sess_limits.rate, sess_type);
56 burst_seconds = parse_burst(sess_limits.burst, sess_type); 97 burst_seconds = parse_burst(sess_limits.burst, sess_type);
57 }; 98 };
99 if sess_limits.max_connections_per_ip then
100 limit_ip_sessions(sess_type, sess_limits.max_connections_per_ip);
101 end
58 end 102 end
59 103
60 if not limits.s2sin and limits.s2s then 104 if not limits.s2sin and limits.s2s then
61 limits.s2sin = limits.s2s; 105 limits.s2sin = limits.s2s;
62 end 106 end
156 end 200 end
157 end); 201 end);
158 202
159 end 203 end
160 end 204 end
205
206 module:add_item("shell-command", {
207 section = "limits";
208 section_desc = "Commands to inspect rate-limiting state";
209 name = "connections";
210 desc = "View per-IP connection limit status";
211 args = {
212 { name = "ip", type = "string" };
213 };
214 handler = function(self, ip_str) -- luacheck: ignore 212/self
215 local check_ip = pton(ip_str);
216 if not check_ip then
217 return false, "Unable to parse IP address: "..ip_str;
218 end
219 if #check_ip == 128 then
220 check_ip = check_ip:sub(1, 8);
221 end
222
223 local found, any_limits = 0;
224
225 for sess_type, sess_limits in it.sorted_pairs(limits_cfg) do
226 if sess_limits.max_connections_per_ip then
227 any_limits = true;
228 local current_ips = module:shared("ips."..sess_type);
229 local count = current_ips[check_ip] or 0;
230 self.session.print(("%s: %d (limit %d)"):format(sess_type, count or 0, sess_limits.max_connections_per_ip));
231 found = found + count;
232 end
233 end
234
235 if not any_limits then
236 return false, "No per-IP limits are configured";
237 end
238
239 return true, ("Total of %d tracked connections with this IP"):format(found);
240 end;
241 });
242
243