From e68b43133238fd74c8a20043f488ebbbe97247a8 Mon Sep 17 00:00:00 2001 From: Georg Pfuetzenreuter Date: Mon, 10 Mar 2025 02:10:59 +0100 Subject: [PATCH] Auth webserver Unix socket support This introduces support for binding to a Unix instead of a TCP/IP socket, which is useful in applications where binding to a TCP/IP socket is not desired due to security and/or performance considerations or constraints of the surrounding system. Closes #8677. Signed-off-by: Georg Pfuetzenreuter --- docs/http-api/index.rst | 6 +- docs/settings.rst | 4 +- pdns/auth-main.cc | 2 +- pdns/iputils.hh | 129 ++++++++++++++++++++++++++++++++++++++++ pdns/logger.cc | 6 ++ pdns/logger.hh | 1 + pdns/sstuff.hh | 3 +- pdns/webserver.cc | 25 +++++++- pdns/webserver.hh | 2 +- 9 files changed, 168 insertions(+), 10 deletions(-) diff --git a/docs/http-api/index.rst b/docs/http-api/index.rst index 100a48a7ce..2051947955 100644 --- a/docs/http-api/index.rst +++ b/docs/http-api/index.rst @@ -15,10 +15,10 @@ The webserver does not allow remote management of the daemon, but allows control The following webserver related configuration items are available: * :ref:`setting-webserver`: If set to anything but 'no', a webserver is launched. -* :ref:`setting-webserver-address`: Address to bind the webserver to. Defaults to 127.0.0.1, which implies that only the local computer is able to connect to the nameserver! To allow remote hosts to connect, change to 0.0.0.0 or the physical IP address of your nameserver. +* :ref:`setting-webserver-address`: IP address or UNIX domain socket path to bind the webserver to. Defaults to 127.0.0.1, which implies that only the local computer is able to connect to the nameserver! To allow remote hosts to connect, change to 0.0.0.0 or the physical IP address of your nameserver. * :ref:`setting-webserver-password`: If set, viewers will have to enter this password in order to gain access to the statistics, in addition to entering the configured API key on the index page. -* :ref:`setting-webserver-port`: Port to bind the webserver to. -* :ref:`setting-webserver-allow-from`: Netmasks that are allowed to connect to the webserver +* :ref:`setting-webserver-port`: Port to bind the webserver to (not relevant if :ref:`setting-webserver-address` is set to a UNIX domain socket). +* :ref:`setting-webserver-allow-from`: Netmasks that are allowed to connect to the webserver (not relevant if :ref:`setting-webserver-address` is set to a UNIX domain socket). * :ref:`setting-webserver-max-bodysize`: Maximum request/response body size in megabytes * :ref:`setting-webserver-connection-timeout`: Request/response timeout in seconds diff --git a/docs/settings.rst b/docs/settings.rst index 25b5dc8d7f..f4c729a7cd 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -2035,7 +2035,7 @@ Start a webserver for monitoring. See :doc:`performance`". - IP Address - Default: 127.0.0.1 -IP Address for webserver/API to listen on. +IP Address or path to UNIX domain socket for webserver/API to listen on. .. _setting-webserver-allow-from: @@ -2046,6 +2046,7 @@ IP Address for webserver/API to listen on. - Default: 127.0.0.1,::1 Webserver/API access is only allowed from these subnets. +Ignored if ``webserver-address`` is set to a UNIX domain socket. .. _setting-webserver-hash-plaintext-credentials: @@ -2141,6 +2142,7 @@ Password required to access the webserver. Since 4.6.0 the password can be hashe - Default: 8081 The port where webserver/API will listen on. +Ignored if ``webserver-address`` is set to a UNIX domain socket. .. _setting-webserver-print-arguments: diff --git a/pdns/auth-main.cc b/pdns/auth-main.cc index d1eb4a4d4f..2f276179e8 100644 --- a/pdns/auth-main.cc +++ b/pdns/auth-main.cc @@ -239,7 +239,7 @@ static void declareArguments() ::arg().setSwitch("webserver", "Start a webserver for monitoring (api=yes also enables the HTTP listener)") = "no"; ::arg().setSwitch("webserver-print-arguments", "If the webserver should print arguments") = "no"; - ::arg().set("webserver-address", "IP Address of webserver/API to listen on") = "127.0.0.1"; + ::arg().set("webserver-address", "IP Address or path to UNIX domain socket for webserver/API to listen on") = "127.0.0.1"; ::arg().set("webserver-port", "Port of webserver/API to listen on") = "8081"; ::arg().set("webserver-password", "Password required for accessing the webserver") = ""; ::arg().set("webserver-allow-from", "Webserver/API access is only allowed from these subnets") = "127.0.0.1,::1"; diff --git a/pdns/iputils.hh b/pdns/iputils.hh index 48e963ae1a..b237d47809 100644 --- a/pdns/iputils.hh +++ b/pdns/iputils.hh @@ -31,6 +31,7 @@ #include "misc.hh" #include #include +#include #include "namespaces.hh" @@ -489,6 +490,134 @@ union ComboAddress }; }; +union SockaddrWrapper +{ + sockaddr_in sin4{}; + sockaddr_in6 sin6; + sockaddr_un sinun; + + [[nodiscard]] socklen_t getSocklen() const + { + if (sin4.sin_family == AF_INET) { + return sizeof(sin4); + } + if (sin6.sin6_family == AF_INET6) { + return sizeof(sin6); + } + if (sinun.sun_family == AF_UNIX) { + return sizeof(sinun); + } + return 0; + } + + SockaddrWrapper() + { + sin4.sin_family = AF_INET; + sin4.sin_addr.s_addr = 0; + sin4.sin_port = 0; + } + + SockaddrWrapper(const struct sockaddr* socketAddress, socklen_t salen) + { + setSockaddr(socketAddress, salen); + }; + + SockaddrWrapper(const struct sockaddr_in6* socketAddress) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + setSockaddr(reinterpret_cast(socketAddress), sizeof(struct sockaddr_in6)); + }; + + SockaddrWrapper(const struct sockaddr_in* socketAddress) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + setSockaddr(reinterpret_cast(socketAddress), sizeof(struct sockaddr_in)); + }; + + SockaddrWrapper(const struct sockaddr_un* socketAddress) + { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + setSockaddr(reinterpret_cast(socketAddress), sizeof(struct sockaddr_un)); + }; + + void setSockaddr(const struct sockaddr* socketAddress, socklen_t salen) + { + if (salen > sizeof(struct sockaddr_un)) { + throw PDNSException("ComboAddress can't handle other than sockaddr_in, sockaddr_in6 or sockaddr_un"); + } + memcpy(this, socketAddress, salen); + } + + explicit SockaddrWrapper(const string& str, uint16_t port = 0) + { + memset(&sinun, 0, sizeof(sinun)); + sin4.sin_family = AF_INET; + sin4.sin_port = 0; + if (str == "\"\"" || str == "''") { + throw PDNSException("Stray quotation marks in address."); + } + if (makeIPv4sockaddr(str, &sin4) != 0) { + sin6.sin6_family = AF_INET6; + if (makeIPv6sockaddr(str, &sin6) < 0) { + sinun.sun_family = AF_UNIX; + // only attempt Unix socket address if address candidate does not contain a port + if (str.find(':') != string::npos || makeUNsockaddr(str, &sinun) < 0) { + throw PDNSException("Unable to convert presentation address '" + str + "'"); + } + } + } + if (sinun.sun_family != AF_UNIX && sin4.sin_port == 0) { // 'str' overrides port! + sin4.sin_port = htons(port); + } + } + + [[nodiscard]] bool isIPv6() const + { + return sin4.sin_family == AF_INET6; + } + [[nodiscard]] bool isIPv4() const + { + return sin4.sin_family == AF_INET; + } + [[nodiscard]] bool isUnixSocket() const + { + return sin4.sin_family == AF_UNIX; + } + + [[nodiscard]] string toString() const + { + if (sinun.sun_family == AF_UNIX) { + return sinun.sun_path; + } + std::array host{}; + if (sin4.sin_family != 0) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + int retval = getnameinfo(reinterpret_cast(this), getSocklen(), host.data(), host.size(), nullptr, 0, NI_NUMERICHOST); + if (retval == 0) { + return host.data(); + } + return "invalid " + string(gai_strerror(retval)); + } + return "invalid"; + } + + [[nodiscard]] string toStringWithPort() const + { + if (sinun.sun_family == AF_UNIX) { + return toString(); + } + if (sin4.sin_family == AF_INET) { + return toString() + ":" + std::to_string(ntohs(sin4.sin_port)); + } + return "[" + toString() + "]:" + std::to_string(ntohs(sin4.sin_port)); + } + + void reset() + { + memset(&sinun, 0, sizeof(sinun)); + } +}; + /** This exception is thrown by the Netmask class and by extension by the NetmaskGroup class */ class NetmaskException : public PDNSException { diff --git a/pdns/logger.cc b/pdns/logger.cc index 52a5f08f72..d01bf6a647 100644 --- a/pdns/logger.cc +++ b/pdns/logger.cc @@ -217,6 +217,12 @@ Logger& Logger::operator<<(const ComboAddress& ca) return *this; } +Logger& Logger::operator<<(const SockaddrWrapper& sockaddr) +{ + *this << sockaddr.toString(); + return *this; +} + void addTraceTS(const timeval& start, ostringstream& str) { const auto& content = str.str(); diff --git a/pdns/logger.hh b/pdns/logger.hh index d1581809c4..7944038db5 100644 --- a/pdns/logger.hh +++ b/pdns/logger.hh @@ -109,6 +109,7 @@ public: Logger& operator<<(const string& s); //!< log a string Logger& operator<<(const DNSName&); Logger& operator<<(const ComboAddress&); //!< log an address + Logger& operator<<(const SockaddrWrapper&); //!< log an address Logger& operator<<(Urgency); //!< set the urgency, << style Logger& operator<<(const QType& qtype) diff --git a/pdns/sstuff.hh b/pdns/sstuff.hh index e673c320dd..50a81622db 100644 --- a/pdns/sstuff.hh +++ b/pdns/sstuff.hh @@ -162,7 +162,8 @@ public: } //! Bind the socket to a specified endpoint - void bind(const ComboAddress& local, bool reuseaddr = true) const + template + void bind(const T& local, bool reuseaddr = true) const { int tmp = 1; // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) diff --git a/pdns/webserver.cc b/pdns/webserver.cc index ae3deb010f..c65bce3ef0 100644 --- a/pdns/webserver.cc +++ b/pdns/webserver.cc @@ -38,6 +38,10 @@ #include #include #include +#include +#include + +namespace filesystem = std::filesystem; json11::Json HttpRequest::json() { @@ -626,10 +630,25 @@ WebServer::WebServer(string listenaddress, int port) : void WebServer::bind() { + if (filesystem::is_socket(d_listenaddress.c_str())) { + int err=unlink(d_listenaddress.c_str()); + if(err < 0 && errno!=ENOENT) { + SLOG(g_log<error(Logr::Error, e.what(), "Listening on HTTP socket failed, unable to remove existing socket", "exception", d_listenaddress)); + d_server = nullptr; + return; + } + } + try { d_server = createServer(); - SLOG(g_log<d_local.toStringWithPort()<info(Logr::Info, "Listening for HTTP requests", "address", Logging::Loggable(d_server->d_local))); + if (d_server->d_local.isUnixSocket()) { + SLOG(g_log<info(Logr::Info, "Listening for HTTP requests", "path", Logging::Loggable(d_listenaddress))); + } else { + SLOG(g_log<d_local.toStringWithPort()<info(Logr::Info, "Listening for HTTP requests", "address", Logging::Loggable(d_server->d_local))); + } } catch(NetworkError &e) { SLOG(g_log<acl(d_acl)) { + if (d_server->d_local.isUnixSocket() || client->acl(d_acl)) { std::thread webHandler(WebServerConnectionThreadStart, this, client); webHandler.detach(); } else { diff --git a/pdns/webserver.hh b/pdns/webserver.hh index 767f986d3e..12d8aec126 100644 --- a/pdns/webserver.hh +++ b/pdns/webserver.hh @@ -179,7 +179,7 @@ public: } virtual ~Server() = default; - ComboAddress d_local; + SockaddrWrapper d_local; std::shared_ptr accept() { return std::shared_ptr(d_server_socket.accept()); -- 2.47.2