Skip to content

Commit

Permalink
rec: Allow access to EDNS options from the gettag() hook
Browse files Browse the repository at this point in the history
If `gettag-needs-edns-options` is set, the EDNS options are extracted
and passed to the `gettag()` hook as a table whose keys are the
EDNS option code and the values are `EDNSOptionView` object.
`EDNSOptionView` has two members, `content` and `size`, with `content`
holding the raw, undecoded option value.
  • Loading branch information
rgacogne authored and Habbie committed Mar 24, 2017
1 parent a77e6c0 commit 00b8cad
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 20 deletions.
7 changes: 6 additions & 1 deletion docs/markdown/recursor/scripting.md
Expand Up @@ -151,7 +151,7 @@ end
This hook does not get the full DNSQuestion object, since filling out the fields
would require packet parsing, which is what we are trying to prevent with `ipfilter`.

### `function gettag(remote, ednssubnet, local, qname, qtype)`
### `function gettag(remote, ednssubnet, local, qname, qtype, ednsoptions)`
The `gettag` function is invoked when the Recursor attempts to discover in which
packetcache an answer is available.

Expand All @@ -168,6 +168,11 @@ e.g. been filtered for certain IPs (this logic should be implemented in the
setting dq.variable to `true`. In the latter case, repeated queries will pass
through the entire Lua script.

`ednsoptions` is a table whose keys are EDNS option codes and values are
`EDNSOptionView` objects, with the EDNS option content size in the `size` member
and the content accessible as a NULL-safe string object via `getContent()`.
This table is empty unless the `gettag-needs-edns-options` parameter is set.

### `function prerpz(dq)`

This hook is called before any filtering policy have been applied, making it
Expand Down
8 changes: 8 additions & 0 deletions docs/markdown/recursor/settings.md
Expand Up @@ -360,6 +360,14 @@ forward queries to other recursive servers.

The DNSSEC notes from [`forward-zones`](#forward-zones) apply here as well.

## `gettag-needs-edns-options`
* Boolean
* Default: no
* Available since: 4.1.0

If set, EDNS options in incoming queries are extracted and passed to the `gettag()`
hook in the `ednsoptions` table.

## `hint-file`
* Path

Expand Down
39 changes: 39 additions & 0 deletions pdns/ednsoptions.cc
Expand Up @@ -66,6 +66,45 @@ int getEDNSOption(char* optRR, const size_t len, uint16_t wantedOption, char **
return ENOENT;
}

/* extract all EDNS0 options from a pointer on the beginning rdLen of the OPT RR */
int getEDNSOptions(const char* optRR, const size_t len, std::map<uint16_t, EDNSOptionView>& options)
{
assert(optRR != NULL);
size_t pos = 0;
if (len < DNS_RDLENGTH_SIZE)
return EINVAL;

const uint16_t rdLen = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]);
size_t rdPos = 0;
pos += DNS_RDLENGTH_SIZE;
if ((pos + rdLen) > len) {
return EINVAL;
}

while(len >= (pos + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE) &&
rdLen >= (rdPos + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE)) {
const uint16_t optionCode = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]);
pos += EDNS_OPTION_CODE_SIZE;
rdPos += EDNS_OPTION_CODE_SIZE;
const uint16_t optionLen = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]);
pos += EDNS_OPTION_LENGTH_SIZE;
rdPos += EDNS_OPTION_LENGTH_SIZE;
if (optionLen > (rdLen - rdPos) || optionLen > (len - pos))
return EINVAL;

EDNSOptionView view;
view.content = optRR + pos;
view.size = optionLen;
options[optionCode] = view;

/* skip this option */
pos += optionLen;
rdPos += optionLen;
}

return 0;
}

void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res)
{
const uint16_t ednsOptionCode = htons(optionCode);
Expand Down
10 changes: 10 additions & 0 deletions pdns/ednsoptions.hh
Expand Up @@ -31,6 +31,16 @@ struct EDNSOptionCode

/* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */
int getEDNSOption(char* optRR, size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize);

struct EDNSOptionView
{
const char* content{nullptr};
uint16_t size{0};
};

/* extract all EDNS0 options from a pointer on the beginning rdLen of the OPT RR */
int getEDNSOptions(const char* optRR, size_t len, std::map<uint16_t, EDNSOptionView>& options);

void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res);

#endif
9 changes: 6 additions & 3 deletions pdns/lua-recursor4.cc
Expand Up @@ -384,7 +384,10 @@ RecursorLua4::RecursorLua4(const std::string& fname)
d_lw->registerMember("type", &DNSRecord::d_type);
d_lw->registerMember("ttl", &DNSRecord::d_ttl);
d_lw->registerMember("place", &DNSRecord::d_place);


