diff options
| author | Stefan Boberg <[email protected]> | 2025-10-13 21:29:32 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2025-10-13 21:29:32 +0200 |
| commit | 306cc66c3e6e7ea5c7ba8e39169b9c06a4cd9405 (patch) | |
| tree | 8e50739abae8871d60166d11221ef931f6ad27bd /src/zenhttp | |
| parent | split storage config from base config (#570) (diff) | |
| download | zen-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')
| -rw-r--r-- | src/zenhttp/servers/httpasio.cpp | 274 |
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 |