pdns icon indicating copy to clipboard operation
pdns copied to clipboard

dnsdist: Allow RDATA access

Open pequalsmp opened this issue 6 years ago • 12 comments

  • Program: dnsdist
  • Issue type: Feature request

Short description

dnsdist is really flexible and provides a lot of functionality, but it seems that it can't iterate over a dns response's records. This may be useful for security, traffic optimization, auditing and various other use-cases.

Usecase

I would like to add RFC1918 filtering, in order to prevent dns-rebinding attacks.

Description

With the proposed changes, dns-rebind in dnsdist might look like:

-- define RFC1918 ranges
rfc1918 = newNMG()
rfc1918:addMask("10.0.0.0/8")
rfc1918:addMask("172.16.0.0/12")
rfc1918:addMask("192.168.0.0/16")

function preventDNSRebind(dr)
    for record in dr.response do
        if rfc1918:match(record.rdata) then
            return DNSResponseAction.Drop, ''
        end
    end

    return DNSResponseAction.None, ""
end

addLuaResponseAction(RCodeRule(dnsdist.NOERROR), preventDNSRebind)

Note: If you're planning on implementing this, you should know that Plex uses RFC1918 addresses, in order to issue valid x509 certificates to their clients.

pequalsmp avatar Jun 26 '18 23:06 pequalsmp

2 cents:

  1. this sample conveniently leaves out the fact that there might be more than one qtype in the response, which doesn't even have to be an A.
  2. i think parsing replies in general is out, but giving access to the full dns packet parsed with a dr:parseData() method might be acceptable?

zeha avatar Jun 26 '18 23:06 zeha

You can already do this in the Recursor today.

Habbie avatar Jun 27 '18 09:06 Habbie

this sample conveniently leaves out the fact that there might be more than one qtype in the response, which doesn't even have to be an A.

The sample is not valid code anyway, its just some pseudocode thrown together. Proper parsing/filtering is TBD if/when its even possible to access the response.

i think parsing replies in general is out, but giving access to the full dns packet parsed with a dr:parseData() method might be acceptable?

Do you know why this is the case? AFAICS there's access to the response headers, why would the contents be any different?

I think there's something similar to what i would like to see, but its just not exposed through an API. Of course its for logging purposes, but the functionality is there.

You can already do this in the Recursor today.

Thanks for the heads-up.

pequalsmp avatar Jun 27 '18 12:06 pequalsmp

Do you know why this is the case? AFAICS there's access to the response headers, why would the contents be any different?

We try hard not to parse the response in dnsdist, because it has a huge performance impact and exposes a lot of code to parse all the DNS record types. We do some parsing for features like protobuf when we need to, but it's limited to a few types and is only done for some responses.

We could implement a way to request the parsing of responses from Lua like suggested by @zeha, exposing the name, type, class, TTL and raw content of records to Lua and perhaps also some helpers to deal with raw content for some types like A or AAAA, but we will most likely never have the kind of knowledge about records that the recursor has

rgacogne avatar Jun 27 '18 13:06 rgacogne

RDATA access will also allow to make some kind of split horizon. For example, to drop all responses with private addresses in RDATA.

dimaslv avatar Jun 28 '18 09:06 dimaslv

I join the question. I'd like to get this functionality to filter the response of private addresses!

ZAZmaster avatar Aug 29 '19 12:08 ZAZmaster

You can already do this in the Recursor today.

Someone came on IRC looking how to do this. So maybe this will help people in the right direction. Put this in a file and set lua-dns-script to the path. This is a quick adaptation of an existing documented example, adjust to taste.

mything = newNMG()
mything:addMasks({"0.0.0.0/32","127.0.0.1/32"})

function postresolve(dq)
  local records = dq:getRecords()
  for k,v in pairs(records) do
    if v.type == pdns.A then
      local IP = newCA(v:getContent())
      if mything:match(IP) then
        pdnslog("Blocking possible rebind on "..dq.qname:toString().." because of "..v:getContent().." request from "..dq.localaddr:toString())
        dq.variable = true -- how long would these pc for otherwise?
        dq:setRecords({})
        dq.rcode = pdns.REFUSED
        return true
      end
    end
  end 

  return false
end

phonedph1 avatar Apr 29 '21 03:04 phonedph1

access to RDATA would be great feature

7c avatar Sep 23 '21 15:09 7c

@rgacogne What would be the correct approach to fixing this problem? When we use PowerDNS as authoritative server, we have this situation:

  • we can't filter the responses to the outside world in PowerDNS directly
  • Recursor can perform filtering but it strips AA flag [1], wreaking havoc when the outside world expects authoritative response
  • dnsdist preserves AA flag but it can't parse the response, so it's useless
  • PowerDNS doesn't support split horizon natively, I don't want it either, it's not recommended [1], and setting it up actually involves fronting two separate instances with dnsdist [2]

In #10235 you mention parsing it "in Lua", but I'm not sure what exactly you meant with that. I can't write parser with a function in the config file because DNSResponse is of type "userdata" which can't be examined in the Lua script.

