WIP: new dedicated SPF, DKIM and DMARC RRs.

This commit is contained in:
Philippe Pittoli 2024-02-27 04:51:18 +01:00
parent bd0c3d5fdd
commit ba872ac4a9

View File

@ -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)