Sovite.Core.Restrictions (sovite v0.6.0)

Copy Markdown View Source

Restriction chains ([restrictions]): lists of checks run at each stage of an SMTP session.

Each stage's list runs in order until a check decides: permit ends the list, a rejection ends the session's request. Restrictions only add checks: relay control and recipient validation always apply, so no restriction can make Sovite an open relay.

Checks

CheckStagesEffect
permit, reject, deferallAccept, 554 5.7.1, or 450 4.7.1.
permit_trustedallAccept clients in smtp.trusted_networks.
permit_authenticatedallAccept clients that logged in.
client_accessallThe access rules for the client IP address.
helo_accessfrom heloThe access rules for the EHLO name.
sender_accessfrom mailThe access rules for the sender address.
recipient_accessrcptThe access rules for the recipient address.
require_fqdn_helofrom helo504 unless the name has a dot or is an address literal.
require_fqdn_sender / require_fqdn_recipientfrom mail / rcpt504 unless the domain has a dot.
require_known_sender_domain / require_known_recipient_domainfrom mail / rcpt550 if the domain has no MX or address records, or a Null MX; 450 when DNS fails.
require_reverse_hostnameall550 5.7.25 unless the client address has a reverse DNS (PTR) name.
require_fcrdnsall550 5.7.25 unless that name also resolves back to the client address (forward-confirmed reverse DNS). Stricter.
reject_forged_helofrom helo550 if the EHLO name is this server's host name or one of its local domains, localhost, or an address literal other than the client's address.
require_known_helofrom helo550 unless the EHLO name has address or MX records. Address literals pass.
require_matching_helofrom helo550 unless the EHLO name resolves to the client address, or is its address literal. Strictest.
check_policy_service ADDRESSallAsk a Postfix policy server, see Sovite.Core.PolicyService.

The DNS checks reply 450 (4.7.25 or 4.7.1) when DNS fails, so a client is never refused for a temporary problem.

Checks whose information is not known yet are skipped: a helo_access in the mail stage of a client that sent no EHLO does nothing.

Access rules

Access rules live in the database (sovitectl access). A rule matches a pattern and has an action:

  • ACCEPT - accept, ending the list.
  • CONTINUE - as if no rule matched: go on with the next check.
  • REJECT [text] - 554 5.7.1.
  • DEFER [text] - 450 4.7.1.
  • 4NN [x.y.z] text / 5NN [x.y.z] text - that reply.
  • DISCARD [text] - accept, then silently drop the message. Ends the list.
  • HOLD [text] - accept, and put the message in the hold queue.
  • WARN text - log, and go on.

Patterns tried, in order:

  • client: the IP address, then for IPv4 the networks 192.0.2, 192.0, 192.
  • EHLO names and domains: the name, then each parent domain as .example.com (subdomains only) and example.com (the domain and its subdomains).
  • addresses: the address, the address without its extension (routing.extension_delimiter), the domain patterns as above, then user@ (any domain). The null sender is <>.

Summary

Types

What a chain looks at

What policy servers added to the message, see Sovite.Core.PolicyService.

The verdict of a chain.

Functions

Whether check name can run at stage.

Like run/3, and also returns what policy servers added to the message, in order.

Checks a restriction name. Returns it, or an error message.

Whether any of the chains (a map of stage to checks) looks at the client's reverse DNS, so it is worth looking up once per session.

Runs checks for stage. WARN results are reported with telemetry [:sovite, :restrictions, :warn] (%{stage, check, text}).

The stages, in session order.

Types

context()

@type context() :: map()

What a chain looks at:

  • :client_ip, :helo, :sender, :recipient - nil when not known yet.
  • :trusted, :authenticated - booleans.
  • :access - the access rule tables (Sovite.Core.Lookup.tables()) by kind: :client, :helo, :sender, :recipient.
  • :resolver - for the DNS checks.
  • :delimiter - the extension delimiter characters.
  • :own_names - this server's host name and local domains, lower-cased, for reject_forged_helo.
  • :client_dns - the result of Sovite.Abuse.ReverseDNS.check/2 for the client, if already known. Looked up when needed otherwise.
  • :policy, :policy_request, :esmtp - for check_policy_service, see Sovite.Core.PolicyService.check/3.

effect()

@type effect() :: Sovite.Core.PolicyService.effect()

What policy servers added to the message, see Sovite.Core.PolicyService.

verdict()

@type verdict() ::
  :ok
  | {:reject, Sovite.SMTP.Reply.t()}
  | {:discard, String.t()}
  | {:hold, String.t()}

The verdict of a chain.

Functions

allowed?(name, stage)

@spec allowed?(String.t(), atom()) :: boolean()

Whether check name can run at stage.

check(checks, stage, context)

@spec check([String.t()], atom(), context()) :: {verdict(), [effect()]}

Like run/3, and also returns what policy servers added to the message, in order.

parse(name)

@spec parse(String.t()) :: {:ok, String.t()} | {:error, String.t()}

Checks a restriction name. Returns it, or an error message.

reverse_dns?(chains)

@spec reverse_dns?(%{required(atom()) => [String.t()]}) :: boolean()

Whether any of the chains (a map of stage to checks) looks at the client's reverse DNS, so it is worth looking up once per session.

run(checks, stage, context)

@spec run([String.t()], atom(), context()) :: verdict()

Runs checks for stage. WARN results are reported with telemetry [:sovite, :restrictions, :warn] (%{stage, check, text}).

stages()

@spec stages() :: [atom()]

The stages, in session order.