dnsruby 1.73.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +16 -4
- data/DNSSEC +3 -2
- data/EXAMPLES +1 -1
- data/README.md +370 -64
- data/RELEASE_NOTES.md +314 -1
- data/Rakefile +0 -3
- data/demo/axfr.rb +24 -6
- data/dnsruby.gemspec +6 -4
- data/lib/dnsruby/cache.rb +279 -49
- data/lib/dnsruby/code_mapper.rb +35 -8
- data/lib/dnsruby/code_mappers.rb +63 -49
- data/lib/dnsruby/config.rb +77 -4
- data/lib/dnsruby/deadline_queue.rb +75 -0
- data/lib/dnsruby/dnssec.rb +148 -45
- data/lib/dnsruby/ipv6.rb +45 -5
- data/lib/dnsruby/key_cache.rb +202 -35
- data/lib/dnsruby/message/decoder.rb +85 -19
- data/lib/dnsruby/message/encoder.rb +27 -16
- data/lib/dnsruby/message/header.rb +8 -12
- data/lib/dnsruby/message/message.rb +53 -22
- data/lib/dnsruby/message/section.rb +12 -34
- data/lib/dnsruby/name.rb +5 -2
- data/lib/dnsruby/packet_sender.rb +664 -207
- data/lib/dnsruby/presentation.rb +147 -0
- data/lib/dnsruby/query_control.rb +132 -0
- data/lib/dnsruby/recursor.rb +798 -341
- data/lib/dnsruby/resolver.rb +472 -176
- data/lib/dnsruby/resource/CAA.rb +67 -16
- data/lib/dnsruby/resource/DLV.rb +6 -0
- data/lib/dnsruby/resource/DNSKEY.rb +108 -16
- data/lib/dnsruby/resource/DS.rb +27 -28
- data/lib/dnsruby/resource/HTTPS.rb +16 -0
- data/lib/dnsruby/resource/IN.rb +21 -2
- data/lib/dnsruby/resource/MX.rb +2 -2
- data/lib/dnsruby/resource/NSEC.rb +6 -10
- data/lib/dnsruby/resource/NSEC3.rb +55 -19
- data/lib/dnsruby/resource/NSEC3PARAM.rb +4 -6
- data/lib/dnsruby/resource/OPT.rb +2 -3
- data/lib/dnsruby/resource/RR.rb +108 -65
- data/lib/dnsruby/resource/RRSIG.rb +5 -5
- data/lib/dnsruby/resource/RRSet.rb +38 -22
- data/lib/dnsruby/resource/SRV.rb +2 -2
- data/lib/dnsruby/resource/SVCB.rb +775 -0
- data/lib/dnsruby/resource/TSIG.rb +266 -231
- data/lib/dnsruby/resource/TXT.rb +34 -42
- data/lib/dnsruby/resource/domain_name.rb +1 -1
- data/lib/dnsruby/resource/generic.rb +31 -0
- data/lib/dnsruby/resource/generic_cache.rb +384 -0
- data/lib/dnsruby/resource/generic_constants.rb +79 -0
- data/lib/dnsruby/select_thread.rb +986 -291
- data/lib/dnsruby/serial_time.rb +25 -0
- data/lib/dnsruby/single_resolver.rb +25 -6
- data/lib/dnsruby/single_verifier.rb +1620 -530
- data/lib/dnsruby/transport_clock.rb +9 -0
- data/lib/dnsruby/update.rb +8 -6
- data/lib/dnsruby/validation_budget.rb +143 -0
- data/lib/dnsruby/validator_thread.rb +139 -58
- data/lib/dnsruby/version.rb +1 -1
- data/lib/dnsruby/zone_reader.rb +196 -129
- data/lib/dnsruby/zone_transfer.rb +157 -25
- data/lib/dnsruby.rb +7 -2
- data/test/ci_watchdog.rb +33 -0
- data/test/fixtures/org-ds-2026-11-05.zone +5 -0
- data/test/fixtures/root-dnskey-2026-11-05.zone +7 -0
- data/test/fork_test_helper.rb +37 -0
- data/test/local_dns_server.rb +57 -0
- data/test/soak_server.rb +152 -0
- data/test/socket_helpers.rb +26 -0
- data/test/spec_helper.rb +8 -13
- data/test/tc_attempt_configuration.rb +137 -0
- data/test/tc_caa.rb +2 -2
- data/test/tc_cache_keys.rb +36 -0
- data/test/tc_cancelled_send_capacity.rb +158 -0
- data/test/tc_dnssec_alias_validation.rb +289 -0
- data/test/tc_dnssec_budget.rb +489 -0
- data/test/tc_dnssec_cache_bounds.rb +197 -0
- data/test/tc_dnssec_cache_lifetime.rb +615 -0
- data/test/tc_dnssec_denial_proofs.rb +285 -0
- data/test/tc_dnssec_recursion_limits.rb +186 -0
- data/test/tc_dnssec_recursor_review_fixes.rb +351 -0
- data/test/tc_dnssec_trust_anchors.rb +286 -0
- data/test/tc_dnssec_trust_races.rb +147 -0
- data/test/tc_dnssec_wildcard_proofs.rb +249 -0
- data/test/tc_ds.rb +5 -5
- data/test/tc_edns_retry_regressions.rb +255 -0
- data/test/tc_escapedchars.rb +8 -0
- data/test/tc_final_review_fixes.rb +167 -0
- data/test/tc_final_transport_contracts.rb +104 -0
- data/test/tc_generic_cache.rb +852 -0
- data/test/tc_header.rb +26 -0
- data/test/tc_ipv6.rb +120 -0
- data/test/tc_key_cache_identity.rb +382 -0
- data/test/tc_malformed_tcp.rb +712 -0
- data/test/tc_name.rb +99 -5
- data/test/tc_nameserver_bootstrap.rb +322 -0
- data/test/tc_nsec.rb +4 -45
- data/test/tc_nsec3.rb +17 -1
- data/test/tc_pipeline_retirement.rb +358 -0
- data/test/tc_pipeline_retry.rb +462 -0
- data/test/tc_pipeline_send.rb +663 -0
- data/test/tc_protocol_review_fixes.rb +95 -0
- data/test/tc_query_control.rb +90 -0
- data/test/tc_record_codec_contracts.rb +107 -0
- data/test/tc_record_regressions.rb +225 -0
- data/test/tc_recur.rb +1 -0
- data/test/tc_recursor_address_fallback.rb +169 -0
- data/test/tc_recursor_limits.rb +139 -0
- data/test/tc_release_auxiliary_fixes.rb +288 -0
- data/test/tc_release_configuration_fixes.rb +612 -0
- data/test/tc_release_dnssec_completion.rb +405 -0
- data/test/tc_release_dnssec_fixes.rb +305 -0
- data/test/tc_release_negative_cache.rb +327 -0
- data/test/tc_release_recursor_cache.rb +212 -0
- data/test/tc_release_review_transport.rb +372 -0
- data/test/tc_release_review_transport_native.rb +56 -0
- data/test/tc_release_transport.rb +906 -0
- data/test/tc_release_transport_fixes.rb +196 -0
- data/test/tc_release_zone_defaults.rb +157 -0
- data/test/tc_release_zone_ttls.rb +39 -0
- data/test/tc_res_config.rb +53 -2
- data/test/tc_res_opt.rb +21 -1
- data/test/tc_resolver.rb +34 -19
- data/test/tc_resolver_concurrency.rb +383 -0
- data/test/tc_resolver_configuration_contracts.rb +31 -0
- data/test/tc_resolver_validation.rb +211 -0
- data/test/tc_review_cache.rb +163 -0
- data/test/tc_review_code_mapper.rb +279 -0
- data/test/tc_review_control_deadlines.rb +169 -0
- data/test/tc_review_followup_transport.rb +583 -0
- data/test/tc_review_packet_contracts.rb +369 -0
- data/test/tc_review_presentation.rb +213 -0
- data/test/tc_review_record_compatibility.rb +101 -0
- data/test/tc_review_transport_fixes.rb +195 -0
- data/test/tc_review_transport_native.rb +415 -0
- data/test/tc_review_validation_priming.rb +178 -0
- data/test/tc_rr-opt.rb +5 -2
- data/test/tc_rr-unknown.rb +19 -0
- data/test/tc_rr.rb +14 -0
- data/test/tc_rrset_grouping.rb +50 -0
- data/test/tc_rrset_review_fixes.rb +58 -0
- data/test/tc_rrsig.rb +21 -1
- data/test/tc_scoped_ipv6.rb +153 -0
- data/test/tc_scoped_ipv6_select.rb +157 -0
- data/test/tc_scoped_ipv6_transport.rb +329 -0
- data/test/tc_selector_wakeup.rb +42 -0
- data/test/tc_soak.rb +118 -196
- data/test/tc_socket_helpers.rb +90 -0
- data/test/tc_supporting_lookup_limits.rb +252 -0
- data/test/tc_svcb.rb +1043 -0
- data/test/tc_svcb_signed_target.rb +145 -0
- data/test/tc_svcb_transport.rb +120 -0
- data/test/tc_tcp.rb +69 -43
- data/test/tc_tcp_pipelining.rb +95 -204
- data/test/tc_transport_fork.rb +179 -0
- data/test/tc_tsig.rb +2 -2
- data/test/tc_tsig_api.rb +251 -0
- data/test/tc_tsig_rfc8945.rb +357 -0
- data/test/tc_v2_release_dnssec.rb +442 -0
- data/test/tc_v2_release_transport.rb +304 -0
- data/test/tc_v2_zone_transfer_native.rb +58 -0
- data/test/tc_zone_reader.rb +217 -1
- data/test/tcp_pipelining_server.rb +150 -0
- data/test/ts_offline.rb +77 -6
- data/test/ts_online.rb +2 -1
- metadata +127 -14
- data/.coveralls.yml +0 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dnsruby
|
|
4
|
+
# Shared lexical handling for RR strings and zone-file continuation lines.
|
|
5
|
+
# Character-string escapes remain intact for the record-specific parser.
|
|
6
|
+
module Presentation #:nodoc: all
|
|
7
|
+
# Header tokens preserve escaped whitespace and punctuation in owners.
|
|
8
|
+
def self.tokens(text)
|
|
9
|
+
text.scan(/(?:\\.|[^\s])+/m)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Decimal escapes keep escaped whitespace intact through older record
|
|
13
|
+
# parsers and layout normalization, without changing the represented octet.
|
|
14
|
+
def self.escape_whitespace(text)
|
|
15
|
+
text.gsub(/\\(.)/m) do |escaped|
|
|
16
|
+
/\s/.match?(escaped[1]) ? format('\\%03d', escaped[1].ord) : escaped
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# TXT.parse historically accepts C-style escapes. Zone files instead use
|
|
21
|
+
# RFC 1035's literal nonnumeric escapes; express them as octets before
|
|
22
|
+
# handing the string to that compatibility parser.
|
|
23
|
+
def self.character_string_escapes(text)
|
|
24
|
+
text.gsub(/\\([^0-9])/m) do |escaped|
|
|
25
|
+
escaped[1].bytes.map { |octet| format('\\%03d', octet) }.join
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.unescaped_quote(text)
|
|
30
|
+
position = 0
|
|
31
|
+
while (token = /\\.|"/m.match(text, position))
|
|
32
|
+
return token.begin(0) if token[0] == '"'
|
|
33
|
+
position = token.end(0)
|
|
34
|
+
end
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# TXT (and its SPF subclass) historically also accept single quotes. Do
|
|
39
|
+
# not extend that convention to domain names or other RDATA formats, where
|
|
40
|
+
# an apostrophe is an ordinary character. Return the end of the type token
|
|
41
|
+
# so that even a leading apostrophe in the owner cannot open a TXT string.
|
|
42
|
+
def self.single_quoted_start(text, owner_omitted: false)
|
|
43
|
+
# Grouping parentheses can touch the type (TXT('value')), while an
|
|
44
|
+
# escaped parenthesis in the owner remains part of that owner's token.
|
|
45
|
+
fields = []
|
|
46
|
+
position = 0
|
|
47
|
+
while fields.length < 4 && (field = /\G[\s()]*((?:\\.|[^\s()])+)/m.match(text, position))
|
|
48
|
+
fields << [field[1], field.end(0)]
|
|
49
|
+
position = field.end(0)
|
|
50
|
+
end
|
|
51
|
+
fields.shift unless owner_omitted
|
|
52
|
+
# Numeric classes learned from received records must not make parsing
|
|
53
|
+
# unrelated records rebuild an ever-growing regular expression.
|
|
54
|
+
ttl_seen = class_seen = false
|
|
55
|
+
2.times do
|
|
56
|
+
token = fields.first&.first
|
|
57
|
+
if token && !ttl_seen && /\A\d/.match?(token)
|
|
58
|
+
ttl_seen = true
|
|
59
|
+
elsif token && !class_seen && (Classes.to_code(token) || /\ACLASS\d+\z/i.match?(token))
|
|
60
|
+
class_seen = true
|
|
61
|
+
else
|
|
62
|
+
break
|
|
63
|
+
end
|
|
64
|
+
fields.shift
|
|
65
|
+
end
|
|
66
|
+
type_token = fields.first&.first
|
|
67
|
+
type_code = Types.to_code(type_token) if type_token
|
|
68
|
+
if !type_code && (numeric = /\ATYPE(\d+)\z/i.match(type_token.to_s))
|
|
69
|
+
type_code = numeric[1].to_i
|
|
70
|
+
end
|
|
71
|
+
fields.first.last if [Types::TXT, Types::SPF].include?(type_code)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.scan(text, depth = 0, quoted = false, allow_unbalanced: false,
|
|
75
|
+
single_quoted_from: nil)
|
|
76
|
+
# Most zone records have no lexical syntax to interpret. Preserve the
|
|
77
|
+
# current continuation depth and only fold physical line endings.
|
|
78
|
+
if !quoted && !/[\\'"();]/.match?(text)
|
|
79
|
+
return [text.tr("\r\n", ' '), depth, false]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
result = +''
|
|
83
|
+
quoted = '"' if quoted == true
|
|
84
|
+
just_closed_quote = false
|
|
85
|
+
position = 0
|
|
86
|
+
|
|
87
|
+
# Copy ordinary text in runs. Quoted TXT values often contain hundreds
|
|
88
|
+
# of characters but only two delimiters; allocating one String for each
|
|
89
|
+
# character makes that common case unnecessarily expensive. Regexp and
|
|
90
|
+
# String offsets both count characters, including for Unicode owners.
|
|
91
|
+
while (syntax = /[\\'"();\r\n]/.match(text, position))
|
|
92
|
+
index = syntax.begin(0)
|
|
93
|
+
if index > position
|
|
94
|
+
result << text[position...index]
|
|
95
|
+
just_closed_quote = false
|
|
96
|
+
end
|
|
97
|
+
char = syntax[0]
|
|
98
|
+
position = index + 1
|
|
99
|
+
after_quote = just_closed_quote
|
|
100
|
+
just_closed_quote = false
|
|
101
|
+
|
|
102
|
+
if char == '\\'
|
|
103
|
+
result << char
|
|
104
|
+
if (escaped = text[position])
|
|
105
|
+
result << escaped
|
|
106
|
+
position += 1
|
|
107
|
+
end
|
|
108
|
+
elsif char == '"' || (single_quoted_from && index >= single_quoted_from && char == "'" &&
|
|
109
|
+
(quoted == "'" || (!quoted && (after_quote || index.zero? || /[\s(]/.match?(text[index - 1])))))
|
|
110
|
+
result << char
|
|
111
|
+
if quoted == char
|
|
112
|
+
quoted = false
|
|
113
|
+
just_closed_quote = true
|
|
114
|
+
elsif !quoted
|
|
115
|
+
quoted = char
|
|
116
|
+
end
|
|
117
|
+
elsif !quoted && char == ';'
|
|
118
|
+
# Comments end only at LF, matching the zone reader's physical
|
|
119
|
+
# line handling. Leave that newline for the ordinary layout branch.
|
|
120
|
+
position = text.index("\n", position) || text.length
|
|
121
|
+
elsif !quoted && (char == '(' || char == ')')
|
|
122
|
+
depth += char == '(' ? 1 : -1
|
|
123
|
+
if depth.negative? && !allow_unbalanced
|
|
124
|
+
raise DecodeError.new('Unmatched closing parenthesis in record')
|
|
125
|
+
end
|
|
126
|
+
# Parentheses separate tokens when neither neighbor is whitespace.
|
|
127
|
+
# Preserve existing spacing otherwise; some older RR parsers use
|
|
128
|
+
# offsets into the original RDATA rather than whitespace tokens.
|
|
129
|
+
if !result.empty? && /\S/.match?(result[-1]) &&
|
|
130
|
+
text[index + 1] && /\S/.match?(text[index + 1])
|
|
131
|
+
result << ' '
|
|
132
|
+
end
|
|
133
|
+
elsif !quoted && (char == "\n" || char == "\r")
|
|
134
|
+
# A physical line break outside a quoted value is layout, not an
|
|
135
|
+
# octet in a TXT string or part of an RFC3597 hexadecimal value.
|
|
136
|
+
result << ' '
|
|
137
|
+
else
|
|
138
|
+
result << char
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
result << text[position..-1] if position < text.length
|
|
142
|
+
|
|
143
|
+
[result, depth, quoted]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
private_constant :Presentation
|
|
147
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require 'dnsruby/transport_clock'
|
|
2
|
+
|
|
3
|
+
module Dnsruby
|
|
4
|
+
# A deadline and cancellation signal shared by related lookups. This carries
|
|
5
|
+
# no DNSSEC policy; the caller decides how a stopped operation is reported.
|
|
6
|
+
class QueryControl # :nodoc: all
|
|
7
|
+
# These must bypass ordinary lookup and verification fallback handlers.
|
|
8
|
+
class Stopped < StandardError; end
|
|
9
|
+
class Cancelled < Stopped; end
|
|
10
|
+
class DeadlineExceeded < Stopped; end
|
|
11
|
+
|
|
12
|
+
# A normal Queue whose producers also wake deadline/cancellation waiters.
|
|
13
|
+
# Keeping the notification alongside push avoids polling and works on Ruby
|
|
14
|
+
# versions which do not support Queue#pop(timeout:).
|
|
15
|
+
class Queue < ::Queue
|
|
16
|
+
def initialize
|
|
17
|
+
super
|
|
18
|
+
@wait_mutex = Mutex.new
|
|
19
|
+
@condition = ConditionVariable.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def push(value)
|
|
23
|
+
@wait_mutex.synchronize do
|
|
24
|
+
super
|
|
25
|
+
@condition.broadcast
|
|
26
|
+
end
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
alias << push
|
|
30
|
+
alias enq push
|
|
31
|
+
|
|
32
|
+
def close
|
|
33
|
+
@wait_mutex.synchronize do
|
|
34
|
+
super
|
|
35
|
+
@condition.broadcast
|
|
36
|
+
end
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wake_waiters
|
|
41
|
+
@wait_mutex.synchronize { @condition.broadcast }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pop_with_control(control)
|
|
45
|
+
control.add_waiter(self)
|
|
46
|
+
@wait_mutex.synchronize do
|
|
47
|
+
loop do
|
|
48
|
+
control.check!
|
|
49
|
+
begin
|
|
50
|
+
return pop(true)
|
|
51
|
+
rescue ThreadError
|
|
52
|
+
return nil if closed?
|
|
53
|
+
remaining = control.deadline && control.deadline - TransportClock.now
|
|
54
|
+
@condition.wait(@wait_mutex, remaining && [remaining, 0].max)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
ensure
|
|
59
|
+
control.remove_waiter(self)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attr_reader :deadline
|
|
64
|
+
|
|
65
|
+
def self.current
|
|
66
|
+
Thread.current[:dnsruby_query_control]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.with(control)
|
|
70
|
+
previous = current
|
|
71
|
+
Thread.current[:dnsruby_query_control] = control
|
|
72
|
+
control.check!
|
|
73
|
+
result = yield
|
|
74
|
+
control.check!
|
|
75
|
+
result
|
|
76
|
+
ensure
|
|
77
|
+
Thread.current[:dnsruby_query_control] = previous
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def initialize(deadline: nil, parent: nil)
|
|
81
|
+
@parent = parent
|
|
82
|
+
@deadline = [deadline, parent&.deadline].compact.min
|
|
83
|
+
@cancelled = false
|
|
84
|
+
@mutex = Mutex.new
|
|
85
|
+
@waiters = []
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cancel
|
|
89
|
+
waiters = @mutex.synchronize do
|
|
90
|
+
@cancelled = true
|
|
91
|
+
@waiters.dup
|
|
92
|
+
end
|
|
93
|
+
waiters.each(&:wake_waiters)
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check!
|
|
98
|
+
@parent&.check!
|
|
99
|
+
raise Cancelled, 'Query cancelled' if @mutex.synchronize { @cancelled }
|
|
100
|
+
raise DeadlineExceeded, 'Query deadline exceeded' if @deadline && TransportClock.now >= @deadline
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def add_waiter(queue)
|
|
105
|
+
@parent&.add_waiter(queue)
|
|
106
|
+
@mutex.synchronize { @waiters << queue }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def remove_waiter(queue)
|
|
110
|
+
@mutex.synchronize do
|
|
111
|
+
index = @waiters.index(queue)
|
|
112
|
+
@waiters.delete_at(index) if index
|
|
113
|
+
end
|
|
114
|
+
@parent&.remove_waiter(queue)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def pop(queue)
|
|
118
|
+
return queue.pop_with_control(self) if queue.is_a?(Queue)
|
|
119
|
+
|
|
120
|
+
# Preserve support for caller-supplied ordinary queues, whose producers
|
|
121
|
+
# cannot notify this control. Internal lookups use the queue above.
|
|
122
|
+
loop do
|
|
123
|
+
check!
|
|
124
|
+
begin
|
|
125
|
+
return queue.pop(true)
|
|
126
|
+
rescue ThreadError
|
|
127
|
+
sleep(@deadline ? (@deadline - TransportClock.now).clamp(0, 0.01) : 0.01)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|