前言
最近需要写一个自动轮询生成指定域名的最快 IP 的工具,本身是一个小工具的属性,直接是用 Ruby 调试加上编写花费了三个小时完成了部分功能,过程比我想象地要难一点,这里直接放出来代码给大家参考
同时我也参考了这段 Gist :https://gist.github.com/jvns/1e5838a53520e45969687e2f90199770
效果
代码和部分讲解
# frozen_string_literal: true
require 'socket'
require 'stringio'
DNS_TYPES = {
1 => "A",
2 => "NS",
5 => "CNAME",
}
class DNSEncapsulationOfWriteOperations
# fixed format, just hardcode
private def make_question_header(query_id)
[query_id, 0x0100, 0x0001, 0x0000, 0x0000, 0x0000].pack('nnnnnn')
end
public def make_dns_query(domain, query_id)
question = domain.split('.').map { |label| [label.length, label].pack('Ca*') }.join + "\x00"
question << [0x0001, 0x0001].pack('nn') # query type and class
make_question_header(query_id) + question
end
end
class DNSEncapsulationOfReadOperations
def read_domain_name(buf)
domain = []
loop do
label_length = buf.read(1).unpack1('C')
break if label_length == 0
if (label_length & 0xc0) == 0xc0
# DNS compression
pointer_offset = ((label_length & 0x3f) << 8) + buf.read(1).unpack1('C')
old_pos = buf.pos
buf.pos = pointer_offset
domain << read_domain_name(buf)
buf.pos = old_pos
break
else
# Normal label
domain << buf.read(label_length)
end
end
domain.join('.')
end
def read_rdata(buf, length)
case DNS_TYPES[@type] || @type
when "CNAME", "NS"
read_domain_name(buf)
when "A"
buf.read(4).unpack1('C4')
else
buf.read(length)
end
end
end
class DNSHeader
attr_reader :id, :flags, :num_questions, :num_answers, :num_auth, :num_additional
def initialize(buf)
hdr = buf.read(12)
@id, @flags, @num_questions, @num_answers, @num_auth, @num_additional = hdr.unpack('nnnnnn')
end
end
class DNSRecord
attr_reader :name, :type, :class, :ttl, :rdlength, :rdata, :parsed_rdata
def initialize(buf)
@dns_read_ops = DNSEncapsulationOfReadOperations.new
@name = @dns_read_ops.read_domain_name(buf)
@type, @class, @ttl, @rdlength = buf.read(10).unpack('nnNn')
@parsed_rdata = read_rdata(buf, @rdlength)
end
def read_rdata(buf, length)
@type_name = DNS_TYPES[@type] || @type
if @type_name == "CNAME" or @type_name == "NS"
@dns_read_ops.read_domain_name(buf)
elsif @type_name == "A"
buf.read(length).unpack('C*').join('.')
else
buf.read(length)
end
end
def to_s
"#{@name}\t\t#{@ttl}\t#{@type_name}\t#{@parsed_rdata}"
end
end
class DNSQuery
attr_reader :domain, :type, :cls
def initialize(buf)
dns_read_ops = DNSEncapsulationOfReadOperations.new
@domain = dns_read_ops.read_domain_name(buf)
@type, @cls = buf.read(4).unpack('nn')
end
end
class DNSResponse
attr_reader :header, :queries, :answers, :authorities, :additionals
def initialize(bytes)
buf = StringIO.new(bytes) # 将字节数组打包成字符串并创建 StringIO 对象
@header = DNSHeader.new(buf)
@queries = (1..@header.num_questions).map { DNSQuery.new(buf) } if @header.num_questions > 0
@answers = (1..@header.num_answers).map { DNSRecord.new(buf) } if @header.num_answers > 0
@authorities = (1..@header.num_auth).map { DNSRecord.new(buf) } if @header.num_auth > 0
@additionals = (1..@header.num_additional).map { DNSRecord.new(buf) } if @header.num_additional > 0
end
end
def send_dns_query(sock, domain, query_id)
dns_write_ops = DNSEncapsulationOfWriteOperations.new
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, [3, 0].pack('l_2')) # timeout - 3s
sock.send(dns_write_ops.make_dns_query(domain, query_id), 0)
end
def receive_dns_response(sock)
reply, _ = sock.recvfrom(1024)
DNSResponse.new(reply)
end
module Resolvfast
class Error < StandardError; end
def self.main(domain)
begin
sock = UDPSocket.new
sock.bind('0.0.0.0', rand(10240..65535))
sock.connect('223.5.5.5', 53)
# send query
send_dns_query(sock, domain, 1)
# receive & parse response
response = receive_dns_response(sock)
print "Answers for #{domain}:\n"
response.answers.each do |record|
puts record
end
ensure
sock.close
end
end
end
if ARGV.empty?
puts "Usage: #{$PROGRAM_NAME} <domain>"
exit 1
end
Resolvfast.main(ARGV[0])
测试
$ ruby resolvfast.rb google.com
Answers for google.com:
google.com 143 A 172.217.160.110
$ ruby resolvfast.rb baidu.com
Answers for baidu.com:
baidu.com 212 A 39.156.66.10
baidu.com 212 A 110.242.68.66
注意要点
我在代码编写的时候,卡在 read_domain_name 非常之久,具体为可以从 WireShark 抓到返回的解析报文,但是整个程序卡死在了 Parse 阶段,因为 DNS 响应中的域名可以是两种形式,即一种是常规的标签序列,另一种使用了指针来压缩域名(以避免在同一个消息中多次出现相同的域名),如果只专注于第一种情况,可能会导致整个程序陷入死循环