aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/servers/httpasio.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2025-10-13 21:29:32 +0200
committerGitHub Enterprise <[email protected]>2025-10-13 21:29:32 +0200
commit306cc66c3e6e7ea5c7ba8e39169b9c06a4cd9405 (patch)
tree8e50739abae8871d60166d11221ef931f6ad27bd /src/zenhttp/servers/httpasio.cpp
parentsplit storage config from base config (#570) (diff)
downloadzen-306cc66c3e6e7ea5c7ba8e39169b9c06a4cd9405.tar.xz
zen-306cc66c3e6e7ea5c7ba8e39169b9c06a4cd9405.zip
make asiohttp work without IPv6 (#562)
this change makes it possible to use zenserver on hosts where IPv6 has been disabled
Diffstat (limited to 'src/zenhttp/servers/httpasio.cpp')
-rw-r--r--src/zenhttp/servers/httpasio.cpp274
1 files changed, 225 insertions, 49 deletions
diff --git a/src/zenhttp/servers/httpasio.cpp b/src/zenhttp/servers/httpasio.cpp
index 492e867fb..e148072c8 100644
--- a/src/zenhttp/servers/httpasio.cpp
+++ b/src/zenhttp/servers/httpasio.cpp
@@ -8,6 +8,7 @@
#include <zencore/memory/llm.h>
#include <zencore/thread.h>
#include <zencore/trace.h>
+#include <zencore/windows.h>
#include <zenhttp/httpserver.h>
#include "httpparser.h"
@@ -21,6 +22,10 @@ ZEN_THIRD_PARTY_INCLUDES_START
#if ZEN_PLATFORM_WINDOWS
# include <conio.h>
# include <mstcpip.h>
+#else
+# include <stdlib.h>
+# include <stdio.h>
+# include <errno.h>
#endif
#include <asio.hpp>
ZEN_THIRD_PARTY_INCLUDES_END
@@ -33,6 +38,85 @@ ZEN_THIRD_PARTY_INCLUDES_END
# define ZEN_TRACE_VERBOSE(fmtstr, ...)
#endif
+#if ZEN_PLATFORM_LINUX
+static bool
+IsIPv6Available()
+{
+ int fd = ::socket(AF_INET6, SOCK_DGRAM, 0);
+ if (fd < 0)
+ {
+ // errors when IPv6 is unavailable/disabled: EAFNOSUPPORT, EPROTONOSUPPORT, ...
+ return false;
+ }
+ ::close(fd);
+ return true;
+}
+#elif ZEN_PLATFORM_WINDOWS
+static bool
+IsIPv6Available()
+{
+ SOCKET s = ::socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
+ if (s == INVALID_SOCKET)
+ {
+ int e = WSAGetLastError();
+ // WSAEAFNOSUPPORT means the IPv6 stack is unavailable
+ return e != WSAEAFNOSUPPORT;
+ }
+ closesocket(s);
+ return true;
+}
+#else
+static bool
+IsIPv6Available()
+{
+ return true;
+}
+#endif
+
+#if ZEN_PLATFORM_LINUX
+static bool
+IsIPv6AvailableSysctl(void)
+{
+ int val = 0;
+ const char* path = "/proc/sys/net/ipv6/conf/all/disable_ipv6";
+
+ if (FILE* f = fopen(path, "r"))
+ {
+ char buf[16];
+ if (fgets(buf, sizeof(buf), f))
+ {
+ fclose(f);
+ // 0 means IPv6 enabled, 1 means disabled
+ val = atoi(buf);
+ }
+ }
+
+ return val == 0;
+}
+
+bool
+IsIPv6Capable()
+{
+ static bool CachedCaps = IsIPv6Available() && IsIPv6AvailableSysctl();
+ return CachedCaps;
+}
+#else
+bool
+IsIPv6Capable()
+{
+ static bool CachedCaps = IsIPv6Available();
+
+ // On Windows it's possible to disable IPv6 via `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
+ // and setting `DisabledComponents` to 0xFF. This should act like Linux does with the setting above but I have not tested
+ // that.
+
+ // On MacOS you can disable IPv6 but allegedly loopback is always still available - this might be worth investigating at
+ // some point for completeness.
+
+ return CachedCaps;
+}
+#endif
+
namespace zen {
const FLLMTag&
@@ -639,7 +723,12 @@ struct HttpAcceptor
, m_Acceptor(m_IoService, asio::ip::tcp::v6())
, m_AlternateProtocolAcceptor(m_IoService, asio::ip::tcp::v4())
{
- m_Acceptor.set_option(asio::ip::v6_only(false));
+ const bool IsUsingIPv6 = IsIPv6Capable();
+ if (!IsUsingIPv6)
+ {
+ m_Acceptor = asio::ip::tcp::acceptor(m_IoService, asio::ip::tcp::v4());
+ }
+
#if ZEN_PLATFORM_WINDOWS
// Special option for Windows settings as !asio::socket_base::reuse_address is not the same as exclusive access on Windows platforms
typedef asio::detail::socket_option::boolean<ASIO_OS_DEF(SOL_SOCKET), SO_EXCLUSIVEADDRUSE> exclusive_address;
@@ -658,59 +747,22 @@ struct HttpAcceptor
m_AlternateProtocolAcceptor.set_option(asio::socket_base::receive_buffer_size(128 * 1024));
m_AlternateProtocolAcceptor.set_option(asio::socket_base::send_buffer_size(256 * 1024));
- asio::ip::address_v6 BindAddress = ForceLoopback ? asio::ip::address_v6::loopback() : asio::ip::address_v6::any();
- uint16_t EffectivePort = BasePort;
+ std::string BoundBaseUrl;
- if (BindAddress.is_loopback())
+ if (IsUsingIPv6)
{
- m_Acceptor.set_option(asio::ip::v6_only(true));
+ BoundBaseUrl = BindAcceptor<asio::ip::address_v6>(BasePort, ForceLoopback);
}
-
- asio::error_code BindErrorCode;
- m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
- if (BindErrorCode == asio::error::access_denied && !BindAddress.is_loopback())
- {
- // Access denied for a public port - lets try fall back to local port only
- BindAddress = asio::ip::address_v6::loopback();
- m_Acceptor.set_option(asio::ip::v6_only(true));
- m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
- }
- if (BindErrorCode == asio::error::address_in_use)
- {
- // Do a retry after a short sleep on same port just to be sure
- ZEN_INFO("Desired port {} is in use, retrying", BasePort);
- Sleep(100);
- m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
- }
- // Sharing violation implies the port is being used by another process
- for (uint16_t PortOffset = 1; (BindErrorCode == asio::error::address_in_use) && (PortOffset < 10); ++PortOffset)
- {
- EffectivePort = BasePort + (PortOffset * 100);
- m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
- }
- if (BindErrorCode == asio::error::access_denied)
- {
- EffectivePort = 0;
- m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
- }
- if (BindErrorCode)
+ else
{
- ZEN_ERROR("Unable open asio service, error '{}'", BindErrorCode.message());
+ ZEN_INFO("NOTE: ipv6 support is disabled, binding to ipv4 only");
+
+ BoundBaseUrl = BindAcceptor<asio::ip::address_v4>(BasePort, ForceLoopback);
}
- else
+
+ if (!IsValid())
{
- if (EffectivePort != BasePort)
- {
- ZEN_WARN("Desired port {} is in use, remapped to port {}", BasePort, EffectivePort);
- }
- if (BindAddress.is_loopback())
- {
- m_AlternateProtocolAcceptor.bind(asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), EffectivePort), BindErrorCode);
- m_UseAlternateProtocolAcceptor = true;
- ZEN_INFO("Registered local-only handler 'http://{}:{}/' - this is not accessible from remote hosts",
- "localhost",
- EffectivePort);
- }
+ return;
}
#if ZEN_PLATFORM_WINDOWS
@@ -730,6 +782,7 @@ struct HttpAcceptor
&OptionNumberOfBytesReturned,
0,
0);
+
if (m_UseAlternateProtocolAcceptor)
{
NativeSocket = m_AlternateProtocolAcceptor.native_handle();
@@ -750,7 +803,7 @@ struct HttpAcceptor
m_AlternateProtocolAcceptor.listen();
}
- ZEN_INFO("Started asio server at 'http://{}:{}'", BindAddress.is_loopback() ? "[::1]" : "*", EffectivePort);
+ ZEN_INFO("Started asio server at '{}", BoundBaseUrl);
}
~HttpAcceptor()
@@ -762,6 +815,120 @@ struct HttpAcceptor
}
}
+ template<typename AddressType>
+ std::string BindAcceptor(uint16_t BasePort, bool ForceLoopback)
+ {
+ uint16_t EffectivePort = BasePort;
+
+ AddressType BindAddress;
+
+ if (ForceLoopback)
+ {
+ BindAddress = AddressType::loopback();
+
+ if constexpr (std::is_same_v<asio::ip::address_v6, AddressType>)
+ {
+ m_Acceptor.set_option(asio::ip::v6_only(true));
+ }
+ }
+ else
+ {
+ BindAddress = AddressType::any();
+ }
+
+ asio::error_code BindErrorCode;
+ BindErrorCode = asio::error::access_denied;
+
+ m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
+
+ if (BindErrorCode == asio::error::access_denied && !BindAddress.is_loopback())
+ {
+ ZEN_INFO("Access denied for public port {}, falling back to loopback", BasePort);
+
+ BindAddress = AddressType::loopback();
+
+ if constexpr (std::is_same_v<asio::ip::address_v6, AddressType>)
+ {
+ m_Acceptor.set_option(asio::ip::v6_only(true));
+ }
+
+ m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
+ }
+
+ if (BindErrorCode == asio::error::address_in_use)
+ {
+ ZEN_INFO("Desired port {} is in use (error: '{}'), retrying", EffectivePort, BindErrorCode.message());
+ Sleep(100);
+ m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
+ }
+
+ // Try some alternative ports
+ for (uint16_t PortOffset = 1; BindErrorCode && (PortOffset < 10); ++PortOffset)
+ {
+ EffectivePort = BasePort + (PortOffset * 100);
+ m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
+ }
+
+ if (BindErrorCode)
+ {
+ ZEN_INFO("Unable to bind to preferred port range, falling back to automatic assignment (error: '{}')", BindErrorCode.message());
+
+ EffectivePort = 0;
+ m_Acceptor.bind(asio::ip::tcp::endpoint(BindAddress, EffectivePort), BindErrorCode);
+
+ if (!BindErrorCode)
+ {
+ EffectivePort = m_Acceptor.local_endpoint().port();
+ }
+ }
+
+ if (BindErrorCode)
+ {
+ ZEN_WARN("Unable to initialize asio service, (error: '{}')", BindErrorCode.message());
+
+ return 0;
+ }
+
+ if (EffectivePort != BasePort)
+ {
+ ZEN_WARN("Desired port {} is in use, remapped to port {}", BasePort, EffectivePort);
+ }
+
+ if constexpr (std::is_same_v<asio::ip::address_v6, AddressType>)
+ {
+ if (BindAddress.is_loopback())
+ {
+ // IPv6 loopback will only respond on the IPv6 loopback address. Not everyone does
+ // IPv6 though so we also bind to IPv4 loopback (localhost/127.0.0.1)
+
+ m_AlternateProtocolAcceptor.bind(asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), EffectivePort), BindErrorCode);
+
+ if (BindErrorCode)
+ {
+ ZEN_WARN("Failed to register secondary IPv4 local-only handler 'http://{}:{}/'", "localhost", EffectivePort);
+ }
+ else
+ {
+ m_UseAlternateProtocolAcceptor = true;
+ ZEN_INFO("Registered local-only handler 'http://{}:{}/' - this is not accessible from remote hosts",
+ "localhost",
+ EffectivePort);
+ }
+ }
+ }
+
+ m_IsValid = true;
+
+ if constexpr (std::is_same_v<asio::ip::address_v6, AddressType>)
+ {
+ return fmt::format("http://{}:{}'", BindAddress.is_loopback() ? "[::1]" : "*", EffectivePort);
+ }
+ else
+ {
+ return fmt::format("http://{}:{}'", BindAddress.is_loopback() ? "127.0.0.1" : "*", EffectivePort);
+ }
+ }
+
void Start()
{
ZEN_MEMSCOPE(GetHttpasioTag());
@@ -778,6 +945,8 @@ struct HttpAcceptor
int GetAcceptPort() { return m_Acceptor.local_endpoint().port(); }
+ bool IsValid() const { return m_IsValid; }
+
private:
void InitAcceptInternal(asio::ip::tcp::acceptor& Acceptor)
{
@@ -828,6 +997,7 @@ private:
asio::ip::tcp::acceptor m_Acceptor;
asio::ip::tcp::acceptor m_AlternateProtocolAcceptor;
bool m_UseAlternateProtocolAcceptor{false};
+ bool m_IsValid{false};
std::atomic<bool> m_IsStopped{false};
};
@@ -994,6 +1164,12 @@ HttpAsioServerImpl::Start(uint16_t Port, bool ForceLooopback, int ThreadCount)
ZEN_INFO("starting asio http with {} service threads", ThreadCount);
m_Acceptor.reset(new asio_http::HttpAcceptor(*this, m_IoService, Port, ForceLooopback));
+
+ if (!m_Acceptor->IsValid())
+ {
+ return 0;
+ }
+
m_Acceptor->Start();
// This should consist of a set of minimum threads and grow on demand to