Hello,

Please find attached a patch updating net/i2pd from 2.61.0 to 3.0.0.

https://github.com/PurpleI2P/i2pd/releases/tag/3.0.0

Besides the version update, the patch includes:

- LibreSSL compatibility fixes, including ML-KEM, Ed25519ph and
  OpenSSL 3 API detection.
- Improved cryptographic error handling in NTCP2, SSU2 and ECIES.
- OpenBSD pledge/unveil initialization using resolved paths and
  the effective configuration.
- Synchronous signal handling to avoid unsafe configuration reloads.
- SSU2 MTU detection using getifaddrs().
- UDP tunnel cleanup and shutdown fixes.
- Updated PLIST, configuration samples and rc.d integration.

The changes preserve OpenBSD's existing package layout and service
configuration.

I would appreciate review, particularly of the pledge/unveil changes
and LibreSSL compatibility.

Thanks,
David.
Index: net/i2pd/Makefile
===================================================================
RCS file: /cvs/ports/net/i2pd/Makefile,v
diff -u -p -u -p -r1.34 Makefile
--- net/i2pd/Makefile	17 Oct 2026 08:47:19 -0000	1.34
+++ net/i2pd/Makefile	21 Oct 2026 15:41:33 -0000
@@ -2,7 +2,7 @@ COMMENT =	client for the I2P anonymous n
 
 GH_ACCOUNT =	PurpleI2P
 GH_PROJECT =	i2pd
-GH_TAGNAME =	2.61.0
+GH_TAGNAME =	3.0.0
 
 CATEGORIES =	net
 HOMEPAGE =	https://i2pd.website
@@ -12,9 +12,11 @@ MAINTAINER =	SystemFailure <openbsd@syst
 # BSD
 PERMIT_PACKAGE = Yes
 
+# uses pledge() and unveil()
 WANTLIB += ${COMPILER_LIBCXX}
-WANTLIB += boost_atomic-mt boost_container-mt boost_filesystem-mt
-WANTLIB += boost_program_options-mt c crypto m miniupnpc ssl z
+WANTLIB += boost_filesystem-mt boost_program_options-mt
+WANTLIB += boost_atomic-mt boost_container-mt boost_json-mt boost_url-mt
+WANTLIB += c crypto m miniupnpc ssl z
 
 COMPILER =	base-clang ports-gcc
 MODULES =	devel/cmake
@@ -29,16 +31,15 @@ CONFIGURE_ARGS =	-DWITH_UPNP=ON
 WRKSRC =	${WRKDIST}/build
 
 post-install:
