changeset 6502:b15601203d0d

mod_firewall: Support 'FROM IP' and 'FROM SUBNET' conditions
author Matthew Wild <mwild1@gmail.com>
date Thu, 26 Mar 2026 13:33:44 +0000
parents f7158c0e2202
children 74774ec3af1d
files mod_firewall/README.md mod_firewall/conditions.lib.lua mod_firewall/mod_firewall.lua
diffstat 3 files changed, 58 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/mod_firewall/README.md	Wed Mar 25 18:52:10 2026 +0000
+++ b/mod_firewall/README.md	Thu Mar 26 13:33:44 2026 +0000
@@ -302,6 +302,29 @@
 stanza. It is not advisable to perform access control or similar rules
 on JIDs in these chains (see the [chain documentation](#chains) for more info).
 
+#### IP matching
+
+  Condition        Matches
+  ---------------- -------------------------------------------------------------
+  `FROM IP`        The origin/session's IP address matches the provided IP address
+  `FROM SUBNET`    The origin/session's IP is within the given (CIDR) subnet range
+
+
+For example:
+
+```
+# Only allow stanzas to user@example.com from clients connected to 127.0.0.1
+NOT FROM IP: 127.0.0.1
+TO: user@example.com
+BOUNCE=policy-violation
+
+FROM SUBNET: 10.0.0.0/8
+PASS.
+
+FROM SUBNET: 2001:db8::/32
+PASS.
+```
+
 #### GeoIP matching
 
   Condition        Matches
--- a/mod_firewall/conditions.lib.lua	Wed Mar 25 18:52:10 2026 +0000
+++ b/mod_firewall/conditions.lib.lua	Thu Mar 26 13:33:44 2026 +0000
@@ -2,6 +2,8 @@
 local condition_handlers = {};
 
 local jid = require "util.jid";
+local iplib = require "util.ip";
+local sha256 = require "util.hashes".sha256;
 local unpack = table.unpack or unpack;
 
 -- Helper to convert user-input strings (yes/true//no/false) to a bool
@@ -381,6 +383,28 @@
 	};
 end
 
+function condition_handlers.FROM_IP(ip_spec)
+	local parsed_ip, err = iplib.new_ip(ip_spec);
+	if not parsed_ip then
+		error("Cannot parse IP address: "..(err or "unknown error"));
+	end
+	local normal_ip = tostring(parsed_ip);
+	return ("session.ip == %q"):format(normal_ip);
+end
+
+function condition_handlers.FROM_SUBNET(subnet_spec)
+	local subnet, bits = iplib.parse_cidr(subnet_spec);
+	if not subnet then
+		error("Cannot parse subnet CIDR");
+	end
+	local spec_hash = sha256(subnet_spec, true):sub(1, 8);
+	return ("iplib.match(session_parsed_ip, parsed_ip_%s, %d)"):format(
+		spec_hash, bits
+	), {
+		"iplib", "session_parsed_ip", "parsed_ip:"..spec_hash..":"..tostring(subnet)
+	};
+end
+
 -- FROM COUNTRY: SE
 -- FROM COUNTRY: code=SE
 -- FROM COUNTRY: SWE
--- a/mod_firewall/mod_firewall.lua	Wed Mar 25 18:52:10 2026 +0000
+++ b/mod_firewall/mod_firewall.lua	Thu Mar 26 13:33:44 2026 +0000
@@ -275,6 +275,17 @@
 	iplib = {
 		global_code = [[local iplib = require "util.ip";]];
 	};
+	session_parsed_ip = {
+		local_code = [[local session_parsed_ip = iplib.new_ip(session.ip)]];
+		depends = { "iplib" };
+	};
+	parsed_ip = {
+		local_code = function (parsed_ip_spec)
+			local ip_hash, ip_str = parsed_ip_spec:match("^(%x+):(.+)$");
+			local code = ([[local parsed_ip_%s = iplib.new_ip(%q)]]):format(ip_hash, ip_str)
+			return code, { "iplib" };
+		end;
+	};
 	geoip_country = {
 		global_code = [[
 local geoip_country = require "geoip.country";