Skip to content

Accept mail arriving through the relay tunnel - #5

Open
cyberb wants to merge 8 commits into
masterfrom
mail-inbound
Open

Accept mail arriving through the relay tunnel#5
cyberb wants to merge 8 commits into
masterfrom
mail-inbound

Conversation

@cyberb

@cyberb cyberb commented Aug 6, 2026

Copy link
Copy Markdown
Member

Mail app side of inbound mail. Completes the chain with syncloud/redirect#63
(terminates port 25, forwards down the tunnel) and syncloud/platform#762
(asks frps for the proxy that carries it).

redirect forwards each message down the frp tunnel, which arrives at the
device on loopback. Postfix needs somewhere to put it that is not the port it
already offers the internet, so this adds an smtpd on 127.0.0.1:10025 — the
port the platform's frpc proxy forwards to.

Two defaults that had to be turned off

mynetworks trusts loopback, and everything through the tunnel arrives from
loopback. Left alone, this port would relay to any destination for anyone who
reached it. Emptied for this service, with a test that it refuses.

The milters had to go too, and this is the one that would have been quiet.
opendkim runs Mode sv with 127.0.0.1 in TrustedHosts, so it classifies
tunnel traffic as internal. It would have signed mail from the whole
internet with this device's key instead of verifying it, and forged mail would
have landed in the user's mailbox looking DKIM-authenticated as their own
domain. rspamd does its own verification when it lands, so nothing is lost by
dropping them here.

Tests

Delivery to a local mailbox through the tunnel port, the port staying on
loopback rather than facing the internet, the relay refusal, and that a
delivered message carries no signature of ours.

Not covered here

rspamd and redis are not packaged yet, so inbound mail is unfiltered: nothing
scans it, and there is no Junk folder to file it into. dovecot sieve is not
enabled either. That is the larger piece of work and wants its own branch —
this one is the part that makes the tunnel deliver at all.

Port 10025 is the contract with the platform side; it appears there as
config.MailInboundPort.

cyberb added 8 commits August 6, 2026 21:24
redirect terminates port 25 and forwards each message down the frp tunnel,
which arrives at the device on loopback. Postfix needs somewhere to put it
that is not the port it already offers the internet, so this adds a smtpd on
127.0.0.1:10025, the port the platform's frpc proxy forwards to.

Two things had to be turned off for that service rather than left at their
defaults.

mynetworks trusts loopback, and everything through the tunnel arrives from
loopback, so without emptying it this port would relay to any destination for
anyone who reached it. There is a test that it refuses.

The milters had to go as well, and this is the one that would have been quiet.
opendkim runs in sign and verify mode with 127.0.0.1 in TrustedHosts, so it
classifies tunnel traffic as internal: it would have signed mail from the
whole internet with this device's key instead of verifying it, and forged mail
would have arrived looking DKIM authenticated as the user's own domain. rspamd
does its own verification when it lands, so nothing is lost by dropping them
here.

Tests cover delivery to a local mailbox, the port staying on loopback, the
relay refusal, and that a delivered message carries no signature of ours.
run_ssh wraps the command in double quotes, so the double quotes inside the
python sent over the heredoc were stripped before python ever saw it and the
first tunnel test died on a syntax error. The run stops at the first failure,
so the other two had never run.
Buster has no python3, so driving the tunnel with smtplib only ever worked on
bookworm. Bash opens the socket itself, which is available on both.
Platform held the port the mail app listens on, so it knew an internal detail
of another snap and opened an smtp proxy on every relayed device whether or not
mail was installed. The app registers its socket on install and refresh, and
platform forwards there and only adds the proxy once something has registered.

Postfix takes the tunnel on a unix socket rather than a loopback port, so the
listener is reachable through the filesystem instead of by anything local that
can open a connection.
The tunnel listens on a unix socket now, which the shell cannot open on its own
and buster has no python for, so the tests were reaching for socat and an apt
install. The mail snap ships php, which speaks unix sockets and is there on
both distros by definition.

run_ssh wraps the command in double quotes, so the script is escaped once and
passed through a heredoc rather than fighting nested quoting.
The app registers its socket on install, but the platform it installs onto is
whatever is released, so a 404 means that platform predates the endpoint rather
than that anything is wrong. The snap installs and simply has no tunnel.

The cli build runs its tests now, which is where the three cases live.
Postfix refuses commands that arrive before it has answered unless it has
advertised pipelining and the client is authorised to use it, so firing the
whole conversation at once got 554 protocol synchronization. The exchange is
lock step now, with the message body the one part written without a read in
between.
It read the ini straight out of the snap, where date.timezone is still
{{ .Timezone }}, so every cli run warned and fell back to utc. The rendered one
lives beside the php-fpm config the service already uses; the path is derived
so the wrapper works outside a snap environment too, which is how the tests
reach it.

Arguments are quoted now so ones with spaces survive.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant