Project

General

Profile

Feature #16937 ยป 0001-Add-DNS-over-HTTP-resolver.patch

DNS over HTTP patch - drbrain (Eric Hodel), 06/07/2020 11:43 PM

View differences:

lib/resolv.rb
end
##
# A DNS over HTTP resolver which follows RFC 8484
#
# This implementation uses HTTP POST with the application/dns-message.
class DoH < DNS
##
# Creates a new DNS over HTTP resolver with the +doh_uri+ of the resolver.
#
# +no_cache+ adds a Cache-Control header of "no-cache" to disable caching
# of HTTP responses.
#
# The three timeouts set their respective Net::HTTP timeouts.
def initialize(doh_uri,
no_cache: false,
open_timeout: 2,
write_timeout: 1,
read_timeout: 1)
@doh_uri = doh_uri
@no_cache = no_cache
@open_timeout = open_timeout
@write_timeout = write_timeout
@read_timeout = read_timeout
@mutex = Thread::Mutex.new
@initialized = nil
@http = nil
end
def lazy_initialize # :nodoc:
@mutex.synchronize {
next if @initialized
require "net/http"
require "uri"
@doh_uri = URI @doh_uri
@doh_uri += "/dns-query"
@http = Net::HTTP.new @doh_uri.hostname, @doh_uri.port
@http.use_ssl = "https" == @doh_uri.scheme.downcase
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
@http.open_timeout = @open_timeout
@http.write_timeout = @write_timeout
@http.read_timeout = @read_timeout
@initialized = true
}
end
##
# Looks up the first IP address for +name+.
def getaddress(name)
each_address(name) {|address| return address}
raise ResolvError.new("DNS over HTTP result has no information for #{name}")
end
##
# Looks up the hostname of +address+
def getname(address)
each_name(address) {|name| return name}
raise ResolvError.new("DNS over HTTP result has no information for #{address}")
end
def fetch_http(name, typeclass) # :nodoc:
lazy_initialize
req = request(name, typeclass)
res = @mutex.synchronize {
@http.start unless @http.started?
@http.request req
}
case res
when Net::HTTPSuccess
age = Integer res["Age"], exception: false
age = 0 unless age
age = 0 if age.negative?
reply = Resolv::DNS::Message.decode res.body
reply.answer.map! { |name, ttl, data|
new_data_ttl = data.ttl - age
data.instance_variable_set(:@ttl, new_data_ttl)
[name, ttl - age, data]
}
reply
when Net::HTTPGatewayTimeout
raise ResolvTimeout
else
raise ResolvError, "DNS over HTTP error #{res.message} (#{res.code}) for #{name}"
end
end
private :fetch_http
def fetch_resource(name, typeclass) # :nodoc:
reply = fetch_http(name, typeclass)
case reply.rcode
when RCode::NoError
if reply.tc == 1
# how?
raise ResolvError, "Truncated DNS over HTTP reply"
else
yield(reply, name)
end
return
when RCode::NXDomain
raise Config::NXDomain.new(name.to_s)
else
raise Config::OtherResolvError.new(name.to_s)
end
end
def request(name, typeclass) # :nodoc:
name = Name.create(name)
name = Name.create("#{name}.") unless name.absolute?
msg = Message.new
msg.rd = 1
msg.add_question(name, typeclass)
req = Net::HTTP::Post.new @doh_uri.path
req["Content-Type"] = "application/dns-message"
req["Accept"] = "application/dns-message"
req["Cache-Control"] = "no-cache" if @no_cache
req.body = msg.encode
req
end
private :request
end
module LOC
##
test/resolv/test_doh.rb
# frozen_string_literal: false
require 'test/unit'
require 'resolv'
class TestResolvDoH < Test::Unit::TestCase
def setup
@uri = "https://dns.example:853"
@doh = Resolv::DoH.new @uri
@doh.lazy_initialize
Resolv::DNS::Message.class_variable_set :@@identifier, -1
end
def stub_http
http = @doh.instance_variable_get(:@http)
def http.res=(res)
@res = res
end
def http.started?
true
end
def http.request(req)
@res
end
http
end
def fake_http_response(response_class, body, header = {})
res = response_class.allocate
res.initialize_http_header header
res.instance_variable_set(:@read, true)
res.body = body
res
end
def test_fetch_http_ok
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
resource = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
reply = Resolv::DNS::Message.new 1
reply.rd = 1
reply.ra = 1
reply.add_question(name, typeclass)
reply.add_answer(name, 30, resource)
header = { "Age" => "5" }
http = stub_http
res = fake_http_response Net::HTTPOK, reply.encode, header
http.res = res
reply = @doh.send(:fetch_http, name, typeclass)
_, _, res = reply.answer.first
assert_equal "192.0.2.1", res.address.to_s
assert_equal 25, res.ttl
end
def test_fetch_http_adjust_ttl
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
resource = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
reply = Resolv::DNS::Message.new 1
reply.rd = 1
reply.ra = 1
reply.add_question(name, typeclass)
reply.add_answer(name, 30, resource)
http = stub_http
http.res = fake_http_response Net::HTTPOK, reply.encode
reply = @doh.send(:fetch_http, name, typeclass)
_, _, res = reply.answer.first
assert_equal "192.0.2.1", res.address.to_s
end
def test_fetch_http_non_200
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
res = fake_http_response Net::HTTPBadRequest, ""
res.instance_variable_set(:@code, "400")
res.instance_variable_set(:@message, "BAD REQUEST")
http = stub_http
http.res = res
e = assert_raise Resolv::ResolvError do
@doh.send(:fetch_http, name, typeclass)
end
assert_equal "DNS over HTTP error BAD REQUEST (400) for www.example.com",
e.message
end
def test_fetch_http_gateway_timeout
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
http = stub_http
http.res = fake_http_response Net::HTTPGatewayTimeout, ""
assert_raise Resolv::ResolvTimeout do
@doh.send(:fetch_http, name, typeclass)
end
end
def test_fetch_resource
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
resource = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
reply = Resolv::DNS::Message.new 1
reply.rd = 1
reply.ra = 1
reply.add_question(name, typeclass)
reply.add_answer(name, 30, resource)
http = stub_http
http.res = fake_http_response Net::HTTPOK, reply.encode
yielded = false
@doh.fetch_resource(name, typeclass) do |rep, name|
yielded = true
_, _, res = rep.answer.first
assert_equal "192.0.2.1", res.address.to_s
end
assert yielded, "#fetch_resource did not yield to the block"
end
def test_fetch_resource_truncated
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
resource = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
reply = Resolv::DNS::Message.new 1
reply.rd = 1
reply.ra = 1
reply.tc = 1
reply.add_question(name, typeclass)
reply.add_answer(name, 30, resource)
http = stub_http
http.res = fake_http_response Net::HTTPOK, reply.encode
yielded = false
e = assert_raise Resolv::ResolvError do
@doh.fetch_resource(name, typeclass)
end
assert_equal "Truncated DNS over HTTP reply", e.message
end
def test_fetch_resource_nxdomain
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
resource = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
reply = Resolv::DNS::Message.new 1
reply.rd = 1
reply.ra = 1
reply.tc = 1
reply.add_question(name, typeclass)
reply.rcode = Resolv::DNS::RCode::NXDomain
http = stub_http
http.res = fake_http_response Net::HTTPOK, reply.encode
yielded = false
e = assert_raise Resolv::DNS::Config::NXDomain do
@doh.fetch_resource(name, typeclass)
end
assert_equal "www.example.com", e.message
end
def test_fetch_resource_other_error
name = Resolv::DNS::Name.create("www.example.com.")
typeclass = Resolv::DNS::Resource::IN::A
resource = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
reply = Resolv::DNS::Message.new 1
reply.rd = 1
reply.ra = 1
reply.tc = 1
reply.add_question(name, typeclass)
reply.rcode = Resolv::DNS::RCode::ServFail
http = stub_http
http.res = fake_http_response Net::HTTPOK, reply.encode
yielded = false
e = assert_raise Resolv::DNS::Config::OtherResolvError do
@doh.fetch_resource(name, typeclass)
end
assert_equal "www.example.com", e.message
end
def test_lazy_initialize
http = @doh.instance_variable_get(:@http)
assert_equal 2, http.open_timeout
assert_equal 1, http.write_timeout
assert_equal 1, http.read_timeout
end
def test_lazy_initialize_timeouts
@doh =
Resolv::DoH.new @uri, open_timeout: 3, write_timeout: 4, read_timeout: 5
@doh.lazy_initialize
http = @doh.instance_variable_get(:@http)
assert_equal 3, http.open_timeout
assert_equal 4, http.write_timeout
assert_equal 5, http.read_timeout
end
def test_request
name = "www.example.com"
typeclass = Resolv::DNS::Resource::IN::A
req = @doh.send(:request, name, typeclass)
assert_kind_of Net::HTTP::Post, req
assert_equal "/dns-query", req.path
assert_equal "application/dns-message", req["Content-Type"]
assert_equal "application/dns-message", req["Accept"]
message = Resolv::DNS::Message.new 0
message.rd = 1
message.add_question(Resolv::DNS::Name.create("#{name}."), typeclass)
expected = message.encode
assert_equal expected, req.body
end
def test_request_no_cache
@doh = Resolv::DoH.new @uri, no_cache: true
@doh.lazy_initialize
name = "www.example.com"
typeclass = Resolv::DNS::Resource::IN::A
req = @doh.send(:request, name, typeclass)
assert_kind_of Net::HTTP::Post, req
assert_equal "/dns-query", req.path
assert_equal "application/dns-message", req["Content-Type"]
assert_equal "application/dns-message", req["Accept"]
assert_equal "no-cache", req["Cache-Control"]
message = Resolv::DNS::Message.new 0
message.rd = 1
message.add_question(Resolv::DNS::Name.create("#{name}."), typeclass)
expected = message.encode
assert_equal expected, req.body
end
end
    (1-1/1)