UNPKG

mx-connect

Version:

Establish TCP connection to a MX server with MTA-STS and DANE/TLSA support

255 lines (191 loc) 13.1 kB
# mx-connect Establish TCP connection to a MX server. This module takes a target domain or email address, resolves appropriate MX servers for this target and tries to get a connection, starting from higher priority servers. Supports unicode hostnames, IPv6, MTA-STS, and DANE/TLSA verification. ``` npm install mx-connect ``` ## Usage ```javascript const mxConnect = require('mx-connect'); // Using promises (recommended) const connection = await mxConnect(options); // Using callbacks mxConnect(options, (err, connection) => { ... }); ``` Where - **options** is the target domain, address, or configuration object - **callback** (optional) is the function to run once connection is established or it fails **Example using async/await** ```javascript const mxConnect = require('mx-connect'); try { const connection = await mxConnect('user@gmail.com'); console.log('Connection to %s:%s', connection.hostname, connection.port); // Connection to aspmx.l.google.com:25 connection.socket.pipe(process.stdout); // 220 mx.google.com ESMTP k11-v6si869487ljk.7 - gsmtp } catch (err) { console.error(err); } ``` **Example using callbacks** ```javascript const mxConnect = require('mx-connect'); mxConnect('user@gmail.com', (err, connection) => { if (err) { console.error(err); } else { console.log('Connection to %s:%s', connection.hostname, connection.port); // Connection to aspmx.l.google.com:25 connection.socket.pipe(process.stdout); // 220 mx.google.com ESMTP k11-v6si869487ljk.7 - gsmtp } }); ``` ### Configuration options You can use a domain name or an email address as the target, for additional configuration you would need to use a configuration object with the following properties (most are optional) - **target** is either a domain name or an email address or an IP address or IP address literal (basically anything you can have after the @-sign in an email address). Unicode is allowed. - **port** is the port number to connect to. Defaults to 25. - **maxConnectTime** is the timeout in milliseconds to wait for a connection to be established (per MX host). Defaults to 5 minutes. - **localAddress** is the local IP address to use for the connection - **localHostname** is the hostname of the local address - **localAddressIPv4** is the local IPv4 address to use for the connection if you want to specify an address both for IPv4 and IPv6 - **localHostnameIPv4** is the local hostname to use for IPv4 connections - **localAddressIPv6** is the local IPv6 address to use for the connection if you want to specify an address both for IPv4 and IPv6 - **localHostnameIPv6** is the local hostname to use for IPv6 connections - **dnsOptions** is an object for IP address related options - **ignoreIPv6** (boolean, defaults to `false`) If true then never use IPv6 addresses for sending - **preferIPv6** (boolean, defaults to `false`) If true then use IPv6 address even if IPv4 address is also available - **blockLocalAddresses** (boolean, defaults to `false`) If true then refuses to connect to IP addresses that are either in loopback, private network or attached to the server. People put every kind of stuff in MX records, you do not want to flood your loopback interface because someone thought it is a great idea to set 127.0.0.1 as the MX server - **resolve** (function, defaults to native `dns.resolve`) Custom callback-style DNS resolver function with signature `resolve(domain, type, callback)` or `resolve(domain, callback)` - **mx** is a hostname string, a resolved MX object, or an array of either, to skip DNS resolving. Useful if you want to connect to a specific host. String entries are treated as hostnames (or IP addresses) with priority 0. - **exchange** is the hostname of the MX - **priority** (defaults to 0) is the MX priority number that is used to sort available MX servers (servers with higher priority are tried first) - **A** is an array of IPv4 addresses. Optional, resolved from exchange hostname if not set - **AAAA** is an array of IPv6 addresses. Optional, resolved from exchange hostname if not set - **tlsaRecords** is an array of pre-resolved TLSA records for DANE verification. Optional, resolved automatically if DANE is enabled - **ignoreMXHosts** is an array of IP addresses to skip when connecting - **mxLastError** is an error object to use if all MX hosts are filtered out by `ignoreMXHosts` - **connectHook** _function (delivery, options, callback)_ is a function handler to run before establishing a tcp connection to current target (defined in `options`). If the `options` object has a `socket` property after the callback then connection is not established. Useful if you want to divert the connection in some cases, for example if the target domain is in the Onion network then you could create a socket against a SOCKS proxy yourself. - **connectError** _function (err, delivery, options)_ is a function handler to run when a connection to a MX fails. - **mtaSts** is an object for MTA-STS configuration - **enabled** - if not `true` then does not run MTA-STS checks, disabled by default - **logger(logObj)** - method to log MTA-STS information, logging is disabled by default - **cache** - an object to manage MTA-STS policy cache - **get(domain)** -> returns cached policy object - **set(domain, policyObj)** -> caches a policy object - **dane** is an object for DANE/TLSA configuration (see [DANE Support](#dane-support) section below) - **enabled** - must be set to `true` to enable DANE verification - **resolveTlsa(tlsaName)** - custom async function to resolve TLSA records. Receives the full TLSA query name (e.g., `_25._tcp.mail.example.com`). If not provided, uses native `dns.resolveTlsa` when available - **logger(logObj)** - method to log DANE information, logging is disabled by default - **verify** - if `true` (default), enforces DANE verification and rejects connections that fail. If `false`, only logs failures ### Connection object Function callback or promise resolution provides a connection object with the following properties: - **socket** is a socket object against target - **hostname** is the hostname of the exchange - **host** is the IP address of the exchange - **port** is the port used to connect - **localAddress** is the local IP address used for the connection - **localHostname** is the local hostname used for the connection - **localPort** is the local port used for the connection - **daneEnabled** is `true` if DANE verification is active for this connection - **daneVerifier** is the DANE certificate verification function (for use during TLS upgrade) - **tlsaRecords** is an array of TLSA records for this MX host (if DANE is enabled) - **requireTls** is `true` when DANE records exist, indicating TLS should be enforced. mx-connect itself does not enforce TLS -- the consuming application should check this flag during TLS upgrade ## DANE Support DANE (DNS-based Authentication of Named Entities) provides a way to authenticate TLS certificates using DNSSEC. This module supports DANE verification for outbound SMTP connections by looking up TLSA records and verifying server certificates against them. ### Security Considerations > **Important**: DANE security relies on DNSSEC validation. Without DNSSEC, a DNS attacker could potentially inject fake TLSA records and pin a malicious certificate, introducing new security vulnerabilities rather than preventing them. Currently, Node.js does not expose the DNSSEC AD (Authenticated Data) flag from DNS responses, which means applications cannot verify that TLSA records were DNSSEC-validated by the resolver. This is tracked in [nodejs/node#57159](https://github.com/nodejs/node/issues/57159). **Recommendations for production use:** 1. **Use a DNSSEC-validating resolver** - Configure your system to use a resolver that performs DNSSEC validation (e.g., Cloudflare's 1.1.1.1, Google's 8.8.8.8, or a local validating resolver like Unbound) 2. **Use DNS-over-HTTPS (DoH)** - A DoH resolver provides transport security via HTTPS, which protects against on-path attackers (though this is not a substitute for DNSSEC validation) 3. **Monitor nodejs/node#57159** - When Node.js adds AD flag support, this module will be updated to optionally require DNSSEC validation For domains with properly configured DNSSEC, DANE provides strong protection against certificate misissuance and man-in-the-middle attacks. For domains without DNSSEC, consider using MTA-STS as an alternative or complementary security mechanism. ### Node.js Version Support Native `dns.resolveTlsa` support was added in: | Node.js Version | TLSA Support | | --------------- | ------------ | | v24.x (Current) | ✅ Native | | v23.9.0+ | ✅ Native | | v22.15.0+ (LTS) | ✅ Native | | v22.0.0-v22.14 | ❌ None | | v20.x (LTS) | ❌ None | | v18.x | ❌ None | **Note:** `dane.enabled` must be set to `true` explicitly to activate DANE. There is no auto-detection. On Node.js versions without native `dns.resolveTlsa`, provide a custom resolver via the `dane.resolveTlsa` option. ### Custom TLSA Resolver For Node.js versions without native TLSA support, you can provide a custom resolver function: ```javascript const mxConnect = require('mx-connect'); const connection = await mxConnect({ target: 'user@example.com', dane: { enabled: true, resolveTlsa: customResolveTlsa, logger: console.log } }); console.log('Connected to %s:%s', connection.hostname, connection.port); console.log('DANE enabled:', connection.daneEnabled); if (connection.tlsaRecords) { console.log('TLSA records:', connection.tlsaRecords.length); } // Use connection.daneVerifier during TLS upgrade // The verifier function can be passed to tls.connect() as checkServerIdentity ``` ### DANE Verification Flow When DANE is enabled, the following flow occurs: 1. **TLSA Lookup**: Before connecting, mx-connect resolves TLSA records for each MX hostname (e.g., `_25._tcp.mail.example.com`) 2. **Connection**: A TCP connection is established to the MX server 3. **TLS Upgrade**: When upgrading to TLS (STARTTLS), use the `connection.daneVerifier` function as the `checkServerIdentity` option 4. **Certificate Verification**: The server's certificate is verified against the TLSA records ### TLSA Record Format TLSA records returned by the resolver should have the following structure: ```javascript { usage: 3, // 0=PKIX-TA, 1=PKIX-EE, 2=DANE-TA, 3=DANE-EE selector: 1, // 0=Full certificate, 1=SubjectPublicKeyInfo mtype: 1, // 0=Full data, 1=SHA-256, 2=SHA-512 cert: Buffer, // Certificate association data ttl: 3600 // TTL in seconds } ``` ### DANE Usage Types | Usage | Name | Description | Support Status | | ----- | ------- | ------------------------------------------------------- | -------------- | | 0 | PKIX-TA | CA constraint - must chain to specified CA | ⚠️ Limited\* | | 1 | PKIX-EE | Service certificate constraint - must match exactly | ✅ Full | | 2 | DANE-TA | Trust anchor assertion - specified cert is trust anchor | ⚠️ Limited\* | | 3 | DANE-EE | Domain-issued certificate - certificate must match | ✅ Full | > **\*Note on DANE-TA and PKIX-TA**: These usage types require access to the full certificate chain, which is not available in the standard TLS `checkServerIdentity` callback. Currently, only the end-entity (leaf) certificate is verified. If the TLSA record matches the end-entity certificate, verification will succeed; otherwise, it will fail even if the record matches a CA certificate in the chain. For most SMTP deployments, DANE-EE (usage=3) is recommended as it provides the strongest security guarantees and is fully supported. ### Combining DANE with MTA-STS DANE and MTA-STS can be used together. DANE provides stronger security guarantees (when DNSSEC is properly configured), while MTA-STS provides a fallback for domains that don't support DNSSEC: ```javascript const connection = await mxConnect({ target: 'user@example.com', mtaSts: { enabled: true, cache: mtaStsCache }, dane: { enabled: true, resolveTlsa: customResolveTlsa } }); // Both MTA-STS and DANE checks are performed ``` ### Accessing DANE Utilities The DANE module is exported for direct use: ```javascript const { dane } = require('mx-connect'); // Check if native TLSA resolution is available console.log('Native TLSA support:', dane.hasNativeResolveTlsa); // DANE constants console.log('DANE Usage Types:', dane.DANE_USAGE); console.log('DANE Selectors:', dane.DANE_SELECTOR); console.log('DANE Matching Types:', dane.DANE_MATCHING_TYPE); // Verify a certificate against TLSA records const result = dane.verifyCertAgainstTlsa(certificate, tlsaRecords); ``` ## License EUPL v1.1 or newer