pdns
pdns copied to clipboard
dnsdist: Allow RDATA access
- 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.
2 cents:
- 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.
- 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?
You can already do this in the Recursor today.
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.
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
RDATA access will also allow to make some kind of split horizon. For example, to drop all responses with private addresses in RDATA.
I join the question. I'd like to get this functionality to filter the response of private addresses!
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
access to RDATA would be great feature
@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/
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 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.
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 :)
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
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))