WIP: new dedicated SPF, DKIM and DMARC RRs.
parent
bd0c3d5fdd
commit
ba872ac4a9
|
@ -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:<IP> 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)
|
||||
|
|
Loading…
Reference in New Issue