From: Remi Gacogne Date: Wed, 15 May 2019 15:04:09 +0000 (+0200) Subject: dnsdist: Implement SNIRule for DoT X-Git-Tag: rec-4.2.0-rc1~3^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=046bac5c16e6199285833a5ac5764cd4235cebd5;p=thirdparty%2Fpdns.git dnsdist: Implement SNIRule for DoT --- diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index f20f3cf8f6..65fcf8ab88 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -546,6 +546,7 @@ const std::vector g_consoleKeywords{ { "showVersion", true, "", "show the current version" }, { "shutdown", true, "", "shut down `dnsdist`" }, { "SkipCacheAction", true, "", "Don’t lookup the cache for this query, don’t store the answer" }, + { "SNIRule", true, "name", "Create a rule which matches on the incoming TLS SNI value, if any (DoT or DoH)" }, { "snmpAgent", true, "enableTraps [, masterSocket]", "enable `SNMP` support. `enableTraps` is a boolean indicating whether traps should be sent and `masterSocket` an optional string specifying how to connect to the master agent"}, { "SNMPTrapAction", true, "[reason]", "send an SNMP trap, adding the optional `reason` string as the query description"}, { "SNMPTrapResponseAction", true, "[reason]", "send an SNMP trap, adding the optional `reason` string as the response description"}, diff --git a/pdns/dnsdist-lua-rules.cc b/pdns/dnsdist-lua-rules.cc index 5fbf7643db..b781e8c79a 100644 --- a/pdns/dnsdist-lua-rules.cc +++ b/pdns/dnsdist-lua-rules.cc @@ -295,6 +295,10 @@ void setupLuaRules() }); #endif + g_lua.writeFunction("SNIRule", [](const std::string& name) { + return std::shared_ptr(new SNIRule(name)); + }); + g_lua.writeFunction("SuffixMatchNodeRule", [](const SuffixMatchNode& smn, boost::optional quiet) { return std::shared_ptr(new SuffixMatchNodeRule(smn, quiet ? *quiet : false)); }); diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index 6c437e270f..8419011661 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -808,6 +808,7 @@ static void handleQuery(std::shared_ptr& state, stru DNSName qname(query, state->d_querySize, sizeof(dnsheader), false, &qtype, &qclass, &consumed); DNSQuestion dq(&qname, qtype, qclass, consumed, &state->d_ids.origDest, &state->d_ci.remote, reinterpret_cast(query), state->d_buffer.size(), state->d_querySize, true, &queryRealTime); dq.dnsCryptQuery = std::move(dnsCryptQuery); + dq.sni = state->d_handler.getServerNameIndication(); state->d_isXFR = (dq.qtype == QType::AXFR || dq.qtype == QType::IXFR); if (state->d_isXFR) { diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 3d32ee2a10..a74d164673 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -75,6 +75,7 @@ struct DNSQuestion #endif Netmask ecs; boost::optional subnet; + std::string sni; /* Server Name Indication, if any (DoT or DoH) */ const DNSName* qname{nullptr}; const ComboAddress* local{nullptr}; const ComboAddress* remote{nullptr}; diff --git a/pdns/dnsdistdist/dnsdist-rules.hh b/pdns/dnsdistdist/dnsdist-rules.hh index a25d8572c0..4827a6aa02 100644 --- a/pdns/dnsdistdist/dnsdist-rules.hh +++ b/pdns/dnsdistdist/dnsdist-rules.hh @@ -525,6 +525,24 @@ private: }; #endif +class SNIRule : public DNSRule +{ +public: + SNIRule(const std::string& name) : d_sni(name) + { + } + bool matches(const DNSQuestion* dq) const override + { + return dq->sni == d_sni; + } + string toString() const override + { + return "SNI == " + d_sni; + } +private: + std::string d_sni; +}; + class SuffixMatchNodeRule : public DNSRule { public: diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index ae38919755..546063d311 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -756,6 +756,15 @@ These ``DNSRule``\ s be one of the following items: :param str regex: The regular expression to match the QNAME. +.. function:: SNIRule(name) + .. versionadded:: 1.4.0 + + Matches against the TLS Server Name Indication value sent by the client, if any. Only makes + sense for DoT or DoH, and for that last one matching on the HTTP Host header might provide + more consistent results. + + :param str name: The exact SNI name to match. + .. function:: SuffixMatchNodeRule(smn[, quiet]) Matches based on a group of domain suffixes for rapid testing of membership. diff --git a/pdns/dnsdistdist/tcpiohandler.cc b/pdns/dnsdistdist/tcpiohandler.cc index 6e77c7840a..bef5ce2faa 100644 --- a/pdns/dnsdistdist/tcpiohandler.cc +++ b/pdns/dnsdistdist/tcpiohandler.cc @@ -352,6 +352,7 @@ public: return got; } + void close() override { if (d_conn) { @@ -359,6 +360,17 @@ public: } } + std::string getServerNameIndication() + { + if (d_conn) { + const char* value = SSL_get_servername(d_conn.get(), TLSEXT_NAMETYPE_host_name); + if (value) { + return std::string(value); + } + } + return std::string(); + } + private: std::unique_ptr d_conn; unsigned int d_timeout; @@ -860,6 +872,23 @@ public: return got; } + std::string getServerNameIndication() + { + if (d_conn) { + unsigned int type; + size_t name_len = 256; + std::string sni; + sni.resize(name_len); + + int res = gnutls_server_name_get(d_conn.get(), const_cast(sni.c_str()), &name_len, &type, 0); + if (res == GNUTLS_E_SUCCESS) { + sni.resize(name_len); + return sni; + } + } + return std::string(); + } + void close() override { if (d_conn) { diff --git a/pdns/tcpiohandler.hh b/pdns/tcpiohandler.hh index 061ab884a4..dd82281a7a 100644 --- a/pdns/tcpiohandler.hh +++ b/pdns/tcpiohandler.hh @@ -16,6 +16,7 @@ public: virtual size_t write(const void* buffer, size_t bufferSize, unsigned int writeTimeout) = 0; virtual IOState tryWrite(std::vector& buffer, size_t& pos, size_t toWrite) = 0; virtual IOState tryRead(std::vector& buffer, size_t& pos, size_t toRead) = 0; + virtual std::string getServerNameIndication() = 0; virtual void close() = 0; protected: @@ -275,6 +276,14 @@ public: } } + std::string getServerNameIndication() + { + if (d_conn) { + return d_conn->getServerNameIndication(); + } + return std::string(); + } + private: std::unique_ptr d_conn{nullptr}; int d_socket{-1}; diff --git a/regression-tests.dnsdist/configCA.conf b/regression-tests.dnsdist/configCA.conf index fa5d736985..ddb427ce01 100644 --- a/regression-tests.dnsdist/configCA.conf +++ b/regression-tests.dnsdist/configCA.conf @@ -18,3 +18,6 @@ countryName = NL [custom_extensions] basicConstraints = CA:true keyUsage = cRLSign, keyCertSign + +[CA_default] +copy_extensions = copy diff --git a/regression-tests.dnsdist/configServer.conf b/regression-tests.dnsdist/configServer.conf index 030cd5959f..f1aa4c7fed 100644 --- a/regression-tests.dnsdist/configServer.conf +++ b/regression-tests.dnsdist/configServer.conf @@ -3,9 +3,18 @@ default_bits = 2048 encrypt_key = no prompt = no distinguished_name = server_distinguished_name +req_extensions = v3_req [server_distinguished_name] CN = tls.tests.dnsdist.org OU = PowerDNS.com BV countryName = NL +[v3_req] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = tls.tests.dnsdist.org +DNS.2 = powerdns.com diff --git a/regression-tests.dnsdist/runtests b/regression-tests.dnsdist/runtests index 251e76a6a7..1f6de2ea1c 100755 --- a/regression-tests.dnsdist/runtests +++ b/regression-tests.dnsdist/runtests @@ -54,7 +54,7 @@ openssl req -new -x509 -days 1 -extensions v3_ca -keyout ca.key -out ca.pem -nod # Generate a new server certificate request openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr -config configServer.conf # Sign the server cert -openssl x509 -req -days 1 -CA ca.pem -CAkey ca.key -CAcreateserial -in server.csr -out server.pem +openssl x509 -req -days 1 -CA ca.pem -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extfile configServer.conf -extensions v3_req # Generate a chain cat server.pem ca.pem > server.chain diff --git a/regression-tests.dnsdist/test_TLS.py b/regression-tests.dnsdist/test_TLS.py index b31c6c2f9b..6973613b45 100644 --- a/regression-tests.dnsdist/test_TLS.py +++ b/regression-tests.dnsdist/test_TLS.py @@ -12,6 +12,7 @@ class TestTLS(DNSDistTest): _config_template = """ newServer{address="127.0.0.1:%s"} addTLSLocal("127.0.0.1:%s", "%s", "%s") + addAction(SNIRule("powerdns.com"), SpoofAction("1.2.3.4")) """ _config_params = ['_testServerPort', '_tlsServerPort', '_serverCert', '_serverKey'] @@ -90,3 +91,44 @@ class TestTLS(DNSDistTest): receivedQuery.id = query.id self.assertEquals(query, receivedQuery) self.assertEquals(response, receivedResponse) + + def testTLSSNIRouting(self): + """ + TLS: SNI Routing + """ + name = 'sni.tls.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=False) + query.flags &= ~dns.flags.RD + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + expectedResponse = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '1.2.3.4') + expectedResponse.answer.append(rrset) + + # this SNI should match so we should get a spoofed answer + conn = self.openTLSConnection(self._tlsServerPort, 'powerdns.com', self._caCert) + + self.sendTCPQueryOverConnection(conn, query, response=None) + receivedResponse = self.recvTCPResponseOverConnection(conn, useQueue=False) + self.assertTrue(receivedResponse) + self.assertEquals(expectedResponse, receivedResponse) + + # this one should not + conn = self.openTLSConnection(self._tlsServerPort, self._serverName, self._caCert) + + self.sendTCPQueryOverConnection(conn, query, response=response) + (receivedQuery, receivedResponse) = self.recvTCPResponseOverConnection(conn, useQueue=True) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse)