2020-12-13 03:56:32 +01:00
|
|
|
require "ipaddress"
|
2020-12-09 19:01:33 +01:00
|
|
|
|
|
|
|
# Store a DNS zone.
|
|
|
|
class DNSManager::Storage::Zone
|
|
|
|
include JSON::Serializable
|
|
|
|
|
|
|
|
property domain : String
|
|
|
|
property resources = [] of DNSManager::Storage::Zone::ResourceRecord
|
2023-05-08 16:36:21 +02:00
|
|
|
property current_rrid : UInt32 = 0
|
2020-12-09 19:01:33 +01:00
|
|
|
|
2020-12-13 05:56:29 +01:00
|
|
|
# We don't want to accept less than 30 seconds TTL.
|
|
|
|
class_property ttl_limit_min = 30
|
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
def initialize(@domain)
|
|
|
|
end
|
|
|
|
|
2020-12-12 05:38:16 +01:00
|
|
|
alias Error = String
|
2020-12-09 19:01:33 +01:00
|
|
|
|
|
|
|
# Store a Resource Record: A, AAAA, TXT, PTR, CNAME…
|
|
|
|
abstract class ResourceRecord
|
|
|
|
include JSON::Serializable
|
|
|
|
|
2020-12-09 23:38:28 +01:00
|
|
|
use_json_discriminator "rrtype", {
|
2020-12-09 19:01:33 +01:00
|
|
|
a: A,
|
|
|
|
aaaa: AAAA,
|
|
|
|
soa: SOA,
|
|
|
|
txt: TXT,
|
|
|
|
ptr: PTR,
|
|
|
|
ns: NS,
|
|
|
|
cname: CNAME,
|
|
|
|
mx: MX,
|
|
|
|
srv: SRV
|
|
|
|
}
|
|
|
|
|
|
|
|
# Used to discriminate between classes.
|
2020-12-12 05:38:16 +01:00
|
|
|
property rrtype : String = ""
|
2020-12-09 19:01:33 +01:00
|
|
|
|
2023-05-08 16:36:21 +02:00
|
|
|
property rrid : UInt32 = 0
|
2020-12-09 19:01:33 +01:00
|
|
|
property name : String
|
|
|
|
property ttl : UInt32
|
|
|
|
property target : String
|
|
|
|
|
2023-06-28 02:25:43 +02:00
|
|
|
# RR entries can be writable or read only.
|
|
|
|
# For example, default SOA and NS entries shouldn't be writable.
|
|
|
|
# No need to allow for (mostly unskilled) users to mess up some ABSOLUTELY NECESSARY entries.
|
|
|
|
# Yes. It already happened. Many, MANY times. I WANT MY FUCKING TIME BACK.
|
|
|
|
property readonly : Bool = false
|
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
# zone class is omited, it always will be IN in our case.
|
|
|
|
def initialize(@name, @ttl, @target)
|
2023-06-28 00:55:39 +02:00
|
|
|
@rrtype = self.class.name.upcase.gsub /DNSMANAGER::STORAGE::ZONE::/, ""
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-12 05:38:16 +01:00
|
|
|
|
2020-12-13 03:56:32 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
[] of Error
|
2020-12-12 05:38:16 +01:00
|
|
|
end
|
2023-05-08 04:01:33 +02:00
|
|
|
|
|
|
|
def to_s(io : IO)
|
2023-06-28 00:55:39 +02:00
|
|
|
io << "(#{ "%4d" % @rrid }) #{ "%30s" % @name} #{ "%6d" % @ttl} IN #{ "%10s" % @rrtype } #{ "%30s" % @target}\n"
|
2023-05-08 04:01:33 +02:00
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class SOA < ResourceRecord
|
|
|
|
# Start of Authority
|
|
|
|
property mname : String # Master Name Server for the zone.
|
|
|
|
property rname : String # admin email address john.doe@example.com => john\.doe.example.com
|
2023-06-28 00:55:39 +02:00
|
|
|
property serial : UInt64 = 0 # Number for tracking new versions of the zone (master-slaves).
|
|
|
|
property refresh : UInt64 = 86400 # #seconds before requesting new zone version (master-slave).
|
|
|
|
property retry : UInt64 = 7200 # #seconds before retry accessing new data from the master.
|
|
|
|
property expire : UInt64 = 3600000 # #seconds slaves should consider master dead.
|
|
|
|
property minttl : UInt64 = 600 # #seconds slaves should consider master dead.
|
|
|
|
|
|
|
|
def initialize(@name, @ttl, @mname, @rname,
|
|
|
|
@serial = 0.to_u64,
|
|
|
|
@refresh = 86400.to_u64,
|
|
|
|
@retry = 7200.to_u64,
|
|
|
|
@expire = 3600000.to_u64,
|
|
|
|
@minttl = 600.to_u64)
|
|
|
|
@target = "" # no use
|
|
|
|
@rrtype = "SOA"
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-14 16:21:28 +01:00
|
|
|
|
2023-05-08 04:01:33 +02:00
|
|
|
def to_s(io : IO)
|
2023-06-28 00:55:39 +02:00
|
|
|
io << "(#{ "%4d" % @rrid }) #{name} #{ttl} IN SOA (#{mname} #{rname}\n"
|
|
|
|
io << "\t\t#{ "%10d" % serial } ; serial\n"
|
|
|
|
io << "\t\t#{ "%10d" % refresh } ; refresh\n"
|
|
|
|
io << "\t\t#{ "%10d" % retry } ; retry\n"
|
|
|
|
io << "\t\t#{ "%10d" % expire } ; expire\n"
|
|
|
|
io << "\t\t#{ "%10d" % minttl } ; minttl\n"
|
2023-05-08 04:01:33 +02:00
|
|
|
io << "\t)\n"
|
|
|
|
end
|
|
|
|
|
2020-12-14 16:21:28 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
# TODO: name
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
|
|
|
errors << "SOA invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# TODO: target
|
|
|
|
# TODO: mname
|
|
|
|
# TODO: rname
|
|
|
|
# TODO: serial
|
|
|
|
# TODO: refresh
|
|
|
|
# TODO: retry
|
|
|
|
# TODO: expire
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class A < ResourceRecord
|
2020-12-13 03:56:32 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @name
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "A invalid subdomain: #{@name}"
|
2020-12-13 03:56:32 +01:00
|
|
|
end
|
|
|
|
|
2020-12-13 05:56:29 +01:00
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "A invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
2020-12-13 03:56:32 +01:00
|
|
|
|
|
|
|
unless Zone.is_ipv4_address_valid? @target
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "A target not valid ipv4: #{@target}"
|
2020-12-13 03:56:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
class AAAA < ResourceRecord
|
2020-12-13 03:56:32 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @name
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "AAAA invalid subdomain: #{@name}"
|
2020-12-13 03:56:32 +01:00
|
|
|
end
|
|
|
|
|
2020-12-13 05:56:29 +01:00
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "AAAA invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
2020-12-13 03:56:32 +01:00
|
|
|
|
|
|
|
unless Zone.is_ipv6_address_valid? @target
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "AAAA target not valid ipv6: #{@target}"
|
2020-12-13 03:56:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
class TXT < ResourceRecord
|
2020-12-13 05:56:29 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @name
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "TXT invalid subdomain: #{@name}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "TXT invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
class PTR < ResourceRecord
|
2020-12-13 05:56:29 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
2020-12-14 16:21:28 +01:00
|
|
|
# TODO: PTR name verification.
|
|
|
|
# PTR name is different from others.
|
|
|
|
# Its name contains numerical-only subdomains.
|
|
|
|
unless Zone.is_ptr_name_valid? @target
|
|
|
|
errors << "PTR invalid subdomain: #{@target}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# The PTR name has to end with in-addr.arpa or ip6.arpa.
|
|
|
|
unless @name.ends_with?(/in-addr.arpa\.?/) || @name.ends_with?(/ip6.arpa\.?/)
|
|
|
|
errors << "PTR invalid name: doesn't end with 'in-addr.arpa' or 'ip6.arpa'"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "PTR invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
|
|
|
end
|
|
|
|
|
|
|
|
unless Zone.is_domain_valid? @target
|
|
|
|
errors << "PTR invalid subdomain: #{@target}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
2020-12-14 16:21:28 +01:00
|
|
|
|
2020-12-13 05:56:29 +01:00
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
class NS < ResourceRecord
|
2020-12-13 05:56:29 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @name
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "NS invalid subdomain: #{@name}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "NS invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
class CNAME < ResourceRecord
|
2020-12-13 05:56:29 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @name
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "CNAME invalid subdomain: #{@name}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "CNAME invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @target
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "CNAME invalid target: #{@target}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class MX < ResourceRecord
|
|
|
|
property priority : UInt32 = 10
|
|
|
|
def initialize(@name, @ttl, @target, @priority = 10)
|
2023-06-28 00:55:39 +02:00
|
|
|
@rrtype = "MX"
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
|
2023-05-08 04:01:33 +02:00
|
|
|
def to_s(io : IO)
|
2023-05-08 17:34:50 +02:00
|
|
|
io << "(#{ "%4d" % @rrid }) #{ "%30s" % @name} #{ "%6d" % @ttl} IN MX #{ "%3d" % @priority} #{ "%30s" % @target}\n"
|
2023-05-08 04:01:33 +02:00
|
|
|
end
|
|
|
|
|
2020-12-13 05:56:29 +01:00
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
unless Zone.is_subdomain_valid? @name
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "MX invalid subdomain: #{@name}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
2020-12-14 16:21:28 +01:00
|
|
|
errors << "MX invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
2023-05-08 04:01:33 +02:00
|
|
|
# MX target can either be a subdomain or a FQDN.
|
|
|
|
unless Zone.is_subdomain_valid? @target
|
|
|
|
unless Zone.is_domain_valid? @target
|
|
|
|
errors << "MX invalid target (domain): #{@target}"
|
|
|
|
end
|
2020-12-13 05:56:29 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class SRV < ResourceRecord
|
|
|
|
property port : UInt16
|
|
|
|
property protocol : String = "tcp"
|
|
|
|
property priority : UInt32 = 10
|
|
|
|
property weight : UInt32 = 10
|
|
|
|
def initialize(@name, @ttl, @target, @port, @protocol = "tcp", @priority = 10, @weight = 10)
|
2023-06-28 00:55:39 +02:00
|
|
|
@rrtype = "SRV"
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-14 16:21:28 +01:00
|
|
|
|
|
|
|
def get_errors : Array(Error)
|
|
|
|
errors = [] of Error
|
|
|
|
|
|
|
|
# SRV name should be created from scratch, client should send an empty name.
|
|
|
|
# WON'T FIX: name verification.
|
|
|
|
|
|
|
|
if @ttl < Zone.ttl_limit_min
|
|
|
|
errors << "SRV invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}"
|
|
|
|
end
|
|
|
|
|
|
|
|
unless Zone.is_domain_valid? @target
|
|
|
|
errors << "SRV invalid target (domain): #{@target}"
|
|
|
|
end
|
|
|
|
|
|
|
|
errors
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
|
|
|
|
2023-05-08 16:36:21 +02:00
|
|
|
def <<(rr : ResourceRecord)
|
|
|
|
rr.rrid = current_rrid
|
|
|
|
@resources << rr
|
|
|
|
@current_rrid += 1
|
|
|
|
end
|
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
def to_s(io : IO)
|
2023-05-08 04:01:33 +02:00
|
|
|
io << "domain: #{@domain}\n"
|
|
|
|
@resources.each do |rr|
|
|
|
|
io << rr
|
|
|
|
end
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|
2020-12-12 05:38:16 +01:00
|
|
|
|
2023-05-07 05:06:15 +02:00
|
|
|
def get_errors? : Array(Error)?
|
2020-12-12 05:38:16 +01:00
|
|
|
errors = [] of Error
|
|
|
|
unless Zone.is_domain_valid? @domain
|
2020-12-13 05:56:29 +01:00
|
|
|
errors << "invalid domain #{@domain}"
|
2020-12-12 05:38:16 +01:00
|
|
|
end
|
|
|
|
|
2020-12-14 16:21:28 +01:00
|
|
|
# Checking each resource.
|
2020-12-12 05:38:16 +01:00
|
|
|
@resources.each do |r|
|
2020-12-13 03:56:32 +01:00
|
|
|
r.get_errors().each do |error|
|
2020-12-12 05:38:16 +01:00
|
|
|
errors << error
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-12-14 16:21:28 +01:00
|
|
|
# Minimal resources: SOA and NS.
|
|
|
|
unless @resources.any? &.is_a?(Zone::SOA)
|
|
|
|
errors << "invalid zone: no SOA record"
|
|
|
|
end
|
|
|
|
unless @resources.any? &.is_a?(Zone::NS)
|
|
|
|
errors << "invalid zone: no NS record"
|
|
|
|
end
|
|
|
|
|
2023-05-07 05:06:15 +02:00
|
|
|
if errors.empty?
|
|
|
|
nil
|
|
|
|
else
|
|
|
|
errors
|
|
|
|
end
|
2020-12-12 05:38:16 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# This regex only is "good enough for now".
|
|
|
|
def self.is_domain_valid?(domain) : Bool
|
|
|
|
if domain =~ /^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*((xn--)?[a-z0-9][a-z0-9\-]{0,60}[a-z0-9]|(xn--)?[a-z0-9]{1,60})\.[a-z]{2,}$/
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
rescue e
|
|
|
|
Baguette::Log.error "invalid zone domain #{domain}: #{e}"
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2020-12-13 03:56:32 +01:00
|
|
|
# This regex only is "good enough for now".
|
|
|
|
def self.is_subdomain_valid?(subdomain) : Bool
|
|
|
|
if subdomain =~ /^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z0-9][a-z0-9\-]{0,60}[a-z0-9]*[a-z]+|[a-z]+|[a-z][a-z0-9]+[a-z]+$/
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
rescue e
|
|
|
|
Baguette::Log.error "invalid zone subdomain #{subdomain}: #{e}"
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2020-12-14 16:21:28 +01:00
|
|
|
# TODO: PTR name verification.
|
|
|
|
def self.is_ptr_name_valid?(subdomain) : Bool
|
|
|
|
if subdomain =~ /^(((?!-))(xn--|_{1,1})?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?[a-z0-9][a-z0-9\-]{0,60}[a-z0-9]*[a-z]+|[a-z]+|[a-z][a-z0-9]+[a-z]+$/
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
rescue e
|
|
|
|
Baguette::Log.error "invalid zone subdomain #{subdomain}: #{e}"
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2020-12-13 03:56:32 +01:00
|
|
|
# This only is "good enough for now".
|
|
|
|
# Regex only matches for invalid characters.
|
|
|
|
def self.is_ipv4_address_valid?(address) : Bool
|
|
|
|
if ! address =~ /^[0-9\.]+$/
|
|
|
|
false
|
|
|
|
elsif ip = IPAddress::IPv4.new address
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
rescue e
|
|
|
|
Baguette::Log.warning "wrong IPv4 address: #{address}"
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
# This only is "good enough for now".
|
|
|
|
# Regex only matches for invalid characters.
|
|
|
|
def self.is_ipv6_address_valid?(address) : Bool
|
|
|
|
if ! address =~ /^[0-9a-f:]+$/
|
|
|
|
false
|
|
|
|
elsif ip = IPAddress::IPv6.new address
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
|
|
|
rescue e
|
2023-05-08 04:01:33 +02:00
|
|
|
Baguette::Log.warning "wrong IPv6 address: #{address}"
|
2020-12-13 03:56:32 +01:00
|
|
|
false
|
|
|
|
end
|
2020-12-12 05:38:16 +01:00
|
|
|
|
2020-12-09 19:01:33 +01:00
|
|
|
end
|