]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Proof of concept of DNS over HTTP/2 client
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 9 Jul 2021 14:52:52 +0000 (16:52 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 13 Sep 2021 13:28:27 +0000 (15:28 +0200)
pdns/dnsdist.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-nghttp2.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-nghttp2.hh [new file with mode: 0644]
pdns/dnsdistdist/doh.cc

index 7fc5aaf8f82599c5665d8a54f3b3b224bc4d62d1..de23e7b93307f5f08f7dcd74ad1db9d8d83c10d9 100644 (file)
@@ -2171,6 +2171,8 @@ static void sighandler(int sig)
 }
 #endif
 
+#include "dnsdist-nghttp2.hh"
+
 int main(int argc, char** argv)
 {
   try {
@@ -2638,6 +2640,8 @@ int main(int argc, char** argv)
       secpollthread.detach();
     }
 
+    sendHTTP2Query();
+
     if(g_cmdLine.beSupervised) {
 #ifdef HAVE_SYSTEMD
       sd_notify(0, "READY=1");
index bc18aaf62ca0450537ca0c9e0089d556a46323e7..5928ca9e8b312329c03eee2b56681513e1ca60f5 100644 (file)
@@ -158,6 +158,7 @@ dnsdist_SOURCES = \
        dnsdist-lua-vars.cc \
        dnsdist-lua-web.cc \
        dnsdist-lua.cc dnsdist-lua.hh \
+       dnsdist-nghttp2.cc dnsdist-nghttp2.hh \
        dnsdist-prometheus.hh \
        dnsdist-protobuf.cc dnsdist-protobuf.hh \
        dnsdist-protocols.cc dnsdist-protocols.hh \
@@ -301,6 +302,7 @@ testrunner_SOURCES = \
 dnsdist_LDFLAGS = \
        $(AM_LDFLAGS) \
        $(PROGRAM_LDFLAGS) \
+       -lnghttp2 \
        -pthread 
 
 dnsdist_LDADD = \
diff --git a/pdns/dnsdistdist/dnsdist-nghttp2.cc b/pdns/dnsdistdist/dnsdist-nghttp2.cc
new file mode 100644 (file)
index 0000000..9e95676
--- /dev/null
@@ -0,0 +1,282 @@
+
+#include <nghttp2/nghttp2.h>
+
+#include "iputils.hh"
+#include "libssl.hh"
+#include "noinitvector.hh"
+#include "tcpiohandler.hh"
+#include "sstuff.hh"
+
+#warning remove me
+#include "dnswriter.hh"
+
+struct MyUserData
+{
+  std::unique_ptr<nghttp2_session, void(*)(nghttp2_session*)> session{nullptr, nghttp2_session_del};
+  std::unique_ptr<TCPIOHandler> handler;
+  PacketBuffer out;
+  PacketBuffer in;
+  size_t outPos{0};
+  size_t inPos{0};
+};
+
+static ssize_t send_callback(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data) {
+  cerr<<"in "<<__PRETTY_FUNCTION__<<endl;
+  cerr<<"asked to send "<<length<<" bytes"<<endl;
+  MyUserData* userData = reinterpret_cast<MyUserData*>(user_data);
+  userData->out.insert(userData->out.end(), data, data + length);
+  userData->handler->write(userData->out.data() + userData->outPos, userData->out.size() - userData->outPos, timeval{2, 0});
+  userData->out.clear();
+  return length;
+}
+
+static int on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) {
+  cerr<<"in "<<__PRETTY_FUNCTION__<<endl;
+  MyUserData* userData = reinterpret_cast<MyUserData*>(user_data);
+  switch (frame->hd.type) {
+  case NGHTTP2_HEADERS:
+    if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
+      cerr<<"All headers received"<<endl;
+    }
+    break;
+  case NGHTTP2_WINDOW_UPDATE:
+    cerr<<"got window update"<<endl;
+    break;
+  case NGHTTP2_SETTINGS:
+    cerr<<"got settings"<<endl;
+    cerr<<frame->settings.niv<<endl;
+    for (size_t idx = 0; idx < frame->settings.niv; idx++) {
+      cerr<<"- "<<frame->settings.iv[idx].settings_id<<" "<<frame->settings.iv[idx].value<<endl;
+    }
+    break;
+  }
+
+  return 0;
+}
+
+static int on_data_chunk_recv_callback(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) {
+  cerr<<"in "<<__PRETTY_FUNCTION__<<endl;
+  MyUserData* userData = reinterpret_cast<MyUserData*>(user_data);
+  cerr<<"Got data of size "<<len<<endl;
+  cerr<<std::string(reinterpret_cast<const char*>(data), len)<<endl;
+  return 0;
+}
+
+static int on_stream_close_callback(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data) {
+  cerr<<"in "<<__PRETTY_FUNCTION__<<endl;
+  MyUserData* userData = reinterpret_cast<MyUserData*>(user_data);
+
+  cerr<<"Stream "<<stream_id<<" closed with error_code="<<error_code<<endl;
+  auto rv = nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR);
+  if (rv != 0) {
+    return NGHTTP2_ERR_CALLBACK_FAILURE;
+  }
+
+  return 0;
+}
+
+static int on_header_callback(nghttp2_session* session, const nghttp2_frame* frame, const uint8_t* name, size_t namelen, const uint8_t* value, size_t valuelen, uint8_t flags, void* user_data) {
+  cerr<<"in "<<__PRETTY_FUNCTION__<<endl;
+  MyUserData* userData = reinterpret_cast<MyUserData*>(user_data);
+
+  switch (frame->hd.type) {
+  case NGHTTP2_HEADERS:
+    if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
+      /* Print response headers for the initiated request. */
+      cerr<<"got header for "<<frame->hd.stream_id<<":"<<endl;
+      cerr<<"- "<<std::string(reinterpret_cast<const char*>(name), namelen)<<endl;
+      cerr<<"- "<<std::string(reinterpret_cast<const char*>(value), valuelen)<<endl;
+      break;
+    }
+  }
+  return 0;
+}
+
+static int on_begin_headers_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) {
+  cerr<<"in "<<__PRETTY_FUNCTION__<<endl;
+  MyUserData* userData = reinterpret_cast<MyUserData*>(user_data);
+
+  switch (frame->hd.type) {
+  case NGHTTP2_HEADERS:
+    if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
+      cerr<<"Response headers for stream ID="<<frame->hd.stream_id<<endl;
+    }
+    break;
+  }
+  return 0;
+}
+
+static void doReadData(MyUserData& userData)
+{
+  do {
+    size_t pos = 0;
+    userData.in.resize(512);
+    cerr<<"trying to read "<<userData.in.size()<<endl;
+    try {
+      pos = userData.handler->read(userData.in.data(), userData.in.size(), timeval{2, 0}, timeval{2, 0}, true);
+      // userData.handler->tryRead(userData.in, pos, userData.in.size());
+      cerr<<"got "<<pos<<endl;
+      userData.in.resize(pos);
+      if (pos > 0) {
+        auto readlen = nghttp2_session_mem_recv(userData.session.get(), userData.in.data(), pos);
+        cerr<<"nghttp2_session_mem_recv returned "<<readlen<<endl;
+        if (readlen < 0) {
+          cerr<<"Fatal error: "<<nghttp2_strerror((int)readlen)<<endl;
+          return;
+        }
+        int rv = nghttp2_session_send(userData.session.get());
+        cerr<<"nghttp2_session_send returned "<<rv<<endl;
+      }
+      else {
+        break;
+      }
+    }
+    catch (const std::exception& e) {
+      cerr<<"got exception "<<e.what()<<endl;
+      break;
+    }
+  }
+  while (true);
+}
+
+#define MAKE_NV(NAME, VALUE, VALUELEN)                                         \
+  {                                                                            \
+    (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, VALUELEN,             \
+        NGHTTP2_NV_FLAG_NONE                                                   \
+  }
+
+#define MAKE_NV2(NAME, VALUE)                                                  \
+  {                                                                            \
+    (uint8_t *)NAME, (uint8_t *)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1,    \
+        NGHTTP2_NV_FLAG_NONE                                                   \
+  }
+void sendHTTP2Query()
+{
+  auto remote = ComboAddress("9.9.9.11:443");
+  std::string host("dns11.quad9.net");
+  std::string path("/dns-query");
+  struct TLSContextParameters tlsParams;
+  tlsParams.d_provider = "openssl";
+  std::shared_ptr<TLSCtx> tlsCtx = getTLSContext(tlsParams);
+
+  Socket sock(remote.sin4.sin_family, SOCK_STREAM);
+  // FIXME
+  auto fd = sock.getHandle();
+  setTCPNoDelay(fd);
+  MyUserData userData;
+  userData.handler = std::make_unique<TCPIOHandler>(host, sock.releaseHandle(), timeval{2, 0}, tlsCtx, time(nullptr));
+  userData.handler->connect(true, remote, timeval{2, 0});
+
+  /* check ALPN:
+SSL_get0_next_proto_negotiated(ssl, &alpn, &alpnlen);
+#if OPENSSL_VERSION_NUMBER >= 0x10002000L
+    if (alpn == NULL) {
+      SSL_get0_alpn_selected(ssl, &alpn, &alpnlen);
+    }
+#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
+
+    if (alpn == NULL || alpnlen != 2 || memcmp("h2", alpn, 2) != 0) {
+      fprintf(stderr, "h2 is not negotiated\n");
+      delete_http2_session_data(session_data);
+      return;
+    }
+  */
+
+  nghttp2_session_callbacks* cbs = nullptr;
+  if (nghttp2_session_callbacks_new(&cbs) != 0) {
+    cerr<<"unable to create a callback object for a new HTTP/2 session"<<endl;
+    return;
+  }
+  std::unique_ptr<nghttp2_session_callbacks, void(*)(nghttp2_session_callbacks*)> callbacks(cbs, nghttp2_session_callbacks_del);
+  cbs = nullptr;
+
+  nghttp2_session_callbacks_set_send_callback(callbacks.get(), send_callback);
+  nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks.get(), on_frame_recv_callback);
+  nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks.get(), on_data_chunk_recv_callback);
+  nghttp2_session_callbacks_set_on_stream_close_callback(callbacks.get(), on_stream_close_callback);
+  nghttp2_session_callbacks_set_on_header_callback(callbacks.get(), on_header_callback);
+  nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks.get(), on_begin_headers_callback);
+
+  nghttp2_session* sess = nullptr;
+  if (nghttp2_session_client_new(&sess, callbacks.get(), &userData) != 0) {
+    cerr<<"Coult not allocate a new HTTP/2 session"<<endl;
+    return;
+  }
+
+  userData.session = std::unique_ptr<nghttp2_session, void(*)(nghttp2_session*)>(sess, nghttp2_session_del);
+  sess = nullptr;
+
+  callbacks.reset();
+
+#warning we should make the 100 configurable here, as we might want a lower number before receiving the one actually supported by the server
+#warning we should also make the window size configurable, but 16M is a nice default
+  nghttp2_settings_entry iv[] = {
+    {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100},
+    {NGHTTP2_SETTINGS_ENABLE_PUSH, 0},
+    {NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, 16*1024*1024}
+  };
+   /* client 24 bytes magic string will be sent by nghttp2 library */
+  int rv = nghttp2_submit_settings(userData.session.get(), NGHTTP2_FLAG_NONE, iv, sizeof(iv)/sizeof(*iv));
+  if (rv != 0) {
+    cerr<<"Could not submit SETTINGS: "<<nghttp2_strerror(rv)<<endl;
+    return;
+  }
+
+  GenericDNSPacketWriter<PacketBuffer> pw(userData.in, DNSName("doh.dnsdist.org."), QType::A, QClass::IN, 0);
+  pw.getHeader()->rd = 1;
+  pw.commit();
+
+  /* we could use nghttp2_nv_flag.NGHTTP2_NV_FLAG_NO_COPY_NAME and nghttp2_nv_flag.NGHTTP2_NV_FLAG_NO_COPY_VALUE
+     to avoid a copy and lowercasing as long as we take care of making sure that the data will outlive the request
+     and that it is already lowercased. */
+  auto payloadSize = std::to_string(userData.in.size());
+  const nghttp2_nv hdrs[] = {
+      MAKE_NV2(":method", "POST"),
+      MAKE_NV2(":scheme", "https"),
+      MAKE_NV(":authority", host.c_str(), host.size()),
+      MAKE_NV(":path", path.c_str(), path.size()),
+      MAKE_NV2("accept", "application/dns-message"),
+      MAKE_NV2("content-type", "application/dns-message"),
+      MAKE_NV("content-length", payloadSize.c_str(), payloadSize.size()),
+      MAKE_NV2("user-agent", "nghttp2-" NGHTTP2_VERSION "/dnsdist")
+  };
+
+  /* f data_prd is not NULL, it provides data which will be sent in subsequent DATA frames. In this case, a method that allows request message bodies (https://tools.ietf.org/html/rfc7231#section-4) must be specified with :method key in nva (e.g. POST). This function does not take ownership of the data_prd. The function copies the members of the data_prd. If data_prd is NULL, HEADERS have END_STREAM set
+   */
+  cerr<<"Remote size window is "<<nghttp2_session_get_remote_window_size(userData.session.get())<<endl;
+
+  nghttp2_data_provider data_provider;
+  data_provider.source.ptr = &userData;
+  data_provider.read_callback = [](nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) -> ssize_t
+  {
+    cerr<<"in data provider"<<endl;
+    auto userData = reinterpret_cast<MyUserData*>(user_data);
+    if (userData->inPos >= userData->in.size()) {
+       *data_flags |= NGHTTP2_DATA_FLAG_EOF;
+       cerr<<"EOF"<<endl;
+       return 0;
+    }
+    size_t remaining = userData->in.size()- userData->inPos;
+    size_t toCopy = length > remaining ? remaining : length;
+    memcpy(buf, &userData->in.at(userData->inPos), toCopy);
+    userData->inPos += toCopy;
+    cerr<<toCopy<<" written"<<endl;
+    return toCopy;
+  };
+
+  auto stream_id = nghttp2_submit_request(userData.session.get(), nullptr, hdrs, sizeof(hdrs)/sizeof(*hdrs), &data_provider, &userData);
+  if (stream_id < 0) {
+    cerr<<"Could not submit HTTP request: "<<nghttp2_strerror(stream_id)<<endl;
+    return;
+  }
+  rv = nghttp2_session_send(userData.session.get());
+
+  setNonBlocking(fd);
+
+  doReadData(userData);
+  cerr<<"After reading data, remote size window is "<<nghttp2_session_get_remote_window_size(userData.session.get())<<endl;
+  cerr<<"Max number of streams from remote is "<<nghttp2_session_get_remote_settings(userData.session.get(), NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS)<<endl;
+  cerr<<"our own is "<<nghttp2_session_get_local_settings(userData.session.get(), NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS)<<endl;
+  // min(nghttp2_session_get_stream_remote_window_size(), nghttp2_session_get_remote_window_size())
+#warning for later: how do we know how many streams are left? the window size?
+}
diff --git a/pdns/dnsdistdist/dnsdist-nghttp2.hh b/pdns/dnsdistdist/dnsdist-nghttp2.hh
new file mode 100644 (file)
index 0000000..49ce323
--- /dev/null
@@ -0,0 +1,3 @@
+#pragma once
+
+void sendHTTP2Query();
index 97294cf40661e0e920d137d5314400db6861cfba..d8f8e6d8c2f5cb7c14ee3438b975f5eedd430029 100644 (file)
@@ -201,6 +201,7 @@ struct DOHServerConfig
 
     h2o_config_init(&h2o_config);
     h2o_config.http2.idle_timeout = idleTimeout * 1000;
+    // perhaps we should make h2o_config.http2.max_concurrent_requests_per_connection configurable (default is 100)
   }
   DOHServerConfig(const DOHServerConfig&) = delete;
   DOHServerConfig& operator=(const DOHServerConfig&) = delete;