Mail HELO

RFC 5321 §4.1.1.1 HELO / EHLO hostname validation primitive + forward-confirmed-reverse-DNS (FCrDNS, RFC 8601 §2.7.6) verifier. Composes b.network.dns.resolver (v0.9.31) for the rDNS + forward lookup pair; pairs with b.guardSmtpCommand (v0.9.32) which gates the command-line SHAPE — this primitive evaluates the SEMANTIC identity claim.

The MX listener (v0.9.36) calls b.mail.helo.evaluate({ ip, claimedName, resolver }) at the EHLO boundary and feeds the verdict into the per-connection policy decision (reject / greylist-anyway / score-tag for downstream SpamAssassin / accept).

## Shape gate (RFC 5321 §4.1.1.1 + §4.1.2)

- **Domain form**: LDH labels per RFC 5321 §2.3.5, FQDN with at least one . (operator can demand multi-label via profile). Bare hostname (no dots) refused under strict; localhost-class claims (localhost, localdomain) always refused regardless of profile. - **Address-literal**: [1.2.3.4] IPv4 or [IPv6:2001:db8::1] IPv6 per RFC 5321 §4.1.3 — accepted when matches the connection IP, refused otherwise (RFC 5321 §4.1.1.1 implies the literal should be the actual host). - **Empty / too-long**: refused under all profiles.

## FCrDNS check (RFC 8601 §2.7.6 / RFC 1912 §2.1)

With resolver provided, evaluate() issues:

1. PTR for .in-addr.arpa (IPv4) or .ip6.arpa (IPv6) — reverse name. 2. A / AAAA for each PTR result — forward name. 3. Match: at least one forward IP must equal the connection IP (the FCrDNS contract).

The returned verdict carries the rDNS name(s) + the per-name forward-match outcome so operator audit pipelines see exactly why FCrDNS passed or failed.

## "Generic rDNS" heuristic (operator-configurable)

Many spam sources have FCrDNS-valid rDNS that's CLEARLY a consumer ISP dynamic pool (pool-xx-xx.dialup.example.com, dsl-1234.foo.example.net, etc.). Operators opt-in via { genericRdnsPatterns: [...] } and the verdict flags genericRdns: true. Pre-shipped pattern list lives in b.mail.helo.GENERIC_RDNS_PATTERNS for the common consumer-ISP shapes; operator extends per-deployment.

## Verdict shape

{
  action:        "accept" | "reject-shape" | "soft-fail-fcrdns" |
                 "match-self-refused" | "literal-mismatch",
  shape:         "domain" | "address-literal-v4" |
                 "address-literal-v6" | "bare-host" | "invalid",
  fcrdns:        {
    checked:     boolean,
    passed:      boolean,
    rdnsNames:   string[],
    forwardIps:  string[],
    matchedIp:   string | null,
  } | null,
  genericRdns:   boolean,
  reason:        string,
}

## CVE / threat model

- **HELO spoofing** — RFC 5321 §4.1.1.1 doesn't require HELO accuracy, but a peer claiming our-mx-cluster.example.com when its FCrDNS resolves elsewhere is suspect. Operator's selfNames list blocks the self-claim spoof. - **Botnet residential-IP class** — generic-rDNS detection + RBL composition catches consumer-ISP dynamic-pool sources before they reach the DATA phase. - **DNS poisoning of PTR** — composed via b.network.dns.resolver, so PTR queries inherit the resolver's safeDns caps, AD-bit surface, and CVE coverage (CVE-2008-1447 / 2022-3204 / 2023-50387 / 50868 / 2024-1737).

## When NOT to enforce FCrDNS strict

IPv6 PTR records are spotty across consumer ISPs; FCrDNS-strict on IPv6 traffic over-rejects. Operator opts to { fcrdnsRequiredFor: ["v4"] } under balanced profile when they need to accept v6 senders without PTR records (common with legitimate cloud / VPS providers that don't auto-publish rDNS).

b.mail.helo.evaluate(ctx, opts?) #

stable0.9.35
{
  profile:               "strict" | "balanced" | "permissive",
  posture:               "hipaa" | "pci-dss" | "gdpr" | "soc2",
  selfNames:             string[],            // operator's MX hostnames; claim of these by a peer refused
  genericRdnsPatterns:   RegExp[],            // additional patterns layered onto built-ins
  fcrdnsRequiredFor:     ("v4" | "v6")[],     // overrides profile's FCrDNS list
  audit:                 b.audit namespace,
}

Evaluate a peer's HELO / EHLO identity claim. Returns a verdict shape the MX listener consumes to drive accept / reject / score-tag policy.

var resolver = b.network.dns.resolver.create();
var v = await b.mail.helo.evaluate({
  ip:          "203.0.113.42",
  claimedName: "mail.example.com",
  resolver:    resolver,
}, { profile: "strict" });
if (v.action === "reject-shape") return reply(550, v.reason);

b.mail.helo.compliancePosture(posture) #

stable0.9.35

Return the effective profile name for a compliance posture, or null for unknown posture names.

b.mail.helo.compliancePosture("hipaa");   // → "strict"

Last updated 2026-08-08T16:39:49.652Z by seeder.