mirror of
https://github.com/cmur2/dyndnsd.git
synced 2025-05-08 22:45:22 +02:00
159 lines
5.1 KiB
Ruby
159 lines
5.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'resolv'
|
|
require 'securerandom'
|
|
|
|
require 'async/dns'
|
|
|
|
module Dyndnsd
|
|
module Updater
|
|
class ZoneTransferServer
|
|
DEFAULT_SERVER_LISTENS = ['0.0.0.0@53'].freeze
|
|
|
|
# @param domain [String]
|
|
# @param updater_params [Hash{String => Object}]
|
|
def initialize(domain, updater_params)
|
|
@domain = domain
|
|
|
|
@server_listens = self.class.parse_endpoints(updater_params['server_listens'] || DEFAULT_SERVER_LISTENS)
|
|
@notify_targets = (updater_params['send_notifies'] || []).map { |e| self.class.parse_endpoints([e]) }
|
|
|
|
@zone_rr_ttl = updater_params['zone_ttl']
|
|
@zone_nameservers = updater_params['zone_nameservers'].map { |n| Resolv::DNS::Name.create(n) }
|
|
@zone_email_address = Resolv::DNS::Name.create(updater_params['zone_email_address'])
|
|
@zone_additional_ips = updater_params['zone_additional_ips'] || []
|
|
|
|
@server = ZoneTransferServerHelper.new(@server_listens, @domain)
|
|
|
|
# run Async::DNS server in background thread
|
|
Thread.new do
|
|
@server.run
|
|
end
|
|
end
|
|
|
|
# @param db [Dyndnsd::Database]
|
|
# @return [void]
|
|
def update(db)
|
|
Helper.span('updater_update') do |span|
|
|
span.set_tag('dyndnsd.updater.name', self.class.name&.split('::')&.last || 'None')
|
|
|
|
soa_rr = Resolv::DNS::Resource::IN::SOA.new(
|
|
@zone_nameservers[0], @zone_email_address,
|
|
db['serial'],
|
|
10_800, # 3h
|
|
300, # 5m
|
|
604_800, # 1w
|
|
3_600 # 1h
|
|
)
|
|
|
|
default_options = {ttl: @zone_rr_ttl}
|
|
|
|
# array containing all resource records for an AXFR request in the right order
|
|
rrs = []
|
|
# AXFR responses need to start with zone's SOA RR
|
|
rrs << [soa_rr, default_options]
|
|
|
|
# return RRs for all of the zone's nameservers
|
|
@zone_nameservers.each do |ns|
|
|
rrs << [Resolv::DNS::Resource::IN::NS.new(ns), default_options]
|
|
end
|
|
|
|
# return A/AAAA RRs for all additional IPv4s/IPv6s for the domain itself
|
|
@zone_additional_ips.each do |ip|
|
|
rrs << [create_addr_rr_for_ip(ip), default_options]
|
|
end
|
|
|
|
# return A/AAAA RRs for the dyndns hostnames
|
|
db['hosts'].each do |hostname, ips|
|
|
ips.each do |ip|
|
|
rrs << [create_addr_rr_for_ip(ip), default_options.merge({name: hostname})]
|
|
end
|
|
end
|
|
|
|
# AXFR responses need to end with zone's SOA RR again
|
|
rrs << [soa_rr, default_options]
|
|
|
|
# point Async::DNS server thread's variable to this new RR array
|
|
@server.axfr_rrs = rrs
|
|
|
|
# only send DNS NOTIFY if there really was a change
|
|
if db.changed?
|
|
send_dns_notify
|
|
end
|
|
end
|
|
end
|
|
|
|
# converts into suitable parameter form for Async::DNS::Resolver or Async::DNS::Server
|
|
#
|
|
# @param endpoint_list [Array{String}]
|
|
# @return [Array{Array{Object}}]
|
|
def self.parse_endpoints(endpoint_list)
|
|
endpoint_list.map { |addr_string| addr_string.split('@') }
|
|
.map { |addr_parts| [addr_parts[0], addr_parts[1].to_i || 53] }
|
|
.map { |addr| [:tcp, :udp].map { |type| [type] + addr } }
|
|
.flatten(1)
|
|
end
|
|
|
|
private
|
|
|
|
# creates correct Resolv::DNS::Resource object for IP address type
|
|
#
|
|
# @param ip_string [String]
|
|
# @return [Resolv::DNS::Resource::IN::A,Resolv::DNS::Resource::IN::AAAA]
|
|
def create_addr_rr_for_ip(ip_string)
|
|
ip = IPAddr.new(ip_string).native
|
|
|
|
if ip.ipv6?
|
|
Resolv::DNS::Resource::IN::AAAA.new(ip.to_s)
|
|
else
|
|
Resolv::DNS::Resource::IN::A.new(ip.to_s)
|
|
end
|
|
end
|
|
|
|
# https://tools.ietf.org/html/rfc1996
|
|
#
|
|
# @return [void]
|
|
def send_dns_notify
|
|
Async::Reactor.run do
|
|
@notify_targets.each do |notify_target|
|
|
target = Async::DNS::Resolver.new(notify_target)
|
|
|
|
# assemble DNS NOTIFY message
|
|
request = Resolv::DNS::Message.new(SecureRandom.random_number(2**16))
|
|
request.opcode = Resolv::DNS::OpCode::Notify
|
|
request.add_question("#{@domain}.", Resolv::DNS::Resource::IN::SOA)
|
|
|
|
_response = target.dispatch_request(request)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class ZoneTransferServerHelper < Async::DNS::Server
|
|
attr_accessor :axfr_rrs
|
|
|
|
def initialize(endpoints, domain)
|
|
super(endpoints, logger: Dyndnsd.logger)
|
|
@domain = domain
|
|
end
|
|
|
|
# @param name [String]
|
|
# @param resource_class [Resolv::DNS::Resource]
|
|
# @param transaction [Async::DNS::Transaction]
|
|
# @return [void]
|
|
def process(name, resource_class, transaction)
|
|
if name != @domain || resource_class != Resolv::DNS::Resource::Generic::Type252_Class1
|
|
transaction.fail!(:NXDomain)
|
|
return
|
|
end
|
|
|
|
# https://tools.ietf.org/html/rfc5936
|
|
transaction.append_question!
|
|
@axfr_rrs.each do |rr|
|
|
transaction.add([rr[0]], rr[1])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|