Hello,
Please find attached a patch updating net/i2pd from 2.59.0 to 2.61.0,
including fixes for regressions introduced by the upstream pledge/unveil
integration.
The OpenBSD-specific changes include:
- Move sandbox initialization until the effective configuration paths
and PID file are resolved, preserving custom paths and user configurations.
- Restrict pledge/unveil permissions to those actually required,
checking errors instead of silently continuing.
- Restore SSU2 interface MTU detection using AF_LINK data from
getifaddrs(), avoiding SIOCGIFMTU, which is restricted by pledge.
- Handle SIGHUP synchronously through sigwait(), preventing unsafe
reload operations and thread self-join failures.
- Fix UDP cleanup timer ownership during tunnel reloads.
The port also updates its dependencies, PLIST, rc script and
documentation, including migration of the default data directory from
/var/lib/i2pd to /var/i2pd.
I tested the installed i2pd 2.61.0 on OpenBSD 8.0 with pledge and unveil
enabled.
Both unprivileged foreground execution and daemon execution as _i2pd
worked. Nine SIGHUP reloads completed without crashes, IPv4 MTU
detection reported 1500, and log rotation and process shutdown behaved
as expected.
The previously reproduced "Resource deadlock avoided" crash did not recur.
Testing did not cover actual SSU2 traffic, IPv6, or long-term stability.
I would appreciate review and additional testing, particularly of the
sandbox changes.
Thank you for your time and consideration.
Best regards,
David.
Index: net/i2pd/Makefile
===================================================================
RCS file: /cvs/ports/net/i2pd/Makefile,v
diff -u -p -u -p -r1.33 Makefile
--- net/i2pd/Makefile 21 Feb 2026 14:20:20 -0000 1.33
+++ net/i2pd/Makefile 16 Oct 2026 21:53:23 -0000
@@ -2,7 +2,8 @@ COMMENT = client for the I2P anonymous n
GH_ACCOUNT = PurpleI2P
GH_PROJECT = i2pd
-GH_TAGNAME = 2.59.0
+GH_TAGNAME = 2.61.0
+REVISION = 0
CATEGORIES = net
HOMEPAGE = https://i2pd.website
@@ -12,9 +13,10 @@ MAINTAINER = SystemFailure <openbsd@syst
# BSD
PERMIT_PACKAGE = Yes
+# uses pledge() and unveil()
WANTLIB += ${COMPILER_LIBCXX}
WANTLIB += boost_filesystem-mt boost_program_options-mt
-WANTLIB += boost_atomic-mt c crypto m miniupnpc ssl z
+WANTLIB += boost_atomic-mt boost_container-mt c crypto m miniupnpc ssl z
COMPILER = base-clang ports-gcc
MODULES = devel/cmake
@@ -31,7 +33,7 @@ WRKSRC = ${WRKDIST}/build
post-install:
${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} ${PREFIX}/share/examples/i2pd/certificates/${dir}
${INSTALL_DATA} ${WRKDIST}/contrib/certificates/${dir}/* \
Index: net/i2pd/distinfo
===================================================================
RCS file: /cvs/ports/net/i2pd/distinfo,v
diff -u -p -u -p -r1.26 distinfo
--- net/i2pd/distinfo 21 Feb 2026 14:20:20 -0000 1.26
+++ net/i2pd/distinfo 16 Oct 2026 21:53:23 -0000
@@ -1,2 +1,2 @@
-SHA256 (i2pd-2.59.0.tar.gz) = Dr6wXk82qzgJRJVhoJXcdnrYIaxqYclWI6tJvk/9OYs=
-SIZE (i2pd-2.59.0.tar.gz) = 743516
+SHA256 (i2pd-2.61.0.tar.gz) = QJzTwCV0kShmEatqr2kJQMckj7iYN3wT+ttlqDbioKs=
+SIZE (i2pd-2.61.0.tar.gz) = 779272
Index: net/i2pd/pkg/PLIST
===================================================================
RCS file: /cvs/ports/net/i2pd/pkg/PLIST,v
diff -u -p -u -p -r1.18 PLIST
--- net/i2pd/pkg/PLIST 21 Feb 2026 14:20:20 -0000 1.18
+++ net/i2pd/pkg/PLIST 16 Oct 2026 21:53:23 -0000
@@ -1,5 +1,5 @@
@newgroup _i2pd:838
-@newuser _i2pd:838:838::i2pd account:${LOCALSTATEDIR}/lib/i2pd:/sbin/nologin
+@newuser _i2pd:838:838::i2pd account:${LOCALSTATEDIR}/i2pd:/sbin/nologin
@rcscript ${RCDIR}/i2pd
@bin bin/i2pd
include/i2pd/
@@ -29,6 +29,7 @@ include/i2pd/I2NPProtocol.h
include/i2pd/I2PEndian.h
include/i2pd/I2PService.h
include/i2pd/I2PTunnel.h
+include/i2pd/IdentMetrics.h
include/i2pd/Identity.h
include/i2pd/KadDHT.h
include/i2pd/LeaseSet.h
@@ -54,6 +55,7 @@ include/i2pd/Socks5.h
include/i2pd/Streaming.h
include/i2pd/Tag.h
include/i2pd/Timestamp.h
+include/i2pd/Torrents.h
include/i2pd/TransitTunnel.h
include/i2pd/TransportSession.h
include/i2pd/Transports.h
@@ -69,14 +71,16 @@ include/i2pd/util.h
include/i2pd/version.h
@static-lib lib/libi2pd.a
@static-lib lib/libi2pdclient.a
+@mode 0750
@owner _i2pd
@group _i2pd
@sample ${SYSCONFDIR}/i2pd/
-@sample ${LOCALSTATEDIR}/lib/i2pd/
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/router/
+@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
@@ -87,139 +91,150 @@ share/examples/i2pd/certificates/family/
share/examples/i2pd/certificates/family/gostcoin.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/gostcoin.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/gostcoin.crt
@owner
@group
share/examples/i2pd/certificates/family/i2p-dev.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/i2p-dev.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/i2p-dev.crt
@owner
@group
share/examples/i2pd/certificates/family/i2pd-dev.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/i2pd-dev.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/i2pd-dev.crt
@owner
@group
share/examples/i2pd/certificates/family/mca2-i2p.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/mca2-i2p.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/mca2-i2p.crt
@owner
@group
share/examples/i2pd/certificates/family/stormycloud.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/stormycloud.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/family/stormycloud.crt
@owner
@group
share/examples/i2pd/certificates/family/volatile.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/family/volatile.crt
+@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}/lib/
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/acetone_at_mail.i2p.crt
@owner
@group
-share/examples/i2pd/certificates/reseed/acetone_at_mail.i2p.crt
+share/examples/i2pd/certificates/reseed/admin_at_likogan.dev.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/acetone_at_mail.i2p.crt
+@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}/lib/i2pd/certificates/reseed/admin_at_stormycloud.org.crt
+@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}/lib/i2pd/certificates/reseed/creativecowpat_at_mail.i2p.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/creativecowpat_at_mail.i2p.crt
@owner
@group
share/examples/i2pd/certificates/reseed/diyarciftci_at_protonmail.com.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/diyarciftci_at_protonmail.com.crt
+@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}/lib/i2pd/certificates/reseed/echelon3_at_mail.i2p.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/echelon3_at_mail.i2p.crt
@owner
@group
share/examples/i2pd/certificates/reseed/hankhill19580_at_gmail.com.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/hankhill19580_at_gmail.com.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/hankhill19580_at_gmail.com.crt
@owner
@group
share/examples/i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/i2p-reseed_at_mk16.de.crt
@owner
@group
share/examples/i2pd/certificates/reseed/igor_at_novg.net.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/igor_at_novg.net.crt
+@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}/lib/i2pd/certificates/reseed/lazygravy_at_mail.i2p.crt
+@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}/lib/i2pd/certificates/reseed/orignal_at_mail.i2p.crt
+@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}/lib/i2pd/certificates/reseed/r4sas-reseed_at_mail.i2p.crt
+@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}/lib/i2pd/certificates/reseed/rambler_at_mail.i2p.crt
+@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}/lib/i2pd/certificates/reseed/reseed_at_diva.exchange.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/reseed_at_diva.exchange.crt
@owner
@group
share/examples/i2pd/certificates/reseed/sahil_at_mail.i2p.crt
@owner _i2pd
@group _i2pd
-@sample ${LOCALSTATEDIR}/lib/i2pd/certificates/reseed/sahil_at_mail.i2p.crt
+@sample ${LOCALSTATEDIR}/i2pd/certificates/reseed/sahil_at_mail.i2p.crt
@owner
@group
-share/examples/i2pd/i2pd.conf
+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
@group
share/examples/i2pd/tunnels.conf
-@owner _i2pd
+@mode 0640
+@owner root
@group _i2pd
@sample ${SYSCONFDIR}/i2pd/tunnels.conf
+@mode
@owner
@group
share/examples/login.conf.d/i2pd
Index: net/i2pd/pkg/README
===================================================================
RCS file: /cvs/ports/net/i2pd/pkg/README,v
diff -u -p -u -p -r1.4 README
--- net/i2pd/pkg/README 16 Apr 2024 15:22:32 -0000 1.4
+++ net/i2pd/pkg/README 16 Oct 2026 21:53:23 -0000
@@ -2,10 +2,52 @@
| Running ${PKGSTEM} on OpenBSD
+-----------------------------------------------------------------------
+Important: upgrading from i2pd < 2.61.0
+=======================================
+
+A major change in this package starting with version 2.61.0 requires
+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
+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:
+
+- terminate gracefully the i2pd process (see below)
+- stop the service:
+ rcctl stop i2pd
+- remove the old package:
+ pkg_delete i2pd
+- delete the _i2pd user and group:
+ userdel _i2pd
+ groupdel _i2pd
+- move /var/lib/i2pd to /var/i2pd:
+ mv /var/lib/i2pd /var/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)
+- restart the i2pd process:
+ rcctl restart i2pd
+
+At this point, the /var/lib directory should be empty and safe to
+remove.
+
+If you edited /etc/newsyslog.conf to rotate i2pd log files
+automatically, you should also adjust the path in it (see below).
+
+However, if you're installing this package for the first time with a
+version equal or greather than 2.61.0, you have nothing special to do.
+
+
Resource Limits: File Descriptors
=================================
-${PKGSTEM} needs to open a lot of file descriptors.
+i2pd needs to open a lot of file descriptors.
For a regular node, you should raise the system-wide maxfiles limit to
8192:
@@ -14,7 +56,7 @@ For a regular node, you should raise the
# echo "kern.maxfiles=8192" >> /etc/sysctl.conf
If you intend to run a floodfill, you should raise this limit even more:
-
+
# sysctl kern.maxfiles=16000
# echo "kern.maxfiles=16000" >> /etc/sysctl.conf
@@ -24,3 +66,73 @@ and also edit /etc/login.conf.d/i2pd:
:openfiles-cur=8192:\
:openfiles-max=8192:\
:tc=daemon:
+
+
+The HTTP interface
+==================
+
+On OpenBSD, i2pd's HTTP interface is disabled by default, because it
+allows any user on the system to perform actions on the daemon, such
+as shutting it down, or access private data, such as the router
+identity and the tunnels' B32 addresses.
+
+If you want to use this interface anyway, you can reenable it in
+${SYSCONFDIR}/i2pd/i2pd.conf under the [http] section.
+
+
+Graceful shutdown
+=================
+
+It is good practice to shutdown the i2pd daemon gracefully, to avoid
+immediately severing all connections, which would disconnect all
+your peers and affect the overall operation of the I2P network.
+
+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)
+
+When it shuts down gracefully, the i2pd daemon waits for all transit
+tunnels to expire, which usually takes 10 minutes.
+
+
+Logging
+=======
+
+By default, this package sends its log messages to syslogd(8), which
+writes them to the /var/log/daemon file.
+
+The default log level of i2pd ("warn") can be very verbose. You may
+want to reduce this log verbosity by changing the "loglevel"
+parameter in ${SYSCONFDIR}/i2pd/i2pd.conf.
+
+If you want log messages to be written to another file, e.g.
+${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
+daemon after rotation.
+
+For example:
+
+ ${LOCALSTATEDIR}/i2pd/i2pd.log _i2pd:_i2pd 644 6 * $D13 Z ${LOCALSTATEDIR}/i2pd/i2pd.pid
+
+Sending SIGHUP is enough for log rotation, and also makes i2pd reload
+its tunnel configuration and rotate transient keys.
+
+
+Known bugs
+==========
+
+Version 2.61.0 of i2pd introduced two small regressions on OpenBSD:
+
+- i2pd is currently unable to autodetect the proper MTU for SSU2
+ packets and falls back to the default value of 1280. If you want to
+ set another value, you can do so by tuning the ssu2.mtu4 and
+ ssu2.mtu6 parameters.
+
+- i2pd runs fine when launched as the default _i2pd user and with the
+ default file paths (ie. when run with the default rc script).
+ However, it doesn't run as another user or with non-standard file
+ paths.
Index: net/i2pd/pkg/i2pd.rc
===================================================================
RCS file: /cvs/ports/net/i2pd/pkg/i2pd.rc,v
diff -u -p -u -p -r1.4 i2pd.rc
--- net/i2pd/pkg/i2pd.rc 11 Mar 2022 19:46:04 -0000 1.4
+++ net/i2pd/pkg/i2pd.rc 16 Oct 2026 21:53:23 -0000
@@ -2,8 +2,13 @@
daemon="${TRUEPREFIX}/bin/i2pd --daemon"
daemon_user="_i2pd"
-daemon_flags="--service --datadir=${LOCALSTATEDIR}/lib/i2pd --conf=${SYSCONFDIR}/i2pd/i2pd.conf --tunconf=${SYSCONFDIR}/i2pd/tunnels.conf --tunnelsdir=${SYSCONFDIR}/i2pd/tunnels.d"
+daemon_flags="--service \
+ --datadir=${LOCALSTATEDIR}/i2pd \
+ --conf=${SYSCONFDIR}/i2pd/i2pd.conf \
+ --tunconf=${SYSCONFDIR}/i2pd/tunnels.conf \
+ --tunnelsdir=${SYSCONFDIR}/i2pd/tunnels.d \
+ --certsdir=${LOCALSTATEDIR}/i2pd/certificates"
. /etc/rc.d/rc.subr
-rc_cmd $1
+rc_cmd "$1"
Index: net/i2pd/patches/patch-contrib_i2pd_conf
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-contrib_i2pd_conf Fri Oct 16 23:36:29 2026
@@ -0,0 +1,44 @@
+Use /etc/i2pd for tunnel configuration and certificates, log through
+syslog, and leave the HTTP console disabled in the packaged configuration.
+
+Index: contrib/i2pd.conf
+--- contrib/i2pd.conf.orig
++++ contrib/i2pd.conf
+@@ -8,16 +8,16 @@
+
+ ## Tunnels config file
+ ## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf
+-# tunconf = /var/lib/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
+-# tunnelsdir = /var/lib/i2pd/tunnels.d
++tunnelsdir = /etc/i2pd/tunnels.d
+
+ ## Path to certificates used for verifying .su3, families
+ ## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates
+-# certsdir = /var/lib/i2pd/certificates
++certsdir = /etc/i2pd/certificates
+
+ ## Where to write pidfile (default: /run/i2pd.pid, not used in Windows)
+ # pidfile = /run/i2pd.pid
+@@ -30,7 +30,7 @@
+ ## * stdout - print log entries to stdout
+ ## * file - log entries to a file
+ ## * syslog - use syslog, see man 3 syslog
+-# log = file
++log = syslog
+ ## Path to logfile (default: autodetect)
+ # logfile = /var/log/i2pd/i2pd.log
+ ## Log messages above this level (debug, info, *warn, error, critical, none)
+@@ -122,7 +122,7 @@ ipv6 = false
+ [http]
+ ## Web Console settings
+ ## Enable the Web Console (default: true)
+-# enabled = true
++enabled = false
+ ## Address and port service will listen on (default: 127.0.0.1:7070)
+ # address = 127.0.0.1
+ # port = 7070
Index: net/i2pd/patches/patch-daemon_Daemon_cpp
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-daemon_Daemon_cpp Fri Oct 16 23:36:29 2026
@@ -0,0 +1,187 @@
+Resolve effective OpenBSD paths before filesystem initialization and sandboxing.
+Keep only startup promises until Unix daemonization and unveil are complete.
+
+Index: daemon/Daemon.cpp
+--- daemon/Daemon.cpp.orig
++++ daemon/Daemon.cpp
+@@ -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,81 +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, "Use default pledge values");
+- // TODO: remove that not need
+- 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 init_unevil = []() {
+- unveil("/usr/lib", "r");
+- unveil("/usr/local/lib", "r");
+- unveil("/usr/libexec/ld.so", "r");
+- unveil("/dev/urandom", "r");
+- unveil("/tmp", "rw");
+- unveil("/etc/i2pd", "r"); // ваще не нужно вроде на весь прям каталог
+-
+- #define UNVEIL_DIR(dir) unveil(dir.c_str(), "rwc")
+-
+- std::string unevil_file; i2p::config::GetOption("openbsd.unevil_file",unevil_file);
+- UNVEIL_DIR(unevil_file);
+- std::string tunnelsdir, certsdir, logfile, datadir, reseed_file, openbsd_pledge_file;
+- i2p::config::GetOption("tunnelsdir", tunnelsdir);
+- UNVEIL_DIR(tunnelsdir);
+- i2p::config::GetOption("certsdir", certsdir);
+- UNVEIL_DIR(certsdir);
+- i2p::config::GetOption("datadir", datadir);
+- UNVEIL_DIR(datadir);
+- i2p::config::GetOption("reseed.file", reseed_file);
+- unveil(reseed_file.c_str(), "r");
+- i2p::config::GetOption("openbsd.pledge_file", openbsd_pledge_file);
+- unveil(openbsd_pledge_file.c_str(), "r");
+- std::string tunconf ;i2p::config::GetOption("tunconf", tunconf); unveil(tunconf.c_str(), "r");
+- std::string conf ;i2p::config::GetOption("tunconf", conf); unveil(conf.c_str(), "r");
+- std::string pidfile ;i2p::config::GetOption("pidfile", pidfile); unveil(pidfile.c_str(), "rwc");
+- i2p::config::GetOption("logfile", logfile); unveil(logfile.c_str(), "rwc");
+- if(unevil_file != "")
+- {
+- std::ifstream f(unevil_file);
+- if (!f) {
+- std::cerr << "Can't open unevil file" << std::endl;
+- exit(1);
+- }
+- std::string line;
+- while(std::getline(f, line)){
+- UNVEIL_DIR(line);
+- }
+- }
+- #undef UNVEIL_DIR
+- unveil(NULL, NULL);
+- };
+- bool openbsd_unevil_enabled; i2p::config::GetOption("openbsd.unevil_enabled", openbsd_unevil_enabled);
+- bool openbsd_pledge_enabled; i2p::config::GetOption("openbsd.pledge_enabled", openbsd_pledge_enabled);
+- if(openbsd_unevil_enabled) init_unevil();
+- if(openbsd_pledge_enabled) init_pledge();
++ // 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",
++ "reseed.file", "reseed.zipfile", "addressbook.hostsfile",
++ "openbsd.pledge_file", "openbsd.unevil_file"})
++ {
++ std::string path;
++ i2p::config::GetOption (option, path);
++ if (!path.empty ()) i2p::config::SetOption (option, AbsolutePath (path));
++ }
+ #endif
+
+ i2p::config::GetOption("daemon", isDaemon);
+@@ -216,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
+@@ -236,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 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
Index: net/i2pd/patches/patch-daemon_UnixDaemon_cpp
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-daemon_UnixDaemon_cpp Fri Oct 16 23:36:29 2026
@@ -0,0 +1,277 @@
+Apply unveil to effective paths after daemonization and PID handling, then
+remove startup pledge promises before starting services. Preserve policy options
+and report every sandbox failure, including before the logger thread starts.
+Consume control signals on the daemon thread, so SIGHUP cannot reload and join
+a destination from its own service thread or perform unsafe work in a handler.
+
+Index: daemon/UnixDaemon.cpp
+--- daemon/UnixDaemon.cpp.orig
++++ daemon/UnixDaemon.cpp
+@@ -18,6 +18,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"
+@@ -27,6 +39,18 @@
+ #include "Transports.h"
+ #include "util.h"
+
++#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)
+ {
+ switch (sig)
+@@ -148,7 +172,10 @@ namespace i2p
+
+ // Pidfile
+ // this code is c-styled and a bit ugly, but we need fd for locking pidfile
+- std::string pidfile; i2p::config::GetOption("pidfile", pidfile);
++#ifndef __OpenBSD__
++ std::string pidfile;
++#endif
++ i2p::config::GetOption("pidfile", pidfile);
+ if (pidfile == "") {
+ pidfile = i2p::fs::DataDirPath("i2pd.pid");
+ }
+@@ -191,8 +218,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;
+@@ -210,13 +258,149 @@ 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);
++ std::string promises = "stdio rpath wpath cpath 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 ()
+@@ -225,6 +409,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
+ if (gracefulShutdownInterval)
+ {
+ gracefulShutdownInterval--; // - 1 second
Index: net/i2pd/patches/patch-libi2pd_FS_cpp
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-libi2pd_FS_cpp Fri Oct 16 23:36:29 2026
@@ -0,0 +1,14 @@
+Use the OpenBSD port data directory for --service without an explicit datadir.
+
+Index: libi2pd/FS.cpp
+--- libi2pd/FS.cpp.orig
++++ libi2pd/FS.cpp
+@@ -113,6 +113,8 @@ namespace fs {
+ dataDir = fs_lib::wpath(commonAppData).string() + "\\" + appName;
+ #endif
+ }
++#elif defined(__OpenBSD__)
++ dataDir = "/var/" + appName;
+ #else
+ dataDir = "/var/lib/" + appName;
+ #endif
Index: net/i2pd/patches/patch-libi2pd_RouterContext_cpp
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-libi2pd_RouterContext_cpp Fri Oct 16 23:36:29 2026
@@ -0,0 +1,20 @@
+Keep an explicitly configured IPv6 SSU2 MTU when the advertised address changes.
+Restored automatic discovery must not overwrite the manual setting on OpenBSD.
+
+Index: libi2pd/RouterContext.cpp
+--- libi2pd/RouterContext.cpp.orig
++++ libi2pd/RouterContext.cpp
+@@ -585,7 +585,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_client_ClientContext_cpp
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-libi2pd_client_ClientContext_cpp Fri Oct 16 23:36:29 2026
@@ -0,0 +1,48 @@
+Rebind the UDP cleanup timer when SIGHUP replaces the shared destination,
+before the old io_context can be destroyed. Serialize timer replacement and
+shutdown with CleanupUDP(), which uses the forwards mutex. OpenBSD only.
+
+Index: libi2pd_client/ClientContext.cpp
+--- libi2pd_client/ClientContext.cpp.orig
++++ libi2pd_client/ClientContext.cpp
+@@ -190,10 +190,15 @@ namespace client
+ }
+
+ LogPrint(eLogInfo, "Clients: Stopping UDP Tunnels timers");
+- if (m_CleanupUDPTimer)
+ {
+- m_CleanupUDPTimer->cancel ();
+- m_CleanupUDPTimer = nullptr;
++#ifdef __OpenBSD__
++ std::lock_guard<std::mutex> lock(m_ForwardsMutex);
++#endif
++ if (m_CleanupUDPTimer)
++ {
++ m_CleanupUDPTimer->cancel ();
++ m_CleanupUDPTimer = nullptr;
++ }
+ }
+
+ {
+@@ -220,6 +225,21 @@ namespace client
+ m_SharedLocalDestination->Release ();
+ CreateNewSharedLocalDestination ();
+
++#ifdef __OpenBSD__
++ // The old destination (still in m_Destinations) owns the timer's
++ // io_context. Rebind before it is stopped and removed below.
++ // CleanupUDP() uses the same mutex when touching/rearming the timer.
++ {
++ std::lock_guard<std::mutex> lock(m_ForwardsMutex);
++ if (m_CleanupUDPTimer)
++ {
++ m_CleanupUDPTimer->cancel ();
++ m_CleanupUDPTimer = std::make_unique<boost::asio::steady_timer>(m_SharedLocalDestination->GetService ());
++ ScheduleCleanupUDP ();
++ }
++ }
++#endif
++
+ // recreate HTTP proxy
+ if (m_HttpProxy)
+ {
Index: net/i2pd/patches/patch-libi2pd_util_cpp
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-libi2pd_util_cpp Fri Oct 16 23:36:29 2026
@@ -0,0 +1,44 @@
+Read the matching AF_LINK interface MTU already returned by getifaddrs on
+OpenBSD. Its NET_RT_IFLIST sysctl is permitted by pledge("inet"); neither
+SIOCGIFMTU nor a newly required route promise is necessary.
+
+Index: libi2pd/util.cpp
+--- libi2pd/util.cpp.orig
++++ libi2pd/util.cpp
+@@ -24,6 +24,10 @@
+ #include <pthread_np.h>
+ #endif
+
++#ifdef __OpenBSD__
++#include <net/if.h>
++#endif
++
+ #if defined(__APPLE__)
+ # include <AvailabilityMacros.h>
+ #endif
+@@ -556,6 +560,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)
+ {
+@@ -569,6 +584,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
===================================================================
--- /dev/null Fri Oct 16 23:53:24 2026
+++ net/i2pd/patches/patch-test_Makefile Fri Oct 16 23:36:29 2026
@@ -0,0 +1,14 @@
+Do not link the test programs against a separate Boost.System library;
+use the header-only implementation from the packaged Boost version.
+
+Index: tests/Makefile
+--- tests/Makefile.orig
++++ tests/Makefile
+@@ -18,7 +18,6 @@ endif
+ endif
+
+ LDLIBS = \
+- -lboost_system$(BOOST_SUFFIX) \
+ -lboost_program_options$(BOOST_SUFFIX) \
+ -lssl \
+ -lcrypto \