d_lw->registerMember("size", &EDNSOptionView::size);
d_lw->registerFunction<std::string(EDNSOptionView::*)()>("getContent", [](const EDNSOptionView& option) { return std::string(option.content, option.size); });

d_lw->registerFunction<string(DNSRecord::*)()>("getContent", [](const DNSRecord& dr) { return dr.d_content->getZoneRepresentation(); });
d_lw->registerFunction<boost::optional<ComboAddress>(DNSRecord::*)()>("getCA", [](const DNSRecord& dr) {
boost::optional<ComboAddress> ret;
Expand Down Expand Up @@ -584,10 +587,10 @@ bool RecursorLua4::ipfilter(const ComboAddress& remote, const ComboAddress& loca
return false; // don't block
}

unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data)
unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>& ednsOptions)
{
if(d_gettag) {
auto ret = d_gettag(remote, ednssubnet, local, qname, qtype);
auto ret = d_gettag(remote, ednssubnet, local, qname, qtype, ednsOptions);

if (policyTags) {
const auto& tags = std::get<1>(ret);
Expand Down
8 changes: 5 additions & 3 deletions pdns/lua-recursor4.hh
Expand Up @@ -25,7 +25,10 @@
#include "namespaces.hh"
#include "dnsrecords.hh"
#include "filterpo.hh"
#include "ednsoptions.hh"

#include <unordered_map>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
Expand Down Expand Up @@ -98,7 +101,7 @@ public:
DNSName followupName;
};

unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data);
unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>&);

bool prerpz(DNSQuestion& dq, int& ret);
bool preresolve(DNSQuestion& dq, int& ret);
Expand All @@ -118,8 +121,7 @@ public:
d_postresolve);
}

typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject> >(ComboAddress, Netmask, ComboAddress, DNSName,
uint16_t)> gettag_t;
typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject> >(ComboAddress, Netmask, ComboAddress, DNSName, uint16_t, const std::map<uint16_t, EDNSOptionView>&)> gettag_t;
gettag_t d_gettag; // public so you can query if we have this hooked

private:
Expand Down
47 changes: 34 additions & 13 deletions pdns/pdns_recursor.cc
Expand Up @@ -154,6 +154,7 @@ static bool g_lowercaseOutgoing;
static bool g_weDistributeQueries; // if true, only 1 thread listens on the incoming query sockets
static bool g_reusePort{false};
static bool g_useOneSocketPerThread;
static bool g_gettagNeedsEDNSOptions{false};

std::unordered_set<DNSName> g_delegationOnly;
RecursorControlChannel s_rcc; // only active in thread 0
Expand Down Expand Up @@ -1299,7 +1300,7 @@ static void makeControlChannelSocket(int processNum=-1)
}
}

