WIP: new dedicated SPF, DKIM and DMARC RRs.
This commit is contained in:
parent
bd0c3d5fdd
commit
ba872ac4a9
@ -1,6 +1,10 @@
|
|||||||
require "ipaddress"
|
require "ipaddress"
|
||||||
|
|
||||||
# Store a DNS zone.
|
# 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
|
class DNSManager::Storage::Zone
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
@ -29,7 +33,12 @@ class DNSManager::Storage::Zone
|
|||||||
NS: NS,
|
NS: NS,
|
||||||
CNAME: CNAME,
|
CNAME: CNAME,
|
||||||
MX: MX,
|
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.
|
# Used to discriminate between classes.
|
||||||
@ -247,6 +256,190 @@ class DNSManager::Storage::Zone
|
|||||||
end
|
end
|
||||||
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
|
class MX < ResourceRecord
|
||||||
property priority : UInt32 = 10
|
property priority : UInt32 = 10
|
||||||
def initialize(@name, @ttl, @target, @priority = 10)
|
def initialize(@name, @ttl, @target, @priority = 10)
|
||||||
|
Loading…
Reference in New Issue
Block a user