Index: src/query.cr ================================================================== --- src/query.cr +++ src/query.cr @@ -1,13 +1,13 @@ require "./dns_class" require "./rr_type" module AsyncDNS class Query - getter name : String - getter class : DNSClass - getter type : RRType + getter domain : String + getter dns_class : DNSClass + getter rr_type : RRType - def initialize(@name : String, @class : DNSClass, @type : RRType) + def initialize(@domain : String, @dns_class : DNSClass, @rr_type : RRType) end end end Index: src/resolver.cr ================================================================== --- src/resolver.cr +++ src/resolver.cr @@ -1,47 +1,131 @@ +require "socket" + +require "./query" +require "./response" require "./rr" require "./settings" -require "./response" module AsyncDNS class Resolver + @v6_sock : UDPSocket? + @v4_sock : UDPSocket? + getter settings : Settings enum Error NO_NAME_SERVER end - private record Context, id : UInt16, settings : Settings, - block : Response | Error -> + private class Context + getter query : Query + getter id : UInt16 + getter settings : Settings + getter block : Response | Error -> + property ns_index : Int32 + property attempt : Int32 + property used_ns : Socket::IPAddress | Nil + getter raw_data : Bytes + + def initialize(@query : Query, @id : UInt16, @settings : Settings, + @block : Response | Error ->) + @ns_index = 0 + @attempt = 0 + @used_ns = nil + @raw_data = Bytes.new(512) + + # Header + + i = 0 + @raw_data[i] = (@id >> 8).to_u8; i += 1 + @raw_data[i] = (@id & 0xFF).to_u8; i += 1 + # RD + @raw_data[i] = 1; i += 1 + i += 1 + # QDCOUNT + i += 1 + @raw_data[i] = 1; i += 1 + # ANCOUNT, NSCOUNT and ARCOUNT + i += 6 + + # Question + + # QNAME + @query.domain.split('.').each do |component| + if component.bytesize > 63 || i + component.bytesize > 512 + raise ArgumentError.new("Domain component too long") + end + + raw_component = component.to_slice + @raw_data[i] = raw_component.bytesize.to_u8; i += 1 + @raw_data[i, raw_component.bytesize].copy_from(raw_component) + i += raw_component.bytesize + end + + # QTYPE + qtype = @query.rr_type.to_i + @raw_data[i] = (qtype >> 8).to_u8; i += 1 + @raw_data[i] = (qtype & 0xFF).to_u8; i+= 1 + + # QCLASS + qclass = @query.dns_class.to_i + @raw_data[i] = (qclass >> 8).to_u8; i += 1 + @raw_data[i] = (qclass & 0xFF).to_u8; i += 1 + end + end def initialize @settings = Settings.new @queries = Hash(UInt16, Context).new @tcp_queries = Hash(Socket, Context).new end - def resolve(query : Query, &block : Response | Error ->) + def resolve(query : Query, &block : Response | Error ->) : Nil id : UInt16 while true id = Random::Secure.rand(UInt16::MIN..UInt16::MAX) break unless @queries.has_key?(id) end + + if query.domain.bytesize > 253 + raise ArgumentError.new("Queried domain is too long") + end if @settings.nameservers.empty? - yield Error::NO_NAME_SERVER + block.call(Error::NO_NAME_SERVER) return end - send(Context.new(id, settings.dup, block)) + send(Context.new(query.dup, id, settings.dup, block)) end - private def send(context : Context) - @queries[context.@id] = context + private def send(context : Context) : Nil + @queries[context.id] = context + + ns = context.settings.nameservers[context.ns_index] + context.used_ns = used_ns = Socket::IPAddress.new(ns, 53) - # TODO + sock : UDPSocket + case used_ns.family + when Socket::Family::INET6 + if @v6_sock.nil? + @v6_sock = s = UDPSocket.new + s.bind "::", 0 + end + sock = @v6_sock.not_nil! + when Socket::Family::INET + if @v4_sock.nil? + @v4_sock = s = UDPSocket.new + s.bind "0.0.0.0", 0 + end + sock = @v4_sock.not_nil! + else + raise ArgumentError.new("Nameserver must be INET or INET6") + end + sock.send(context.raw_data, used_ns) end - def stop + def stop : Nil # TODO end end end Index: src/response.cr ================================================================== --- src/response.cr +++ src/response.cr @@ -1,14 +1,14 @@ require "./rr" module AsyncDNS class Response - property name : String + property domain : String property answer : Array(RR) property authority : Array(RR) property additional : Array(RR) - def initialize(@name : String, @answer : Array(RR), @authority : Array(RR), - @additional : Array(RR)) + def initialize(@domain : String, @answer : Array(RR), + @authority : Array(RR), @additional : Array(RR)) end end end Index: src/rr.cr ================================================================== --- src/rr.cr +++ src/rr.cr @@ -1,15 +1,15 @@ require "socket" module AsyncDNS abstract struct RR property name : String - property class : DNSClass + property dns_class : DNSClass property type : RRType property ttl : UInt32 - def initialize(@name : String, @class : DNSClass, @type : RRType, + def initialize(@name : String, @dns_class : DNSClass, @type : RRType, @ttl : UInt32) end struct A < RR property address : Socket::IPAddress