263 Commits

Author SHA1 Message Date
Wez Furlong 41be60dfce Evaluate MTA-STS during MX resolution to fix aliasing issue
refs: https://github.com/KumoCorp/kumomta/issues/484

Briefly, the issue is that if some random domain that shares MX records
with another (eg: someone is using google apps or icloud for their
vanity domain) publishes a broken MTA-STS policy that requires eg:
cloudflare MX hosts then because we roll up by site name, that broken
MTA-STS policy bleeds into all the other domains that share those MX
records.

The resolution is simple, but is technically a breaking change.

Moving the policy resolution to happen during site_name resolution
allows us to resolve both per-domain things at the same and have the
MTA-STS policy amend the effective set of MX hosts.  The output of that
is then used for site_name aggregation/rollup.

The consequence of this is quite nice: an MTA-STS policy that is more
restrictive than the full set of MX hosts now prevents delivering to
any of the excluded hosts, and a totally broken policy that prevents all
of its MX hosts is now completely undeliverable and will produce
transient failures.

The downside is that for users that had previously disabled mta-sts in
their default shaping block, they will need to change a different config
option to continue to prevent MTA-STS from being consulted.  One example
of this that I recall is that one user's network posture prevented
MTA-STS from making HTTPS requests to fetch the policy.  Another user
just wanted to cut out the additional DNS traffic.  Those use cases
require altering the new kumo.dns.set_mta_sts_enabled enabled to false
during `init`.
2026-07-27 08:02:37 +01:00
Wez Furlong dfb17abd0d dane: engage for secure CNAME into unsigned zone
Per RFC 7672 section 2.2.2, an MX host that is a securely published
CNAME remains DANE-eligible at its original name even when the alias
target lands in an unsigned zone: it is the secure TLSA RRset, not the
address records, that authenticates the peer. When the address chain is
insecure but MX selection was secure, an explicit CNAME query isolates
the alias's own DNSSEC status; a secure alias engages DANE, an
indeterminate status defers for downgrade resistance.

refs: https://github.com/KumoCorp/kumomta/pull/545#discussion_r3472353021
2026-07-06 11:46:51 +01:00
Wez Furlong 4fc3cbca92 add treat_mx_list_as_secure to SmtpProtocol
This allows you to explicit indicate that an mx_list should
be treated as DNSSEC secure.

We cannot simply assume that lua code setting mx_list be treated
as secure, because the lua code may be simply passing through
the result of some other DNS lookup--we need an explicit way
to thread through the overall secure flag in order for the
secure property to be correctly upheld.
2026-07-06 11:46:51 +01:00
Wez Furlong e13734d186 address review feedback on https://github.com/KumoCorp/kumomta/pull/545
Address most of the easy points from the review, others will land
separately.
2026-07-06 11:46:51 +01:00
Wez Furlong 3b61117145 dns: add RFC 5011 managed trust anchor option for unbound
trust_anchor_file now accepts `{ managed = "<path>" }` for an RFC 5011
auto-maintained anchor file (unbound only; hickory errors). Unbound seeds
the file from the bundled root anchors when the file is absent.
2026-07-06 11:46:51 +01:00
Wez Furlong 1874dc09f7 enable DNSSEC when using hickory, allows enable_dane w/o unbound
When we first built out DANE, the hickory implementation was
all or nothing when DNSSEC validation was enabled, making it unsuitable
for use in an MTA.

Since that time hickory has matured; this commit wires up the
DNSSEC signals and that enables its use with DANE.

We do still require openssl for establishing the connection when
DANE is enabled; that's a different constraint.
2026-07-06 11:46:50 +01:00
Wez Furlong 7331cb4ec7 new allow_smtp_auth_plain_without_valid_certificate option
This is technically a breaking change, but in a good way.
We will no longer send SMTP AUTH PLAIN creds when we didn't
validate the peer certificate.  This is what you want in
all production deployments.  The new option facilitates
test setups or other unusual configurations where the certs
cannot be verified but you have contrived to trust the
remote system through some other means.
2026-07-06 11:46:50 +01:00
Wez Furlong a4c4b40fbe add dane_result_count metric
This surfaces aggregate dane results
2026-07-06 11:46:50 +01:00
Wez Furlong d920ffb5c4 fix: DANE downgrade resistance
We weren't checking the DNSSEC status for A/AAAA when considering
DANE, and were overly permissive in a couple of related cases.

