]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
doh: send out HTTP TTL
authorPetr Špaček <petr.spacek@nic.cz>
Tue, 2 Apr 2019 13:49:17 +0000 (15:49 +0200)
committerPetr Špaček <petr.spacek@nic.cz>
Thu, 11 Apr 2019 07:12:48 +0000 (09:12 +0200)
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).

modules/http/http_doh.lua
modules/http/http_doh.test.lua

index 9b0f6a35c73c06797550af207045b1acc2747f0c..b9b1e757106165459cba421e629b004875353c66 100644 (file)
@@ -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
index 148e66014cddc9a4e03833f1b0afd700209417f7..2b41edf547ac4917933257d937a18491252a0865 100644 (file)
@@ -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