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
2642 2642

  
2643 2643
  end
2644 2644

  
2645
  ##
2646
  # A DNS over HTTP resolver which follows RFC 8484
2647
  #
2648
  # This implementation uses HTTP POST with the application/dns-message.
2649

  
2650
  class DoH < DNS
2651

  
2652
    ##
2653
    # Creates a new DNS over HTTP resolver with the +doh_uri+ of the resolver.
2654
    #
2655
    # +no_cache+ adds a Cache-Control header of "no-cache" to disable caching
2656
    # of HTTP responses.
2657
    #
2658
    # The three timeouts set their respective Net::HTTP timeouts.
2659

  
2660
    def initialize(doh_uri,
2661
                   no_cache: false,
2662
                   open_timeout: 2,
2663
                   write_timeout: 1,
2664
                   read_timeout: 1)
2665
      @doh_uri       = doh_uri
2666
      @no_cache      = no_cache
2667
      @open_timeout  = open_timeout
2668
      @write_timeout = write_timeout
2669
      @read_timeout  = read_timeout
2670

  
2671
      @mutex = Thread::Mutex.new
2672
      @initialized = nil
2673
      @http = nil
2674
    end
2675

  
2676
    def lazy_initialize # :nodoc:
2677
      @mutex.synchronize {
2678
        next if @initialized
2679

  
2680
        require "net/http"
2681
        require "uri"
2682

  
2683
        @doh_uri = URI @doh_uri
2684
        @doh_uri += "/dns-query"
2685

  
2686
        @http = Net::HTTP.new @doh_uri.hostname, @doh_uri.port
2687
        @http.use_ssl = "https" == @doh_uri.scheme.downcase
2688
        @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
2689
        @http.open_timeout  = @open_timeout
2690
        @http.write_timeout = @write_timeout
2691
        @http.read_timeout  = @read_timeout
2692

  
2693
        @initialized = true
2694
      }
2695
    end
2696

  
2697
    ##
2698
    # Looks up the first IP address for +name+.
2699

  
2700
    def getaddress(name)
2701
      each_address(name) {|address| return address}
2702
      raise ResolvError.new("DNS over HTTP result has no information for #{name}")
2703
    end
2704

  
2705
    ##
2706
    # Looks up the hostname of +address+
2707

  
2708
    def getname(address)
2709
      each_name(address) {|name| return name}
2710
      raise ResolvError.new("DNS over HTTP result has no information for #{address}")
2711
    end
2712

  
2713
    def fetch_http(name, typeclass) # :nodoc:
2714
      lazy_initialize
2715

  
2716
      req = request(name, typeclass)
2717

  
2718
      res = @mutex.synchronize {
2719
        @http.start unless @http.started?
2720

  
2721
        @http.request req
2722
      }
2723

  
2724
      case res
2725
      when Net::HTTPSuccess
2726
        age = Integer res["Age"], exception: false
2727
        age = 0 unless age
2728
        age = 0 if age.negative?
2729

  
2730
        reply = Resolv::DNS::Message.decode res.body
2731
        reply.answer.map! { |name, ttl, data|
2732
          new_data_ttl = data.ttl - age
2733

  
2734
          data.instance_variable_set(:@ttl, new_data_ttl)
2735

  
2736
          [name, ttl - age, data]
2737
        }
2738
        reply
2739
      when Net::HTTPGatewayTimeout
2740
        raise ResolvTimeout
2741
      else
2742
        raise ResolvError, "DNS over HTTP error #{res.message} (#{res.code}) for #{name}"
2743
      end
2744
    end
2745

  
2746
    private :fetch_http
2747

  
2748
    def fetch_resource(name, typeclass) # :nodoc:
2749
      reply = fetch_http(name, typeclass)
2750

  
2751
      case reply.rcode
2752
      when RCode::NoError
2753
        if reply.tc == 1
2754
          # how?
2755
          raise ResolvError, "Truncated DNS over HTTP reply"
2756
        else
2757
          yield(reply, name)
2758
        end
2759
        return
2760
      when RCode::NXDomain
2761
        raise Config::NXDomain.new(name.to_s)
2762
      else
2763
        raise Config::OtherResolvError.new(name.to_s)
2764
      end
2765
    end
2766

  
2767
    def request(name, typeclass) # :nodoc:
2768
      name = Name.create(name)
2769
      name = Name.create("#{name}.") unless name.absolute?
2770

  
2771
      msg = Message.new
2772
      msg.rd = 1
2773
      msg.add_question(name, typeclass)
2774

  
2775
      req = Net::HTTP::Post.new @doh_uri.path
2776
      req["Content-Type"]  = "application/dns-message"
2777
      req["Accept"]        = "application/dns-message"
2778
      req["Cache-Control"] = "no-cache" if @no_cache
2779
      req.body = msg.encode
2780

  
2781
      req
2782
    end
2783

  
2784
    private :request
2785
  end
2786

  
2645 2787
  module LOC
2646 2788

  
2647 2789
    ##
test/resolv/test_doh.rb
1
# frozen_string_literal: false
2
require 'test/unit'
3
require 'resolv'
4

  
5
class TestResolvDoH < Test::Unit::TestCase
6
  def setup
7
    @uri = "https://dns.example:853"
8
    @doh = Resolv::DoH.new @uri
9
    @doh.lazy_initialize
10

  
11
    Resolv::DNS::Message.class_variable_set :@@identifier, -1
12
  end
13

  
14
  def stub_http
15
    http = @doh.instance_variable_get(:@http)
16

  
17
    def http.res=(res)
18
      @res = res
19
    end
20

  
21
    def http.started?
22
      true
23
    end
24

  
25
    def http.request(req)
26
      @res
27
    end
28

  
29
    http
30
  end
31

  
32
  def fake_http_response(response_class, body, header = {})
33
    res = response_class.allocate
34
    res.initialize_http_header header
35
    res.instance_variable_set(:@read, true)
36
    res.body = body
37
    res
38
  end
39

  
40
  def test_fetch_http_ok
41
    name      = Resolv::DNS::Name.create("www.example.com.")
42
    typeclass = Resolv::DNS::Resource::IN::A
43
    resource  = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
44

  
45
    reply = Resolv::DNS::Message.new 1
46
    reply.rd = 1
47
    reply.ra = 1
48
    reply.add_question(name, typeclass)
49
    reply.add_answer(name, 30, resource)
50

  
51
    header = { "Age" => "5" }
52

  
53
    http = stub_http
54
    res = fake_http_response Net::HTTPOK, reply.encode, header
55
    http.res = res
56

  
57
    reply = @doh.send(:fetch_http, name, typeclass)
58

  
59
    _, _, res = reply.answer.first
60

  
61
    assert_equal "192.0.2.1", res.address.to_s
62
    assert_equal 25, res.ttl
63
  end
64

  
65
  def test_fetch_http_adjust_ttl
66
    name      = Resolv::DNS::Name.create("www.example.com.")
67
    typeclass = Resolv::DNS::Resource::IN::A
68
    resource  = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
69

  
70
    reply = Resolv::DNS::Message.new 1
71
    reply.rd = 1
72
    reply.ra = 1
73
    reply.add_question(name, typeclass)
74
    reply.add_answer(name, 30, resource)
75

  
76
    http = stub_http
77
    http.res = fake_http_response Net::HTTPOK, reply.encode
78

  
79
    reply = @doh.send(:fetch_http, name, typeclass)
80

  
81
    _, _, res = reply.answer.first
82

  
83
    assert_equal "192.0.2.1", res.address.to_s
84
  end
85

  
86
  def test_fetch_http_non_200
87
    name      = Resolv::DNS::Name.create("www.example.com.")
88
    typeclass = Resolv::DNS::Resource::IN::A
89

  
90
    res = fake_http_response Net::HTTPBadRequest, ""
91
    res.instance_variable_set(:@code, "400")
92
    res.instance_variable_set(:@message, "BAD REQUEST")
93

  
94
    http = stub_http
95
    http.res = res
96

  
97
    e = assert_raise Resolv::ResolvError do
98
      @doh.send(:fetch_http, name, typeclass)
99
    end
100

  
101
    assert_equal "DNS over HTTP error BAD REQUEST (400) for www.example.com",
102
                 e.message
103
  end
104

  
105
  def test_fetch_http_gateway_timeout
106
    name      = Resolv::DNS::Name.create("www.example.com.")
107
    typeclass = Resolv::DNS::Resource::IN::A
108

  
109
    http = stub_http
110
    http.res = fake_http_response Net::HTTPGatewayTimeout, ""
111

  
112
    assert_raise Resolv::ResolvTimeout do
113
      @doh.send(:fetch_http, name, typeclass)
114
    end
115
  end
116

  
117
  def test_fetch_resource
118
    name      = Resolv::DNS::Name.create("www.example.com.")
119
    typeclass = Resolv::DNS::Resource::IN::A
120
    resource  = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
121

  
122
    reply = Resolv::DNS::Message.new 1
123
    reply.rd = 1
124
    reply.ra = 1
125
    reply.add_question(name, typeclass)
126
    reply.add_answer(name, 30, resource)
127

  
128
    http = stub_http
129
    http.res = fake_http_response Net::HTTPOK, reply.encode
130

  
131
    yielded = false
132

  
133
    @doh.fetch_resource(name, typeclass) do |rep, name|
134
      yielded = true
135

  
136
      _, _, res = rep.answer.first
137

  
138
      assert_equal "192.0.2.1", res.address.to_s
139
    end
140

  
141
    assert yielded, "#fetch_resource did not yield to the block"
142
  end
143

  
144
  def test_fetch_resource_truncated
