]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add support for outgoing X-Forwarded-* headers
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 26 Aug 2021 13:34:49 +0000 (15:34 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 13 Sep 2021 13:28:28 +0000 (15:28 +0200)
pdns/dnsdist-lua.cc
pdns/dnsdist-tcp.cc
pdns/dnsdist.hh
pdns/dnsdistdist/dnsdist-nghttp2.cc
pdns/dnsdistdist/doh.cc
pdns/doh.hh

index 5ea08281c68c6b994e66155bd31aa4b54613930a..3042c8b59732da923ec04ab3d4dcdf89e127aae5 100644 (file)
@@ -536,6 +536,10 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
           if (ret->d_tlsCtx) {
             setupDoHClientProtocolNegotiation(ret->d_tlsCtx);
           }
+
+          if (vars.count("addXForwardedHeaders")) {
+            ret->d_addXForwardedHeaders = boost::get<bool>(vars.at("addXForwardedHeaders"));
+          }
 #else /* HAVE_NGHTTP2 */
           throw std::runtime_error("Outgoing DNS over HTTPS support requested (via 'dohPath' on newServer()) but nghttp2 support is not available");
 #endif /* HAVE_NGHTTP2 */
index 2752ea8bf21fb07f54f34881fda7000b3ac9b8e0..1b478530c95405bb865076e0110029eedaf71cff 100644 (file)
@@ -616,6 +616,7 @@ static void handleQuery(std::shared_ptr<IncomingTCPConnectionState>& state, cons
   IDState ids;
   setIDStateFromDNSQuestion(ids, dq, std::move(qname));
   ids.origID = dh->id;
+  ids.cs = state->d_ci.cs;
 
   ++state->d_currentQueriesCount;
 
index 067a6e77dfc65c93311a925ef608cb1bfcd6ce29..aca96105309dadbf10da6d2ebd90039a3db7f214 100644 (file)
@@ -602,9 +602,14 @@ struct ClientState
     return udpFD == -1;
   }
 
+  bool isDoH() const
+  {
+    return dohFrontend != nullptr;
+  }
+
   bool hasTLS() const
   {
-    return tlsFrontend != nullptr || dohFrontend != nullptr;
+    return tlsFrontend != nullptr || (dohFrontend != nullptr && dohFrontend->isHTTPS());
   }
 
   std::string getType() const
@@ -612,7 +617,12 @@ struct ClientState
     std::string result = udpFD != -1 ? "UDP" : "TCP";
 
     if (dohFrontend) {
-      result += " (DNS over HTTPS)";
+      if (dohFrontend->isHTTPS()) {
+        result += " (DNS over HTTPS)";
+      }
+      else {
+        result += " (DNS over HTTP)";
+      }
     }
     else if (tlsFrontend) {
       result += " (DNS over TLS)";
@@ -743,6 +753,7 @@ struct DownstreamState
   bool reconnectOnUp{false};
   bool d_tcpCheck{false};
   bool d_tcpOnly{false};
+  bool d_addXForwardedHeaders{false}; // for DoH backends
 
   bool isUp() const
   {
index 1f79a6269e3a9026b1859b1bab268e7941e9aa7b..899391b187968da4243134a99a49a6e8a27e0190 100644 (file)
@@ -70,6 +70,9 @@ private:
   static void handleWritableIOCallback(int fd, FDMultiplexer::funcparam_t& param);
   static void handleIO(std::shared_ptr<DoHConnectionToBackend>& conn, const struct timeval& now);
 
+  static void addStaticHeader(std::vector<nghttp2_nv>& headers, const std::string& nameKey, const std::string& valueKey);
+  static void addDynamicHeader(std::vector<nghttp2_nv>& headers, const std::string& nameKey, const std::string& value);
+
   class PendingRequest
   {
   public:
@@ -188,29 +191,96 @@ bool DoHConnectionToBackend::canBeReused() const
 const std::unordered_map<std::string, std::string> DoHConnectionToBackend::s_constants = {
   {"method-name", ":method"},
   {"method-value", "POST"},
+  {"scheme-name", ":scheme"},
+  {"scheme-value", "https"},
+  {"accept-name", "accept"},
+  {"accept-value", "application/dns-message"},
+  {"content-type-name", "content-type"},
+  {"content-type-value", "application/dns-message"},
+  {"user-agent-name", "user-agent"},
+  {"user-agent-value", "nghttp2-" NGHTTP2_VERSION "/dnsdist"},
+  {"authority-name", ":authority"},
+  {"path-name", ":path"},
+  {"content-length-name", "content-length"},
+  {"x-forwarded-for-name", "x-forwarded-for"},
+  {"x-forwarded-port-name", "x-forwarded-port"},
+  {"x-forwarded-proto-name", "x-forwarded-proto"},
+  {"x-forwarded-proto-value-dns-over-udp", "dns-over-udp"},
+  {"x-forwarded-proto-value-dns-over-tcp", "dns-over-tcp"},
+  {"x-forwarded-proto-value-dns-over-tls", "dns-over-tls"},
+  {"x-forwarded-proto-value-dns-over-http", "dns-over-http"},
+  {"x-forwarded-proto-value-dns-over-https", "dns-over-https"},
 };
 
+void DoHConnectionToBackend::addStaticHeader(std::vector<nghttp2_nv>& headers, const std::string& nameKey, const std::string& valueKey)
+{
+  const auto& name = s_constants.at(nameKey);
+  const auto& value = s_constants.at(valueKey);
+
+  headers.push_back({const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(name.c_str())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(value.c_str())), name.size(), value.size(), NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE});
+}
+
+void DoHConnectionToBackend::addDynamicHeader(std::vector<nghttp2_nv>& headers, const std::string& nameKey, const std::string& value)
+{
+  const auto& name = s_constants.at(nameKey);
+
+  headers.push_back({const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(name.c_str())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(value.c_str())), name.size(), value.size(), NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE});
+}
+
 void DoHConnectionToBackend::queueQuery(std::shared_ptr<TCPQuerySender>& sender, TCPQuery&& query)
 {
-  /* 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(query.d_buffer.size());
   d_currentQuery = std::move(query);
   d_queryPos = 0;
-  const nghttp2_nv hdrs[] = {
-    {const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(s_constants.at("method-name").c_str())), const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(s_constants.at("method-value").c_str())), s_constants.at("method-name").size(), s_constants.at("method-value").size(), NGHTTP2_NV_FLAG_NO_COPY_NAME | NGHTTP2_NV_FLAG_NO_COPY_VALUE},
-    MAKE_NV2(":scheme", "https"),
-    MAKE_NV(":authority", d_ds->d_tlsSubjectName.c_str(), d_ds->d_tlsSubjectName.size()),
-    MAKE_NV(":path", d_ds->d_dohPath.c_str(), d_ds->d_dohPath.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")};
-
-  /* if 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
-   */
 
+  bool addXForwarded = d_ds->d_addXForwardedHeaders;
+
+  /* We 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 but we need to make sure that the data will outlive the request (nghttp2_on_frame_send_callback called), and that it is already lowercased. */
+  std::vector<nghttp2_nv> headers;
+  // these need to live until after the request headers have been processed
+  std::string remote;
+  std::string remotePort;
+  headers.reserve(8 + (addXForwarded ? 3 : 0));
+
+  /* Pseudo-headers need to come first (rfc7540 8.1.2.1) */
+  addStaticHeader(headers, "method-name", "method-value");
+  addStaticHeader(headers, "scheme-name", "scheme-value");
+  addDynamicHeader(headers, "authority-name", d_ds->d_tlsSubjectName);
+  addDynamicHeader(headers, "path-name", d_ds->d_dohPath);
+  addStaticHeader(headers, "accept-name", "accept-value");
+  addStaticHeader(headers, "content-type-name", "content-type-value");
+  addStaticHeader(headers, "user-agent-name", "user-agent-value");
+  addDynamicHeader(headers, "content-length-name", payloadSize);
+  /* no need to add these headers for health-check queries */
+  if (addXForwarded && d_currentQuery.d_idstate.origRemote.getPort() != 0) {
+    remote = d_currentQuery.d_idstate.origRemote.toString();
+    remotePort = std::to_string(d_currentQuery.d_idstate.origRemote.getPort());
+    addDynamicHeader(headers, "x-forwarded-for-name", remote);
+    addDynamicHeader(headers, "x-forwarded-port-name", remotePort);
+    if (d_currentQuery.d_idstate.cs != nullptr) {
+      if (d_currentQuery.d_idstate.cs->isUDP()) {
+        addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-udp");
+      }
+      else if (d_currentQuery.d_idstate.cs->isDoH()) {
+        if (d_currentQuery.d_idstate.cs->hasTLS()) {
+          addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-https");
+        }
+        else {
+          addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-http");
+        }
+      }
+      else if (d_currentQuery.d_idstate.cs->hasTLS()) {
+        addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-tls");
+      }
+      else {
+        addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-tcp");
+      }
+    }
+  }
+
+  /* if 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 (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.
+   */
   nghttp2_data_provider data_provider;
   data_provider.source.ptr = this;
   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 {
@@ -229,7 +299,7 @@ void DoHConnectionToBackend::queueQuery(std::shared_ptr<TCPQuerySender>& sender,
     return toCopy;
   };
 
-  auto stream_id = nghttp2_submit_request(d_session.get(), nullptr, hdrs, sizeof(hdrs) / sizeof(*hdrs), &data_provider, this);
+  auto stream_id = nghttp2_submit_request(d_session.get(), nullptr, headers.data(), headers.size(), &data_provider, this);
   if (stream_id < 0) {
     d_connectionDied = true;
     throw std::runtime_error("Error submitting HTTP request:" + std::string(nghttp2_strerror(stream_id)));
index a7a43e26d81e005f57ece6169595e96a591ce71d..ef08f48e68a12933db32e89803674d31d21a4485 100644 (file)
@@ -1457,7 +1457,7 @@ static void setupAcceptContext(DOHAcceptContext& ctx, DOHServerConfig& dsc, bool
   nativeCtx->hosts = dsc.h2o_config.hosts;
   ctx.d_ticketsKeyRotationDelay = dsc.df->d_tlsConfig.d_ticketsKeyRotationDelay;
 
-  if (setupTLS && !dsc.df->d_tlsConfig.d_certKeyPairs.empty()) {
+  if (setupTLS && dsc.df->isHTTPS()) {
     try {
       setupTLSContext(ctx,
                       dsc.df->d_tlsConfig,
@@ -1521,7 +1521,7 @@ void DOHFrontend::setup()
 
   d_dsc = std::make_shared<DOHServerConfig>(d_idleTimeout, d_internalPipeBufferSize);
 
-  if  (!d_tlsConfig.d_certKeyPairs.empty()) {
+  if  (isHTTPS()) {
     try {
       setupTLSContext(*d_dsc->accept_ctx,
                       d_tlsConfig,
index a30a0ac6f924581121f146103c176240753e2c43..e9cc9df5a8050221f4364f0f329b5f541c585bfb 100644 (file)
@@ -121,6 +121,11 @@ struct DOHFrontend
     return d_tlsConfig.d_ticketsKeyRotationDelay;
   }
 
+  bool isHTTPS() const
+  {
+    return !d_tlsConfig.d_certKeyPairs.empty();
+  }
+
 #ifndef HAVE_DNS_OVER_HTTPS
   void setup()
   {