-	${INSTALL_DATA_DIR} ${PREFIX}/include/i2pd \
-		${PREFIX}/share/examples/i2pd
+	${INSTALL_DATA_DIR} ${PREFIX}/include/i2pd
 	${INSTALL_DATA} ${WRKDIST}/libi2pd{,_client}/*.h \
-		 ${PREFIX}/include/i2pd
+		${PREFIX}/include/i2pd
 .for dir in family reseed
-	${INSTALL_DATA_DIR} ${WRKINST}/var/i2pd/certificates/${dir}
+	${INSTALL_DATA_DIR} ${PREFIX}/share/examples/i2pd/certificates/${dir}
 	${INSTALL_DATA} ${WRKDIST}/contrib/certificates/${dir}/* \
-		${WRKINST}/var/i2pd/certificates/${dir}
+		${PREFIX}/share/examples/i2pd/certificates/${dir}
 .endfor
-	${SUBST_DATA} ${WRKDIST}/contrib/i2pd.conf \
+	${INSTALL_DATA} ${WRKDIST}/contrib/i2pd.conf \
 		${PREFIX}/share/examples/i2pd/i2pd.conf
 	${INSTALL_DATA} ${WRKDIST}/contrib/tunnels.conf \
 		${PREFIX}/share/examples/i2pd/tunnels.conf
Index: net/i2pd/distinfo
===================================================================
RCS file: /cvs/ports/net/i2pd/distinfo,v
diff -u -p -u -p -r1.27 distinfo
--- net/i2pd/distinfo	17 Oct 2026 08:47:19 -0000	1.27
+++ net/i2pd/distinfo	21 Oct 2026 15:41:33 -0000
@@ -1,2 +1,2 @@
-SHA256 (i2pd-2.61.0.tar.gz) = QJzTwCV0kShmEatqr2kJQMckj7iYN3wT+ttlqDbioKs=
-SIZE (i2pd-2.61.0.tar.gz) = 779272
+SHA256 (i2pd-3.0.0.tar.gz) = O+vJVnZYlE04jDgvvsYbe8WD36mYICi0K5bus1T3m9M=
+SIZE (i2pd-3.0.0.tar.gz) = 861084
Index: net/i2pd/patches/patch-contrib_i2pd_conf
===================================================================
RCS file: /cvs/ports/net/i2pd/patches/patch-contrib_i2pd_conf,v
diff -u -p -u -p -r1.1 patch-contrib_i2pd_conf
--- net/i2pd/patches/patch-contrib_i2pd_conf	17 Oct 2026 08:47:19 -0000	1.1
+++ net/i2pd/patches/patch-contrib_i2pd_conf	21 Oct 2026 15:41:33 -0000
@@ -1,23 +1,29 @@
+Set the packaged configuration paths and defaults for the OpenBSD i2pd layout.
+
 Index: contrib/i2pd.conf
 --- contrib/i2pd.conf.orig
 +++ contrib/i2pd.conf
-@@ -8,16 +8,16 @@
+@@ -7,17 +7,17 @@
+ ## by removing the "#" symbol.
  
  ## Tunnels config file
- ## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf
+-## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf
 -# tunconf = /var/lib/i2pd/tunnels.conf
-+tunconf = ${SYSCONFDIR}/i2pd/tunnels.conf
++## Default: ~/.i2pd/tunnels.conf or /var/i2pd/tunnels.conf
++tunconf = /etc/i2pd/tunnels.conf
  
  ## Tunnels config files path
  ## Use that path to store separated tunnels in different config files.
- ## Default: ~/.i2pd/tunnels.d or /var/lib/i2pd/tunnels.d
+-## Default: ~/.i2pd/tunnels.d or /var/lib/i2pd/tunnels.d
 -# tunnelsdir = /var/lib/i2pd/tunnels.d
-+tunnelsdir = ${SYSCONFDIR}/i2pd/tunnels.d
++## Default: ~/.i2pd/tunnels.d or /var/i2pd/tunnels.d
++tunnelsdir = /etc/i2pd/tunnels.d
  
  ## Path to certificates used for verifying .su3, families
- ## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates
+-## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates
 -# certsdir = /var/lib/i2pd/certificates
-+certsdir = ${SYSCONFDIR}/i2pd/certificates
++## Default: ~/.i2pd/certificates or /var/i2pd/certificates
++certsdir = /etc/i2pd/certificates
  
  ## Where to write pidfile (default: /run/i2pd.pid, not used in Windows)
  # pidfile = /run/i2pd.pid
Index: net/i2pd/patches/patch-daemon_Daemon_cpp
===================================================================
RCS file: /cvs/ports/net/i2pd/patches/patch-daemon_Daemon_cpp,v
diff -u -p -u -p -r1.1 patch-daemon_Daemon_cpp
--- net/i2pd/patches/patch-daemon_Daemon_cpp	17 Oct 2026 08:47:19 -0000	1.1
+++ net/i2pd/patches/patch-daemon_Daemon_cpp	21 Oct 2026 15:41:33 -0000
@@ -1,14 +1,266 @@
-neuter broken pledge/unveil
+Resolve configured paths before daemon startup changes directories, and prepare
+OpenBSD sandbox settings using those absolute paths.
 
 Index: daemon/Daemon.cpp
 --- daemon/Daemon.cpp.orig
 +++ daemon/Daemon.cpp
-@@ -107,7 +107,7 @@ namespace util
- 		i2p::config::ParseConfig(config);
+@@ -11,7 +11,12 @@
+ #include <regex>
+ 
+ #ifdef __OpenBSD__
+-#	include<unistd.h>
++#include <unistd.h>
++#include <limits.h>
++#include <stdexcept>
++#include <cerrno>
++#include <cstring>
++#include <cstdlib>
+ #endif
+ 
+ #include "Daemon.h"
+@@ -43,6 +48,19 @@ namespace i2p
+ {
+ namespace util
+ {
++#ifdef __OpenBSD__
++	// Keep paths stable across DaemonUnix::start()'s chdir(). The leaf need
++	// not exist yet (log files, for example), so realpath() is unsuitable.
++	static std::string AbsolutePath (const std::string& path)
++	{
++		if (path.empty () || path.front () == '/') return path;
++		char cwd[PATH_MAX];
++		if (!getcwd (cwd, sizeof (cwd)))
++			throw std::runtime_error ("Daemon: Can't resolve working directory");
++		return std::string (cwd) + "/" + path;
++	}
++#endif
++
+ 	class Daemon_Singleton::Daemon_Singleton_Private
+ 	{
+ 	public:
+@@ -91,7 +109,11 @@ namespace util
+ 		}
+ 
+ 		i2p::fs::DetectDataDir(datadir, IsService());
++#ifdef __OpenBSD__
++		i2p::fs::DetectDataDir (AbsolutePath (i2p::fs::GetDataDir ()), IsService ());
++#else
+ 		i2p::fs::Init();
++#endif
+ 
+ 		datadir = i2p::fs::GetDataDir();
+ 
+@@ -108,156 +130,23 @@ namespace util
  		i2p::config::Finalize();
  
+ #ifdef __OpenBSD__
+-		auto init_pledge = []() {
+-			std::string pledge_file; i2p::config::GetOption("openbsd.pledge_file", pledge_file);
+-			if (pledge_file == "")
+-			{
+-				LogPrint(eLogDebug, "Default pledge values are set");
+-				// TODO: remove that not need
+-				// proc used in daemonunix, flock used in daemonunix, unix using
+-				pledge("stdio rpath wpath cpath inet dns unix recvfd sendfd proc error mcast chown flock",nullptr);
+-			} else {
+-				std::ifstream f(pledge_file);
+-				if(!f) {
+-					std::cerr << "Can't open pledge file " << pledge_file<<std::endl;
+-					exit(1);
+-				}
+-				std::string line;
+-				std::vector<std::string> rules;
+-				while(std::getline(f, line)){
+-					rules.push_back(line);
+-				}
+-				if(f.bad()) {
+-					std::cerr << "IO error with pledge file" << std::endl;
+-				}
+-				std::ostringstream out;
+-				for(auto r : rules)
+-					out << r << " ";
+-				pledge(out.str().c_str(), nullptr);
+-			}		
+-
+-
+-		};
+-		auto unveil_path = [](std::string path, std::string rules = "r")
++		// A datadir in the configuration is not available during config lookup.
++		std::string configuredDataDir = DaemonDataDir;
++		if (configuredDataDir.empty ())
++			i2p::config::GetOption ("datadir", configuredDataDir);
++		i2p::fs::DetectDataDir (configuredDataDir, IsService ());
++		i2p::fs::DetectDataDir (AbsolutePath (i2p::fs::GetDataDir ()), IsService ());
++		datadir = i2p::fs::GetDataDir ();
++		i2p::fs::Init ();
++		i2p::config::SetOption ("conf", AbsolutePath (config));
++		for (const char * option : {"tunconf", "tunnelsdir", "certsdir", "logfile", "pidfile",
++			"reseed.file", "reseed.zipfile", "addressbook.hostsfile",
++			"openbsd.pledge_file", "openbsd.unevil_file"})
+ 		{
+-			if(path=="")
+-			{
+-				LogPrint(eLogDebug, "empty path for unveil");
+-				return;
+-			}
+-			if ( unveil(path.c_str(), rules.c_str()) == -1 )
+-			{
+-				LogPrint(eLogError, "Can't unveil " + path + " with rules " + rules);
+-				throw std::runtime_error("can't unveil");
+-			}
+-			LogPrint(eLogInfo, "unveil " + path + " with rules " + rules);
+-		};
+-		auto init_unveil = [unveil_path](std::string datadir, std::string config) {
+-
+-			std::string tunnelsdir, certsdir, logfile,  reseed_file, openbsd_pledge_file,
+-				tunconf, pidfile, i2pcontrol_cert, i2pcontrol_key, unveil_file;
+-
+-			i2p::config::GetOption("openbsd.unveil_file",unveil_file);
+-			i2p::config::GetOption("certsdir", certsdir);
+-			if(certsdir == "")
+-			{
+-				certsdir = datadir+"/certificates";
+-			}
+-			i2p::config::GetOption("tunnelsdir", tunnelsdir);
+-			if(tunnelsdir == "")
+-			{
+-				tunnelsdir = datadir+"/tunnels.d";
+-			}
+-			i2p::config::GetOption("reseed.file", reseed_file);
+-			i2p::config::GetOption("openbsd.pledge_file", openbsd_pledge_file);
+-			i2p::config::GetOption("tunconf", tunconf); 
+-			if(tunconf == "")
+-			{
+-				tunconf = datadir+"/tunnels.conf";
+-			}
+-			if(config == "")
+-			{
+-				config = datadir+"/i2pd.conf";
+-			}
+-			i2p::config::GetOption("pidfile", pidfile); 
+-			if(pidfile == "")
+-			{
+-				pidfile = datadir+"/i2pd.pid";
+-			}
+-			i2p::config::GetOption("logfile", logfile);
+-			if(logfile == "")
+-			{
+-				logfile = datadir+"/logfile"; //for --service
+-			}
+-			i2p::config::GetOption("i2pcontrol.cert", i2pcontrol_cert);
+-			i2p::config::GetOption("i2pcontrol.key", i2pcontrol_key);
+-			if(i2pcontrol_cert == "i2pcontrol.crt.pem")
+-			{
+-				i2pcontrol_cert=datadir+"/"+i2pcontrol_cert;
+-			}
+-			if(i2pcontrol_key == "i2pcontrol.key.pem")
+-			{
+-				i2pcontrol_key=datadir+"/"+i2pcontrol_key;
+-			}
+-			//constexpr can't be here and it's eat a lot of memory...
+-/*
+-			auto rules = std::map<std::string,std::string>{
+-				{datadir,"rwc"},
+-				{"/tmp","rwc"},
+-				{unveil_file, "r"},
+-				{tunnelsdir, "r"},
+-				{certsdir, "r"},
+-				{reseed_file, "r"},
+-				{openbsd_pledge_file, "r"},
+-				{tunconf, "r"},
+-				{config, "r"},
+-				{pidfile, "rwc"},
+-				{logfile, "rwc"},
+-				{i2pcontrol_cert, "rwc"},
+-				{i2pcontrol_key, "rwc"}
+-			};
+-
+-			for (const auto& [path, privilegies] : rules)
+-			{
+-				unveil_path(path, privilegies);
+-			}
+-*/
+-			unveil_path(datadir, "rwc");
+-			unveil_path(unveil_file, "r");
+-			unveil_path(tunnelsdir, "r");
+-			unveil_path(certsdir, "r");
+-			unveil_path(reseed_file, "r");
+-			unveil_path(openbsd_pledge_file, "r");
+-			unveil_path(tunconf, "r");
+-			unveil_path(config, "r");
+-			unveil_path(pidfile, "rwc");
+-			unveil_path(logfile, "rwc");
+-			unveil_path(i2pcontrol_cert, "rwc");
+-			unveil_path(i2pcontrol_key, "rwc");
+-
+-
+-			if( unveil_file.length() )
+-			{
+-				std::ifstream f(unveil_file);
+-				if (!f) {
+-					std::cerr << "Can't open unveil file" << std::endl;
+-					exit(1);
+-				}
+-				std::string line;
+-				while(std::getline(f, line)){
+-					//UNVEIL_DIR(line);// maybe not need c
+-					unveil_path(line.c_str(), "rwc");
+-				}
+-			}
+-
+-			if( unveil(NULL, NULL) == -1 )
+-			{
+-				throw std::runtime_error("Can't unveil something");
+-			}
+-		};
+-		bool openbsd_unveil_enabled; i2p::config::GetOption("openbsd.unveil_enabled", openbsd_unveil_enabled);
+-		bool openbsd_pledge_enabled; i2p::config::GetOption("openbsd.pledge_enabled", openbsd_pledge_enabled);
+-		if(openbsd_unveil_enabled) init_unveil(datadir, config);
++			std::string path;
++			i2p::config::GetOption (option, path);
++			if (!path.empty ()) i2p::config::SetOption (option, AbsolutePath (path));
++		}
+ #endif
+ 
+ 		i2p::config::GetOption("daemon", isDaemon);
+@@ -291,6 +180,9 @@ namespace util
+ 		} else if (logs == "file") {
+ 			if (logfile == "")
+ 				logfile = i2p::fs::DataDirPath("i2pd.log");
++#ifdef __OpenBSD__
++			i2p::config::SetOption ("logfile", logfile);
++#endif
+ 			LogPrint(eLogInfo, "Log: Sending messages to ", logfile);
+ 			i2p::log::Logger().SendTo (logfile);
+ #ifndef _WIN32
+@@ -311,6 +203,25 @@ namespace util
+ 		bool ssu; i2p::config::GetOption("ssu", ssu);
+ 		if (!ssu && i2p::config::IsDefault ("precomputation.elgamal"))
+ 			precomputation = false; // we don't elgamal table if no ssu, unless it's specified explicitly
++#ifdef __OpenBSD__
++		// Keep startup-only promises until fork/setsid/setrlimit and unveil.
++		// Explicit pledge policies retain their existing override semantics.
++		bool usePledge; i2p::config::GetOption ("openbsd.pledge_enabled", usePledge);
++		std::string policy; i2p::config::GetOption ("openbsd.pledge_file", policy);
++		if (usePledge && policy.empty ())
++		{
++			std::string promises = "stdio rpath wpath cpath fattr inet dns unix flock proc";
++			bool useUnveil; i2p::config::GetOption ("openbsd.unevil_enabled", useUnveil);
++			if (useUnveil) promises += " unveil";
++			bool upnp; i2p::config::GetOption ("upnp.enabled", upnp);
++			if (upnp) promises += " mcast";
++			if (pledge (promises.c_str (), nullptr) == -1)
++			{
++				std::cerr << "Daemon: startup pledge: " << strerror (errno) << std::endl;
++				std::exit (EXIT_FAILURE);
++			}
++		}
++#endif
+ 		i2p::crypto::InitCrypto (precomputation);
+ 
+ 		i2p::transport::InitAddressFromIface (); // get address4/6 from interfaces
+@@ -472,4 +382,0 @@ namespace util
 -#ifdef __OpenBSD__
-+#if 0
- 		auto init_pledge = []() {
- 			std::string pledge_file; i2p::config::GetOption("openbsd.pledge_file", pledge_file);
- 			if (pledge_file == "")
+-//SIOCGIFMTU is not found, SOCK DGRAM not found
+-		if(openbsd_pledge_enabled) init_pledge();
+-#endif
Index: net/i2pd/patches/patch-daemon_I2PControl_cpp
===================================================================
RCS file: net/i2pd/patches/patch-daemon_I2PControl_cpp
diff -N net/i2pd/patches/patch-daemon_I2PControl_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-daemon_I2PControl_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,15 @@
+Use the OpenSSL 3 RSA helper only with OpenSSL; LibreSSL follows the
+compatible legacy key creation path.
+
+Index: daemon/I2PControl.cpp
+--- daemon/I2PControl.cpp.orig
++++ daemon/I2PControl.cpp
+@@ -485,7 +485,7 @@ namespace client
+ 	void I2PControlService::CreateCertificate (const char *crt_path, const char *key_path)
+ 	{
+ 		FILE *f = NULL;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 		EVP_PKEY *  pkey = EVP_RSA_gen(4096); // e = 65537
+ #else
+ 		EVP_PKEY * pkey = EVP_PKEY_new ();
Index: net/i2pd/patches/patch-daemon_UnixDaemon_cpp
===================================================================
RCS file: net/i2pd/patches/patch-daemon_UnixDaemon_cpp
diff -N net/i2pd/patches/patch-daemon_UnixDaemon_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-daemon_UnixDaemon_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,263 @@
+Handle OpenBSD signals in the daemon loop and install pledge and unveil rules
+after startup-only operations have finished.
+
+Index: daemon/UnixDaemon.cpp
+--- daemon/UnixDaemon.cpp.orig
++++ daemon/UnixDaemon.cpp
+@@ -19,6 +19,18 @@
+ #include <sys/stat.h>
+ #include <sys/resource.h>
+ 
++#ifdef __OpenBSD__
++#include <fstream>
++#include <vector>
++#include <cerrno>
++#include <cstring>
++#include <cstdlib>
++#include <limits.h>
++#include <map>
++#include <syslog.h>
++#include <pthread.h>
++#endif
++
+ #include "Config.h"
+ #include "FS.h"
+ #include "Log.h"
+@@ -33,6 +45,18 @@ const int GRACEFUL_SHUTDOWN_INTERVAL = 1
+ // a handler may only touch volatile sig_atomic_t, everything else is done by the daemon loop
+ static volatile sig_atomic_t g_PendingSignals[NSIG] = {};
+ 
++#ifdef __OpenBSD__
++// Blocked before service threads are created; consumed only by run().
++static sigset_t daemonSignals;
++
++static bool SignalError (const char * operation, int error)
++{
++	std::cerr << "Daemon: " << operation << ": " << strerror (error) << std::endl;
++	syslog (LOG_DAEMON | LOG_ERR, "Daemon: %s: %s", operation, strerror (error));
++	return false;
++}
++#endif
++
+ void handle_signal(int sig)
+ {
+ 	int savedErrno = errno;
+@@ -210,8 +234,29 @@ namespace i2p
+ 			gracefulShutdownInterval = 0; // not specified
+ 
+ 			// handle signal TSTP
+-			bool handleTSTP; i2p::config::GetOption("unix.handle_sigtstp", handleTSTP);
++			bool handleTSTP = false; i2p::config::GetOption("unix.handle_sigtstp", handleTSTP);
+ 
++#ifdef __OpenBSD__
++			// ReloadConfig() can stop and join destination threads. Never run
++			// it from an asynchronous handler on an arbitrary service thread.
++			sigemptyset (&daemonSignals);
++			for (int sig : {SIGHUP, SIGUSR1, SIGABRT, SIGTERM, SIGINT, SIGPIPE})
++				sigaddset (&daemonSignals, sig);
++			if (handleTSTP)
++			{
++				sigaddset (&daemonSignals, SIGTSTP);
++				sigaddset (&daemonSignals, SIGCONT);
++			}
++			int error = pthread_sigmask (SIG_BLOCK, &daemonSignals, nullptr);
++			if (error) return SignalError ("pthread_sigmask", error);
++			// Inherited SIG_IGN dispositions could discard a blocked signal.
++			struct sigaction sa{};
++			sa.sa_handler = SIG_DFL;
++			sigemptyset (&sa.sa_mask);
++			for (int sig = 1; sig < NSIG; ++sig)
++				if (sigismember (&daemonSignals, sig) == 1 && sigaction (sig, &sa, nullptr) == -1)
++					return SignalError ("sigaction", errno);
++#else
+ 			// Signal handler
+ 			struct sigaction sa;
+ 			sa.sa_handler = handle_signal;
+@@ -229,13 +274,150 @@ namespace i2p
+ 				sigaction(SIGCONT, &sa, 0);
+ 			}
+ 
++#endif
++
++#ifdef __OpenBSD__
++			// Resolve/read sandbox policy before the first unveil hides it.
++			bool useUnveil, usePledge;
++			i2p::config::GetOption ("openbsd.unevil_enabled", useUnveil);
++			i2p::config::GetOption ("openbsd.pledge_enabled", usePledge);
++			// CreatePrivateFile() uses chmod when saving router and destination keys.
++			std::string promises = "stdio rpath wpath cpath fattr inet dns unix flock";
++			bool upnp; i2p::config::GetOption ("upnp.enabled", upnp);
++			if (upnp) promises += " mcast";
++			std::vector<std::string> extraPaths;
++			auto fail = [](const std::string& message) {
++				std::cerr << message << std::endl;
++				// The logger thread has not started, and daemon stderr is /dev/null.
++				syslog (LOG_DAEMON | LOG_ERR, "%s", message.c_str ());
++				return false;
++			};
++			auto readPolicy = [&fail](const char * option, std::vector<std::string>& lines) {
++				std::string path; i2p::config::GetOption (option, path);
++				if (path.empty ()) return true;
++				std::ifstream f (path);
++				if (!f)
++				{
++					return fail (std::string ("Daemon: Can't open ") + option + ": " + path);
++				}
++				std::string line;
++				while (std::getline (f, line))
++					if (!line.empty ()) lines.push_back (line);
++				if (f.bad ())
++				{
++					return fail (std::string ("Daemon: Can't read ") + option + ": " + path);
++				}
++				return true;
++			};
++			if (usePledge)
++			{
++				std::string policy; i2p::config::GetOption ("openbsd.pledge_file", policy);
++				if (!policy.empty ())
++				{
++					std::vector<std::string> lines;
++					if (!readPolicy ("openbsd.pledge_file", lines)) return false;
++					promises.clear ();
++					for (const auto& line : lines) promises += line + " ";
++				}
++			}
++			if (useUnveil && !readPolicy ("openbsd.unevil_file", extraPaths)) return false;
++			if (useUnveil)
++			{
++				// Coalesce aliases before unveil: repeated rules may only reduce
++				// permissions. For example, certsdir may equal datadir.
++				std::map<std::string, std::string> rules;
++				auto allow = [&rules, &fail](std::string path, const char * permissions) {
++					if (path.empty ()) return true;
++					while (path.size () > 1 && path.back () == '/') path.pop_back ();
++					char resolved[PATH_MAX];
++					if (realpath (path.c_str (), resolved)) path = resolved;
++					else
++					{
++						if (errno != ENOENT)
++							return fail ("Daemon: resolve " + path + ": " + strerror (errno));
++						// A missing leaf is remembered in its existing parent by
++						// unveil, without giving access to sibling files.
++						auto slash = path.find_last_of ('/');
++						std::string parent = slash == std::string::npos ? "." : path.substr (0, slash + 1);
++						if (!realpath (parent.c_str (), resolved))
++							return fail ("Daemon: resolve parent of " + path + ": " + strerror (errno));
++						path = std::string (resolved) + "/" + path.substr (slash == std::string::npos ? 0 : slash + 1);
++					}
++					auto& mask = rules[path];
++					for (const char * p = permissions; *p; ++p)
++						if (mask.find (*p) == std::string::npos) mask += *p;
++					return true;
++				};
++				if (!allow (i2p::fs::GetDataDir (), "rwc") ||
++					!allow (i2p::fs::GetCertsDir (), "r") || !allow (pidfile, "rwc")) return false;
++				for (const char * option : {"conf", "tunconf", "tunnelsdir", "reseed.file", "reseed.zipfile"})
++				{
++					std::string path; i2p::config::GetOption (option, path);
++					if (!allow (path, "r")) return false;
++				}
++				for (const char * option : {"logfile", "addressbook.hostsfile"})
++				{
++					std::string path; i2p::config::GetOption (option, path);
++					if (!allow (path, "rwc")) return false;
++				}
++				bool control; i2p::config::GetOption ("i2pcontrol.enabled", control);
++				if (control)
++				{
++					for (const char * option : {"i2pcontrol.cert", "i2pcontrol.key"})
++					{
++						std::string path; i2p::config::GetOption (option, path);
++						if (!path.empty () && path.front () != '/') path = i2p::fs::DataDirPath (path);
++						if (!allow (path, "rwc")) return false;
++					}
++					uint16_t port; i2p::config::GetOption ("i2pcontrol.port", port);
++					if (!port)
++					{
++						std::string path; i2p::config::GetOption ("i2pcontrol.address", path);
++						if (!allow (path, "rwc")) return false;
++					}
++				}
++				bool outproxy; i2p::config::GetOption ("socksproxy.outproxy.enabled", outproxy);
++				uint16_t port; i2p::config::GetOption ("socksproxy.outproxyport", port);
++				if (outproxy && !port)
++				{
++					std::string path; i2p::config::GetOption ("socksproxy.outproxy", path);
++					if (!allow (path, "w")) return false;
++				}
++				// miniupnpc consults MiniSSDPD before multicast discovery.
++				if (upnp && !allow ("/var/run/minissdpd.sock", "w")) return false;
++				for (const auto& path : extraPaths)
++					if (!allow (path, "rwc")) return false;
++				for (const auto& rule : rules)
++					if (unveil (rule.first.c_str (), rule.second.c_str ()) == -1)
++						return fail ("Daemon: unveil " + rule.first + ": " + strerror (errno));
++				if (unveil (nullptr, nullptr) == -1)
++				{
++					return fail (std::string ("Daemon: unveil lock: ") + strerror (errno));
++				}
++			}
++			// fork/setsid/setrlimit and PID locking are complete; no proc,
++			// id, chown, descriptor passing or error promise is needed.
++			if (usePledge && pledge (promises.c_str (), nullptr) == -1)
++			{
++				return fail (std::string ("Daemon: pledge: ") + strerror (errno));
++			}
++#endif
++
+ 			return Daemon_Singleton::start();
+ 		}
+ 
+ 		bool DaemonUnix::stop()
+ 		{
++#ifdef __OpenBSD__
++			// Keep the PID lock while router data is still being saved.
++			bool stopped = Daemon_Singleton::stop();
++			i2p::fs::Remove(pidfile);
++			close (pidFH);
++			return stopped;
++#else
+ 			i2p::fs::Remove(pidfile);
+ 			return Daemon_Singleton::stop();
++#endif
+ 		}
+ 
+ 		void DaemonUnix::run ()
+@@ -244,6 +426,36 @@ namespace i2p
+ 			while (running)
+ 			{
+ 				std::this_thread::sleep_for (std::chrono::seconds(1));
++#ifdef __OpenBSD__
++				// Poll at the existing one-second shutdown tick. Only this thread
++				// consumes these signals, so a pending signal cannot be stolen
++				// between sigpending() and sigwait().
++				sigset_t pending;
++				if (sigpending (&pending) == -1)
++				{
++					SignalError ("sigpending", errno);
++					running = false;
++					break;
++				}
++				for (int sig = 1; sig < NSIG; ++sig)
++					if (sigismember (&daemonSignals, sig) == 1 && sigismember (&pending, sig) == 1)
++					{
++						sigset_t one;
++						sigemptyset (&one);
++						sigaddset (&one, sig);
++						int received;
++						int error = sigwait (&one, &received);
++						if (error)
++						{
++							SignalError ("sigwait", error);
++							running = false;
++							break;
++						}
++						handle_signal (received);
++						if (!running) break;
++					}
++				if (!running) break;
++#endif
+ 				HandlePendingSignals();
+ 				if (gracefulShutdownInterval)
+ 				{
Index: net/i2pd/patches/patch-libi2pd_Config_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Config_cpp
diff -N net/i2pd/patches/patch-libi2pd_Config_cpp
--- net/i2pd/patches/patch-libi2pd_Config_cpp	17 Oct 2026 08:47:19 -0000	1.1
+++ /dev/null	1 Jan 1970 00:00:00 -0000
@@ -1,23 +0,0 @@
-neuter broken pledge/unveil
-
-Index: libi2pd/Config.cpp
---- libi2pd/Config.cpp.orig
-+++ libi2pd/Config.cpp
-@@ -396,7 +396,7 @@ namespace config {
- 		;
- #endif
- 
--#ifdef __OpenBSD__
-+#if 0
- 		options_description openbsd_specific("OpenBSD specific options");
- 		openbsd_specific.add_options()
- 			("openbsd.pledge_file", value<std::string>()->default_value(""), "OpenbSD file with pledge rules")
-@@ -433,7 +433,7 @@ namespace config {
- #ifdef __linux__
- 			.add(unix_specific)
- #endif
--#ifdef __OpenBSD__
-+#if 0
- 			.add(openbsd_specific)
- #endif
- 		;
Index: net/i2pd/patches/patch-libi2pd_Crypto_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Crypto_cpp
diff -N net/i2pd/patches/patch-libi2pd_Crypto_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Crypto_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,100 @@
+Select OpenSSL 3 APIs only for OpenSSL and validate cryptographic key and
+parameter operations on the supported crypto backends.
+
+Index: libi2pd/Crypto.cpp
+--- libi2pd/Crypto.cpp.orig
++++ libi2pd/Crypto.cpp
+@@ -11,13 +11,14 @@
+ #include <vector>
+ #include <mutex>
+ #include <memory>
++#include <limits>
+ #include <openssl/dh.h>
+ #include <openssl/md5.h>
+ #include <openssl/crypto.h>
+ #include "TunnelBase.h"
+ #include <openssl/ssl.h>
+ #include <openssl/kdf.h>
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ #include <openssl/param_build.h>
+ #include <openssl/core_names.h>
+ #endif
+@@ -149,7 +150,7 @@ namespace crypto
+ 	#define dsap GetCryptoConstants ().dsap
+ 	#define dsaq GetCryptoConstants ().dsaq
+ 	#define dsag GetCryptoConstants ().dsag
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 	EVP_PKEY * CreateDSA (BIGNUM * pubKey, BIGNUM * privKey)
+ 	{
+ 		EVP_PKEY * pkey = nullptr;
+@@ -650,40 +651,39 @@ namespace crypto
+ 	static bool AEADChaCha20Poly1305 (EVP_CIPHER_CTX * ctx, const uint8_t * msg, size_t msgLen,
+ 		const uint8_t * ad, size_t adLen, const uint8_t * key, const uint8_t * nonce, uint8_t * buf, size_t len, bool encrypt)
+ 	{
+-		if (!ctx || len < msgLen) return false;
+-		if (encrypt && len < msgLen + 16) return false;
+-		bool ret = true;
+-		int outlen = 0;
++		if (!ctx || len < msgLen || (encrypt && len - msgLen < 16)) return false;
++		// EVP update lengths are int in both OpenSSL and LibreSSL.
++		if (msgLen > (size_t)std::numeric_limits<int>::max () ||
++			adLen > (size_t)std::numeric_limits<int>::max ()) return false;
++		int outlen = 0, finalLen = 0;
+ 		if (encrypt)
+ 		{
+-			EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), 0, 0, 0);
+-			EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, 0);
+-			EVP_EncryptInit_ex(ctx, NULL, NULL, key, nonce);
+-			EVP_EncryptUpdate(ctx, NULL, &outlen, ad, adLen);
+-			EVP_EncryptUpdate(ctx, buf, &outlen, msg, msgLen);
+-			EVP_EncryptFinal_ex(ctx, buf + outlen, &outlen);
+-			EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, buf + msgLen);
++			if (EVP_EncryptInit_ex (ctx, EVP_chacha20_poly1305 (), nullptr, nullptr, nullptr) != 1 ||
++				EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, nullptr) != 1 ||
++				EVP_EncryptInit_ex (ctx, nullptr, nullptr, key, nonce) != 1) return false;
++			if (adLen && EVP_EncryptUpdate (ctx, nullptr, &outlen, ad, (int)adLen) != 1) return false;
++			outlen = 0;
++			if (msgLen && EVP_EncryptUpdate (ctx, buf, &outlen, msg, (int)msgLen) != 1) return false;
++			if (outlen != (int)msgLen || EVP_EncryptFinal_ex (ctx, buf + outlen, &finalLen) != 1 || finalLen != 0) return false;
++			return EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_GET_TAG, 16, buf + msgLen) == 1;
+ 		}
+-		else
+-		{
+ #if defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x4000000fL
+-			std::vector<uint8_t> m(msgLen + 16);
+-			if (msg == buf)
+-			{
+-				// we have to use different buffers otherwise verification fails
+-				memcpy (m.data (), msg, msgLen + 16);
+-				msg = m.data ();
+-			}
+-#endif
+-			EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), 0, 0, 0);
+-			EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, 0);
+-			EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, (uint8_t *)(msg + msgLen));
+-			EVP_DecryptInit_ex(ctx, NULL, NULL, key, nonce);
+-			EVP_DecryptUpdate(ctx, NULL, &outlen, ad, adLen);
+-			EVP_DecryptUpdate(ctx, buf, &outlen, msg, msgLen);
+-			ret = EVP_DecryptFinal_ex(ctx, buf + outlen, &outlen) > 0;
++		std::vector<uint8_t> m;
++		if (msg == buf)
++		{
++			// Older LibreSSL cannot authenticate in-place decryption.
++			m.assign (msg, msg + msgLen + 16);
++			msg = m.data ();
+ 		}
+-		return ret;
++#endif
++		if (EVP_DecryptInit_ex (ctx, EVP_chacha20_poly1305 (), nullptr, nullptr, nullptr) != 1 ||
++			EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, nullptr) != 1 ||
++			EVP_DecryptInit_ex (ctx, nullptr, nullptr, key, nonce) != 1 ||
++			EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_AEAD_SET_TAG, 16, (void *)(msg + msgLen)) != 1) return false;
++		if (adLen && EVP_DecryptUpdate (ctx, nullptr, &outlen, ad, (int)adLen) != 1) return false;
++		outlen = 0;
++		if (msgLen && EVP_DecryptUpdate (ctx, buf, &outlen, msg, (int)msgLen) != 1) return false;
++		return outlen == (int)msgLen && EVP_DecryptFinal_ex (ctx, buf + outlen, &finalLen) == 1 && finalLen == 0;
+ 	}
+ 
+ 	bool AEADChaCha20Poly1305 (const uint8_t * msg, size_t msgLen, const uint8_t * ad, size_t adLen,
Index: net/i2pd/patches/patch-libi2pd_Crypto_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Crypto_h
diff -N net/i2pd/patches/patch-libi2pd_Crypto_h
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Crypto_h	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,31 @@
+Detect post-quantum and Ed25519ph support separately for OpenSSL and LibreSSL.
+
+Index: libi2pd/Crypto.h
+--- libi2pd/Crypto.h.orig
++++ libi2pd/Crypto.h
+@@ -31,10 +31,15 @@
+ #if (!defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER != 0x030000000)) // 3.0.0, regression in SipHash, not implemented in LibreSSL
+ #	define OPENSSL_SIPHASH 1
+ #endif
+-#if (OPENSSL_VERSION_NUMBER >= 0x030500000) || ( (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER >= 0x4030000fL)) )  // 3.5.0 openssl, 4.3.0 libressl
++#if (!defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x030500000) || ( (defined(LIBRESSL_VERSION_NUMBER) && (LIBRESSL_VERSION_NUMBER >= 0x4030000fL)) )  // 3.5.0 openssl, 4.3.0 libressl
+ #	define OPENSSL_PQ 1
+ #endif
+ 
++// The Ed25519ph instance parameter first appeared in OpenSSL 3.2.
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30200000L
++#	define OPENSSL_ED25519PH 1
++#endif
++
+ namespace i2p
+ {
+ namespace crypto
+@@ -42,7 +47,7 @@ namespace crypto
+ 	bool bn2buf (const BIGNUM * bn, uint8_t * buf, size_t len);
+ 
+ 	// DSA
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 	EVP_PKEY * CreateDSA (BIGNUM * pubKey = nullptr, BIGNUM * privKey = nullptr);
+ #else
+ 	DSA * CreateDSA ();
Index: net/i2pd/patches/patch-libi2pd_Destination_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Destination_cpp
diff -N net/i2pd/patches/patch-libi2pd_Destination_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Destination_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,35 @@
+Reject unsupported post-quantum encryption key types when parsing destinations.
+
+Index: libi2pd/Destination.cpp
+--- libi2pd/Destination.cpp.orig
++++ libi2pd/Destination.cpp
+@@ -15,6 +15,7 @@
+ #include <future>
+ #include <boost/algorithm/string.hpp>
+ #include "Crypto.h"
++#include "PostQuantum.h"
+ #include "ECIESX25519AEADRatchetSession.h"
+ #include "Log.h"
+ #include "FS.h"
+@@ -1051,7 +1052,10 @@ namespace client
+ 					try
+ 					{
+ 						i2p::data::CryptoKeyType cryptoType = std::stoi(it1);
+-#if !OPENSSL_PQ
++#if OPENSSL_PQ
++						if (cryptoType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD ||
++							i2p::crypto::IsMLKEMSupported (cryptoType))
++#else
+ 						if (cryptoType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD) // skip PQ keys if not supported
+ #endif
+ 						{
+@@ -1085,6 +1089,9 @@ namespace client
+ 
+ 		for (auto& it: encryptionKeyTypes)
+ 		{
++#if OPENSSL_PQ
++			if (it > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD && !i2p::crypto::IsMLKEMSupported (it)) continue;
++#endif
+ 			auto encryptionKey = std::make_shared<i2p::crypto::LocalEncryptionKey> (it);
+ 			if (IsPublic ())
+ 				PersistTemporaryKeys (encryptionKey);
Index: net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp
diff -N net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_ECIESX25519AEADRatchetSession_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,60 @@
+Check ML-KEM key creation and key operations before continuing ECIES ratchet
+handshakes.
+
+Index: libi2pd/ECIESX25519AEADRatchetSession.cpp
+--- libi2pd/ECIESX25519AEADRatchetSession.cpp.orig
++++ libi2pd/ECIESX25519AEADRatchetSession.cpp
+@@ -308,7 +308,7 @@ namespace garlic
+ 						len -= keyLen + 16;
+ 
+ 						m_PQKeys = i2p::crypto::CreateMLKEMKeys (cryptoType);
+-						m_PQKeys->SetPublicKey (encapsKey.data ());
++						if (!m_PQKeys || !m_PQKeys->SetPublicKey (encapsKey.data ())) return false;
+ 					}
+ 				}
+ 				// if PQ failed it's valid situation meaning regular ECIES_X25519(type 4)
+@@ -589,7 +589,7 @@ namespace garlic
+ 		{
+ 			i2p::crypto::InitNoiseIKStateMLKEM (GetNoiseState (), m_RemoteStaticKeyType, m_RemoteStaticKey); // bpk
+ 			m_PQKeys = i2p::crypto::CreateMLKEMKeys (m_RemoteStaticKeyType);
+-			m_PQKeys->GenerateKeys ();
++			if (!m_PQKeys || !m_PQKeys->GenerateKeys ()) return false;
+ 		}
+ 		else
+ #endif
+@@ -607,7 +607,7 @@ namespace garlic
+ 		{
+ 			auto keyLen = i2p::crypto::GetMLKEMPublicKeyLen (m_RemoteStaticKeyType);
+ 			std::vector<uint8_t> encapsKey(keyLen);
+-			m_PQKeys->GetPublicKey (encapsKey.data ());
++			if (!m_PQKeys || !m_PQKeys->GetPublicKey (encapsKey.data ())) return false;
+ 			// encrypt encapsKey
+ 			if (!Encrypt (encapsKey.data (), out + offset, keyLen))
+ 			{
+@@ -698,7 +698,7 @@ namespace garlic
+ 			memcpy (m_NSRCK->data (), m_CK, 64);
+ 			size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_RemoteStaticKeyType);
+ 			std::vector<uint8_t> kemCiphertext(cipherTextLen);
+-			m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++			if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return false;
+ 
+ 			if (!Encrypt (kemCiphertext.data (), out + offset, cipherTextLen))
+ 			{
+@@ -770,7 +770,7 @@ namespace garlic
+ 				size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_RemoteStaticKeyType);
+ 				std::vector<uint8_t> kemCiphertext(cipherTextLen);
+ 				uint8_t sharedSecret[32];
+-				m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++				if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return false;
+ 
+ 				memcpy (m_CK, m_NSRCK->data (), 64); // restore key
+ 				if (!Encrypt (kemCiphertext.data (), out + offset, cipherTextLen))
+@@ -854,7 +854,7 @@ namespace garlic
+ 			buf += cipherTextLen + 16;
+ 			len -= cipherTextLen + 16;
+ 			// decaps
+-			m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret);
++			if (!m_PQKeys || !m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret)) return false;
+ 			MixKey (sharedSecret);
+ 		}
+ #endif
Index: net/i2pd/patches/patch-libi2pd_FS_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_FS_cpp
diff -N net/i2pd/patches/patch-libi2pd_FS_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_FS_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,18 @@
+Use /var/i2pd for the OpenBSD service data directory.
+
+Index: libi2pd/FS.cpp
+--- libi2pd/FS.cpp.orig
++++ libi2pd/FS.cpp
+@@ -108,8 +108,12 @@ namespace fs {
+ 			else
+ 				dataDir = (fs_lib::path(commonAppData)/appName).string ();
+ #else
++#ifdef __OpenBSD__
++			dataDir = "/var/" + appName;
++#else
+ 			dataDir = "/var/lib/" + appName;
+ #endif
++#endif
+ 			return;
+ 		}
+ #endif
Index: net/i2pd/patches/patch-libi2pd_Family_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Family_cpp
diff -N net/i2pd/patches/patch-libi2pd_Family_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Family_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,74 @@
+Identify supported family signing curves on both OpenSSL and LibreSSL, and
+reject invalid or oversized family names.
+
+Index: libi2pd/Family.cpp
+--- libi2pd/Family.cpp.orig
++++ libi2pd/Family.cpp
+@@ -18,6 +18,20 @@ namespace i2p
+ {
+ namespace data
+ {
++	static int GetFamilyKeyCurve (EVP_PKEY * pkey)
++	{
++		if (!pkey || EVP_PKEY_base_id (pkey) != EVP_PKEY_EC) return -1;
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L
++		char groupName[80];
++		if (EVP_PKEY_get_group_name (pkey, groupName, sizeof (groupName), nullptr) != 1) return -1;
++		return OBJ_txt2nid (groupName);
++#else
++		const EC_KEY * ec = EVP_PKEY_get0_EC_KEY (pkey);
++		const EC_GROUP * group = ec ? EC_KEY_get0_group (ec) : nullptr;
++		return group ? EC_GROUP_get_curve_name (group) : -1;
++#endif
++	}
++
+ 	Families::Families ()
+ 	{
+ 	}
+@@ -51,15 +65,8 @@ namespace data
+ 					auto pkey = X509_get_pubkey (cert);
+ 					if (pkey)
+ 					{	
+-						int curve = 0;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
+-						char groupName[20];
+-						if (EVP_PKEY_get_group_name(pkey, groupName, sizeof(groupName), NULL) == 1)
+-							curve = OBJ_txt2nid (groupName);
+-						else
+-							curve = -1;
+-#endif						
+-						if (!curve || curve == NID_X9_62_prime256v1)
++						int curve = GetFamilyKeyCurve (pkey);
++						if (curve == NID_X9_62_prime256v1)
+ 						{	
+ 							if (!m_SigningKeys.emplace (cn, std::make_pair(pkey, (int)m_SigningKeys.size () + 1)).second)
+ 							{
+@@ -68,7 +75,10 @@ namespace data
+ 							}
+ 						}
+ 						else
++						{
++							EVP_PKEY_free (pkey);
+ 							LogPrint (eLogWarning, "Family: elliptic curve ", curve, " is not supported");
++						}
+ 					}	
+ 				}
+ 			}
+@@ -156,15 +166,8 @@ namespace data
+ 		{
+ 			SSL * ssl = SSL_new (ctx);
+ 			EVP_PKEY * pkey = SSL_get_privatekey (ssl);
+-			int curve = 0;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
+-			char groupName[20];
+-			if (EVP_PKEY_get_group_name(pkey, groupName, sizeof(groupName), NULL) == 1)
+-				curve = OBJ_txt2nid (groupName);
+-			else
+-				curve = -1;
+-#endif	
+-			if ((!curve || curve == NID_X9_62_prime256v1) && family.length () + 32 <= 100)
++			int curve = GetFamilyKeyCurve (pkey);
++			if (curve == NID_X9_62_prime256v1 && family.length () + 32 <= 100)
+ 			{
+ 				uint8_t buf[100], sign[72], signature[64];
+ 				size_t len = family.length ();
Index: net/i2pd/patches/patch-libi2pd_LeaseSet_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_LeaseSet_cpp
diff -N net/i2pd/patches/patch-libi2pd_LeaseSet_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_LeaseSet_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,25 @@
+Validate post-quantum key types when choosing encryption keys for lease sets.
+
+Index: libi2pd/LeaseSet.cpp
+--- libi2pd/LeaseSet.cpp.orig
++++ libi2pd/LeaseSet.cpp
+@@ -9,6 +9,7 @@
+ #include <string.h>
+ #include "I2PEndian.h"
+ #include "Crypto.h"
++#include "PostQuantum.h"
+ #include "Log.h"
+ #include "Tag.h"
+ #include "Timestamp.h"
+@@ -420,7 +421,10 @@ namespace data
+ 				i2p::crypto::GetCryptoPublicKeyLen (keyType) == encryptionKeyLen) // key length mismatch or unknown key type
+ 			{
+ 				// we pick max key type if preferred not found
+-#if !OPENSSL_PQ
++#if OPENSSL_PQ
++				if (keyType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD ||
++					i2p::crypto::IsMLKEMSupported (keyType))
++#else
+ 				if (keyType <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD) // skip PQ keys if not supported
+ #endif
+ 				{
Index: net/i2pd/patches/patch-libi2pd_NTCP2_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_NTCP2_cpp
diff -N net/i2pd/patches/patch-libi2pd_NTCP2_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_NTCP2_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,78 @@
+Check ML-KEM setup and key operations during NTCP2 handshakes so failures
+abort the handshake instead of using invalid key material.
+
+Index: libi2pd/NTCP2.cpp
+--- libi2pd/NTCP2.cpp.orig
++++ libi2pd/NTCP2.cpp
+@@ -163,7 +163,7 @@ namespace transport
+             encryption.Encrypt (pub, 32, m_IV, m_Buffer); // X
+             //  ML-KEM encap_key
+             m_PQKeys = i2p::crypto::CreateMLKEMKeys (m_CryptoType);
+-            m_PQKeys->GenerateKeys ();
++            if (!m_PQKeys || !m_PQKeys->GenerateKeys ()) return false;
+         }
+         else
+             encryption.Encrypt (GetPub (), 32, m_IV, m_Buffer); // X
+@@ -183,7 +183,7 @@ namespace transport
+             // ML-KEM frame
+             auto keyLen = i2p::crypto::GetMLKEMPublicKeyLen (m_CryptoType);
+ 			std::vector<uint8_t> encapsKey(keyLen);
+-			m_PQKeys->GetPublicKey (encapsKey.data ());
++			if (!m_PQKeys || !m_PQKeys->GetPublicKey (encapsKey.data ())) return false;
+ 			// encrypt encapsKey
+ 			if (!Encrypt (encapsKey.data (), m_Buffer + offset, keyLen))
+ 			{
+@@ -255,12 +255,13 @@ namespace transport
+ 		maxPaddingLength -= 64;
+ 		size_t maxMsgSize = m_MaxMsgSize;
+ #if OPENSSL_PQ
+-        if (m_PQKeys)
++        if (m_CryptoType > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)
+         {
++			if (!m_PQKeys) return false;
+             size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_CryptoType);
+ 			std::vector<uint8_t> kemCiphertext(cipherTextLen);
+ 			uint8_t sharedSecret[32];
+-			m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++			if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return false;
+ 			if (!Encrypt (kemCiphertext.data (), m_Buffer + offset, cipherTextLen))
+ 			{
+ 				LogPrint (eLogWarning, "NTCP2: SessionCreated ML-KEM ciphertext section AEAD encryption failed");
+@@ -378,7 +379,7 @@ namespace transport
+                 MixHash (m_Buffer + offset, keyLen + 16);
+                 offset += keyLen + 16;
+                 m_PQKeys = i2p::crypto::CreateMLKEMKeys (m_CryptoType);
+-                m_PQKeys->SetPublicKey (encapsKey.data ());
++                if (!m_PQKeys || !m_PQKeys->SetPublicKey (encapsKey.data ())) return false;
+             }
+         }
+ #endif
+@@ -450,8 +451,9 @@ namespace transport
+ 			return false;
+ 		}
+ #if OPENSSL_PQ
+-        if (m_CryptoType > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD && m_PQKeys)
++        if (m_CryptoType > i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD)
+ 		{
++			if (!m_PQKeys) return false;
+ 			// decrypt kem_ciphertext  frame
+ 			size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (m_CryptoType);
+ 			std::vector<uint8_t> kemCiphertext(cipherTextLen);
+@@ -465,7 +467,7 @@ namespace transport
+ 			offset += cipherTextLen + 16;
+ 			// decaps
+ 			uint8_t sharedSecret[32];
+-			m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret);
++			if (!m_PQKeys || !m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret)) return false;
+ 			MixKey (sharedSecret);
+ 		}
+ #endif
+@@ -2318,7 +2320,7 @@ namespace transport
+ 	{
+ #if OPENSSL_PQ
+ #ifdef LIBRESSL_VERSION_NUMBER
+-	if (version == 3 || version == 5) version = 2; // 3 and 5 are not supported by LibreSSL
++		if (version == 3) version = 2; // ML-KEM-512 is not supported by LibreSSL
+ #endif
+         m_Version = version;
+ #endif
Index: net/i2pd/patches/patch-libi2pd_PostQuantum_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_PostQuantum_cpp
diff -N net/i2pd/patches/patch-libi2pd_PostQuantum_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_PostQuantum_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,350 @@
+Use rank-specific LibreSSL ML-KEM keys and securely release operation buffers;
+propagate failures from key generation and encapsulation operations.
+
+Index: libi2pd/PostQuantum.cpp
+--- libi2pd/PostQuantum.cpp.orig
++++ libi2pd/PostQuantum.cpp
+@@ -6,6 +6,8 @@
+ * See full license text in LICENSE file at top of project tree
+ */
+ 
++#include <cstring>
++#include <openssl/crypto.h>
+ #include "Log.h"
+ #include "PostQuantum.h"
+ 
+@@ -14,227 +16,173 @@
+ #ifndef LIBRESSL_VERSION_NUMBER
+ #include <openssl/param_build.h>
+ #include <openssl/core_names.h>
+-#else
+-#include<openssl/mlkem.h>
+ #endif
+ 
+ namespace i2p
+ {
+ namespace crypto
+ {
++#ifdef LIBRESSL_VERSION_NUMBER
++	// LibreSSL allocates operation outputs. Always wipe and release them,
++	// including when an operation or a subsequent length check fails.
++	struct MLKEMBuffer
++	{
++		MLKEMBuffer () = default;
++		MLKEMBuffer (const MLKEMBuffer&) = delete;
++		MLKEMBuffer& operator= (const MLKEMBuffer&) = delete;
++		uint8_t * data = nullptr;
++		size_t len = 0;
++		~MLKEMBuffer ()
++		{
++			if (data) OPENSSL_cleanse (data, len);
++			OPENSSL_free (data);
++		}
++	};
++#endif
++
+ 	MLKEMKeys::MLKEMKeys (MLKEMTypes type):
+-#ifndef LIBRESSL_VERSION_NUMBER
+-		m_Name (std::get<0>(MLKEMS[type])), m_KeyLen (std::get<1>(MLKEMS[type])),
+-		m_CTLen (std::get<2>(MLKEMS[type])),
+-#else
+-		m_Name (std::get<0>(MLKEMS[eMLKEM768])), m_KeyLen (std::get<1>(MLKEMS[eMLKEM768])),
+-		m_CTLen (std::get<2>(MLKEMS[eMLKEM768])),
++		m_Name (std::get<0>(MLKEMS.at (type))), m_KeyLen (std::get<1>(MLKEMS.at (type))),
++		m_CTLen (std::get<2>(MLKEMS.at (type))), m_Pkey (nullptr)
++#ifdef LIBRESSL_VERSION_NUMBER
++		, m_PublicKey (nullptr),
++		m_Rank (type == eMLKEM768 ? MLKEM768_RANK : type == eMLKEM1024 ? MLKEM1024_RANK : 0)
+ #endif
+-		m_Pkey (nullptr)
+ 	{
+ 	}
+ 
+ 	MLKEMKeys::~MLKEMKeys ()
+ 	{
+-		FreeKeys();
++		FreeKeys ();
+ 	}
+ 
+ 	void MLKEMKeys::FreeKeys ()
+ 	{
+ #ifndef LIBRESSL_VERSION_NUMBER
+-		if (m_Pkey) EVP_PKEY_free (m_Pkey);
++		EVP_PKEY_free (m_Pkey);
+ #else
+-	    if (m_Pkey) MLKEM_private_key_free (m_Pkey);
++		MLKEM_private_key_free (m_Pkey);
++		MLKEM_public_key_free (m_PublicKey);
++		m_PublicKey = nullptr;
+ #endif
+-		if (m_Pkey) m_Pkey = nullptr;
++		m_Pkey = nullptr;
+ 	}
+ 
+-	void MLKEMKeys::GenerateKeys ()
++	bool MLKEMKeys::GenerateKeys ()
+ 	{
+-		FreeKeys();
++		FreeKeys ();
+ #ifndef LIBRESSL_VERSION_NUMBER
+-		m_Pkey = EVP_PKEY_Q_keygen(NULL, NULL, m_Name.c_str ());
+-		LogPrint(eLogDebug, "MLKEM: GenerateKeys [ openssl ]");
++		m_Pkey = EVP_PKEY_Q_keygen (nullptr, nullptr, m_Name.c_str ());
++		if (m_Pkey) return true;
+ #else
+-		m_Pkey = MLKEM_private_key_new( MLKEM768_RANK);
+-
+-		uint8_t * pub_key = nullptr;
+-		size_t pub_key_len = 0;
+-		uint8_t * seed = nullptr;
+-		size_t seed_len = 0;
+-
+-		if (MLKEM_generate_key(m_Pkey, &pub_key, &pub_key_len, &seed, &seed_len) == 1)
+-		{
+-			LogPrint(eLogDebug, "MLKEM: GenerateKeys [ libressl ] success");
+-			if (pub_key_len <= sizeof(m_CachedPub))
+-			{
+-				memcpy(m_CachedPub, pub_key, pub_key_len);
+-				m_IsPubCached = true;
+-				LogPrint(eLogDebug, "MLKEM [libressl] cache the pub success");
+-			} else
+-			{
+-				 LogPrint(eLogError, "MLKEM: can't cache private key [libressl]");
+-			}
+-			OPENSSL_free(pub_key);
+-			if (seed) OPENSSL_free(seed);
+-		} else
+-		{
+-			LogPrint(eLogError, "MLKEM: GenerateKeys [ libressl ] failed");
+-			MLKEM_private_key_free(m_Pkey);
+-			m_Pkey = nullptr;
+-		}
++		MLKEMBuffer pub;
++		// No seed is needed: LibreSSL wipes its temporary seed internally.
++		m_Pkey = MLKEM_private_key_new (m_Rank);
++		m_PublicKey = MLKEM_public_key_new (m_Rank);
++		if (m_Pkey && m_PublicKey &&
++			MLKEM_generate_key (m_Pkey, &pub.data, &pub.len, nullptr, nullptr) == 1 &&
++			pub.len == m_KeyLen &&
++			MLKEM_public_from_private (m_Pkey, m_PublicKey) == 1) return true;
+ #endif
+-	} // end method
++		FreeKeys ();
++		LogPrint (eLogError, "MLKEM: Failed to generate keys");
++		return false;
++	}
+ 
+-	void MLKEMKeys::GetPublicKey (uint8_t * pub) const
++	bool MLKEMKeys::GetPublicKey (uint8_t * pub) const
+ 	{
+-		if (m_Pkey)
+-		{
+ #ifndef LIBRESSL_VERSION_NUMBER
+-			size_t len = m_KeyLen;
+-			EVP_PKEY_get_octet_string_param (m_Pkey, OSSL_PKEY_PARAM_PUB_KEY, pub, m_KeyLen, &len);
++		size_t len = 0;
++		if (m_Pkey && EVP_PKEY_get_octet_string_param (m_Pkey,
++			OSSL_PKEY_PARAM_PUB_KEY, pub, m_KeyLen, &len) == 1 && len == m_KeyLen) return true;
+ #else
+-			if (!m_Pkey) return;
+-
+-			LogPrint(eLogDebug, "MLKEM: GetPublicKey [ libressl ]");
+-
+-			if (m_IsPubCached)
+-			{
+-				memcpy(pub, m_CachedPub, MLKEM768_KEY_LENGTH);
+-				LogPrint(eLogDebug,"MLKEM [libressl]: copy pubkey");
+-			}
+-			else
+-				LogPrint(eLogError, "MLKEM: Public key not cached!");
+-#endif
++		MLKEMBuffer encoded;
++		if (m_PublicKey && MLKEM_marshal_public_key (m_PublicKey, &encoded.data, &encoded.len) == 1 &&
++			encoded.len == m_KeyLen)
++		{
++			memcpy (pub, encoded.data, m_KeyLen);
++			return true;
+ 		}
++#endif
++		LogPrint (eLogError, "MLKEM: Failed to export public key");
++		return false;
+ 	}
+ 
+-
+-	void MLKEMKeys::SetPublicKey (const uint8_t * pub)
++	bool MLKEMKeys::SetPublicKey (const uint8_t * pub)
+ 	{
++		// A failed import must not leave a previous key available for use.
++		FreeKeys ();
+ #ifndef LIBRESSL_VERSION_NUMBER
+-		if (m_Pkey)
+-		{
+-			EVP_PKEY_free (m_Pkey);
+-			m_Pkey = nullptr;
+-		}
+ 		OSSL_PARAM params[] =
+ 		{
+-			OSSL_PARAM_octet_string(OSSL_PKEY_PARAM_PUB_KEY, (void*)pub, m_KeyLen),
++			OSSL_PARAM_octet_string (OSSL_PKEY_PARAM_PUB_KEY, (void *)pub, m_KeyLen),
+ 			OSSL_PARAM_END
+ 		};
+-
+-		EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_name(NULL, m_Name.c_str(), NULL);
+-		if (ctx)
+-		{
+-			EVP_PKEY_fromdata_init(ctx);
+-			if (EVP_PKEY_fromdata(ctx, &m_Pkey, OSSL_KEYMGMT_SELECT_PUBLIC_KEY, params) <= 0)
+-				LogPrint(eLogError, "MLKEM: Failed to set public key data");
+-			EVP_PKEY_CTX_free(ctx);
+-		}
+-		else
+-			LogPrint(eLogError, "MLKEM: can't create PKEY context");
++		auto ctx = EVP_PKEY_CTX_new_from_name (nullptr, m_Name.c_str (), nullptr);
++		bool ok = ctx && EVP_PKEY_fromdata_init (ctx) == 1 &&
++			EVP_PKEY_fromdata (ctx, &m_Pkey, OSSL_KEYMGMT_SELECT_PUBLIC_KEY, params) == 1;
++		EVP_PKEY_CTX_free (ctx);
++		if (ok) return true;
+ #else
+-		MLKEM_public_key * pub_key = MLKEM_public_key_new(MLKEM768_RANK);
+-		if (MLKEM_parse_public_key(pub_key, pub, m_KeyLen))
+-		{
+-			memcpy(m_CachedPub, pub, m_KeyLen);
+-			m_IsPubCached = true;
+-			LogPrint(eLogDebug, "MLKEM: SetPublicKey [ libressl ] success");
+-		}
+-		else
+-			LogPrint(eLogError, "MLKEM: failed to parse public key");
+-		if (pub_key) MLKEM_public_key_free(pub_key);
++		m_PublicKey = MLKEM_public_key_new (m_Rank);
++		if (m_PublicKey && MLKEM_parse_public_key (m_PublicKey, pub, m_KeyLen) == 1) return true;
+ #endif
++		FreeKeys ();
++		LogPrint (eLogError, "MLKEM: Failed to parse public key");
++		return false;
+ 	}
+ 
+-	void MLKEMKeys::Encaps (uint8_t * ciphertext, uint8_t * shared)
++	bool MLKEMKeys::Encaps (uint8_t * ciphertext, uint8_t * shared)
+ 	{
+ #ifndef LIBRESSL_VERSION_NUMBER
+-		if (!m_Pkey)
+-		{
+-			LogPrint(eLogError, "MLKEM: No public key for Encaps");
+-			return;
+-        }
+-		auto ctx = EVP_PKEY_CTX_new_from_pkey (NULL, m_Pkey, NULL);
+-		if (ctx)
+-		{
+-			EVP_PKEY_encapsulate_init (ctx, NULL);
+-			size_t len = m_CTLen, sharedLen = 32;
+-			EVP_PKEY_encapsulate (ctx, ciphertext, &len, shared, &sharedLen);
+-			EVP_PKEY_CTX_free (ctx);
+-		}
+-		else
+-			LogPrint (eLogError, "MLKEM can't create PKEY context");
++		auto ctx = m_Pkey ? EVP_PKEY_CTX_new_from_pkey (nullptr, m_Pkey, nullptr) : nullptr;
++		size_t len = m_CTLen, sharedLen = 32;
++		bool ok = ctx && EVP_PKEY_encapsulate_init (ctx, nullptr) == 1 &&
++			EVP_PKEY_encapsulate (ctx, ciphertext, &len, shared, &sharedLen) == 1 &&
++			len == m_CTLen && sharedLen == 32;
++		EVP_PKEY_CTX_free (ctx);
++		if (ok) return true;
+ #else
+-		if (!m_IsPubCached)
+-		{
+-			LogPrint(eLogError, "MLKEM: No public key for Encaps");
+-			return;
+-		}
+-		auto pub_key = MLKEM_public_key_new(MLKEM768_RANK);
+-		if (MLKEM_parse_public_key (pub_key, m_CachedPub, m_KeyLen) != 1)
++		MLKEMBuffer ct, secret;
++		if (m_PublicKey && MLKEM_encap (m_PublicKey, &ct.data, &ct.len, &secret.data, &secret.len) == 1 &&
++			ct.len == m_CTLen && secret.len == 32)
+ 		{
+-			LogPrint(eLogError, "MLKEM can't parse cached public key");
+-			return;
++			memcpy (ciphertext, ct.data, m_CTLen);
++			memcpy (shared, secret.data, 32);
++			return true;
+ 		}
+-		uint8_t * out_ct = nullptr;
+-		size_t out_ct_len = 0;
+-		uint8_t * out_ss = nullptr;
+-		size_t out_ss_len = 0;
+-		if (MLKEM_encap(pub_key, &out_ct, &out_ct_len, &out_ss, &out_ss_len) == 1)
+-		{
+-			memcpy(ciphertext, out_ct, out_ct_len);
+-			memcpy(shared, out_ss, out_ss_len);
+-			OPENSSL_cleanse(out_ct, out_ct_len);
+-			OPENSSL_cleanse(out_ss, out_ss_len);
+-			OPENSSL_free(out_ct);
+-			OPENSSL_free(out_ss);
+-			LogPrint(eLogDebug, "MLKEM [libressl] successfully encaps");
+-		}
+-		else
+-			LogPrint(eLogError, "MLKEM [libressl]: encapsulation failed");
+-		MLKEM_public_key_free(pub_key);
+ #endif
++		OPENSSL_cleanse (shared, 32);
++		LogPrint (eLogError, "MLKEM: Encapsulation failed");
++		return false;
+ 	}
+ 
+-	void MLKEMKeys::Decaps (const uint8_t * ciphertext, uint8_t * shared)
++	bool MLKEMKeys::Decaps (const uint8_t * ciphertext, uint8_t * shared)
+ 	{
+-		if (!m_Pkey) return;
+ #ifndef LIBRESSL_VERSION_NUMBER
+-		auto ctx = EVP_PKEY_CTX_new_from_pkey (NULL, m_Pkey, NULL);
+-		if (ctx)
++		auto ctx = m_Pkey ? EVP_PKEY_CTX_new_from_pkey (nullptr, m_Pkey, nullptr) : nullptr;
++		size_t sharedLen = 32;
++		bool ok = ctx && EVP_PKEY_decapsulate_init (ctx, nullptr) == 1 &&
++			EVP_PKEY_decapsulate (ctx, shared, &sharedLen, ciphertext, m_CTLen) == 1 && sharedLen == 32;
++		EVP_PKEY_CTX_free (ctx);
++		if (ok) return true;
++#else
++		MLKEMBuffer secret;
++		// Success includes implicit rejection. The following Noise AEAD
++		// authentication detects an invalid, correctly sized ciphertext.
++		if (m_Pkey && MLKEM_decap (m_Pkey, ciphertext, m_CTLen, &secret.data, &secret.len) == 1 && secret.len == 32)
+ 		{
+-			EVP_PKEY_decapsulate_init (ctx, NULL);
+-			size_t sharedLen = 32;
+-			EVP_PKEY_decapsulate (ctx, shared, &sharedLen, ciphertext, m_CTLen);
+-			EVP_PKEY_CTX_free (ctx);
++			memcpy (shared, secret.data, 32);
++			return true;
+ 		}
+-		else
+-			LogPrint (eLogError, "MLKEM can't create PKEY context");
+-#else
+-			uint8_t * out_shared_secret = nullptr;
+-			size_t out_shared_secret_len = 0;
+-			if (MLKEM_decap(m_Pkey, ciphertext, m_CTLen, &out_shared_secret, &out_shared_secret_len) == 1)
+-			{
+-				memcpy(shared, out_shared_secret, out_shared_secret_len);
+-
+-				OPENSSL_cleanse(out_shared_secret, out_shared_secret_len);
+-
+-				OPENSSL_free(out_shared_secret);
+-				LogPrint(eLogDebug, "MLKEM [libressl] successfully decrypt");
+-			}
+-			else
+-			{
+-				LogPrint(eLogError, "MLKEM [libressl]: decapsulation failed");
+-			}
+ #endif
++		OPENSSL_cleanse (shared, 32);
++		LogPrint (eLogError, "MLKEM: Decapsulation failed");
++		return false;
+ 	}
+ 
+ 	std::unique_ptr<MLKEMKeys> CreateMLKEMKeys (i2p::data::CryptoKeyType type)
+ 	{
+-		if (type <= i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD ||
+-		    type - i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD > (int)MLKEMS.size ()) return nullptr;
++		if (!IsMLKEMSupported (type)) return nullptr;
+ 		return std::make_unique<MLKEMKeys>((MLKEMTypes)(type - i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD - 1));
+ 	}
+ 
Index: net/i2pd/patches/patch-libi2pd_PostQuantum_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_PostQuantum_h
diff -N net/i2pd/patches/patch-libi2pd_PostQuantum_h
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_PostQuantum_h	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,67 @@
+Expose ML-KEM only when the selected crypto library supports it and retain
+separate public and private keys for LibreSSL.
+
+Index: libi2pd/PostQuantum.h
+--- libi2pd/PostQuantum.h.orig
++++ libi2pd/PostQuantum.h
+@@ -15,7 +15,7 @@
+ #include <tuple>
+ #include "Crypto.h"
+ #include "Identity.h"
+-#ifdef LIBRESSL_VERSION_NUMBER
++#if OPENSSL_PQ && defined(LIBRESSL_VERSION_NUMBER)
+ #	include <openssl/mlkem.h>
+ #endif
+ 
+@@ -60,6 +60,16 @@ namespace crypto
+ 		return std::get<2>(MLKEMS[type - i2p::data::CRYPTO_KEY_TYPE_ECIES_X25519_AEAD - 1]);
+ 	}
+ 
++	constexpr bool IsMLKEMSupported (i2p::data::CryptoKeyType type)
++	{
++		if (!GetMLKEMPublicKeyLen (type)) return false;
++#ifdef LIBRESSL_VERSION_NUMBER
++		return type != i2p::data::CRYPTO_KEY_TYPE_ECIES_MLKEM512_X25519_AEAD;
++#else
++		return true;
++#endif
++	}
++
+ 	class MLKEMKeys
+ 	{
+ 		public:
+@@ -67,13 +77,18 @@ namespace crypto
+ 			MLKEMKeys (MLKEMTypes type);
+ 			~MLKEMKeys ();
+ 
+-			void GenerateKeys ();
+-			void GetPublicKey (uint8_t * pub) const;
+-			void SetPublicKey (const uint8_t * pub);
++			MLKEMKeys (const MLKEMKeys&) = delete;
++			MLKEMKeys& operator= (const MLKEMKeys&) = delete;
++
++			// Buffers must hold GetKeyLen(), GetCTLen(), or 32 bytes as appropriate.
++			// On false, callers must abort the handshake and discard outputs.
++			[[nodiscard]] bool GenerateKeys ();
++			[[nodiscard]] bool GetPublicKey (uint8_t * pub) const;
++			[[nodiscard]] bool SetPublicKey (const uint8_t * pub);
+ 			size_t GetKeyLen () const { return m_KeyLen; };
+ 			size_t GetCTLen () const { return m_CTLen; };
+-			void Encaps (uint8_t * ciphertext, uint8_t * shared);
+-			void Decaps (const uint8_t * ciphertext, uint8_t * shared);
++			[[nodiscard]] bool Encaps (uint8_t * ciphertext, uint8_t * shared);
++			[[nodiscard]] bool Decaps (const uint8_t * ciphertext, uint8_t * shared);
+ 
+ 
+ 		private:
+@@ -88,8 +103,8 @@ namespace crypto
+ 			EVP_PKEY * m_Pkey;
+ #else
+ 			MLKEM_private_key * m_Pkey;
+-			uint8_t m_CachedPub[MLKEM768_KEY_LENGTH];
+-			bool m_IsPubCached = false;
++			MLKEM_public_key * m_PublicKey;
++			const int m_Rank;
+ #endif
+ 	};
+ 
Index: net/i2pd/patches/patch-libi2pd_Reseed_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Reseed_cpp
diff -N net/i2pd/patches/patch-libi2pd_Reseed_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Reseed_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,32 @@
+Use OpenSSL 3 certificate APIs only with OpenSSL, preserving LibreSSL support.
+
+Index: libi2pd/Reseed.cpp
+--- libi2pd/Reseed.cpp.orig
++++ libi2pd/Reseed.cpp
+@@ -16,7 +16,7 @@
+ #include <boost/algorithm/string.hpp>
+ #include <openssl/ssl.h>
+ #include <openssl/err.h>
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ #include <openssl/core_names.h>
+ #endif
+ #include <zlib.h>
+@@ -537,7 +537,7 @@ namespace data
+ 				// extract RSA key (we need n only, e = 65537)
+ 				EVP_PKEY * pubKey = X509_get_pubkey (cert);
+ 				const BIGNUM * n = nullptr;
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 				BIGNUM * n1 = BN_new ();
+ 				if (EVP_PKEY_get_bn_param (pubKey, OSSL_PKEY_PARAM_RSA_N, &n1) > 0)
+ 					n = n1;
+@@ -557,7 +557,7 @@ namespace data
+ 				}
+ 				else
+ 					LogPrint (eLogError, "Reseed: Can't extract RSA key from ", filename);
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 				BN_free (n1);
+ #endif
+ 			}
Index: net/i2pd/patches/patch-libi2pd_RouterContext_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_RouterContext_cpp
diff -N net/i2pd/patches/patch-libi2pd_RouterContext_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_RouterContext_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,19 @@
+Preserve an explicitly configured SSU2 MTU when the router address changes.
+
+Index: libi2pd/RouterContext.cpp
+--- libi2pd/RouterContext.cpp.orig
++++ libi2pd/RouterContext.cpp
+@@ -591,7 +591,13 @@ namespace i2p
+ 			if (addr && (addr->host != host || !addr->ssu->mtu))
+ 			{
+ 				addr->host = host;
++#ifdef __OpenBSD__
++				// Address updates must not replace an explicitly configured MTU.
++				uint16_t configuredMTU; i2p::config::GetOption ("ssu2.mtu6", configuredMTU);
++				if (!configuredMTU && m_StatusV6 != eRouterStatusProxy)
++#else
+ 				if (m_StatusV6 != eRouterStatusProxy)
++#endif
+ 				{
+ 					// update MTU
+ 					auto mtu = i2p::util::net::GetMTU (host);
Index: net/i2pd/patches/patch-libi2pd_SSU2Session_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_SSU2Session_cpp
diff -N net/i2pd/patches/patch-libi2pd_SSU2Session_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_SSU2Session_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,103 @@
+Check ML-KEM key generation, export and encapsulation before sending SSU2 data.
+
+Index: libi2pd/SSU2Session.cpp
+--- libi2pd/SSU2Session.cpp.orig
++++ libi2pd/SSU2Session.cpp
+@@ -754,7 +754,7 @@ namespace transport
+ 		{
+ 			i2p::data::CryptoKeyType cryptoType = (i2p::data::CryptoKeyType)(m_Version + 2);
+ 			m_PQKeys = i2p::crypto::CreateMLKEMKeys (cryptoType);
+-            m_PQKeys->GenerateKeys ();
++            if (!m_PQKeys || !m_PQKeys->GenerateKeys ()) return false;
+             offset = m_PQKeys->GetKeyLen () + 16;
+ 			payloadSize += offset;
+ 		}
+@@ -804,7 +804,7 @@ namespace transport
+ 		{
+ 			size_t keyLen = m_PQKeys->GetKeyLen ();
+ 			std::vector<uint8_t> encapsKey(keyLen);
+-			m_PQKeys->GetPublicKey (encapsKey.data ());
++			if (!m_PQKeys || !m_PQKeys->GetPublicKey (encapsKey.data ())) return false;
+ 			if (!m_NoiseState->Encrypt (encapsKey.data (), payload, keyLen))
+ 			{
+ 				LogPrint (eLogWarning, "SSU2: SessionRequest ML-KEM encap_key frame AEAD encryption failed ");
+@@ -917,6 +917,7 @@ namespace transport
+         {
+ 			auto cryptoType = (i2p::data::CryptoKeyType)(m_Version + 2);
+             auto keyLen = i2p::crypto::GetMLKEMPublicKeyLen (cryptoType);
++			if (!i2p::crypto::IsMLKEMSupported (cryptoType) || offset + keyLen + 16 > len) return false;
+             std::vector<uint8_t> encapsKey(keyLen);
+             if (!m_NoiseState->Decrypt (buf + offset, encapsKey.data (), keyLen))
+             {
+@@ -926,7 +927,7 @@ namespace transport
+ 			m_NoiseState->MixHash (buf + offset, keyLen + 16);
+ 			offset += keyLen + 16;
+ 			m_PQKeys = i2p::crypto::CreateMLKEMKeys (cryptoType);
+-			m_PQKeys->SetPublicKey (encapsKey.data ());
++			if (!m_PQKeys || !m_PQKeys->SetPublicKey (encapsKey.data ())) return false;
+         }
+ #endif
+ 		if (offset + 16 > len)
+@@ -989,11 +990,12 @@ namespace transport
+ 		size_t maxPayloadSize = m_MaxPayloadSize - 48;
+ 		size_t payloadSize = 0, offset = 0;
+ #if OPENSSL_PQ
+-        if (m_Version > 2 && m_PQKeys)
++        if (m_Version > 2)
+         {
++			if (!m_PQKeys) return;
+             size_t cipherTextLen = m_PQKeys->GetCTLen ();
+ 			std::vector<uint8_t> kemCiphertext(cipherTextLen);
+-			m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret);
++			if (!m_PQKeys || !m_PQKeys->Encaps (kemCiphertext.data (), sharedSecret)) return;
+ 			if (!m_NoiseState->Encrypt (kemCiphertext.data (), payload, cipherTextLen))
+ 			{
+ 				LogPrint (eLogWarning, "SSU2: SessionCreated ML-KEM ciphertext section AEAD encryption failed");
+@@ -1060,6 +1062,11 @@ namespace transport
+ 			LogPrint (eLogWarning, "SSU2: Unexpected SessionCreated message");
+ 			return false;
+ 		}
++		if (len < 80)
++		{
++			LogPrint (eLogWarning, "SSU2: SessionCreated message too short ", len);
++			return false;
++		}
+ 		Header header;
+ 		memcpy (header.buf, buf, 16);
+ 		header.ll[0] ^= CreateHeaderMask (m_Address->i, buf + (len - 24));
+@@ -1069,11 +1076,7 @@ namespace transport
+ 		if (header.h.type != eSSU2SessionCreated)
+ 		// this situation is valid, because it might be Retry with different encryption
+ 			return false;
+-		if (len < 80)
+-		{
+-			LogPrint (eLogWarning, "SSU2: SessionCreated message too short ", len);
+-			return false;
+-		}
++
+ 		m_HandshakeInterval = i2p::util::GetMillisecondsSinceEpoch () - m_HandshakeInterval;
+ 		const uint8_t nonce[12] = {0};
+ 		uint8_t headerX[48];
+@@ -1086,10 +1089,12 @@ namespace transport
+ 		m_NoiseState->MixKey (sharedSecret);
+ 		size_t offset = 64;
+ #if OPENSSL_PQ
+-		if (m_Version > 2 && m_PQKeys)
++		if (m_Version > 2)
+ 		{
++			if (!m_PQKeys) return false;
+ 			i2p::data::CryptoKeyType cryptoType = (i2p::data::CryptoKeyType)(m_Version + 2);
+ 			size_t cipherTextLen = i2p::crypto::GetMLKEMCipherTextLen (cryptoType);
++			if (!i2p::crypto::IsMLKEMSupported (cryptoType) || offset + cipherTextLen + 16 > len) return false;
+ 			std::vector<uint8_t> kemCiphertext(cipherTextLen);
+ 			if (!m_NoiseState->Decrypt (buf + offset, kemCiphertext.data (), cipherTextLen))
+ 			{
+@@ -1098,7 +1103,7 @@ namespace transport
+ 			}
+ 			m_NoiseState->MixHash (buf + offset, cipherTextLen + 16);
+ 			offset += cipherTextLen + 16;
+-			m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret);
++			if (!m_PQKeys || !m_PQKeys->Decaps (kemCiphertext.data (), sharedSecret)) return false;
+ 			m_NoiseState->MixKey (sharedSecret);
+ 		}
+ #endif
Index: net/i2pd/patches/patch-libi2pd_Signature_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Signature_cpp
diff -N net/i2pd/patches/patch-libi2pd_Signature_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Signature_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,105 @@
+Gate OpenSSL 3 signature APIs away from LibreSSL and safely handle Ed25519ph
+signing and verification.
+
+Index: libi2pd/Signature.cpp
+--- libi2pd/Signature.cpp.orig
++++ libi2pd/Signature.cpp
+@@ -8,7 +8,8 @@
+ 
+ #include <memory>
+ #include <openssl/evp.h>
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#include <openssl/crypto.h>
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ #include <openssl/core_names.h>
+ #include <openssl/param_build.h>
+ #endif
+@@ -19,7 +20,7 @@ namespace i2p
+ {
+ namespace crypto
+ {
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 	DSAVerifier::DSAVerifier ():
+ 		m_PublicKey (nullptr)
+ 	{
+@@ -172,7 +173,7 @@ namespace crypto
+ 	}
+ #endif
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 	ECDSAVerifier::ECDSAVerifier (int curve, size_t keyLen, const EVP_MD * hash):
+ 		m_Curve(curve), m_KeyLen (keyLen), m_Hash (hash), m_PublicKey (nullptr)
+ 	{
+@@ -405,7 +406,7 @@ namespace crypto
+ 			LogPrint (eLogError, "EdDSA signing key is not set");
+ 	}
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000)
++#if OPENSSL_ED25519PH
+ 	static const OSSL_PARAM EDDSA25519phParams[] =
+ 	{
+ 		OSSL_PARAM_utf8_string ("instance", (char *)"Ed25519ph", 9),
+@@ -415,19 +416,13 @@ namespace crypto
+ 	bool EDDSA25519phVerifier::Verify (const uint8_t * buf, size_t len, const uint8_t * signature) const
+ 	{
+ 		auto pkey = GetPkey ();
+-		if (pkey)
+-		{
+-			uint8_t digest[64];
+-			SHA512 (buf, len, digest);
+-			EVP_MD_CTX * ctx = EVP_MD_CTX_create ();
+-			EVP_DigestVerifyInit_ex (ctx, NULL, NULL, NULL, NULL, pkey, EDDSA25519phParams);
+-			auto ret = EVP_DigestVerify (ctx, signature, 64, digest, 64);
+-			EVP_MD_CTX_destroy (ctx);
+-			return ret;
+-		}
+-		else
+-			LogPrint (eLogError, "EdDSA verification key is not set");
+-		return false;
++		EVP_MD_CTX * ctx = EVP_MD_CTX_new ();
++		// The provider applies SHA-512 for the Ed25519ph instance.
++		bool ret = pkey && ctx &&
++			EVP_DigestVerifyInit_ex (ctx, nullptr, nullptr, nullptr, nullptr, pkey, EDDSA25519phParams) == 1 &&
++			EVP_DigestVerify (ctx, signature, 64, buf, len) == 1;
++		EVP_MD_CTX_free (ctx);
++		return ret;
+ 	}
+ 
+ 	EDDSA25519phSigner::EDDSA25519phSigner (const uint8_t * signingPrivateKey):
+@@ -438,21 +433,21 @@ namespace crypto
+ 	void EDDSA25519phSigner::Sign (const uint8_t * buf, int len, uint8_t * signature) const
+ 	{
+ 		auto pkey = GetPkey ();
+-		if (pkey)
++		EVP_MD_CTX * ctx = EVP_MD_CTX_new ();
++		size_t l = 64;
++		uint8_t sig[64];
++		bool ok = pkey && ctx && len >= 0 &&
++			EVP_DigestSignInit_ex (ctx, nullptr, nullptr, nullptr, nullptr, pkey, EDDSA25519phParams) == 1 &&
++			EVP_DigestSign (ctx, sig, &l, buf, len) == 1 && l == sizeof (sig);
++		if (ok)
++			memcpy (signature, sig, sizeof (sig));
++		else
+ 		{
+-			uint8_t digest[64];
+-			SHA512 (buf, len, digest);
+-			EVP_MD_CTX * ctx = EVP_MD_CTX_create ();
+-			size_t l = 64;
+-			uint8_t sig[64];
+-			EVP_DigestSignInit_ex (ctx, NULL, NULL, NULL, NULL, pkey, EDDSA25519phParams);
+-			if (!EVP_DigestSign (ctx, sig, &l, digest, 64))
+-				LogPrint (eLogError, "EdDSA signing failed");
+-			memcpy (signature, sig, 64);
+-			EVP_MD_CTX_destroy (ctx);
++			memset (signature, 0, sizeof (sig));
++			LogPrint (eLogError, "Ed25519ph signing failed");
+ 		}
+-		else
+-			LogPrint (eLogError, "EdDSA signing key is not set");
++		OPENSSL_cleanse (sig, sizeof (sig));
++		EVP_MD_CTX_free (ctx);
+ 	}
+ #endif
+ }
Index: net/i2pd/patches/patch-libi2pd_Signature_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_Signature_h
diff -N net/i2pd/patches/patch-libi2pd_Signature_h
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_Signature_h	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,51 @@
+Select signature key classes according to the APIs available in OpenSSL and
+LibreSSL, including optional Ed25519ph support.
+
+Index: libi2pd/Signature.h
+--- libi2pd/Signature.h.orig
++++ libi2pd/Signature.h
+@@ -62,7 +62,7 @@ namespace crypto
+ 
+ 		private:
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 			EVP_PKEY * m_PublicKey;
+ #else
+ 			DSA * m_PublicKey;
+@@ -82,7 +82,7 @@ namespace crypto
+ 
+ 		private:
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 			EVP_PKEY * m_PrivateKey;
+ #else
+ 			DSA * m_PrivateKey;
+@@ -96,7 +96,7 @@ namespace crypto
+ 	constexpr size_t ECDSAP384_KEY_LENGTH = 96;
+ 	constexpr size_t ECDSAP521_KEY_LENGTH = 132;
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x30000000L // since 3.0.0
+ 	class ECDSAVerifier: public Verifier
+ 	{
+ 		public:
+@@ -382,7 +382,7 @@ namespace crypto
+ 			EVP_PKEY * GetPkey () const { return m_Pkey; };
+ 	};
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if OPENSSL_ED25519PH
+ 	class EDDSA25519phVerifier: public EDDSA25519Verifier
+ 	{
+ 		public:
+@@ -428,7 +428,7 @@ namespace crypto
+ 			EDDSA25519SignerCompat * m_Fallback;
+ 	};
+ 
+-#if (OPENSSL_VERSION_NUMBER >= 0x030000000) // since 3.0.0
++#if OPENSSL_ED25519PH
+ 	class EDDSA25519phSigner: public EDDSA25519Signer
+ 	{
+ 		public:
Index: net/i2pd/patches/patch-libi2pd_client_UDPTunnel_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_client_UDPTunnel_cpp
diff -N net/i2pd/patches/patch-libi2pd_client_UDPTunnel_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_client_UDPTunnel_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,50 @@
+Serialize cancellation and rearming of the UDP server cleanup timer. Keep only a
+weak reference in its callback so queued handlers cannot access a destroyed
+tunnel or keep a stopped tunnel alive.
+
+Index: libi2pd_client/UDPTunnel.cpp
+--- libi2pd_client/UDPTunnel.cpp.orig
++++ libi2pd_client/UDPTunnel.cpp
+@@ -484,7 +484,7 @@ namespace client
+ 
+ 	void I2PUDPServerTunnel::Start ()
+ 	{
+-		m_IsStarted = true;
++		m_IsStarted.store (true, std::memory_order_release);
+ 		m_LocalDest->Start ();
+ 
+ 		auto dgram = m_LocalDest->CreateDatagramDestination (m_Gzip);
+@@ -505,11 +505,14 @@ namespace client
+ 
+ 	void I2PUDPServerTunnel::ScheduleCleanupTimer ()
+ 	{
+-		if (m_CleanupTimer)
++		std::lock_guard<std::mutex> lock (m_CleanupTimerMutex);
++		if (m_IsStarted.load (std::memory_order_acquire) && m_CleanupTimer)
+ 		{
+ 			m_CleanupTimer->expires_after (std::chrono::seconds (I2P_UDP_SESSION_CLEANUP_INTERVAL));
+-			m_CleanupTimer->async_wait (std::bind (&I2PUDPServerTunnel::HandleCleanupTimer,
+-				this, std::placeholders::_1));
++			std::weak_ptr<I2PUDPServerTunnel> weak = shared_from_this ();
++			m_CleanupTimer->async_wait ([weak](const boost::system::error_code& ecode) {
++				if (auto self = weak.lock ()) self->HandleCleanupTimer (ecode);
++			});
+ 		}
+ 	}
+ 
+@@ -558,10 +561,12 @@ namespace client
+ 		// a config reload builds a second tunnel object and drops it when the same
+ 		// one is already there. Its destructor must not reset the receiver of the
+ 		// live tunnel: they share the datagram destination
+-		if (!m_IsStarted) return;
+-		m_IsStarted = false;
++		if (!m_IsStarted.exchange (false, std::memory_order_acq_rel)) return;
+ 		if (m_StatsTimer) m_StatsTimer->cancel ();
+-		if (m_CleanupTimer) m_CleanupTimer->cancel ();
++		{
++			std::lock_guard<std::mutex> lock (m_CleanupTimerMutex);
++			if (m_CleanupTimer) m_CleanupTimer->cancel ();
++		}
+ 		auto dgram = m_LocalDest->GetDatagramDestination ();
+ 		if (dgram) {
+ 			dgram->ResetReceiver (m_inPort);
Index: net/i2pd/patches/patch-libi2pd_client_UDPTunnel_h
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_client_UDPTunnel_h
diff -N net/i2pd/patches/patch-libi2pd_client_UDPTunnel_h
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_client_UDPTunnel_h	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,34 @@
+Track UDP server tunnel shutdown atomically and guard cleanup timer operations
+shared by the event loop and configuration reload.
+
+Index: libi2pd_client/UDPTunnel.h
+--- libi2pd_client/UDPTunnel.h.orig
++++ libi2pd_client/UDPTunnel.h
+@@ -13,6 +13,8 @@
+ #include <string>
+ #include <string_view>
+ #include <memory>
++#include <atomic>
++#include <mutex>
+ #include <thread>
+ #include <vector>
+ #include <list>
+@@ -148,7 +150,7 @@ namespace client
+ 	typedef std::shared_ptr<UDPSession> UDPSessionPtr;
+ 
+ 	/** server side udp tunnel, many i2p inbound to 1 ip outbound */
+-	class I2PUDPServerTunnel
++	class I2PUDPServerTunnel: public std::enable_shared_from_this<I2PUDPServerTunnel>
+ 	{
+ 		public:
+ 
+@@ -194,7 +196,8 @@ namespace client
+ 			UDPSessionPtr m_LastSession;
+ 			uint16_t m_inPort;
+ 			bool m_Gzip;
+-			bool m_IsStarted = false; // a tunnel that never started shares nothing to tear down
++			std::atomic<bool> m_IsStarted {false}; // a tunnel that never started shares nothing to tear down
++			std::mutex m_CleanupTimerMutex;
+ 			uint32_t m_NumRawNoSession = 0;
+ 			size_t m_MaxWindow = I2P_UDP_MIN_MAX_NUM_UNACKED_DATAGRAMS;
+ 			std::unique_ptr<boost::asio::steady_timer> m_StatsTimer;
Index: net/i2pd/patches/patch-libi2pd_util_cpp
===================================================================
RCS file: net/i2pd/patches/patch-libi2pd_util_cpp
diff -N net/i2pd/patches/patch-libi2pd_util_cpp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-libi2pd_util_cpp	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,43 @@
+Read interface MTU data through getifaddrs on OpenBSD, avoiding an ioctl that
+is unavailable under the port pledge.
+
+Index: libi2pd/util.cpp
+--- libi2pd/util.cpp.orig
++++ libi2pd/util.cpp
+@@ -26,6 +26,10 @@
+ #include <pthread_np.h>
+ #endif
+ 
++#ifdef __OpenBSD__
++#include <net/if.h>
++#endif
++
+ #if defined(__APPLE__)
+ # include <AvailabilityMacros.h>
+ #endif
+@@ -570,6 +574,17 @@ namespace net
+ 		int mtu = fallback;
+ 		if (ifa && family)
+ 		{ // interface found?
++#ifdef __OpenBSD__
++			// getifaddrs supplies if_data for AF_LINK entries. NET_RT_IFLIST
++			// is permitted by pledge("inet"), unlike SIOCGIFMTU.
++			for (auto link = ifaddr; link; link = link->ifa_next)
++				if (link->ifa_addr && link->ifa_addr->sa_family == AF_LINK &&
++					link->ifa_data && !strcmp (link->ifa_name, ifa->ifa_name))
++				{
++					mtu = static_cast<const struct if_data *> (link->ifa_data)->ifi_mtu;
++					break;
++				}
++#else
+ 			int fd = socket(family, SOCK_DGRAM, 0);
+ 			if (fd > 0)
+ 			{
+@@ -583,6 +598,7 @@ namespace net
+ 			}
+ 			else
+ 				LogPrint(eLogError, "NetIface: Failed to create datagram socket");
++#endif
+ 		}
+ 		else
+ 			LogPrint(eLogWarning, "NetIface: Interface for local address", localAddress.to_string(), " not found");
Index: net/i2pd/patches/patch-test_Makefile
===================================================================
RCS file: net/i2pd/patches/patch-test_Makefile
diff -N net/i2pd/patches/patch-test_Makefile
--- net/i2pd/patches/patch-test_Makefile	17 Oct 2026 08:47:19 -0000	1.1
+++ /dev/null	1 Jan 1970 00:00:00 -0000
@@ -1,11 +0,0 @@
-Index: tests/Makefile
---- tests/Makefile.orig
-+++ tests/Makefile
-@@ -18,7 +18,6 @@ ifneq (, $(findstring mingw, $(SYS))$(findstring windo
- endif
- 
- LDLIBS = \
--	-lboost_system$(BOOST_SUFFIX) \
- 	-lboost_program_options$(BOOST_SUFFIX) \
- 	-lssl \
- 	-lcrypto \
Index: net/i2pd/patches/patch-tests_Makefile
===================================================================
RCS file: net/i2pd/patches/patch-tests_Makefile
diff -N net/i2pd/patches/patch-tests_Makefile
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ net/i2pd/patches/patch-tests_Makefile	21 Oct 2026 15:41:33 -0000
@@ -0,0 +1,14 @@
+Drop the separate Boost.System link dependency, which is not needed by the
+current Boost configuration.
+
+Index: tests/Makefile
+--- tests/Makefile.orig
++++ tests/Makefile
+@@ -18,7 +18,6 @@ ifneq (, $(findstring mingw, $(SYS))$(fi
+ endif
+ 
+ LDLIBS = \
+-	-lboost_system$(BOOST_SUFFIX) \
+ 	-lboost_program_options$(BOOST_SUFFIX) \
+ 	-lssl \
+ 	-lcrypto \
Index: net/i2pd/pkg/PLIST
===================================================================
RCS file: /cvs/ports/net/i2pd/pkg/PLIST,v
diff -u -p -u -p -r1.19 PLIST
--- net/i2pd/pkg/PLIST	17 Oct 2026 08:47:19 -0000	1.19
+++ net/i2pd/pkg/PLIST	21 Oct 2026 15:41:33 -0000
@@ -1,5 +1,5 @@
 @newgroup _i2pd:838
-@newuser _i2pd:838:838::i2pd account:/var/i2pd:/sbin/nologin
+@newuser _i2pd:838:838::i2pd account:${LOCALSTATEDIR}/i2pd:/sbin/nologin
 @rcscript ${RCDIR}/i2pd
 @bin bin/i2pd
 include/i2pd/
@@ -7,6 +7,7 @@ include/i2pd/AddressBook.h
 include/i2pd/BOB.h
 include/i2pd/Base.h
 include/i2pd/Blinding.h
+include/i2pd/BoostStream.h
 include/i2pd/CPU.h
 include/i2pd/ClientContext.h
 include/i2pd/Config.h
@@ -56,6 +57,9 @@ include/i2pd/Streaming.h
 include/i2pd/Tag.h
 include/i2pd/Timestamp.h
 include/i2pd/Torrents.h
+include/i2pd/TorrentsDHT.h
+include/i2pd/TorrentsRPC.h
+include/i2pd/TorrentsTunnel.h
 include/i2pd/TransitTunnel.h
 include/i2pd/TransportSession.h
 include/i2pd/Transports.h
@@ -69,18 +73,239 @@ include/i2pd/UDPTunnel.h
 include/i2pd/api.h
 include/i2pd/util.h
 include/i2pd/version.h
+include/libi2pd/
+include/libi2pd/Base.h
+include/libi2pd/Blinding.h
+include/libi2pd/CPU.h
+include/libi2pd/Config.h
+include/libi2pd/Crypto.h
+include/libi2pd/CryptoKey.h
+include/libi2pd/Datagram.h
+include/libi2pd/Destination.h
+include/libi2pd/ECIESX25519AEADRatchetSession.h
+include/libi2pd/Ed25519.h
+include/libi2pd/Elligator.h
+include/libi2pd/FS.h
+include/libi2pd/Family.h
+include/libi2pd/Garlic.h
+include/libi2pd/Gost.h
+include/libi2pd/Gzip.h
+include/libi2pd/HTTP.h
+include/libi2pd/I2NPProtocol.h
+include/libi2pd/I2PEndian.h
+include/libi2pd/IdentMetrics.h
+include/libi2pd/Identity.h
+include/libi2pd/KadDHT.h
+include/libi2pd/LeaseSet.h
+include/libi2pd/LittleBigEndian.h
+include/libi2pd/Log.h
+include/libi2pd/NTCP2.h
+include/libi2pd/NetDb.hpp
+include/libi2pd/NetDbRequests.h
+include/libi2pd/PostQuantum.h
+include/libi2pd/Profiling.h
+include/libi2pd/Queue.h
+include/libi2pd/Reseed.h
+include/libi2pd/RouterContext.h
+include/libi2pd/RouterInfo.h
+include/libi2pd/SSU2.h
+include/libi2pd/SSU2OutOfSession.h
+include/libi2pd/SSU2Session.h
+include/libi2pd/Signature.h
+include/libi2pd/Siphash.h
+include/libi2pd/Socks5.h
+include/libi2pd/Streaming.h
+include/libi2pd/Tag.h
+include/libi2pd/Timestamp.h
+include/libi2pd/TransitTunnel.h
+include/libi2pd/TransportSession.h
+include/libi2pd/Transports.h
+include/libi2pd/Tunnel.h
+include/libi2pd/TunnelBase.h
+include/libi2pd/TunnelConfig.h
+include/libi2pd/TunnelEndpoint.h
+include/libi2pd/TunnelGateway.h
+include/libi2pd/TunnelPool.h
+include/libi2pd/api.h
+include/libi2pd/util.h
+include/libi2pd/version.h
+include/libi2pdclient/
+include/libi2pdclient/AddressBook.h
+include/libi2pdclient/BOB.h
+include/libi2pdclient/BoostStream.h
+include/libi2pdclient/ClientContext.h
+include/libi2pdclient/HTTPProxy.h
+include/libi2pdclient/I2CP.h
+include/libi2pdclient/I2PService.h
+include/libi2pdclient/I2PTunnel.h
+include/libi2pdclient/MatchedDestination.h
+include/libi2pdclient/SAM.h
+include/libi2pdclient/SOCKS.h
+include/libi2pdclient/Torrents.h
+include/libi2pdclient/TorrentsDHT.h
+include/libi2pdclient/TorrentsRPC.h
+include/libi2pdclient/TorrentsTunnel.h
+include/libi2pdclient/UDPTunnel.h
+include/libi2pdlang/
+include/libi2pdlang/I18N.h
+include/libi2pdlang/I18N_langs.h
+lib/cmake/i2pd/
+lib/cmake/i2pd/i2pdConfig.cmake
+lib/cmake/i2pd/i2pdConfigVersion.cmake
 @static-lib lib/libi2pd.a
 @static-lib lib/libi2pdclient.a
+@mode 0750
+@owner _i2pd
+@group _i2pd
+@sample ${SYSCONFDIR}/i2pd/
+@mode
+@sample ${LOCALSTATEDIR}/i2pd/
+@sample ${LOCALSTATEDIR}/i2pd/certificates/
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/
+@sample ${LOCALSTATEDIR}/i2pd/certificates/router/
+@owner
+@group
 @static-lib lib/libi2pdlang.a
 share/doc/pkg-readmes/${PKGSTEM}
 share/examples/i2pd/
-share/examples/i2pd/i2pd.conf
-@mode 0750
+share/examples/i2pd/certificates/
+share/examples/i2pd/certificates/family/
+share/examples/i2pd/certificates/family/gostcoin.crt
 @owner _i2pd
 @group _i2pd
-@sample ${SYSCONFDIR}/i2pd/
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/gostcoin.crt
+@owner
+@group
+share/examples/i2pd/certificates/family/i2p-dev.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/i2p-dev.crt
+@owner
+@group
+share/examples/i2pd/certificates/family/i2pd-dev.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/i2pd-dev.crt
+@owner
+@group
+share/examples/i2pd/certificates/family/mca2-i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/mca2-i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/family/stormycloud.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/stormycloud.crt
+@owner
+@group
+share/examples/i2pd/certificates/family/volatile.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/volatile.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/
+share/examples/i2pd/certificates/reseed/acetone_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/acetone_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/admin_at_likogan.dev.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/admin_at_likogan.dev.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/admin_at_stormycloud.org.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/admin_at_stormycloud.org.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/creativecowpat_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/creativecowpat_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/damian.buho_at_proton.me.crt
+share/examples/i2pd/certificates/reseed/diyarciftci_at_protonmail.com.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/diyarciftci_at_protonmail.com.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/echelon3_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/echelon3_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/freedompeer_at_skhron.eu.crt
+share/examples/i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/idk_at_stormycloud.org.crt
+share/examples/i2pd/certificates/reseed/igor_at_novg.net.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/igor_at_novg.net.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/lazygravy_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/lazygravy_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/orignal_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/orignal_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/r4sas-reseed_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/r4sas-reseed_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/rambler_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/rambler_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/reseed_at_diva.exchange.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/reseed_at_diva.exchange.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/reseedserver_at_mail.i2p.crt
+share/examples/i2pd/certificates/reseed/sahil_at_mail.i2p.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/sahil_at_mail.i2p.crt
+@owner
+@group
+share/examples/i2pd/certificates/reseed/vserod1488_at_proton.me.crt
+@owner _i2pd
+@group _i2pd
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/vserod1488_at_proton.me.crt
+@owner
+@group
+share/examples/i2pd/i2pd.conf
 @mode 0640
 @owner root
+@group _i2pd
 @sample ${SYSCONFDIR}/i2pd/i2pd.conf
 @mode
 @owner
@@ -95,32 +320,3 @@ share/examples/i2pd/tunnels.conf
 @group
 share/examples/login.conf.d/i2pd
 @sample ${SYSCONFDIR}/login.conf.d/i2pd
-@cwd /var
-@owner _i2pd
-@group _i2pd
-i2pd/
-i2pd/certificates/
-i2pd/certificates/family/
-i2pd/certificates/family/gostcoin.crt
-i2pd/certificates/family/i2p-dev.crt
-i2pd/certificates/family/i2pd-dev.crt
-i2pd/certificates/family/mca2-i2p.crt
-i2pd/certificates/family/stormycloud.crt
-i2pd/certificates/family/volatile.crt
-i2pd/certificates/reseed/
-i2pd/certificates/reseed/acetone_at_mail.i2p.crt
-i2pd/certificates/reseed/admin_at_likogan.dev.crt
-i2pd/certificates/reseed/admin_at_stormycloud.org.crt
-i2pd/certificates/reseed/creativecowpat_at_mail.i2p.crt
-i2pd/certificates/reseed/diyarciftci_at_protonmail.com.crt
-i2pd/certificates/reseed/echelon3_at_mail.i2p.crt
-i2pd/certificates/reseed/hankhill19580_at_gmail.com.crt
-i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
-i2pd/certificates/reseed/igor_at_novg.net.crt
-i2pd/certificates/reseed/lazygravy_at_mail.i2p.crt
-i2pd/certificates/reseed/orignal_at_mail.i2p.crt
-i2pd/certificates/reseed/r4sas-reseed_at_mail.i2p.crt
-i2pd/certificates/reseed/rambler_at_mail.i2p.crt
-i2pd/certificates/reseed/reseed_at_diva.exchange.crt
-i2pd/certificates/reseed/sahil_at_mail.i2p.crt
-i2pd/certificates/reseed/vserod1488_at_proton.me.crt
Index: net/i2pd/pkg/README
===================================================================
RCS file: /cvs/ports/net/i2pd/pkg/README,v
diff -u -p -u -p -r1.5 README
--- net/i2pd/pkg/README	17 Oct 2026 08:47:19 -0000	1.5
+++ net/i2pd/pkg/README	21 Oct 2026 15:41:33 -0000
@@ -6,13 +6,13 @@ Important: upgrading from i2pd < 2.61.0
 =======================================
 
 A major change in this package starting with version 2.61.0 requires
-an intervention from all upgrading users: the running directory of i2pd,
-which is also the $HOME directory of the _i2pd user, moves from
-/var/lib/i2pd to /var/i2pd.
+an intervention from most users: the running directory of i2pd, which
+is also the $HOME directory of the _i2pd user, moves from /var/lib/i2pd
+to /var/i2pd.
 
 If you installed a version of this package older than 2.61.0, you'll
-change the _i2pd user's home directory and move all files under
-/var/lib/i2pd to /var/i2pd.
+probably have to change the _i2pd user $HOME directory and move all
+files under /var/lib/i2pd to /var/i2pd.
 
 Unless you installed or configured i2pd in a non-standard way, here's
 how most users should proceed:
@@ -107,15 +107,16 @@ want to reduce this log verbosity by cha
 parameter in ${SYSCONFDIR}/i2pd/i2pd.conf.
 
 If you want log messages to be written to another file, e.g.
-/var/i2pd/i2pd.log, you can change the "log" and "logfile"
+${LOCALSTATEDIR}/i2pd/i2pd.log, you can change the "log" and "logfile"
+
 parameters in ${SYSCONFDIR}/i2pd/i2pd.conf. To have this log file
 rotated automatically, you can add an entry to /etc/newsyslog.conf
-using the i2pd pid file so that newsyslog(8) can send SIGHUP to the
+using the i2pd pid file so that newsyslog(8) can send SIGUSR1 to the
 daemon after rotation.
 
 For example:
 
-	/var/i2pd/i2pd.log  _i2pd:_i2pd  644  6  *  $D13  Z /var/i2pd/i2pd.pid
+	${LOCALSTATEDIR}/i2pd/i2pd.log  _i2pd:_i2pd  644  6  *  $D13  Z ${LOCALSTATEDIR}/i2pd/i2pd.pid SIGUSR1
 
-Sending SIGHUP is enough for log rotation, and also makes i2pd reload
-its tunnel configuration and rotate transient keys.
+Send SIGUSR1 to reopen the log file. Send SIGHUP to reload tunnel
+configuration and rotate transient keys.
Index: net/i2pd/pkg/i2pd.rc
===================================================================
RCS file: /cvs/ports/net/i2pd/pkg/i2pd.rc,v
diff -u -p -u -p -r1.5 i2pd.rc
--- net/i2pd/pkg/i2pd.rc	17 Oct 2026 08:47:19 -0000	1.5
+++ net/i2pd/pkg/i2pd.rc	21 Oct 2026 15:41:33 -0000
@@ -3,12 +3,12 @@
 daemon="${TRUEPREFIX}/bin/i2pd --daemon"
 daemon_user="_i2pd"
 daemon_flags="--service \
-	--datadir=/var/i2pd \
+	--datadir=${LOCALSTATEDIR}/i2pd \
 	--conf=${SYSCONFDIR}/i2pd/i2pd.conf \
 	--tunconf=${SYSCONFDIR}/i2pd/tunnels.conf \
 	--tunnelsdir=${SYSCONFDIR}/i2pd/tunnels.d \
-	--certsdir=/var/i2pd/certificates"
+	--certsdir=${LOCALSTATEDIR}/i2pd/certificates"
 
 . /etc/rc.d/rc.subr
 
-rc_cmd $1
+rc_cmd "$1"

Reply via email to