Skip to content

Commit d12fe2a

Browse files
authored
Merge pull request #2 from taketo1113/doh-h1-net-http
Add DoH client (HTTP/1.1)
2 parents debedbf + dea51d5 commit d12fe2a

File tree

8 files changed

+252
-23
lines changed

8 files changed

+252
-23
lines changed

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ dot.aaaa
108108
=> ["2001:4860:4860::8844", "2001:4860:4860::8888"]
109109
```
110110

111+
- DoH (HTTP/1.1)
112+
```ruby
113+
doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', dohpath: '/dns-query{?dns}').lookup
114+
=> #<Ddig::Resolver::DohH1:0x00000001023ed020 @a=["8.8.4.4", "8.8.8.8"], @aaaa=["2001:4860:4860::8888", "2001:4860:4860::8844"], @address=nil, @dohpath="/dns-query{?dns}", @hostname="dns.google", @open_timeout=10, @port=443, @server="dns.google">
115+
116+
doh.a
117+
=> ["8.8.4.4", "8.8.8.8"]
118+
doh.aaaa
119+
=> ["2001:4860:4860::8844", "2001:4860:4860::8888"]
120+
```
121+
111122
### CLI
112123

113124
- UDP(Do53)
@@ -124,15 +135,28 @@ dns.google AAAA 2001:4860:4860::8888
124135
- DoT
125136
```sh
126137
$ ddig --dot --nameserver 8.8.8.8 dns.google
127-
dns.google A 8.8.4.4
128138
dns.google A 8.8.8.8
129-
dns.google AAAA 2001:4860:4860::8844
139+
dns.google A 8.8.4.4
130140
dns.google AAAA 2001:4860:4860::8888
141+
dns.google AAAA 2001:4860:4860::8844
131142

