mailrelay

command module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

README

mailrelay

A stateless reverse relay that lets a mailnite server behind NAT (a home lab, a laptop, any machine with no public IP) serve real mail and web on the public internet — using only a cheap VDS that has a public IP and nothing else.

The relay stores no mail and holds no user data. It binds the public ports (25, 465, 587, 143, 993, 995, 80, 443) on the VDS and forwards their raw bytes to your mailnite instance over a single, mutually-authenticated value-rpc connection that mailnite dials outbound — so your NAT/firewall never needs an inbound hole.

   public internet                         your LAN / behind NAT
  ┌──────────-─────┐    outbound, mutual-TLS   ┌──────────────────┐
  │    mailrelay   │◀───── value-rpc ─────-────│     mailnite     │
  │  (public VDS)  │   (mailnite dials out)    │  (no public IP)  │
  │                │                           │                  │
  │ binds :25 :443 │   raw bytes both ways     │ Serve(listener)  │
  │  :465 :587 …   │══════════════════════════▶│  as if local     │
  └───────▲────────┘                           └──────────────────┘
          │ SMTP/IMAP/HTTPS
      the world

Why this exists

Standing up mail is the hardest part of self-hosting: you need a static public IP, the ability to bind port 25 and friends, reverse DNS, and open inbound ports. Most home/lab machines have none of these. mailrelay reduces the requirement to one VDS with a public IP and an SSH login — the relay is disposable and storage-free, so the sensitive parts (mail, keys, search index) stay on your hardware.

How it works

  • mailnite is the value-rpc client; the relay is the server. mailnite dials out, so NAT is a non-issue. The relay listens on exactly one control port.
  • Three RPCs carry everything (see protocol): a session chat (open the public ports; stream back one event per inbound connection), a conn chat per tunneled connection (raw bytes both ways), and a ping.
  • The reverse net.Listener. On the mailnite side, relayclient turns each bound public port into an ordinary net.Listener. mailnite's mail/web servers call Serve(listener) exactly as they would for a local net.Listen — they never know the socket is a thousand miles away.
  • Mutual TLS. A private CA (created on the mailnite side) signs one relay server cert and one mailnite client cert; each end trusts only that CA. See pki.
  • One relay, many clients. Each connecting client gets its own independent set of public listeners, so several instances can share a single relay — one binding :25, another :110, and so on. Public ports are first-come, first-served; a per-connection capability secret (only ever sent to the owning client) keeps one client from attaching to another's tunneled connections.

The whole shape in one picture