145
    name      = Resolv::DNS::Name.create("www.example.com.")
146
    typeclass = Resolv::DNS::Resource::IN::A
147
    resource  = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
148

  
149
    reply = Resolv::DNS::Message.new 1
150
    reply.rd = 1
151
    reply.ra = 1
152
    reply.tc = 1
153
    reply.add_question(name, typeclass)
154
    reply.add_answer(name, 30, resource)
155

  
156
    http = stub_http
157
    http.res = fake_http_response Net::HTTPOK, reply.encode
158

  
159
    yielded = false
160

  
161
    e = assert_raise Resolv::ResolvError do
162
      @doh.fetch_resource(name, typeclass)
163
    end
164

  
165
    assert_equal "Truncated DNS over HTTP reply", e.message
166
  end
167

  
168
  def test_fetch_resource_nxdomain
169
    name      = Resolv::DNS::Name.create("www.example.com.")
170
    typeclass = Resolv::DNS::Resource::IN::A
171
    resource  = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
172

  
173
    reply = Resolv::DNS::Message.new 1
174
    reply.rd = 1
175
    reply.ra = 1
176
    reply.tc = 1
177
    reply.add_question(name, typeclass)
178
    reply.rcode = Resolv::DNS::RCode::NXDomain
179

  
180
    http = stub_http
181
    http.res = fake_http_response Net::HTTPOK, reply.encode
182

  
183
    yielded = false
184

  
185
    e = assert_raise Resolv::DNS::Config::NXDomain do
186
      @doh.fetch_resource(name, typeclass)
187
    end
188

  
189
    assert_equal "www.example.com", e.message
190
  end
191

  
192
  def test_fetch_resource_other_error
193
    name      = Resolv::DNS::Name.create("www.example.com.")
194
    typeclass = Resolv::DNS::Resource::IN::A
195
    resource  = Resolv::DNS::Resource::IN::A.new("192.0.2.1")
196

  
197
    reply = Resolv::DNS::Message.new 1
198
    reply.rd = 1
199
    reply.ra = 1
200
    reply.tc = 1
201
    reply.add_question(name, typeclass)
202
    reply.rcode = Resolv::DNS::RCode::ServFail
203

  
204
    http = stub_http
205
    http.res = fake_http_response Net::HTTPOK, reply.encode
206

  
207
    yielded = false
208

  
209
    e = assert_raise Resolv::DNS::Config::OtherResolvError do
210
      @doh.fetch_resource(name, typeclass)
211
    end
212

  
213
    assert_equal "www.example.com", e.message
214
  end
215

  
216
  def test_lazy_initialize
217
    http = @doh.instance_variable_get(:@http)
218

  
219
    assert_equal 2, http.open_timeout
220
    assert_equal 1, http.write_timeout
221
    assert_equal 1, http.read_timeout
222
  end
223

  
224
  def test_lazy_initialize_timeouts
225
    @doh =
226
      Resolv::DoH.new @uri, open_timeout: 3, write_timeout: 4, read_timeout: 5
227

  
228
    @doh.lazy_initialize
229

  
230
    http = @doh.instance_variable_get(:@http)
231

  
232
    assert_equal 3, http.open_timeout
233
    assert_equal 4, http.write_timeout
234
    assert_equal 5, http.read_timeout
235
  end
236

  
237
  def test_request
238
    name      = "www.example.com"
239
    typeclass = Resolv::DNS::Resource::IN::A
240

  
241
    req = @doh.send(:request, name, typeclass)
242

  
243
    assert_kind_of Net::HTTP::Post, req
244
    assert_equal "/dns-query", req.path
245
    assert_equal "application/dns-message", req["Content-Type"]
246
    assert_equal "application/dns-message", req["Accept"]
247

  
248
    message = Resolv::DNS::Message.new 0
249
    message.rd = 1
250
    message.add_question(Resolv::DNS::Name.create("#{name}."), typeclass)
251
    expected = message.encode
252

  
253
    assert_equal expected, req.body
254
  end
255

  
256
  def test_request_no_cache
257
    @doh = Resolv::DoH.new @uri, no_cache: true
258
    @doh.lazy_initialize
259

  
260
    name      = "www.example.com"
261
    typeclass = Resolv::DNS::Resource::IN::A
262

  
263
    req = @doh.send(:request, name, typeclass)
264

  
265
    assert_kind_of Net::HTTP::Post, req
266
    assert_equal "/dns-query", req.path
267
    assert_equal "application/dns-message", req["Content-Type"]
268
    assert_equal "application/dns-message", req["Accept"]
269
    assert_equal "no-cache", req["Cache-Control"]
270

  
271
    message = Resolv::DNS::Message.new 0
272
    message.rd = 1
273
    message.add_question(Resolv::DNS::Name.create("#{name}."), typeclass)
274
    expected = message.encode
275

  
276
    assert_equal expected, req.body
277
  end
278

  
279
end
0
-