static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, EDNSSubnetOpts* ednssubnet)
static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, EDNSSubnetOpts* ednssubnet, std::map<uint16_t, EDNSOptionView>* options)
{
bool found = false;
const struct dnsheader* dh = (struct dnsheader*)question.c_str();
Expand All @@ -1313,14 +1314,29 @@ static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uin
if(ntohs(dh->arcount) == 1 && questionLen > pos + 11) { // this code can extract one (1) EDNS Subnet option
/* OPT root label (1) followed by type (2) */
if(question.at(pos)==0 && question.at(pos+1)==0 && question.at(pos+2)==QType::OPT) {
char* ecsStart = nullptr;
size_t ecsLen = 0;
int res = getEDNSOption((char*)question.c_str()+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen);
if (res == 0 && ecsLen > 4) {
EDNSSubnetOpts eso;
if(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)) {
*ednssubnet=eso;
found = true;
if (!options) {
char* ecsStart = nullptr;
size_t ecsLen = 0;
int res = getEDNSOption((char*)question.c_str()+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen);
if (res == 0 && ecsLen > 4) {
EDNSSubnetOpts eso;
if(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)) {
*ednssubnet=eso;
found = true;
}
}
}
else {
int res = getEDNSOptions((char*)question.c_str()+pos+9, questionLen - pos - 9, *options);
if (res == 0) {
const auto& it = options->find(EDNSOptionCode::ECS);
if (it != options->end() && it->second.content != nullptr && it->second.size > 0) {
EDNSSubnetOpts eso;
if(getEDNSSubnetOptsFromString(it->second.content, it->second.size, &eso)) {
*ednssubnet=eso;
found = true;
}
}
}
}
}
Expand Down Expand Up @@ -1405,12 +1421,13 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
if(needECS || (t_pdl->get() && (*t_pdl)->d_gettag)) {

try {
std::map<uint16_t, EDNSOptionView> ednsOptions;
dc->d_ecsParsed = true;
dc->d_ecsFound = getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, &dc->d_ednssubnet);
dc->d_ecsFound = getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr);

if(t_pdl->get() && (*t_pdl)->d_gettag) {
try {
dc->d_tag = (*t_pdl)->gettag(conn->d_remote, dc->d_ednssubnet.source, dest, qname, qtype, &dc->d_policyTags, dc->d_data);
dc->d_tag = (*t_pdl)->gettag(conn->d_remote, dc->d_ednssubnet.source, dest, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions);
}
catch(std::exception& e) {
if(g_logCommonErrors)
Expand Down Expand Up @@ -1576,13 +1593,14 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr

if(needECS || (t_pdl->get() && (*t_pdl)->d_gettag)) {
try {
ecsFound = getQNameAndSubnet(question, &qname, &qtype, &qclass, &ednssubnet);
std::map<uint16_t, EDNSOptionView> ednsOptions;
ecsFound = getQNameAndSubnet(question, &qname, &qtype, &qclass, &ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr);
qnameParsed = true;
ecsParsed = true;

if(t_pdl->get() && (*t_pdl)->d_gettag) {
try {
ctag=(*t_pdl)->gettag(fromaddr, ednssubnet.source, destaddr, qname, qtype, &policyTags, data);
ctag=(*t_pdl)->gettag(fromaddr, ednssubnet.source, destaddr, qname, qtype, &policyTags, data, ednsOptions);
}
catch(std::exception& e) {
if(g_logCommonErrors)
Expand Down Expand Up @@ -2804,6 +2822,8 @@ static int serviceMain(int argc, char*argv[])
g_numThreads = g_numWorkerThreads + g_weDistributeQueries;
g_maxMThreads = ::arg().asNum("max-mthreads");

g_gettagNeedsEDNSOptions = ::arg().mustDo("gettag-needs-edns-options");

#ifdef SO_REUSEPORT
g_reusePort = ::arg().mustDo("reuseport");
#endif
Expand Down Expand Up @@ -3184,6 +3204,7 @@ int main(int argc, char **argv)
::arg().setSwitch( "root-nx-trust", "If set, believe that an NXDOMAIN from the root means the TLD does not exist")="yes";
::arg().setSwitch( "any-to-tcp","Answer ANY queries with tc=1, shunting to TCP" )="no";
::arg().setSwitch( "lowercase-outgoing","Force outgoing questions to lowercase")="no";
::arg().setSwitch("gettag-needs-edns-options", "If EDNS Options should be extracted before calling the gettag() hook")="no";
::arg().set("udp-truncation-threshold", "Maximum UDP response size before we truncate")="1680";
::arg().set("edns-outgoing-bufsize", "Outgoing EDNS buffer size")="1680";
::arg().set("minimum-ttl-override", "Set under adverse conditions, a minimum TTL")="0";
Expand Down
2 changes: 2 additions & 0 deletions pdns/recursordist/Makefile.am
Expand Up @@ -185,6 +185,7 @@ testrunner_SOURCES = \
dnsrecords.cc \
dnssecinfra.cc \
dnswriter.cc dnswriter.hh \
ednscookies.cc ednscookies.hh \
ednsoptions.cc ednsoptions.hh \
ednssubnet.cc ednssubnet.hh \
gettime.cc gettime.hh \
Expand Down Expand Up @@ -212,6 +213,7 @@ testrunner_SOURCES = \
test-dnsname_cc.cc \
test-dnsparser_hh.cc \
test-dnsrecords_cc.cc \
test-ednsoptions_cc.cc \
test-iputils_hh.cc \
test-misc_hh.cc \
test-nmtree.cc \
Expand Down
1 change: 1 addition & 0 deletions pdns/recursordist/ednscookies.cc
1 change: 1 addition & 0 deletions pdns/recursordist/ednscookies.hh
115 changes: 115 additions & 0 deletions pdns/recursordist/test-ednsoptions_cc.cc
@@ -0,0 +1,115 @@
#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_NO_MAIN

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <boost/test/unit_test.hpp>
#include <utility>

#include "dnsname.hh"
#include "dnswriter.hh"
#include "ednscookies.hh"
#include "ednsoptions.hh"
#include "ednssubnet.hh"
#include "iputils.hh"

/* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */
int getEDNSOption(char* optRR, size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize);

BOOST_AUTO_TEST_SUITE(ednsoptions_cc)

static void getRawQueryWithECSAndCookie(const DNSName& name, const Netmask& ecs, const std::string& clientCookie, const std::string& serverCookie, std::vector<uint8_t>& query)
{
DNSPacketWriter pw(query, name, QType::A, QClass::IN, 0);
pw.commit();

EDNSCookiesOpt cookiesOpt;
cookiesOpt.client = clientCookie;
cookiesOpt.server = serverCookie;
string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt);
EDNSSubnetOpts ecsOpts;
ecsOpts.source = ecs;
string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts);
DNSPacketWriter::optvect_t opts;
opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr));
opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr));
opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr));
pw.addOpt(512, 0, 0, opts);
pw.commit();
}

