diff --git a/src/storage/zone.cr b/src/storage/zone.cr index 0898624..2d44866 100644 --- a/src/storage/zone.cr +++ b/src/storage/zone.cr @@ -1,6 +1,10 @@ require "ipaddress" # Store a DNS zone. +# +# Several common resource record types are accepted: SOA PTR A AAAA TXT CNAME NS MX SRV. +# Some special entries are accepted too, which makes it simpler to deal with configuration errors. +# Thus, we accept new resource records named SPF, DKIM and DMARC to provide insightful errors to novice users. class DNSManager::Storage::Zone include JSON::Serializable @@ -29,7 +33,12 @@ class DNSManager::Storage::Zone NS: NS, CNAME: CNAME, MX: MX, - SRV: SRV + SRV: SRV, + + # Special resource records, which actually are TXT records. + SPF: SPF, + DKIM: DKIM, + DMARC: DMARC } # Used to discriminate between classes. @@ -247,6 +256,190 @@ class DNSManager::Storage::Zone end end + # TODO: verifications + print. + # baguette.netlib.re. 3600 IN TXT "v=spf1 a mx ip4: a:mail.baguette.netlib.re ~all" + class SPF < ResourceRecord + + # SPF mechanisms are about policy, which comes with the form of a single character: + # - '?' means no policy (neutral), + # - '+' means PASS result. Default behavior for any mechanism except `all`. + # - '~' (tilde) means SOFTFAIL, a debugging aid between NEUTRAL and FAIL. + # SOFTFAIL messages are accepted but tagged for later analysis. + # - '-' (minus) means FAIL, the mail should be rejected. + # + # The following `q` character is the behavior for the `all` mechanism. + # The `all` mechanism won't be included in the final domain zone if the + # `REDIRECT` modifier provided. + enum Qualifier + Pass # + + None # ? + SoftFail # ~ + HardFail # - + end + + # Modifiers allow future extensions of the framework. + # See RFC 4408. + class Modifier + include JSON::Serializable + enum Type + # "explanation", used for debug purposes, not accepted for now. + EXP + + # REDIRECT: replaces the ALL mechanism (new default behavior). + # From RFC 4408: + # ``` + # This facility is intended for use by organizations that wish to apply + # the same record to multiple domains. For example: + # + # la.example.com. TXT "v=spf1 redirect=_spf.example.com" + # ny.example.com. TXT "v=spf1 redirect=_spf.example.com" + # sf.example.com. TXT "v=spf1 redirect=_spf.example.com" + # _spf.example.com. TXT "v=spf1 mx:example.com -all" + # ``` + REDIRECT + end + property t : Type # type of modifier + property v : String # value + + def initialize(@t, @v) + end + + # TODO + def get_errors : Array(Error) + errors = [] of Error + errors + end + end + + class Mechanism + include JSON::Serializable + enum Type + A # address (domain) + IP4 # ipv4 or range + IP6 # ipv6 or range + MX # use the MX record of the domain + PTR # "pretty complicated for no reason" type, will be discarded for now + EXISTS # TODO + INCLUDE # include foreign SPF policy + end + + property q : Qualifier = Qualifier::Pass + property t : Type # type of mechanism + property v : String # value + + def initialize(@t, @v, @q) + end + + # TODO + def get_errors : Array(Error) + errors = [] of Error + case @q + when Mechanism::Type::A + when Mechanism::Type::IP4 + when Mechanism::Type::IP6 + when Mechanism::Type::MX + when Mechanism::Type::PTR + when Mechanism::Type::EXISTS + when Mechanism::Type::INCLUDE + end + errors + end + end + + property v : String = "spf1" + property mechanisms : Array(Mechanism) + property modifiers : Array(Modifier)? = nil + + # The `all` mechanism is the default behavior for email coming from non conforming + # IP addresses, meaning senders not respecting the SPF record. + property q : Qualifier = Qualifier::None + + def initialize(@name, @ttl, @target, @v, @mechanisms, @q) + @rrtype = "SPF" + end + + # TODO + def get_errors : Array(Error) + errors = [] of Error + + unless Zone.is_subdomain_valid? @name + errors << "CNAME invalid subdomain: #{@name}" + end + + if @ttl < Zone.ttl_limit_min + errors << "CNAME invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}" + end + + @mechanisms.each do |m| + m.get_errors.each do |e| + errors << e + end + end + + if mods = @modifiers + mods.each do |m| + m.get_errors.each do |e| + errors << e + end + end + end + + errors + end + + def to_s(io : IO) + io << "TODO" + end + + def to_bind9(io : IO) + io << "TODO" + end + end + + # TODO + class DKIM < ResourceRecord + property v : String = "DKIM1" # DKIM version + property h : String = "sha256" # encrypting cryptographic algorithm + property k : String = "rsa" # signing cryptographic algorithm + property p : String # public key + def initialize(@name, @ttl, @target, @v, @h, @k, @p) + @rrtype = "DKIM" + end + + def get_errors : Array(Error) + errors = [] of Error + + unless Zone.is_subdomain_valid? @name + errors << "CNAME invalid subdomain: #{@name}" + end + + if @ttl < Zone.ttl_limit_min + errors << "CNAME invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}" + end + errors + end + end + + # TODO + class DMARC < ResourceRecord + def initialize(@name, @ttl, @target) + @rrtype = "DMARC" + end + + def get_errors : Array(Error) + errors = [] of Error + + unless Zone.is_subdomain_valid? @name + errors << "CNAME invalid subdomain: #{@name}" + end + + if @ttl < Zone.ttl_limit_min + errors << "CNAME invalid ttl: #{@ttl}, shouldn't be less than #{Zone.ttl_limit_min}" + end + errors + end + end + class MX < ResourceRecord property priority : UInt32 = 10 def initialize(@name, @ttl, @target, @priority = 10)