132143
# SERVER(Address): 8.8.8.8
133144
# PORT: 853
134145
```
135146

147+
- DoH (HTTP/1.1)
148+
```sh
149+
$ ddig --doh-h1 --nameserver dns.google --doh-path /dns-query{?dns} dns.google
150+
dns.google A 8.8.8.8
151+
dns.google A 8.8.4.4
152+
dns.google AAAA 2001:4860:4860::8888
153+
dns.google AAAA 2001:4860:4860::8844
154+
155+
# SERVER(Hostname): dns.google
156+
# SERVER(Path): /dns-query{?dns}
157+
# PORT: 443
158+
```
159+
136160
## Development
137161

138162
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

ddig.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ Gem::Specification.new do |spec|
3131
spec.require_paths = ["lib"]
3232

3333
spec.add_dependency "resolv", "~> 0.3.0"
34+
spec.add_dependency "base64" if RUBY_VERSION >= '3.3'
3435
end

lib/ddig.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require_relative "ddig/nameserver"
55
require_relative "ddig/resolver/do53"
66
require_relative "ddig/resolver/dot"
7+
require_relative "ddig/resolver/doh_h1"
78
require_relative "ddig/ddr"
89

910
module Ddig

lib/ddig/cli.rb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def parse_options
2626
opts.on("-t", "--type={all|do53|dot}", "resolve type (default: all)") { |v| @options[:type] = v }
2727
opts.on("--udp", "use resolve type of udp(do53)") { |v| @options[:type] = 'do53' }
2828
opts.on("--dot", "use resolve type of dot") { |v| @options[:type] = 'dot' }
29-
opts.on("-@", "--nameserver=ipaddress", "nameserver") { |v| @options[:nameserver] = v }
29+
opts.on("--doh-h1", "use resolve type of doh (http/1.1)") { |v| @options[:type] = 'doh_h1' }
30+
opts.on("--doh-path=doh-path", "doh service path") { |v| @options[:doh_path] = v }
31+
opts.on("-@", "--nameserver=ipaddress|doh-hostname", "nameserver") { |v| @options[:nameserver] = v }
3032
opts.on("-p", "--port=port", "port") { |v| @options[:port] = v }
3133
opts.on("--format={text|json}", "output format (default: text)") { |v| @options[:format] = v }
3234

@@ -50,6 +52,8 @@ def exec
5052
resolve_do53
5153
when "dot"
5254
resolve_dot
55+
when "doh_h1"
56+
resolve_doh_h1
5357
end
5458
end
5559

@@ -97,5 +101,28 @@ def resolve_dot
97101
#puts "# SERVER(Hostname): #{dot.server_name}"
98102
puts "# PORT: #{dot.port}"
99103
end
104+
105+
def resolve_doh_h1
106+
if @options[:nameserver].nil? || @options[:doh_path].nil?
107+
puts 'ddig: doh needs option of --doh-path=doh-path'
108+
exit
109+
end
110+
111+
doh = Ddig::Resolver::DohH1.new(hostname: @hostname, server: @options[:nameserver], dohpath: @options[:doh_path], port: @options[:port]).lookup
112+
113+
doh.a.each do |address|
114+
rr_type = 'A'
115+
puts "#{@hostname}\t#{rr_type}\t#{address}"
116+
end
117+
doh.aaaa.each do |address|
118+
rr_type = 'AAAA'
119+
puts "#{@hostname}\t#{rr_type}\t#{address}"
120+
end
121+
122+
puts
123+
puts "# SERVER(Hostname): #{doh.server}"
124+
puts "# SERVER(Path): #{doh.dohpath}"
125+
puts "# PORT: #{doh.port}"
126+
end
100127
end
101128
end

lib/ddig/resolver/dns_message.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
require 'resolv'
2+
3+
module Ddig
4+
module Resolver
5+
class DnsMessage
6+
def self.encode(hostname, typeclass)
7+
if hostname.nil?
8+
return nil
9+
end
10+
if typeclass.nil?
11+
return nil
12+
end
13+
14+
message = Resolv::DNS::Message.new
15+
message.rd = 1 # recursive query
16+
message.add_question(hostname, typeclass)
17+
18+
message.encode
19+
end
20+
21+
def self.decode(payload)
22+
if payload.nil?
23+
return nil
24+
end
25+
26+
Resolv::DNS::Message.decode(payload)
27+
end
28+
29+
def self.getresources(payload)
30+
if payload.nil?
31+
return []
32+
end
33+
34+
response = self.decode(payload)
35+
36+
return response.answer.map { |name, ttl, resource| resource }
37+
end
38+
end
39+
end
40+
end

lib/ddig/resolver/doh_h1.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
require 'net/http'
2+
require 'resolv'
3+
require 'base64'
4+
5+
require_relative 'dns_message'
6+
7+
module Ddig
8+
module Resolver
9+
# DNS over HTTPS (HTTP/1.1)
10+
class DohH1
11+
attr_reader :hostname, :server, :address, :dohpath, :port
12+
attr_reader :a, :aaaa
13+
14+
def initialize(hostname:, server:, address: nil, dohpath:, port: 443)
15+
@hostname = hostname
16+
@server = server
17+
@address = address
18+
@dohpath = dohpath
19+
@port = port || 443
20+
21+
@open_timeout = 10
22+
end
23+
24+
def lookup
25+
if @server.nil?
26+
return nil
27+
end
28+
29+
@a = get_resources(@hostname, Resolv::DNS::Resource::IN::A).map { |resource| resource.address.to_s if resource.is_a?(Resolv::DNS::Resource::IN::A) }.compact
30+
31+
@aaaa = get_resources(@hostname, Resolv::DNS::Resource::IN::AAAA).map { |resource| resource.address.to_s if resource.is_a?(Resolv::DNS::Resource::IN::AAAA) }.compact
32+
33+
self
34+
end
35+
36+
def get_resources(hostname, typeclass)
37+
# send query
38+
payload = DnsMessage.encode(hostname, typeclass)
39+
40+
path_with_query = @dohpath.gsub('{?dns}', '?dns=' + Base64.urlsafe_encode64(payload, padding: false))
41+
42+
http_response = Net::HTTP.start(@server, @port, use_ssl: true, ipaddr: @address) do |http|
43+
header = {}
44+
header['Accept'] = 'application/dns-message'
45+
#http.open_timeout = @open_timeout
46+
47+
http.get(path_with_query, header)
48+
end
49+
50+
case http_response
51+
when Net::HTTPSuccess
52+
# recive answer
53+
return DnsMessage.getresources(http_response.body)
54+
else
55+
http_response.value
56+
return []
57+
end
58+
end
59+
end
60+
end
61+
end

lib/ddig/resolver/dot.rb

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require 'openssl'
22
require 'resolv'
33

4+
require_relative 'dns_message'
5+
46
module Ddig
57
module Resolver
68
# DNS over TLS/TCP
@@ -33,16 +35,14 @@ def get_resources(hostname, typeclass)
3335
ssl_socket = get_socket
3436

3537
# send query
36-
message = dns_message(hostname, typeclass)
38+
payload = DnsMessage.encode(hostname, typeclass)
39+
request = [payload.length].pack('n') + payload
3740

38-
request = [message.encode.length].pack('n') + message.encode
3941
ssl_socket.write(request)
4042

4143
# recive answer
4244
len = ssl_socket.read(2).unpack1('n')
43-
response = Resolv::DNS::Message.decode(ssl_socket.read(len))
44-
45-
resources = response.answer.map { |name, ttl, resource| resource }
45+
resources = DnsMessage.getresources(ssl_socket.read(len))
4646

4747
resources
4848
end
@@ -74,21 +74,6 @@ def get_socket
7474
ssl_socket
7575
end
7676
end
77-
78-
def dns_message(hostname, typeclass)
79-
if hostname.nil?
80-
return nil
81-
end
82-
if typeclass.nil?
83-
return nil
84-
end
85-
86-
message = Resolv::DNS::Message.new
87-
message.rd = 1 # recursive query
88-
message.add_question(hostname, typeclass)
89-
90-
message
91-
end
9277
end
9378
end
9479
end

spec/ddig/resolver/doh_h1_spec.rb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Ddig::Resolver::DohH1 do
4+
context "#lookup" do
5+
before(:each) do
6+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', dohpath: '/dns-query{?dns}')
7+
@doh.lookup
8+
end
9+
10+
it "a/aaaa return values" do
11+
# a
12+
expect(@doh.a).to include "8.8.8.8"
13+
expect(@doh.a).to include "8.8.4.4"
14+
15+
# aaaa
16+
expect(@doh.aaaa).to include "2001:4860:4860::8844"
17+
expect(@doh.aaaa).to include "2001:4860:4860::8888"
18+
end
19+
end
20+
21+
context "#lookup: with server (ipv4) / address" do
22+
before(:each) do
23+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', address: '8.8.8.8', dohpath: '/dns-query{?dns}')
24+
@doh.lookup
25+
end
26+
27+
it "a/aaaa return values" do
28+
# a
29+
expect(@doh.a).to include "8.8.8.8"
30+
expect(@doh.a).to include "8.8.4.4"
31+
32+
# aaaa
33+
expect(@doh.aaaa).to include "2001:4860:4860::8844"
34+
expect(@doh.aaaa).to include "2001:4860:4860::8888"
35+
end
36+
end
37+
38+
context "#lookup: with server (ipv6) / address" do
39+
before(:each) do
40+
skip 'IPv6 is not available' unless enable_ipv6?
41+
42+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', address: '2001:4860:4860::8888', dohpath: '/dns-query{?dns}')
43+
@doh.lookup
44+
end
45+
46+
it "a/aaaa return values" do
47+
# a
48+
expect(@doh.a).to include "8.8.8.8"
49+
expect(@doh.a).to include "8.8.4.4"
50+
51+
# aaaa
52+
expect(@doh.aaaa).to include "2001:4860:4860::8844"
53+
expect(@doh.aaaa).to include "2001:4860:4860::8888"
54+
end
55+
end
56+
57+
context "#lookup: with invalid address" do
58+
before(:each) do
59+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'example.com', address: '8.8.8.8', dohpath: '/dns-query{?dns}')
60+
end
61+
62+
it "raise Error" do
63+
expect {
64+
@doh.lookup
65+
}.to raise_error(StandardError)
66+
# ruby2.7+: OpenSSL::SSL::SSLError: certificate verify failed (hostname mismatch)
67+
# ruby2.6: Net::HTTPServerException: 404 "Not Found"
68+
end
69+
end
70+
71+
context "set port attribute" do
72+
it "return default port without port" do
73+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', dohpath: '/dns-query{?dns}')
74+
75+
expect(@doh.port).to eq 443
76+
end
77+
78+
it "return default port wit nil value of port" do
79+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', dohpath: '/dns-query{?dns}', port: nil)
80+
81+
expect(@doh.port).to eq 443
82+
end
83+
84+
it "return port with port value" do
85+
@doh = Ddig::Resolver::DohH1.new(hostname: 'dns.google', server: 'dns.google', dohpath: '/dns-query{?dns}', port: 8443)
86+
87+
expect(@doh.port).to eq 8443
88+
end
89+
end
90+
end

0 commit comments

Comments
 (0)