Skip to content

Commit 44ca8c7

Browse files
committed
feat: better non-blocking guarantees
when using crystal lang 1.19+
1 parent a2304fa commit 44ca8c7

File tree

5 files changed

+64
-6
lines changed

5 files changed

+64
-6
lines changed

shard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: dns
2-
version: 1.2.0
2+
version: 1.3.0
33

44
development_dependencies:
55
ameba:

spec/dns_spec.cr

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ describe DNS do
1212
response.first.ip_address.address.should eq "127.0.0.1"
1313
end
1414

15+
it "should not perform queries for IP addresses" do
16+
response = DNS.query("127.0.0.1", [DNS::RecordType::A])
17+
response.size.should eq 1
18+
response.first.ip_address.address.should eq "127.0.0.1"
19+
20+
response = DNS.query("::1", [DNS::RecordType::AAAA])
21+
response.size.should eq 1
22+
response.first.ip_address.address.should eq "::1"
23+
24+
response = DNS.query("127.0.0.1", [DNS::RecordType::AAAA])
25+
response.size.should eq 0
26+
27+
response = DNS.query("::1", [DNS::RecordType::A])
28+
response.size.should eq 0
29+
end
30+
1531
it "queries for A, AAAA and SVCB records" do
1632
response = DNS.query(
1733
"www.microsoft.com",

spec/ext_spec.cr

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
describe DNS do
2+
it "should resolve hostnames using the ext methods" do
3+
client = TCPSocket.new("www.google.com", 80)
4+
client.close
5+
end
6+
7+
it "should resolve ips using the ext methods" do
8+
channel = Channel(Nil).new(1)
9+
server = TCPServer.new("127.0.0.1", 1234)
10+
spawn do
11+
channel.send(nil)
12+
while server.accept?
13+
end
14+
end
15+
16+
channel.receive
17+
client = TCPSocket.new("127.0.0.1", 1234)
18+
client.close
19+
server.close
20+
channel.close
21+
end
22+
end

src/dns.cr

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,18 @@ module DNS
9898
#
9999
# NOTE:: A or AAAA answers may include cname and other records that are not directly relevent to the query.
100100
# It is up to the consumer to filter for the relevant results
101+
# ameba:disable Metrics/CyclomaticComplexity
101102
def self.query(domain : String, query_records : Enumerable(RecordType | UInt16), &) : Nil
103+
if Socket::IPAddress.valid?(domain)
104+
record_type = Socket::IPAddress.valid_v4?(domain) ? RecordType::A : RecordType::AAAA
105+
record_type_int = record_type.to_u16
106+
if query_records.includes?(record_type) || query_records.includes?(record_type_int)
107+
resource = record_type.a? ? Resource::A.new(domain) : Resource::AAAA.new(domain)
108+
yield DNS::Packet::ResourceRecord.new(domain, record_type_int, ClassCode::Internet.to_u16, 1.day, resource)
109+
end
110+
return
111+
end
112+
102113
# RFC 3986 says:
103114
# > When a non-ASCII registered name represents an internationalized domain name
104115
# > intended for resolution via the DNS, the name must be transformed to the IDNA

src/dns/ext/addrinfo.cr

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
require "socket"
22
require "../../dns"
33

4+
{% begin %}
45
struct Socket::Addrinfo
56
QUERY_INET = [DNS::Resource::A::RECORD_TYPE]
67
QUERY_INET6 = [DNS::Resource::AAAA::RECORD_TYPE]
78
QUERY_UNSPEC = [DNS::Resource::AAAA::RECORD_TYPE, DNS::Resource::A::RECORD_TYPE]
89

9-
private def self.getaddrinfo(domain, service, family, type, protocol, timeout, &)
10+
private def self.getaddrinfo(domain, service, family, type, protocol, timeout,
11+
{% if compare_versions(Crystal::VERSION, "1.19.0") >= 0 %}
12+
flags = 0,
13+
{% end %}
14+
&)
1015
# fallback to the original implementation in these cases
11-
if family.unix? || Socket::IPAddress.valid?(domain) || domain.includes?('/') || DNS.select_resolver(domain).is_a?(DNS::Resolver::System)
16+
if family.unix? || domain.includes?('/') || DNS.select_resolver(domain).is_a?(DNS::Resolver::System)
1217
domain = URI::Punycode.to_ascii domain
1318
Crystal::System::Addrinfo.getaddrinfo(domain, service, family, type, protocol, timeout) do |addrinfo|
1419
yield addrinfo
@@ -35,9 +40,12 @@ struct Socket::Addrinfo
3540
ip_address = record.ip_address.address
3641
found = true
3742

38-
# NOTE:: ideally we set AI_NUMERICHOST, supported on all platforms, to ensure no blocking takes place
39-
# currently not possible in crystal as we don't have direct access to `ai_flags` field
40-
Crystal::System::Addrinfo.getaddrinfo(ip_address, service, family, type, protocol, timeout) do |addrinfo|
43+
# We set AI_NUMERICHOST, supported on all platforms, to ensure no blocking takes place
44+
Crystal::System::Addrinfo.getaddrinfo(ip_address, service, family, type, protocol, timeout,
45+
{% if compare_versions(Crystal::VERSION, "1.19.0") >= 0 %}
46+
::LibC::AI_NUMERICHOST
47+
{% end %}
48+
) do |addrinfo|
4149
yield addrinfo
4250
end
4351
end
@@ -50,3 +58,4 @@ struct Socket::Addrinfo
5058
raise Socket::Addrinfo::Error.new(message: "Hostname lookup for #{domain} failed: No address found", cause: error)
5159
end
5260
end
61+
{% end %}

0 commit comments

Comments
 (0)