This diff ended up fairly large because the bulk of it is to
facilitate testing.

refs: https://github.com/KumoCorp/kumomta/issues/543
refs: https://github.com/KumoCorp/kumomta/pull/517
2026-07-06 11:46:49 +01:00
Wez Furlong 3f11963acd docs: fix broken link 2026-06-27 18:58:12 +01:00
Wez Furlong 407379dd36 track sched->readyq fan in, clean up some internals
This commit removes the queue_name_for_config_change_purposes_only wart
from inside the readyq machinery.  This field had a bunch of caveats
and could in some cases become stale if the originating scheduled
queue had aged out.

It's been replaced by a HashSet of Weak refs back to the associated
scheduled queues so that it is now possible to walk the reverse of
the fan-in graph when we need to.
2026-06-25 13:41:01 +01:00
Wez Furlong a1a7e16759 docs: fixup links 2026-06-23 10:13:11 +01:00
Wez Furlong 2a657f68fb included scheduled queue config in resolve-egress-path output
and also in resolve-shaping-domain, for consistency.

We can use the message rate from the scheduled queue config to refine
the computed ceilings/constraints too, which is nice.
2026-06-23 10:00:07 +01:00
Wez Furlong 61bb95a558 resolve-shaping-domain: expand output
This changes the output to be a bit more human friendly by default
by rendering as toml.

It also includes the rate ceilings added in the prior commit.
2026-06-23 09:59:32 +01:00
Wez Furlong 423abc4704 add per-dispatcher state tracking and watchdog
The ready queue maintainer now tracks for each dispatcher whether it
is making progress.  If it exceeds dispatcher_progress_watchdog_timeout
then the task will be aborted and any message(s) it held onto will
be returned to the scheduled queue.  This will free up the connection
slot and allow another connection to be attempted, potentially
making further progress.

refs: https://github.com/KumoCorp/kumomta/issues/539
2026-06-23 09:56:45 +01:00
Wez Furlong 002d6593f0 new: suspend_when_unplumbed and suspend_when_proxy_unhealthy source options
Egress sources can now be configured to auto-suspend when their local
bind address appears unplumbed or when their configured proxy server
appears unreachable.  A suspended source is skipped during pool selection
until the configured duration elapses.
2026-06-23 09:22:27 +01:00
Wez Furlong e21bb1c672 ha_proxy_server and socks5_proxy_server now also resolve host names
We'll expand these to a list of candidate addresses, trying each in
turn.  We use the same overall connect timeout regardless of how
many candidates are present, to respect that timeout bound.

closes: https://github.com/KumoCorp/kumomta/pull/402
2026-06-23 09:11:53 +01:00
Wez Furlong 9987248313 spool: surface and handle rocksdb background errors
We recently investigated an issue where a rocksdb had been damaged by
corrupting/removing SST files (it sounded like this was accidentally
self-inflicted by some backup/orchestration infrastructure) leaving the
system in a silently-broken state: writes just wouldn't make progress
and there were no error messages.

Inspecting the `/var/spool/kumomta/data/LOG` log file (which is a
readable text file) revealed messages like:

```
2026/06/12-14:55:31.884227 2875746 [ERROR] [db/compaction/compaction.cc:262] Unable to load table properties for file 29704 --- IO error: No such file or directory: While open a file for random read: /var/spool/kumomta/data/029704.sst: No such file or directory
2026/06/12-14:55:31.884311 2875746 [ERROR] [db/db_impl/db_impl_compaction_flush.cc:3385] Waiting after background compaction error: IO error: No such file or directory: While open a file for random read: /var/spool/kumomta/data/029704.sst: No such file or directory, Accumulated background error counts: 6363
```

This commit improves the observability in this situation by proactively
checking for error conditions:

1. The store() and remove() operations now use our own polling within
   a deadline loop rather than spawning a blocking task and delegating
   to rocksdb's blocking interface.  This allows us to inspect the
   background error count and be cancellable, safely respecting and
   caller provided smtp max transaction duration.

2. All read and write operations check for IO and Corruption errors
   and immediately latch an error state

3. The metrics monitoring task inspects and track background error
   counts and latch us into an unhealthy state when the background
   error state appears unhealthy and persistent.

4. Additional metrics are exposed to help monitoring and alerting

While adding integration test coverage for this, I found a typo that
meant that spool errors were ignored in the message crate; they got
silently converted to `true` in all cases rather than just mapping
the success case to a `true`.