I tried using the present but currently unused (unused in dnsdist) MOADNSParser to pass the records from C and use them as I would in Recursor, but it returns UnknownRecordContent type, so they're useless UserData once again.

In the end I used DNSPacketMangler to walk through the records, compare the hardcoded values for RFC1918 addresses numerically with values in the response and drop it if such address is found in any of the returned A records. But that solution is ugly and inappropriate for a pull request.

I'd love to help fix this as I need this feature in a more usable state than I have now, but I'm not really a C++/Lua programmer, so I'm stuck, and I didn't figure out why it works in Recursor, but not in dnsdist. I'd need some help with that part.

[1] https://pdns-users.mailman.powerdns.narkive.com/jOnbBmuk/recursor-to-respond-authoritatively-for-all-queries [2] https://www.frank.be/implementing-bind-views-with-powerdns/

danijelt avatar Jan 03 '22 17:01 danijelt

In #10235 you mention parsing it "in Lua", but I'm not sure what exactly you meant with that. I can't write parser with a function in the config file because DNSResponse is of type "userdata" which can't be examined in the Lua script.

We do provide several Lua bindings to interact with that object, though, see 1. I'm not sure we provide an easy to access the content of the DNS packet in these bindings, we should clearly add that in that case.

I tried using the present but currently unused (unused in dnsdist) MOADNSParser to pass the records from C and use them as I would in Recursor, but it returns UnknownRecordContent type, so they're useless UserData once again.

Right, as stated in #10235 does not how to parse DNS records. We do not intend to change that, since we believe it's out of scope for dnsdist.

In the end I used DNSPacketMangler to walk through the records, compare the hardcoded values for RFC1918 addresses numerically with values in the response and drop it if such address is found in any of the returned A records. But that solution is ugly and inappropriate for a pull request. I'd love to help fix this as I need this feature in a more usable state than I have now, but I'm not really a C++/Lua programmer, so I'm stuck, and I didn't figure out why it works in Recursor, but not in dnsdist. I'd need some help with that part.

I'm currently working on providing a way to get more information about the response from Lua, and in particular the list of records and the following information for each records:

  • owner name
  • type
  • class
  • ttl
  • content length
  • content

I expect that feature to land in 1.8.0, in a few months. However parsing the content itself will still have to be done in Lua. That's not too hard for A, AAAA and CNAME types, for example, but would still require significant work for other types.

rgacogne avatar Jan 04 '22 15:01 rgacogne

@rgacogne Any news on this? I'm fine with parsing the records myself (as long as that means parsing it in a function in Lua in the config file, instead of the mess I hardcoded in C++ months ago). Commit 1bf2f3b2f126cd26378ae6b848585e0182bf45d4 looks promising, but I can't tell if that's all I need to process the response in Lua.

danijelt avatar Jul 05 '22 16:07 danijelt

If you are fine with parsing the records in Lua then yes, DNSResponse::getContent should get you the whole DNS payload. It was even backported to 1.7.2 :)

rgacogne avatar Jul 05 '22 17:07 rgacogne

We now have helpers to make it possible to look at the records (the RDATA content is still not decoded, though, and will likely never be): https://github.com/PowerDNS/pdns/pull/12022

rgacogne avatar Nov 28 '22 16:11 rgacogne

I can confirm that this feature works as intended. In case someone needs it, here's my script that blocks responses with RFC 1918 addresses (IPv6 behavior not thoroughly tested, included only as an example). Record parsing code inspired by this gist.

local parsers = {};
local recordTypes = {[1] = 'A', [28] = 'AAAA'};

function parsers.A(packet, pos, contentLength)
  if contentLength == 4 then
    return newCA(table.concat({packet:byte(pos, pos+3)}, "."));
  end
end

function parsers.AAAA(packet, pos, contentLength)
  if contentLength == 16 then
    local t = { packet:byte(pos, pos+15) };
    for i=1,8 do
      t[i] = ("%x"):format(t[i*2-1]*256+t[i*2]);
    end
    return newCA(table.concat(t, ":", 1, 8));
  end
end

local rfc1918 = newNMG();
rfc1918:addMask("10.0.0.0/8");
rfc1918:addMask("172.16.0.0/12");
rfc1918:addMask("192.168.0.0/16");
rfc1918:addMask("fc00::/7");

local function postResolve(dr)
  local packet = dr:getContent();
  local overlay = newDNSPacketOverlay(packet);
  local count = overlay:getRecordsCountInSection(DNSSection.Answer);
  for i=0, count-1 do
    local record = overlay:getRecord(i);
    local parser = parsers[recordTypes[record.type]];
    if parser then
      local ca = parser(packet, record.contentOffset+1, record.contentLength);
      if rfc1918:match(ca) then
        print("Private IP: "..ca:tostring()..", dropping");
        return DNSResponseAction.Drop;
      end
    end
  end
  return DNSResponseAction.None;
end

addResponseAction(AllRule(), LuaResponseAction(postResolve))

danijelt avatar Mar 10 '23 12:03 danijelt