mailrelay is one half of a deliberate split between infrastructure (which stays private) and the public surface (which is all a relay ever exposes):

  INTERNAL — loopback :8480, never exposed, never relayed     PUBLIC — direct, or via mailrelay
  ┌────────────────────────────────────────────────┐        ┌────────────────────────────────────┐
  │ Internal admin console                           │        │ Webmail HTTPS  (SPA + /api/*:        │
  │  • late-stage onboarding (relay, TLS, ports)     │        │    user + webmail-admin + mobile API │
  │  • infra admin: TLS · Storage · DNS · Queue/logs │        │    — never /api/admin infra routes)  │
  │    · Mail-server ports · Backup · Relay          │        │ SMTP 25 · submission 587 · SMTPS 465 │
  │  • value-rpc admin control plane (loopback)      │        │ IMAP/STARTTLS 143 · IMAPS 993        │
  └────────────────────────────────────────────────┘        └────────────────────────────────────┘
        configured locally, replaces the relay                    bound directly, or by the relay
  • Onboarding is two-tier. First run (database-free) defines only storage + limits + the admin account, then restarts. Everything public — the relay, TLS, which ports — is configured afterwards on the internal admin console at 127.0.0.1:8480, which is also where you later replace or reconfigure the relay.
  • Infra never leaves the box. The internal console and the value-rpc control plane bind loopback and are absent from the relay's allow-list, so no relay.* setting can push them onto the internet.
  • A single seam picks direct vs. relay. In mailnite a ListenerFactory hands each public server its net.Listener — a local bind, or the relay's reverse tunnel — so the servers Serve(listener) unchanged and only relay-exposable services (SMTP/submission/IMAP/POP3, and the public web server) can route through it.

Transports

Selectable at runtime (--transport) — the name is the carrier the tunnel rides; all three run under TLS (tls is accepted as a legacy alias of tcp):

Transport Auth When
tcp (default) mutual TLS (private CA client cert), or self-signed cert + token (key-authenticated mode) the robust default for a direct VDS
quic mutual TLS over QUIC (TLS 1.3, connection migration) lossy/mobile networks
ws (wss) server-cert TLS + handshake token riding 443 behind a CDN / L7 proxy

ws uses a token rather than a client certificate because the WebSocket client dials with the system trust store; give the relay a publicly-trusted cert (e.g. Let's Encrypt for a real domain) for that mode.

Quick start

The one-command way — on the VDS, with the key from the mailnite admin console (Mail relay → step 1):

curl -fsSL https://get.mailnite.com/relay | sudo bash -s -- --token <KEY>

That downloads the right binary, verifies its sha256 against the channel manifest, installs a hardened systemd service that may bind ports below 1024 (via AmbientCapabilities, which — unlike setcap — survives binary updates), starts it in key-authenticated mode, and enables a daily self-update timer. It ends by printing the relay address to paste back into the console.

Updates land on their own via that daily timer, but to pull the latest release now — on the VDS:

# re-run the installer in update mode (upgrades only if the channel moved,
# keeping the existing key/transport/bind), then restarts the service:
curl -fsSL https://get.mailnite.com/relay | sudo bash -s -- --update

# …or trigger the built-in updater immediately without re-fetching the script:
sudo systemctl start mailrelay-update.service

The same installer also pins versions (--version vX.Y.Z) and uninstalls (--uninstall); see install.sh.

Prefer to drive it from your workstation over SSH, with mutual TLS instead of the shared key? From this repo:

# 1. build the relay for the VDS
GOOS=linux GOARCH=amd64 go build -o mailrelay ./

# 2. create the tunnel CA and issue certs (--hosts = how mailnite reaches the relay)
./mailrelay gen-ca    --out ./relay-pki
./mailrelay gen-certs --hosts relay.example.com --out ./relay-pki

# 3. deploy to the VDS over SSH and start it (installs a systemd unit)
./mailrelay deploy \
  --host relay.example.com --user root --binary ./mailrelay --privileged \
  --transport tcp --bind 0.0.0.0:8443 \
  --ca ./relay-pki/ca.crt --cert ./relay-pki/relay.crt --key ./relay-pki/relay.key

--privileged grants the binary CAP_NET_BIND_SERVICE (via setcap) so it can bind 25/443 without running as root; add --sysctl to flip net.ipv4.ip_unprivileged_port_start=0 instead. The mailnite side then dials the relay with the ca.crt + mailnite-client.crt + mailnite-client.key bundle (this is what the onboarding wizard's "No public IP" path automates).

Commands

mailrelay serve        run the relay on the VDS
mailrelay gen-ca       generate the tunnel certificate authority
mailrelay gen-certs    issue relay + mailnite certs and a handshake token
mailrelay gen-ssh-key  generate the SSH keypair used to deploy
mailrelay deploy       ship the relay to a VDS over SSH and start it

deploy authenticates to the VDS by public key by preference: it tries an explicit --ssh-key (passphrase-protected keys are supported via --ssh-key-passphrase), then a running ssh-agent (SSH_AUTH_SOCK), then the default ~/.ssh/id_ed25519 / id_ecdsa / id_rsa — so mailrelay deploy --host relay.example.com just works with your existing key setup, no password. --password remains as a fallback (and supplies sudo when --user isn't root); --no-agent / --no-default-keys narrow the search when you need to.

See DESIGN.md for the architecture, the security model, and how the relayclient listener drops into mailnite's existing server factories.

Security notes

  • The relay is a byte pump: it never terminates application TLS and never sees plaintext mail. STARTTLS/implicit-TLS on the mail ports is between the remote peer and mailnite, end to end through the tunnel.
  • Guard ca.key — it is the trust root for the whole tunnel. gen-* write keys 0600.
  • SSH host keys are trust-on-first-use by default (the fingerprint is printed); pass --host-key to pin.

Contributing

Issues and pull requests are welcome. Please keep changes gofmt-clean and run the tests before submitting:

go build ./...
go test ./... -race

By contributing you agree that your contributions are licensed under the project's Apache-2.0 license.

License

Licensed under the Apache License, Version 2.0. Copyright 2022-present Karagatan LLC. It builds on the arpabet libraries (value-rpc, servion, cligo) under their own licenses; see NOTICE.

Documentation

Overview

Command mailrelay is the public-facing half of a mailnite deployment behind NAT. It runs on a cheap VDS that has only two things mailnite lacks there: a public IP and the ability to bind low ports. It stores no mail and terminates no application TLS — it binds the requested public ports and reverse-tunnels their raw bytes to a mailnite instance over a mutually-authenticated value-rpc connection that mailnite dials outbound.

Subcommands:

mailrelay serve        run the relay (on the VDS)
mailrelay gen-ca       generate the tunnel certificate authority
mailrelay gen-certs    issue relay + mailnite certs and a token from the CA
mailrelay gen-ssh-key  generate the SSH keypair used to deploy
mailrelay deploy       ship the relay to a VDS over SSH and start it

Directories

Path Synopsis
Package deploy pushes a mailrelay to a bare VDS over SSH: it ships the binary and the tunnel's CA + server certificate, grants the binary the one privilege it needs (binding ports below 1024), installs a systemd unit and starts it.
Package deploy pushes a mailrelay to a bare VDS over SSH: it ships the binary and the tunnel's CA + server certificate, grants the binary the one privilege it needs (binding ports below 1024), installs a systemd unit and starts it.
Package pki issues the small private CA the mailnite<->relay tunnel trusts.
Package pki issues the small private CA the mailnite<->relay tunnel trusts.
Package protocol defines the wire contract between a mailnite instance (behind NAT) and a mailrelay running on a public VDS.
Package protocol defines the wire contract between a mailnite instance (behind NAT) and a mailrelay running on a public VDS.
Package relay is the VDS-side reverse tunnel: it holds the public listeners and pumps their bytes to mailnite clients over value-rpc.
Package relay is the VDS-side reverse tunnel: it holds the public listeners and pumps their bytes to mailnite clients over value-rpc.
Package relayclient is the mailnite-side of the tunnel.
Package relayclient is the mailnite-side of the tunnel.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL