Stuart Henderson wrote:
On 2026/10/21 15:50, David Uhden Collado wrote:
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.
the strongest of the pledges in here is:
stdio rpath wpath cpath fattr inet dns unix flock
what's the point?
- SSU2 MTU detection using getifaddrs().
and afaik this is only because pledge
You're right that the pledge is fairly broad, since i2pd needs access to
the filesystem, network and Unix sockets to work properly.
Even so, it still prevents i2pd from executing other programs, creating
new processes after daemonization, passing file descriptors and
accessing unrelated system interfaces.
The initial pledge allows "proc" during startup, but that permission is
removed once daemonization is complete.
- Synchronous signal handling to avoid unsafe configuration reloads.
- UDP tunnel cleanup and shutdown fixes.
- Updated PLIST, configuration samples and rc.d integration.
the configuration samples change undoes my work that was specifically
to remove a common issue when people add new certs and forget the
@sample. and guess what...you added 5 certs and 1 dir and forgot the
@sample for them.
you also undid some patches that were changing /etc to ${SYSCONFDIR}.
i think you should probably stop sending diffs for a bit.
I didn't pay attention to the latest changes. That makes sense. I've
attached the corrected patch.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 22 Oct 2026 05:25:43 -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
@@ -32,11 +34,11 @@ post-install:
${INSTALL_DATA_DIR} ${PREFIX}/include/i2pd \
${PREFIX}/share/examples/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} ${WRKINST}${LOCALSTATEDIR}/i2pd/certificates/${dir}
${INSTALL_DATA} ${WRKDIST}/contrib/certificates/${dir}/* \
- ${WRKINST}/var/i2pd/certificates/${dir}
+ ${WRKINST}${LOCALSTATEDIR}/i2pd/certificates/${dir}
.endfor
${SUBST_DATA} ${WRKDIST}/contrib/i2pd.conf \
${PREFIX}/share/examples/i2pd/i2pd.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 22 Oct 2026 05:25:43 -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 22 Oct 2026 05:25:43 -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
++## Default: ~/.i2pd/tunnels.conf or ${LOCALSTATEDIR}/i2pd/tunnels.conf
+tunconf = ${SYSCONFDIR}/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
++## Default: ~/.i2pd/tunnels.d or ${LOCALSTATEDIR}/i2pd/tunnels.d
+tunnelsdir = ${SYSCONFDIR}/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 ${LOCALSTATEDIR}/i2pd/certificates
++certsdir = ${LOCALSTATEDIR}/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 22 Oct 2026 05:25:43 -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 22 Oct 2026 05:25:43 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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 22 Oct 2026 05:25:44 -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,6 +73,85 @@ 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
@static-lib lib/libi2pdlang.a
@@ -112,15 +195,18 @@ i2pd/certificates/reseed/acetone_at_mail
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/damian.buho_at_proton.me.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/freedompeer_at_skhron.eu.crt
i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
+i2pd/certificates/reseed/idk_at_stormycloud.org.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/reseedserver_at_mail.i2p.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 22 Oct 2026 05:25:44 -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 ${LOCALSTATEDIR}/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 ${LOCALSTATEDIR}/i2pd.
Unless you installed or configured i2pd in a non-standard way, here's
how most users should proceed:
@@ -25,12 +25,12 @@ how most users should proceed:
- delete the _i2pd user and group:
userdel _i2pd
groupdel _i2pd
-- move /var/lib/i2pd to /var/i2pd:
- mv /var/lib/i2pd /var/i2pd
+- move /var/lib/i2pd to ${LOCALSTATEDIR}/i2pd:
+ mv /var/lib/i2pd ${LOCALSTATEDIR}/i2pd
- install the new package:
pkg_add i2pd
-- edit /etc/i2pd/i2pd.conf (you can take inspiration from
- /usr/local/share/examples/i2pd/i2pd.conf)
+- edit ${SYSCONFDIR}/i2pd/i2pd.conf (you can take inspiration from
+ ${TRUEPREFIX}/share/examples/i2pd/i2pd.conf)
- restart the i2pd process:
rcctl restart i2pd
@@ -60,7 +60,7 @@ If you intend to run a floodfill, you sh
# sysctl kern.maxfiles=16000
# echo "kern.maxfiles=16000" >> /etc/sysctl.conf
-and also edit /etc/login.conf.d/i2pd:
+and also edit ${SYSCONFDIR}/login.conf.d/i2pd:
i2pd:\
:openfiles-cur=8192:\
@@ -90,7 +90,7 @@ your peers and affect the overall operat
You can initiate a graceful shutdown without the HTTP interface by
sending a signal to the i2pd daemon like this:
- kill -INT $(cat /var/i2pd/i2pd.pid)
+ kill -INT $(cat ${LOCALSTATEDIR}/i2pd/i2pd.pid)
When it shuts down gracefully, the i2pd daemon waits for all transit
tunnels to expire, which usually takes 10 minutes.
@@ -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 22 Oct 2026 05:25:44 -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"