From: Petr Špaček Date: Tue, 2 Apr 2019 13:49:17 +0000 (+0200) Subject: doh: send out HTTP TTL X-Git-Tag: v4.0.0~10^2~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=161dc3357b59f55773dce09ca1d1f015d8ecbe77;p=thirdparty%2Fknot-resolver.git doh: send out HTTP TTL We intentionally compute max-age header as minimum over all RRs, doing so only over ANSWER section does not make sense (and RFC 8484 allows us to do so). --- diff --git a/modules/http/http_doh.lua b/modules/http/http_doh.lua index 9b0f6a35c..b9b1e7571 100644 --- a/modules/http/http_doh.lua +++ b/modules/http/http_doh.lua @@ -3,35 +3,9 @@ local ffi = require('ffi') local condition = require('cqueues.condition') local function get_http_ttl(pkt) - -- minimum TTL from all RRs in ANSWER - if true then - local an_records = pkt:section(kres.section.ANSWER) - local is_negative = #an_records <= 0 - return ffi.C.packet_ttl(pkt, is_negative) - end - - -- garbage - if an_count > 0 then - local min_ttl = 4294967295 - for i = 1, an_count do - local rr = an_records[i] - min_ttl = math.min(rr.ttl, min_ttl) - end - return min_ttl - end - - -- no ANSWER records, try SOA - local auth_records = pkt:section(kres.section.AUTHORITY) - local auth_count = #auth_records - if auth_count > 0 then - for i = 1, an_count do - local rr = an_records[i] - if rr.type == kres.type.SOA then - knot_soa_minimum() - end - end - return 0 -- no SOA, uncacheable - end + local an_records = pkt:section(kres.section.ANSWER) + local is_negative = #an_records <= 0 + return ffi.C.packet_ttl(pkt, is_negative) end -- Trace execution of DNS queries @@ -73,7 +47,8 @@ local function serve_doh(h, stream) -- end -- Output buffer - local output = '' + local output + local output_ttl -- Wait for the result of the query -- Note: We can't do non-blocking write to stream directly from resolve callbacks @@ -84,8 +59,7 @@ local function serve_doh(h, stream) local finish_cb = function (answer, req) print(tostring(answer)) -- FIXME - print('TTL: ', get_http_ttl(answer)) - + output_ttl = get_http_ttl(answer) -- binary output output = ffi.string(answer.wire, answer.size) if waiting then @@ -119,7 +93,7 @@ local function serve_doh(h, stream) if not done then return 504, 'huh?' -- FIXME end - return output, nil, 'application/dns-message' + return output, nil, 'application/dns-message', output_ttl end -- Export endpoints diff --git a/modules/http/http_doh.test.lua b/modules/http/http_doh.test.lua index 148e66014..2b41edf54 100644 --- a/modules/http/http_doh.test.lua +++ b/modules/http/http_doh.test.lua @@ -1,17 +1,62 @@ local basexx = require('basexx') local ffi = require('ffi') -function parse_pkt(input) +local function gen_varying_ttls(_, req) + local qry = req:current() + local answer = req.answer + ffi.C.kr_pkt_make_auth_header(answer) + + answer:rcode(kres.rcode.NOERROR) + + -- varying TTLs in ANSWER section + answer:begin(kres.section.ANSWER) + answer:put(qry.sname, 1800, answer:qclass(), kres.type.AAAA, + '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1') + answer:put(qry.sname, 900, answer:qclass(), kres.type.A, '\127\0\0\1') + answer:put(qry.sname, 20000, answer:qclass(), kres.type.NS, '\2ns\4test\0') + + -- shorter TTL than all other RRs + answer:begin(kres.section.AUTHORITY) + answer:put('\4test\0', 300, answer:qclass(), kres.type.SOA, + '\2ns\4test\0\6nobody\7invalid\0\0\0\0\1\0\0\14\16\0\0\4\176\0\9\58\128\0\0\42\48') + return kres.DONE +end + +function parse_pkt(input, desc) local wire = ffi.cast("void *", input) local pkt = ffi.C.knot_pkt_new(wire, #input, nil); - assert(output, 'failed to create new packet') + assert(pkt, desc .. ': failed to create new packet') local result = ffi.C.knot_pkt_parse(pkt, 0) - ok(result > 0, 'knot_pkt_parse works on received answer') + ok(result == 0, desc .. ': knot_pkt_parse works on received answer') print(pkt) return pkt end +local function check_ok(req, desc) + local headers, stream, errno = req:go(5) -- TODO: randomly chosen timeout + if errno then + local errmsg = stream + nok(errmsg, desc .. ': ' .. errmsg) + return + end + same(tonumber(headers:get(':status')), 200, desc .. ': status 200') + same(headers:get('content-type'), 'application/dns-message', desc .. ': content-type') + local body = assert(stream:get_body_as_string()) + local pkt = parse_pkt(body, desc) + return headers, pkt +end + +local function check_err(req, exp_status, desc) + local headers, errmsg, errno = req:go(5) -- TODO: randomly chosen timeout + if errno then + nok(errmsg, desc .. ': ' .. errmsg) + return + end + local got_status = headers:get(':status') + same(got_status, exp_status, desc) +end + -- check prerequisites local has_http = pcall(require, 'kres_modules.http') and pcall(require, 'http.request') if not has_http then @@ -29,6 +74,9 @@ else endpoints = endpoints, } } + policy.add(policy.suffix(policy.DROP, policy.todnames({'servfail.test.'}))) + policy.add(policy.suffix(policy.DENY, policy.todnames({'nxdomain.test.'}))) + policy.add(policy.suffix(gen_varying_ttls, policy.todnames({'noerror.test.'}))) local server = http.servers[1] ok(server ~= nil, 'creates server instance') @@ -38,37 +86,56 @@ else local req_templ = assert(request.new_from_uri(uri_templ)) req_templ.headers:upsert('content-type', 'application/dns-message') - -- helper for returning useful values to test on - local function eval_req(req) - local headers, stream = req:go() - same(tonumber(headers:get(':status')), 200, 'status 200') - same(headers:get('content-type'), 'application/dns-message') - local body = assert(stream:get_body_as_string()) - -- parse packet! - local pkt = parse_pkt(body) - return pkt + -- test a valid DNS query using POST + local function test_doh_servfail() + local desc = 'valid POST query which ends with SERVFAIL' + local req = req_templ:clone() + req.headers:upsert(':method', 'POST') + req:set_body(basexx.from_base64( -- servfail.test. A + 'FZUBAAABAAAAAAAACHNlcnZmYWlsBHRlc3QAAAEAAQ==')) + local headers, pkt = check_ok(req, desc) + if not (headers and pkt) then + return + end + -- uncacheable + same(headers:get('cache-control'), 'max-age=0', desc .. ': TTL 0') + same(pkt:rcode(), kres.rcode.SERVFAIL, desc .. ': rcode matches') end - local function check_err(req, exp_status, desc) - local headers, errmsg, errno = req:go(5) -- TODO: randomly chosen timeout - if errno then - nok(errmsg, desc .. ': ' .. errmsg) + local function test_doh_noerror() + local desc = 'valid POST query which ends with NOERROR' + local req = req_templ:clone() + req.headers:upsert(':method', 'GET') + req.headers:upsert(':path', '/doh?dns=' -- noerror.test. A + .. 'vMEBAAABAAAAAAAAB25vZXJyb3IEdGVzdAAAAQAB') + local headers, pkt = check_ok(req, desc) + if not (headers and pkt) then return end - local got_status = headers:get(':status') - same(got_status, exp_status, desc) + -- HTTP TTL is minimum from all RRs in the answer + same(headers:get('cache-control'), 'max-age=300', desc .. ': TTL 900') + same(pkt:rcode(), kres.rcode.NOERROR, desc .. ': rcode matches') + same(pkt:ancount(), 3, desc .. ': ANSWER is present') + same(pkt:nscount(), 1, desc .. ': AUTHORITY is present') + same(pkt:arcount(), 0, desc .. ': ADDITIONAL is empty') end - -- test whether http interface responds and binds - local function test_doh_post() - local code, body, mime - - -- simple static page + local function test_doh_nxdomain() + local desc = 'valid POST query which ends with NXDOMAIN' local req = req_templ:clone() - local headers, stream = req:go() - code, body, mime = eval_req(req) + req.headers:upsert(':method', 'POST') + req:set_body(basexx.from_base64( -- servfail.test. A + 'viABAAABAAAAAAAACG54ZG9tYWluBHRlc3QAAAEAAQ==')) + local headers, pkt = check_ok(req, desc) + if not (headers and pkt) then + return + end + same(headers:get('cache-control'), 'max-age=10800', desc .. ': TTL 10800') + same(pkt:rcode(), kres.rcode.NXDOMAIN, desc .. ': rcode matches') + same(pkt:nscount(), 1, desc .. ': AUTHORITY is present') end + local function test_unsupp_method() local req = assert(req_templ:clone()) req.headers:upsert(':method', 'PUT') @@ -123,12 +190,14 @@ else -- plan tests local tests = { test_unsupp_method, - -- test_doh_post, test_post_short_input, test_post_long_input, test_get_long_input, test_post_unparseable_input, - test_post_unsupp_type + test_post_unsupp_type, + test_doh_servfail, + test_doh_nxdomain, + test_doh_noerror } return tests