changeset 6490:771944f2a7c7

mod_s2s_v6mesh: Update configuration syntax and associated documentation
author Matthew Wild <mwild1@gmail.com>
date Tue, 24 Mar 2026 13:20:51 +0000
parents dff042647892
children 0a9b516e388e
files mod_s2s_v6mesh/README.md mod_s2s_v6mesh/mod_s2s_v6mesh.lua
diffstat 2 files changed, 118 insertions(+), 52 deletions(-) [+]
line wrap: on
line diff
--- a/mod_s2s_v6mesh/README.md	Mon Mar 23 19:47:03 2026 +0100
+++ b/mod_s2s_v6mesh/README.md	Tue Mar 24 13:20:51 2026 +0000
@@ -63,29 +63,37 @@
 
 ### Connection security
 
-By default, Prosody requires TLS for server-to-server connections. However, it
+Prosody typically requires TLS for server-to-server connections. However, it
 is not possible to obtain CA-issued certificates for `.v6.alt` domains. This
-module disables certificate validation (it treats all certificates as trusted)
-for all `v6.alt` domains where the IP address matches the domain name that the
-remote server is claiming to be (the actual certificate contents are
-irrelevant and never checked, so self-signed certificates are fine).
+raises challenges around authentication of peers.
 
-Additionally, if the IP is within one of the CIDR ranges listed in the
-`s2s_v6mesh_secure_ranges` configuration option, then the module will tell
-Prosody to disable TLS encryption for the connection.
+By default, this module treats all certificates as trusted for all `v6.alt`
+domains where the IP address matches the domain name that the remote server is
+claiming to be (the actual certificate contents are irrelevant and never
+checked, so self-signed certificates are fine). This provides a decent balance
+between convenience and security.
+
+Connections using v6.alt are automatically safe from whole categories of
+attacks such as DNS spoofing/poisoning (because such domains do not use DNS
+resolvers). However, any attack which enables an attacker to route an IP
+address to their own machine, for example, may be susceptible to a
+machine-in-the-middle attack, which cannot be ruled out on the internet.
 
