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"