BOOST_AUTO_TEST_CASE(test_getEDNSOption) {
DNSName name("www.powerdns.com.");
Netmask ecs("127.0.0.1/32");
vector<uint8_t> query;

getRawQueryWithECSAndCookie(name, ecs, "deadbeef", "deadbeef", query);

const struct dnsheader* dh = reinterpret_cast<struct dnsheader*>(query.data());
size_t questionLen = query.size();
unsigned int consumed = 0;
DNSName dnsname = DNSName(reinterpret_cast<const char*>(query.data()), questionLen, sizeof(dnsheader), false, nullptr, nullptr, &consumed);

size_t pos = sizeof(dnsheader) + consumed + 4;
/* at least OPT root label (1), type (2), class (2) and ttl (4) + OPT RR rdlen (2) = 11 */
BOOST_REQUIRE_EQUAL(ntohs(dh->arcount), 1);
BOOST_REQUIRE(questionLen > pos + 11);
/* OPT root label (1) followed by type (2) */
BOOST_REQUIRE_EQUAL(query.at(pos), 0);
BOOST_REQUIRE(query.at(pos+2) == QType::OPT);

char* ecsStart = nullptr;
size_t ecsLen = 0;
int res = getEDNSOption(reinterpret_cast<char*>(query.data())+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen);
BOOST_CHECK_EQUAL(res, 0);

EDNSSubnetOpts eso;
BOOST_REQUIRE(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso));

BOOST_CHECK(eso.source == ecs);
}

BOOST_AUTO_TEST_CASE(test_getEDNSOptions) {
DNSName name("www.powerdns.com.");
Netmask ecs("127.0.0.1/32");
vector<uint8_t> query;

getRawQueryWithECSAndCookie(name, ecs, "deadbeef", "deadbeef", query);

const struct dnsheader* dh = reinterpret_cast<struct dnsheader*>(query.data());
size_t questionLen = query.size();
unsigned int consumed = 0;
DNSName dnsname = DNSName(reinterpret_cast<const char*>(query.data()), questionLen, sizeof(dnsheader), false, nullptr, nullptr, &consumed);

size_t pos = sizeof(dnsheader) + consumed + 4;
/* at least OPT root label (1), type (2), class (2) and ttl (4) + OPT RR rdlen (2) = 11 */
BOOST_REQUIRE_EQUAL(ntohs(dh->arcount), 1);
BOOST_REQUIRE(questionLen > pos + 11);
/* OPT root label (1) followed by type (2) */
BOOST_REQUIRE_EQUAL(query.at(pos), 0);
BOOST_REQUIRE(query.at(pos+2) == QType::OPT);

std::map<uint16_t, EDNSOptionView> options;
int res = getEDNSOptions(reinterpret_cast<char*>(query.data())+pos+9, questionLen - pos - 9, options);
BOOST_REQUIRE_EQUAL(res, 0);

/* 3 EDNS options but two of them are EDNS Cookie, so we only keep one */
BOOST_CHECK_EQUAL(options.size(), 2);

auto it = options.find(EDNSOptionCode::ECS);
BOOST_REQUIRE(it != options.end());
BOOST_REQUIRE(it->second.content != nullptr);
BOOST_REQUIRE_GT(it->second.size, 0);

EDNSSubnetOpts eso;
BOOST_REQUIRE(getEDNSSubnetOptsFromString(it->second.content, it->second.size, &eso));
BOOST_CHECK(eso.source == ecs);

it = options.find(EDNSOptionCode::COOKIE);
BOOST_REQUIRE(it != options.end());
BOOST_REQUIRE(it->second.content != nullptr);
BOOST_REQUIRE_GT(it->second.size, 0);
}

BOOST_AUTO_TEST_SUITE_END()

0 comments on commit 00b8cad

Please sign in to comment.