Feature #16937 ยป 0001-Add-DNS-over-HTTP-resolver.patch
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
|