]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Merge pull request #9229 from rgacogne/dnsdist-webserver-allow-from
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 17 Jun 2020 09:09:20 +0000 (11:09 +0200)
committerGitHub <noreply@github.com>
Wed, 17 Jun 2020 09:09:20 +0000 (11:09 +0200)
dnsdist: Implement an ACL in the internal web server

1  2 
pdns/dnsdist-lua.cc
pdns/dnsdist-web.cc
pdns/dnsdist.hh
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_API.py

diff --combined pdns/dnsdist-lua.cc
index 4077d9b75bee3f220ab193aea855bcb15556a752,8b3c2412ad012a34313a5f1ca38d507e7bdb0499..39d2e595ad94d6206e3447098856f7e5ef3ef8a7
@@@ -41,6 -41,7 +41,7 @@@
  #endif /* LUAJIT_VERSION */
  #include "dnsdist-rings.hh"
  #include "dnsdist-secpoll.hh"
+ #include "dnsdist-web.hh"
  
  #include "base64.hh"
  #include "dnswriter.hh"
@@@ -796,7 -797,7 +797,7 @@@ static void setupLuaConfig(bool client
        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"));
  
          frontend->d_trustForwardedForHeader = boost::get<bool>((*vars)["trustForwardedForHeader"]);
        }
  
 +      if (vars->count("internalPipeBufferSize")) {
 +        frontend->d_internalPipeBufferSize = boost::get<int>((*vars)["internalPipeBufferSize"]);
 +      }
 +
        parseTLSConfig(frontend->d_tlsConfig, "addDOHLocal", vars);
      }
      g_dohlocals.push_back(frontend);
diff --combined pdns/dnsdist-web.cc
index 4766403924aa92b3274eab188bc4cbcf1fac2c4e,1ec5cf3058e94428ffd698c89700b9c2e9e909dd..5a738ca0ac5af60a616b91e14943e25deca45f8e
   * 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;
