fix(mail): claim inbound IPv6 (2a01:4f8:c012:9d85::2) so IMAP/MX over v6 work; pin Postfix outbound to v4 #97

Closed
opened 2026-06-05 11:22:56 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Problem

FreeScout (support.cloonar.dev, hosted on web-arm) fails to connect to IMAP:

stream_socket_client(): Unable to connect to ssl://imap.cloonar.com:993 (Connection timed out)

Root cause: imap.cloonar.com / mail.cloonar.com publish an AAAA record 2a01:4f8:c012:9d85::2, but the mail host never configures that address. Confirmed live via the read-only diag channel on mail:

  • enp1s0 carries only 91.107.201.241/32 + link-local fe80::9400:2ff:fe23:b445/64no global IPv6.
  • ip -6 route shows only fe80::/64no default v6 route.
  • Dovecot already listens on [::]:993 (and 25/143/389/465/587/636 on [::]:*).

So inbound v6 SYNs to [2a01:4f8:c012:9d85::2]:993 reach mail's link but the host doesn't own the address → packets are black-holed ("timed out", not "refused").

Why it regressed now: when web-arm gained outbound IPv6 (ADR 0010 / #81), RFC 6724 made it prefer the v6 destination for imap.cloonar.com. PHP's stream_socket_client() (webklex/php-imap) has no happy-eyeballs / no v6→v4 fallback, so it connects to ::2 and hangs. Inbound MX-over-v6 to mail is broken for the same reason.

Fix (coupled — both land in one PR)

1. hosts/mail/configuration.nix — claim the published AAAA + standard Hetzner gateway (mirrors web-arm; interface confirmed as enp1s0):

networking.interfaces.enp1s0.ipv6.addresses = [
  { address = "2a01:4f8:c012:9d85::2"; prefixLength = 64; }   # ::2 to match the existing AAAA
];
networking.defaultGateway6 = { address = "fe80::1"; interface = "enp1s0"; };

2. hosts/mail/modules/postfix.nix — pin outbound to IPv4:

services.postfix.settings.main.smtp_address_preference = "ipv4";

Rationale: mail's v6 address has no PTR and is in no sending domain's SPF. Without the pin, Postfix (default any) would prefer the v6 source to dual-stack MXes (Gmail/Microsoft) → spam-filed. The pin keeps outbound delivery exactly as today (over IPv4).

Dovecot needs no change — it already listens on [::]:993; claiming ::2 is sufficient.

Decisions (settled with maintainer)

  • Outbound stays on IPv4 for now. Real v6 sending is separate future work: requires a PTR for ::2 in Hetzner and adding ::2 to the SPF of every sending domain (cloonar.com, optiprot.eu, superbros.tv, szaku-consulting.at, scana11y.com, macher.solutions, fueltide, docfast.dev).
  • v6 exposure mirrors today's v4 exposure. Once ::2 is up, the family-agnostic firewall makes every open port reachable over v6 too — including LDAP 389/636. Explicitly accepted (it mirrors the existing v4 exposure; scoping LDAP to wireguard is out of scope here).

Docs

  • Add a new ADR cross-referencing ADR 0010: "mail claims its inbound IPv6; outbound pinned to IPv4."

Verification

  • Pre-commit dry-build is eval-only — it cannot verify the literal address/interface (ADR 0010's known gap). Re-confirm 2a01:4f8:c012:9d85::2 + enp1s0 against the Hetzner console before merge.
  • Post-deploy:
    • ip -6 route shows default via fe80::1 dev enp1s0.
    • External IMAPS to [2a01:4f8:c012:9d85::2]:993 connects (and FreeScout fetches mail).
    • postconf smtp_address_preferenceipv4.
    • A test mail to a Gmail address still leaves over IPv4 (check the Received: header).
  • Admin/SSH stays on IPv4, so a bad v6 route can't lock anyone out.

Refs

  • ADR 0010 (static IPv6 on web-arm) — the precedent and the Postfix-coupling rationale.
  • Related inbound-v6 workstream: #82 / #83 (web-arm AAAA rollout). This is the mail equivalent, except the AAAA already exists, so it's a host-config fix not a DNS rollout.
  • Interface (enp1s0), missing global v6, and Dovecot's [::]:993 listener all confirmed via the read-only diag channel on mail.
> *This was generated by AI during triage.* ## Problem FreeScout (`support.cloonar.dev`, hosted on **web-arm**) fails to connect to IMAP: > `stream_socket_client(): Unable to connect to ssl://imap.cloonar.com:993 (Connection timed out)` **Root cause:** `imap.cloonar.com` / `mail.cloonar.com` publish an AAAA record `2a01:4f8:c012:9d85::2`, but the **mail** host never configures that address. Confirmed live via the read-only diag channel on `mail`: - `enp1s0` carries only `91.107.201.241/32` + link-local `fe80::9400:2ff:fe23:b445/64` — **no global IPv6**. - `ip -6 route` shows only `fe80::/64` — **no default v6 route**. - Dovecot already listens on `[::]:993` (and 25/143/389/465/587/636 on `[::]:*`). So inbound v6 SYNs to `[2a01:4f8:c012:9d85::2]:993` reach mail's link but the host doesn't own the address → packets are **black-holed** ("timed out", not "refused"). **Why it regressed now:** when **web-arm** gained outbound IPv6 (ADR 0010 / #81), RFC 6724 made it prefer the v6 destination for `imap.cloonar.com`. PHP's `stream_socket_client()` (webklex/php-imap) has **no happy-eyeballs / no v6→v4 fallback**, so it connects to `::2` and hangs. Inbound **MX-over-v6** to mail is broken for the same reason. ## Fix (coupled — both land in one PR) **1. `hosts/mail/configuration.nix`** — claim the published AAAA + standard Hetzner gateway (mirrors web-arm; interface confirmed as `enp1s0`): ```nix networking.interfaces.enp1s0.ipv6.addresses = [ { address = "2a01:4f8:c012:9d85::2"; prefixLength = 64; } # ::2 to match the existing AAAA ]; networking.defaultGateway6 = { address = "fe80::1"; interface = "enp1s0"; }; ``` **2. `hosts/mail/modules/postfix.nix`** — pin outbound to IPv4: ```nix services.postfix.settings.main.smtp_address_preference = "ipv4"; ``` Rationale: mail's v6 address has no PTR and is in no sending domain's SPF. Without the pin, Postfix (default `any`) would prefer the v6 source to dual-stack MXes (Gmail/Microsoft) → spam-filed. The pin keeps outbound delivery exactly as today (over IPv4). **Dovecot needs no change** — it already listens on `[::]:993`; claiming `::2` is sufficient. ## Decisions (settled with maintainer) - **Outbound stays on IPv4** for now. Real v6 sending is separate future work: requires a PTR for `::2` in Hetzner **and** adding `::2` to the SPF of every sending domain (cloonar.com, optiprot.eu, superbros.tv, szaku-consulting.at, scana11y.com, macher.solutions, fueltide, docfast.dev). - **v6 exposure mirrors today's v4 exposure.** Once `::2` is up, the family-agnostic firewall makes every open port reachable over v6 too — **including LDAP 389/636**. Explicitly accepted (it mirrors the existing v4 exposure; scoping LDAP to wireguard is out of scope here). ## Docs - Add a **new ADR** cross-referencing ADR 0010: "mail claims its inbound IPv6; outbound pinned to IPv4." ## Verification - Pre-commit dry-build is **eval-only** — it cannot verify the literal address/interface (ADR 0010's known gap). Re-confirm `2a01:4f8:c012:9d85::2` + `enp1s0` against the Hetzner console before merge. - Post-deploy: - `ip -6 route` shows `default via fe80::1 dev enp1s0`. - External IMAPS to `[2a01:4f8:c012:9d85::2]:993` connects (and FreeScout fetches mail). - `postconf smtp_address_preference` → `ipv4`. - A test mail to a Gmail address still leaves over IPv4 (check the `Received:` header). - Admin/SSH stays on IPv4, so a bad v6 route can't lock anyone out. ## Refs - **ADR 0010** (static IPv6 on web-arm) — the precedent and the Postfix-coupling rationale. - Related inbound-v6 workstream: #82 / #83 (web-arm AAAA rollout). This is the **mail** equivalent, except the AAAA already exists, so it's a host-config fix not a DNS rollout. - Interface (`enp1s0`), missing global v6, and Dovecot's `[::]:993` listener all confirmed via the read-only diag channel on `mail`.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos#97
No description provided.