Integration tests handle the case where an SST file is corrupted
(truncated) during runtime, as well as starting up when an SST file
is missing.   These excercise both the foreground and background
error detection paths.
2026-06-23 09:11:48 +01:00
Wez Furlong 62de291279 docs: suggest folks prefix their metadata names
Should help to avoid possible conflicts with the core metadata names.
2026-05-12 14:35:16 +01:00
Wez Furlong 37236c40b1 docs: update for the 2026.04.09-ea3b2a9b release 2026-04-29 08:07:13 +01:00
Wez Furlong 937611feb9 rfc5321: improve handling of quoted local parts in envelope addresses
This commit refactors the EnvelopeAddress types(!) so that the brains
of them are centralized in the rfc5321 crate, removes the one from
the log-types crate, and replaces the internals of the one in the
message crate with the rfc5321 implementation.

This revised implementation accommodates quoted local parts more
consistently and exposes the local part via a normalizing accessor which
is in turn used when comparing addresses for equality.

This means that `"foo"@` and `foo@` now compare the same, and if you
have setup a maildir that generates per-user maildirs, we'll use the
normalized form of the local part rather than whatever is on the
incoming address.

Another side effect of this commit is that we can now accept exotic
quoted addresses like `"info@"@example.com` without falsely complaining
about having too many `@` signs in the address.

closes:  https://github.com/KumoCorp/kumomta/issues/495
2026-03-28 07:20:35 +00:00
Wez Furlong 654f9b88f5 new: ip_lookup_strategy option in make_egress_path
This provides control over IPv4 and IPv6 lookups based on the source
and/or destination.
2026-03-04 16:33:32 +00:00
Wez Furlong 4eef49df3c docs: remove suspended option from docs
This was removed from the product over a year ago in
16fb1ae316
2026-03-04 15:14:50 +00:00
Wez Furlong 93e8e7dd01 docs: update since('dev') macros for stable release 2026-03-04 08:03:21 +00:00
Mike Hillyer c4c161543f Typo. 2026-03-03 13:53:31 -05:00
Wez Furlong 7d521fd493 docs: clarify enable_mta_sts 2026-03-03 17:19:25 +00:00
Wez Furlong 1690e6526a refactor: move start_proxy_listener to proxy module
We're keeping a reference to it via kumo.start_proxy_listener briefly,
just in case someone is using that live somewhere.

We don't need to changelog this, as this whole feature hasn't
been in a stable tag yet.

refs: https://github.com/KumoCorp/kumomta/pull/472
2026-02-06 07:31:30 +00:00
Aditya Ganti f898e2a7e6 Add Prometheus metrics to proxy-server
Co-authored-by: Wez Furlong <wez@wezfurlong.org>

closes: https://github.com/KumoCorp/kumomta/pull/472
2026-02-06 07:11:27 +00:00
Wez Furlong 4f636a0fae skip_hosts: downgrade to a transient failure
This is a conceptually breaking change, but shouldn't be harmful
in practice.

Recently, Microsoft hosted domains have had a number of incidences where
the DNS stopped returning IPv4 addresses, which lead to systems that
employ skip_hosts to disable their IPv6 addresses (presumably due to
difficulties in warmup and reputation) to trigger the terminal case
where we would sweep the ready queue away as a permanent routing
failure.

In the context of a transient failure to route ipv4 the permanent
disposition was surprising and unwanted so it feels better to make
the effect of this situation more closely map to how we handle the
case where no MX records are returned.
2026-02-04 13:12:14 +00:00
Wez Furlong 87b64c8d2b docs: flesh out docs for a number of metrics
Give some suggestions on how to interpret a number of the metrics,
as well as cross linking to relevant documentation for them.
2026-02-04 11:45:11 +00:00
Wez Furlong c5a6a1fb00 Adjust docs based on code review 2026-02-03 08:29:00 +00:00
kay ozaki bf6f9104dc exposing memory config parameters 2026-02-03 08:29:00 +00:00
Wez Furlong d6eb5176ad docs: remove rapidoc HTTP explorer
It has been replaced by our own static generation.
2026-01-30 16:37:12 +00:00
Wez Furlong 74d95f81a6 docs: refine recent proxy server doc changes
refs: #459
2026-01-19 07:45:10 +00:00
Wez Furlong 2369162e75 proxy: flip no_splice to use_splice in proxy policy file
This option was named `--no-splice` on the command line because that
was the easiest UX there.  The negative logic makes it harder to
understand how to enable/disable, so let's flip it and default
use_splice=true.

refs: #459
2026-01-19 07:45:09 +00:00
Kevin Vu 8e832ad071 docs: add kumo.start_proxy_listener and changelog entry
Fixes #459
2026-01-19 07:45:09 +00:00
Wez Furlong 1d8ce2bb74 http inject: add integration test for request_body_limit
This commit doesn't change any behavior, it just adds an explicit
test for exceeding request_body_limit, and expands on the docs
for request_body_limit to clarify the response when the limit
is exceeded.
2026-01-15 13:52:10 +00:00
Wez Furlong b551d6b4c7 docs: update for release 2025.12.02-67ee9e96 2025-12-02 11:11:05 +00:00
Wez Furlong 3781ff7d95 docs: format 2025-11-19 07:57:37 +00:00
Wez Furlong 6f53931fde add kumo.invoke_get_egress_(pool|source)
These functions are similar to the existing
invoke_get_egress_path_config and invoke_get_queue_config functions;
they return the configuration information that is defined by your
overall policy in the same way that the system does.

The intent is for more advanced policies that need that info to
decide how to configure other parts of the policy.

Docs are included, and I fleshed out the missing docs for the two
pre-existing functions.
2025-11-14 07:04:58 +00:00
Wez Furlong 4c89acae96 docs and changelog for require_proxy_protocol #440 2025-11-13 15:27:39 +00:00
Wez Furlong 26a0c86c49 Expose version number to lua via kumo.version
I'm surprised that we didn't already do this!
It allows phrasing the policy to accommodate different versions
of kumo.
2025-11-13 06:30:35 +00:00
Wez Furlong 4917ee6245 prohibited_hosts: add v4 and v6 any address
Saw an MX record that expands to 0.0.0.0 in the wild.

As well as being nonsense, that causes a mail loop, so we should add it
to prohibited_hosts.

I noticed that we also did not document its default value, so the
doc update here shows the before and after for this.
2025-11-10 15:09:11 +00:00
Wez Furlong 7ac9a5eac0 smtp_dispatcher: handle pathologic all-452 case
Continuing in the same vein as 94628f06ab,
ensure that we have a reasonable outcome when a batch of size > 1
encounters 100% ambiguous 452 responses.

Without special consideration we'd end up in an infinite retry loop for
these, as we'd think that we'd hit the recpient limit on the connection
every time.

This commit introduces a small amount of state into the dispatcher so
that we can remember the set of (spoolid, recipient) from the prior
transaction on the session.

When deciding whether something is eligible for immediate retry
we will gate that choice on the current (spoolid, recipient) being
absent from the prior set of ids.  In this way, we bound the immediate
retry for a recipient to one per connection.

To make things a bit more robust when trying to decide whether a 452
means "too many recipients" or not, if we're not sure, we now call out
to the bounce classifier which can be configured to emit a
`TooManyRecipients` classification.  If the classifier produces either
that or some non-Uncategorized response, then we have a confident
assessment.  Otherwise, we're unsure and will factor that into the
retry strategy.
2025-11-07 08:13:36 +00:00
Wez Furlong 49cdee1ec5 lruttl: expose pre-defined cache info to the docs
This commit adds some plumbing to allow us to collect metadata
about the caches and spit it out into a json file that we can
then use in the docs to show the predefined caches, capacities
and brief comments about their purpose.

We put that info on the set_lruttl_cache_capacity doc page.
2025-11-01 07:40:40 +00:00
Wez Furlong aac842439f smtp_server: increase line_length_hard_limit default to 1000
We were including the CRLF in the measurement that we were making,
while the default value was the value from the spec excluding the CRLF.
Let's increase the default value to 1000 to make things match up.
2025-10-13 09:53:03 +01:00
Wez Furlong a81493a17e docs: remove extraneous quotes 2025-10-11 07:07:54 +01:00
Wez Furlong 794113f0b0 smtp_dispatcher: add max_recipients_per_batch
This sets a client side shaping control to manage the maximum number of
RCPT TO commands issued in a single transaction.
2025-10-07 07:53:15 +01:00
Wez Furlong f968935315 add smtp_server_split_transaction event
This provides full control over how an incoming smtp transaction
will be split into batches.
2025-10-07 07:53:15 +01:00
Wez Furlong 981de67f1c docs: update for 2025.10.06-5ec871ab release 2025-10-07 07:38:10 +01:00