@@@ -88,8 -89,6 +89,8 @@@ const std::map<std::string, MetricDefin
    { "dyn-blocked",            MetricDefinition(PrometheusMetricType::counter, "Number of queries dropped because of a dynamic block")},
    { "dyn-block-nmg-size",     MetricDefinition(PrometheusMetricType::gauge,   "Number of dynamic blocks entries") },
    { "security-status",        MetricDefinition(PrometheusMetricType::gauge,   "Security status of this software. 0=unknown, 1=OK, 2=upgrade recommended, 3=upgrade mandatory") },
 +  { "doh-query-pipe-full",    MetricDefinition(PrometheusMetricType::counter, "Number of DoH queries dropped because the internal pipe used to distribute queries was full") },
 +  { "doh-response-pipe-full", MetricDefinition(PrometheusMetricType::counter, "Number of DoH responses dropped because the internal pipe used to distribute responses was full") },
    { "udp-in-errors",          MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp InErrors") },
    { "udp-noport-errors",      MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp NoPorts") },
    { "udp-recvbuf-errors",     MetricDefinition(PrometheusMetricType::counter, "From /proc/net/snmp RcvbufErrors") },
@@@ -223,6 -222,12 +224,12 @@@ static bool isMethodAllowed(const YaHTT
    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");
@@@ -1228,6 -1233,17 +1235,17 @@@ void setWebserverPassword(const std::st
    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);
@@@ -1239,15 -1255,21 +1257,21 @@@ void dnsdistWebserverThread(int sock, c
  {
    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());
      }
    }
diff --combined pdns/dnsdist.hh
index 1b532df341e329ee1a9744269ecaf7bb0e665d6b,8454cd695444313f48080cf58070191021b7a94e..1ffb3aeb7454981277997cba7aae49cc897a24f6
@@@ -263,8 -263,6 +263,8 @@@ struct DNSDistStat
    stat_t cacheMisses{0};
    stat_t latency0_1{0}, latency1_10{0}, latency10_50{0}, latency50_100{0}, latency100_1000{0}, latencySlow{0}, latencySum{0};
    stat_t securityStatus{0};
 +  stat_t dohQueryPipeFull{0};
 +  stat_t dohResponsePipeFull{0};
  
    double latencyAvg100{0}, latencyAvg1000{0}, latencyAvg10000{0}, latencyAvg1000000{0};
    typedef std::function<uint64_t(const std::string&)> statfunction_t;
      {"dyn-blocked", &dynBlocked},
      {"dyn-block-nmg-size", [](const std::string&) { return g_dynblockNMG.getLocal()->size(); }},
      {"security-status", &securityStatus},
 +    {"doh-query-pipe-full", &dohQueryPipeFull},
 +    {"doh-response-pipe-full", &dohResponsePipeFull},
      // Latency histogram
      {"latency-sum", &latencySum},
      {"latency-count", getLatencyCount},
@@@ -1098,19 -1094,6 +1098,6 @@@ struct dnsheader
  
  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);
index 9f6c8a73e94e1441eb11c1bc56bee3c300845ea8,d6c3f7c6e6f19a14cfaa9e3c555fa875c5e8c04d..82fa5d9a30eac5b535c6dafe4bf6862941dc8776
@@@ -113,7 -113,7 +113,7 @@@ Listen Socket
    .. versionadded:: 1.4.0
  
    .. versionchanged:: 1.5.0
 -    ``sendCacheControlHeaders``, ``sessionTimeout``, ``trustForwardedForHeader`` options added.
 +    ``internalPipeBufferSize``, ``sendCacheControlHeaders``, ``sessionTimeout``, ``trustForwardedForHeader`` options added.
      ``url`` now defaults to ``/dns-query`` instead of ``/``. Added ``tcpListenQueueSize`` parameter.
  
    Listen on the specified address and TCP port for incoming DNS over HTTPS connections, presenting the specified X.509 certificate.
    * ``sendCacheControlHeaders``: bool - Whether to parse the response to find the lowest TTL and set a HTTP Cache-Control header accordingly. Default is true.
    * ``trustForwardedForHeader``: bool - Whether to parse any existing X-Forwarded-For header in the HTTP query and use the right-most value as the client source address and port, for ACL checks, rules, logging and so on. Default is false.
    * ``tcpListenQueueSize=SOMAXCONN``: int - Set the size of the listen queue. Default is ``SOMAXCONN``.
 +  * ``internalPipeBufferSize=0``: int - Set the size in bytes of the internal buffer of the pipes used internally to pass queries and responses between threads. Requires support for ``F_SETPIPE_SZ`` which is present in Linux since 2.6.35. The actual size might be rounded up to a multiple of a page size. 0 means that the OS default size is used.
  
  .. function:: addTLSLocal(address, certFile(s), keyFile(s) [, options])
  
@@@ -297,7 -296,10 +297,10 @@@ Control Socket, Console and Webserve
  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
  ~~~~~~~~~~~~~~~~~~~~
  
index bfabe5c61552e145306cdd9ac2ad609a6a37b2bf,d73a1f8c1677d95afe93efa6660509b591bffaa6..b894323cb4cd59d71272ae91beb0278a455aceb9
@@@ -236,8 -236,7 +236,8 @@@ class TestAPIBasics(DNSDistTest)
                      'noncompliant-responses', 'rdqueries', 'empty-queries', 'cache-hits',
                      'cache-misses', 'cpu-iowait', 'cpu-steal', 'cpu-sys-msec', 'cpu-user-msec', 'fd-usage', 'dyn-blocked',
                      'dyn-block-nmg-size', 'rule-servfail', 'security-status',
 -                    'udp-in-errors', 'udp-noport-errors', 'udp-recvbuf-errors', 'udp-sndbuf-errors']
 +                    'udp-in-errors', 'udp-noport-errors', 'udp-recvbuf-errors', 'udp-sndbuf-errors',
 +                    'doh-query-pipe-full', 'doh-response-pipe-full']
  
          for key in expected:
              self.assertIn(key, values)
@@@ -517,3 -516,39 +517,39 @@@ class TestAPIAuth(DNSDistTest)
  
          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)