-**Warning:** Prosody will treat connections within any configured ranges as
-already totally secure, and **TLS will be disabled**. Be *very* sure you trust
-the underlying network's security layer before adding an IP range to this
-list, and never add IPs that use the public internet. If you want similar
-behaviour for other (non-`v6.alt`) domains, look at [mod_secure_interfaces](https://modules.prosody.im/mod_secure_interfaces)
-and configure it with your mesh interface's address. If in doubt, just don't
-set any secure ranges, and use self-signed certificates with TLS instead.
+For ultimate security across untrusted networks, use self-signed certificates
+and pin fingerprints in the configuration on both sides using
+[mod_s2s_auth_fingerprint](https://modules.prosody.im/mod_s2s_auth_fingerprint).
+
+If you do this, or if you only want v6.alt certificates to be trusted within
+explicitly configured network ranges, you can disable the default certificate
+trusting behaviour using `s2s_v6mesh_trust_all_certs = false`.
 
-Finally, because of the above mechanisms in place, it is strongly recommended
-to keep Prosody's default settings of `s2s_require_encryption = true` and
-`s2s_secure_auth = true` so that connections not covered by this module are
-always encrypted and authenticated properly.
+You can configure connection security and trust settings per range, using the
+`s2s_v6mesh_ranges` configuration option (more details below).
+
+Regardless of any other options, we always recommend keeping Prosody's default
+settings of `s2s_require_encryption = true` and `s2s_secure_auth = true` so
+that connections not covered by this module are always encrypted and
+authenticated properly.
 
 ### Connecting from clients
 
@@ -110,9 +118,27 @@
 
 ## Configuration
 
-  | Name                     | Default| Description
-  |--------------------------|--------|-------------------------------------------------------------------------
-  | s2s_v6mesh_secure_ranges | `{}`   | A list of IP ranges to treat as secure (bypassing TLS)
+  | Name                         | Default | Description
+  |------------------------------|-------- |-------------------------------------------------------------------------
+  | s2s_v6mesh_ranges            | `{}`    | A list of IP ranges and associated configuration (see below)
+  | s2s_v6mesh_trust_all_certs   | `true`  | Whether to trust all certs from verified v6.alt addresses (even in unconfigured ranges)
+
+### Range configuration
+
+By default, this module will use TLS, and trust any certificate from a v6.alt
+domain if the IP address matches. You can override this behaviour on a
+per-range basis:
+
+```lua
+s2s_v6mesh_ranges = {
+    ["200::/7"] = {
+        -- disable TLS requirement for this IP range (overrides s2s_require_encryption)
+        use_tls = false;
+        -- trust all certificates (even self-signed) within this range (overrides s2s_secure_auth)
+        trust_all_certs = true;
+    };
+}
+```
 
 ## Compatibility
 
--- a/mod_s2s_v6mesh/mod_s2s_v6mesh.lua	Mon Mar 23 19:47:03 2026 +0100
+++ b/mod_s2s_v6mesh/mod_s2s_v6mesh.lua	Tue Mar 24 13:20:51 2026 +0000
@@ -9,15 +9,16 @@
 local ip = require "prosody.util.ip";
 local parse_cidr = require "prosody.util.ip".parse_cidr;
 
-local secure_ranges = module:get_option_set("s2s_v6mesh_secure_ranges", {});
+local mesh_ranges = module:get_option("s2s_v6mesh_ranges", {});
+local trust_all_certs = module:get_option_boolean("s2s_v6mesh_trust_all_certs", true);
 
 local function is_secure_range(remote_ip)
-	for range in secure_ranges do
+	for range, range_config in pairs(mesh_ranges) do
 		if ip.match(remote_ip, parse_cidr(range)) then
-			return true, range;
+			return range, range_config;
 		end
 	end
-	return false;
+	return nil;
 end
 
 function lookup(dom)
@@ -38,6 +39,30 @@
 	return net.ntop(raw), raw;
 end
 
+local function apply_range_config(host_module, host, session, target_ip)
+	local range, range_config = is_secure_range(target_ip);
+
+	if range then
+		if range_config.use_tls == false then
+			host_module:log("debug", "Treating non-TLS connection to %s as secure because of range configuration", host);
+			session.secure = true;
+		end
+		if range_config.trust_all_certs ~= false then
+			module:log("debug", "Auto-trusting certificate for %s because of range configuration", host);
+			session.authenticated_remote = true;
+			session.cert_chain_status = "valid";
+			session.cert_identity_status = "valid";
+		else
+			module:log("debug", "Not auto-trusting certificate for $s (IP in unconfigured range): %s", host);
+		end
+	elseif trust_all_certs then
+		module:log("debug", "Auto-trusting certificate for %s because of default policy", host);
+		session.authenticated_remote = true;
+		session.cert_chain_status = "valid";
+		session.cert_identity_status = "valid";
+	end
+end
+
 function module.add_host(host_module)
 	host_module:hook("s2sout-pre-connect", function(event)
 		local session = event.session;
@@ -54,13 +79,8 @@
 
 		event.resolver = basic_resolver.new(target_ip, 5269, "tcp", {});
 
-		if is_secure_range(ip.new_ip(target_ip, "IPv6")) then
-			host_module:log("debug", "Treating non-TLS connection to %s as secure because it's in a secure IP range", host);
-			session.secure = true;
-			session.authenticated_remote = true;
-			session.cert_chain_status = "valid";
-			session.cert_identity_status = "valid";
-		end
+		local range_ip = ip.new_ip(target_ip, "IPv6");
+		apply_range_config(host_module, host, session, range_ip);
 	end);
 
 	host_module:hook("s2s-stream-features", function (event)
@@ -80,13 +100,7 @@
 			return;
 		end
 
-		if is_secure_range(remote_ip) then
-			host_module:log("debug", "Treating non-TLS connection from %s as secure because it's in a secure IP range", host);
-			session.secure = true;
-			session.authenticated_remote = true;
-			session.cert_chain_status = "valid";
-			session.cert_identity_status = "valid";
-		end
+		apply_range_config(host_module, host, session, remote_ip);
 	end, 200);
 end
 
@@ -105,10 +119,29 @@
 		return;
 	end
 
-	module:log("debug", "Trusting certificate for %s because of IP match", host);
-	session.cert_chain_status = "valid";
-	session.cert_identity_status = "valid";
-	return true;
+	local range, range_config = is_secure_range(remote_ip);
+	if range then
+		if range_config.trust_all_certs ~= false then
+			module:log("debug", "Auto-trusting certificate for %s because of IP match", host);
+			session.cert_chain_status = "valid";
+			session.cert_identity_status = "valid";
+		else
+			module:log("debug", "Not auto-trusting certificate for $s (IP in unconfigured range): %s", host);
+		end
+	elseif trust_all_certs then
+		module:log("debug", "Auto-trusting certificate for %s because of default policy", host);
+		session.authenticated_remote = true;
+		session.cert_chain_status = "valid";
+		session.cert_identity_status = "valid";
+		return true;
+	end
+
+	if not session.conn.extra then
+		session.conn.extra = {};
+	end
+	-- This can be used by mod_s2s_auth_certs to validate IP certificates,
+	-- if supported.
+	session.conn.extra.secure_hostname = tostring(remote_ip);
 end, 600);
 
 module:add_item("shell-command", {
@@ -160,23 +193,30 @@
 module:add_item("shell-command", {
 	section = "v6alt";
 	section_desc = module.name.." utility commands";
-	name = "is_secure_range";
-	desc = "Check whether an IP address is considered secure";
+	name = "range";
+	desc = "Check whether a host/IP address is within a configured range";
 	args = {
 		{ name = "ip", type = "string" };
 	};
-	handler = function(self, ip_str) --luacheck: ignore 212/self 212/host
+	handler = function(self, str) --luacheck: ignore 212/self 212/host
+		local encoded = str:match("([%w-]+)%.v6%.alt$");
+
+		local ip_str = encoded and lookup(encoded) or str;
 		local user_ip = ip.new_ip(ip_str, "IPv6");
 		if not user_ip then
-			return nil, "Invalid IP address: "..tostring(ip_str);
+			return nil, "Invalid domain or IP: "..tostring(str);
 		end
 
-		local secure, range = is_secure_range(user_ip);
+		local range, range_config = is_secure_range(user_ip);
 
-		if not secure then
-			return true, "Not secure";
+		if not range then
+			return true, "Not configured";
 		end
 
-		return true, ("In secure range: %s"):format(range);
+		self.session.print(("In range: %s"):format(range));
+		self.session.print(("Use TLS: %s"):format(range_config.use_tls == false and "no" or "yes"));
+		self.session.print(("Trust certs: %s"):format(range_config.trust_all_certs == false and "no" or "yes"));
+
+		return true, "IP is in configured range";
 	end;
 });