#endif /* LUAJIT_VERSION */
#include "dnsdist-rings.hh"
#include "dnsdist-secpoll.hh"
+#include "dnsdist-web.hh"
#include "base64.hh"
#include "dnswriter.hh"
g_carbon.setState(ours);
});
- g_lua.writeFunction("webserver", [client,configCheck](const std::string& address, const std::string& password, const boost::optional<std::string> apiKey, const boost::optional<std::map<std::string, std::string> > customHeaders) {
+ g_lua.writeFunction("webserver", [client,configCheck](const std::string& address, const std::string& password, const boost::optional<std::string> apiKey, const boost::optional<std::map<std::string, std::string> > customHeaders, const boost::optional<std::string> acl) {
setLuaSideEffect();
ComboAddress local;
try {
SSetsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
SBind(sock, local);
SListen(sock, 5);
- auto launch=[sock, local, password, apiKey, customHeaders]() {
+ auto launch=[sock, local, password, apiKey, customHeaders, acl]() {
setWebserverPassword(password);
setWebserverAPIKey(apiKey);
setWebserverCustomHeaders(customHeaders);
+ if (acl) {
+ setWebserverACL(*acl);
+ }
thread t(dnsdistWebserverThread, sock, local);
t.detach();
};
setWebserverAPIKey(apiKey);
}
+ if (vars->count("acl")) {
+ const std::string acl = boost::get<std::string>(vars->at("acl"));
+
+ setWebserverACL(acl);
+ }
if(vars->count("customHeaders")) {
const boost::optional<std::map<std::string, std::string> > headers = boost::get<std::map<std::string, std::string> >(vars->at("customHeaders"));
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
-#include "dnsdist.hh"
-#include "dnsdist-healthchecks.hh"
-#include "dnsdist-prometheus.hh"
-#include "sstuff.hh"
-#include "ext/json11/json11.hpp"
-#include "ext/incbin/incbin.h"
-#include "dolog.hh"
-#include <thread>
-#include "threadname.hh"
+#include <boost/format.hpp>
#include <sstream>
-#include <yahttp/yahttp.hpp>
-#include "namespaces.hh"
#include <sys/time.h>
#include <sys/resource.h>
+#include <thread>
+
#include "ext/incbin/incbin.h"
-#include "htmlfiles.h"
+#include "ext/json11/json11.hpp"
+#include <yahttp/yahttp.hpp>
+
#include "base64.hh"
+#include "dnsdist.hh"
+#include "dnsdist-healthchecks.hh"
+#include "dnsdist-prometheus.hh"
+#include "dnsdist-web.hh"
+#include "dolog.hh"
#include "gettime.hh"
-#include <boost/format.hpp>
+#include "htmlfiles.h"
+#include "threadname.hh"
+#include "sstuff.hh"
bool g_apiReadWrite{false};
WebserverConfig g_webserverConfig;
return false;
}
+static bool isClientAllowedByACL(const ComboAddress& remote)
+{
+ std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
+ return g_webserverConfig.acl.match(remote);
+}
+
static void handleCORS(const YaHTTP::Request& req, YaHTTP::Response& resp)
{
const auto origin = req.headers.find("Origin");
g_webserverConfig.password = password;
}
+void setWebserverACL(const std::string& acl)
+{
+ NetmaskGroup newACL;
+ newACL.toMasks(acl);
+
+ {
+ std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
+ g_webserverConfig.acl = std::move(newACL);
+ }
+}
+
void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders)
{
std::lock_guard<std::mutex> lock(g_webserverConfig.lock);
{
setThreadName("dnsdist/webserv");
warnlog("Webserver launched on %s", local.toStringWithPort());
+
for(;;) {
try {
ComboAddress remote(local);
int fd = SAccept(sock, remote);
- vinfolog("Got connection from %s", remote.toStringWithPort());
+ if (!isClientAllowedByACL(remote)) {
+ vinfolog("Connection to webserver from client %s is not allowed, closing", remote.toStringWithPort());
+ close(fd);
+ continue;
+ }
+ vinfolog("Got a connection to the webserver from %s", remote.toStringWithPort());
std::thread t(connectionThread, fd, remote);
t.detach();
}
- catch(std::exception& e) {
+ catch (const std::exception& e) {
errlog("Had an error accepting new webserver connection: %s", e.what());
}
}
vector<std::function<void(void)>> setupLua(bool client, const std::string& config);
-struct WebserverConfig
-{
- std::string password;
- std::string apiKey;
- boost::optional<std::map<std::string, std::string> > customHeaders;
- std::mutex lock;
-};
-
-void setWebserverAPIKey(const boost::optional<std::string> apiKey);
-void setWebserverPassword(const std::string& password);
-void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders);
-
-void dnsdistWebserverThread(int sock, const ComboAddress& local);
void tcpAcceptorThread(void* p);
#ifdef HAVE_DNS_OVER_HTTPS
void dohThread(ClientState* cs);
dnsdist-snmp.cc dnsdist-snmp.hh \
dnsdist-systemd.cc dnsdist-systemd.hh \
dnsdist-tcp.cc \
- dnsdist-web.cc \
+ dnsdist-web.cc dnsdist-web.hh \
dnsdist-xpf.cc dnsdist-xpf.hh \
dnslabeltext.cc \
dnsname.cc dnsname.hh \
--- /dev/null
+#pragma once
+
+struct WebserverConfig
+{
+ WebserverConfig()
+ {
+ acl.toMasks("127.0.0.1, ::1");
+ }
+
+ NetmaskGroup acl;
+ std::string password;
+ std::string apiKey;
+ boost::optional<std::map<std::string, std::string> > customHeaders;
+ std::mutex lock;
+};
+
+void setWebserverAPIKey(const boost::optional<std::string> apiKey);
+void setWebserverPassword(const std::string& password);
+void setWebserverACL(const std::string& acl);
+void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::string> > customHeaders);
+
+void dnsdistWebserverThread(int sock, const ComboAddress& local);
Now point your browser at http://127.0.0.1:8083 and log in with any username, and that password. Enjoy!
+Since 1.5.0, only connections from 127.0.0.1 and ::1 are allowed by default. To allow connections from 192.0.2.1, instead:
+
+.. code-block:: lua
+
+ webserver("127.0.0.1:8083", "supersecretpassword", "supersecretAPIkey", {}, "192.0.2.1")
+
+
Security of the Webserver
-------------------------
.. code-block:: lua
- webserver("127.0.0.1:8080", "supersecret", "apikey", {["X-Frame-Options"]= "", ["X-Custom"]="custom"}
+ webserver("127.0.0.1:8080", "supersecret", "apikey", {["X-Frame-Options"]= "", ["X-Custom"]="custom"})
Credentials can be changed over time using the :func:`setWebserverConfig` function.
Webserver configuration
~~~~~~~~~~~~~~~~~~~~~~~
-.. function:: webserver(listen_address, password[, apikey[, custom_headers]])
+.. function:: webserver(listen_address, password[, apikey[, custom_headers[, acl]]])
+
+ .. versionchanged:: 1.5.0
+ ``acl`` optional parameter added.
Launch the :doc:`../guides/webserver` with statistics and the API.
:param str password: The password required to access the webserver
:param str apikey: The key required to access the API
:param {[str]=str,...} custom_headers: Allows setting custom headers and removing the defaults
+ :param str acl: List of IP addresses, as a string, that are allowed to open a connection to the web server. Defaults to "127.0.0.1, ::1".
.. function:: setAPIWritable(allow [,dir])
.. versionadded:: 1.3.3
+ .. versionchanged:: 1.5.0
+ ``acl`` optional parameter added.
+
Setup webserver configuration. See :func:`webserver`.
:param table options: A table with key: value pairs with webserver options.
* ``password=newPassword``: string - Changes the API password
* ``apiKey=newKey``: string - Changes the API Key (set to an empty string do disable it)
* ``custom_headers={[str]=str,...}``: map of string - Allows setting custom headers and removing the defaults.
-
+ * ``acl=newACL``: string - List of IP addresses, as a string, that are allowed to open a connection to the web server. Defaults to "127.0.0.1, ::1".
+
Access Control Lists
~~~~~~~~~~~~~~~~~~~~
r = requests.get(url, headers=headers, timeout=self._webTimeout)
self.assertEquals(r.status_code, 401)
+
+class TestAPIACL(DNSDistTest):
+
+ _webTimeout = 2.0
+ _webServerPort = 8083
+ _webServerBasicAuthPassword = 'secret'
+ _webServerAPIKey = 'apisecret'
+ _consoleKey = DNSDistTest.generateConsoleKey()
+ _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+ _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey']
+ _config_template = """
+ setKey("%s")
+ controlSocket("127.0.0.1:%s")
+ setACL({"127.0.0.1/32", "::1/128"})
+ newServer{address="127.0.0.1:%s"}
+ webserver("127.0.0.1:%s", "%s", "%s", {}, "192.0.2.1")
+ """
+
+ def testACLChange(self):
+ """
+ API: Should be denied by ACL then allowed
+ """
+
+ url = 'http://127.0.0.1:' + str(self._webServerPort) + "/"
+ try:
+ r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
+ self.assertTrue(False)
+ except requests.exceptions.ConnectionError as exp:
+ pass
+
+ # reset the ACL
+ self.sendConsoleCommand('setWebserverConfig({acl="127.0.0.1"})')
+
+ r = requests.get(url, auth=('whatever', self._webServerBasicAuthPassword), timeout=self._webTimeout)
+ self.assertTrue(r)
+ self.assertEquals(r.status_code, 200)