31 luglio 2026 10 min di lettura

Testare SMTP su Linux: dalla connessione TCP all'e-mail consegnata

Quando un'appliance non consegna più e-mail, un test SMTP manuale è più utile di qualsiasi log. Come verificare livello per livello con gli strumenti integrati, cosa indicano i vari errori e perché un Load Balancer può falsare la diagnosi.

Quando un gateway di posta improvvisamente non consegna più nulla, i log dell’appliance spesso mostrano solo il risultato finale: una consegna non riesce, la coda cresce, un messaggio di errore segnala un timeout. La causa effettiva emerge solo con un test manuale dalla riga di comando. SMTP è un protocollo in chiaro che può essere gestito interamente a mano, ed è proprio questo a renderlo uno strumento diagnostico disponibile ovunque senza installazioni aggiuntive.

La seconda ragione per il test manuale: sulle appliance di solito non è possibile installare nulla. Nessun gestore di pacchetti, nessun diritto di root, nessun swaks. Tutti i passaggi seguenti funzionano quindi con ciò che è già disponibile praticamente su ogni sistema Linux.

Separare i livelli

L’invio di un’e-mail non riuscito può fallire a cinque livelli diversi, e ciascuno produce un quadro di errore differente:

  1. Risoluzione dei nomi: l’host di destinazione non può essere tradotto in un indirizzo IP.
  2. Connessione TCP: la connessione alla porta non viene stabilita oppure viene resettata.
  3. Dialogo SMTP: la connessione è attiva, ma il server rifiuta mittente, destinatario o contenuto.
  4. Crittografia del trasporto: STARTTLS non è disponibile, il certificato non è valido oppure la versione TLS non è compatibile.
  5. Verifica del mittente: l’e-mail viene accettata e scartata dal destinatario per SPF, DKIM o DMARC.

La diagnosi migliora enormemente se si verificano questi livelli uno dopo l’altro e singolarmente, invece di inviare subito un’e-mail di prova completa. Un tentativo complessivo fallito dice soltanto che qualcosa non funziona. La verifica a livelli indica cosa.

Passo 1: risoluzione dei nomi

getent hosts relay.example.com
Opzioni spiegate
OpzioneEffetto
hostsDatabase NSS da interrogare; utilizza le stesse Fonti e lo stesso ordine del sistema stesso, secondo nsswitch.conf
relay.example.comNome host da risolvere

Se l’output rimane vuoto, nessun server dei nomi è raggiungibile su questo host oppure non risponde ai nomi esterni. Nella pratica accade regolarmente: le appliance in zone isolate ricevono spesso soltanto un resolver interno, che conosce esclusivamente le proprie zone.

cat /etc/resolv.conf; grep hosts: /etc/nsswitch.conf
Opzioni spiegate
OpzioneEffetto
/etc/resolv.confFile emesso da cat con i server dei nomi configurati
hosts:Modello di ricerca per grep: la riga che stabilisce l’ordine delle Fonti di risoluzione (file, DNS)
/etc/nsswitch.confFile contenente la configurazione NSS, cercato da grep

Se manca la risoluzione, nei passaggi seguenti testate direttamente l’indirizzo IP. Per la diagnosi è del tutto sufficiente e separa nettamente il problema DNS dal problema di trasporto. Per l’esercizio in produzione, naturalmente, la risoluzione mancante resta un problema a sé stante da correggere.

Passo 2: raggiungibilità della porta

Per il puro controllo TCP è sufficiente bash. Il dispositivo pseudo /dev/tcp apre una connessione senza che sia necessario avere installati nc o telnet:

timeout 10 bash -c 'exec 3<>/dev/tcp/192.0.2.25/25' ; echo "exit=$?"
Opzioni spiegate
OpzioneEffetto
timeout 10Interrompe il comando successivo dopo 10 secondi e restituisce quindi il codice di uscita 124
bash -c '…'Esegue la catena di comandi in una bash; necessario perché /dev/tcp è una funzionalità di bash
exec 3<>/dev/tcp/192.0.2.25/25Apre il descrittore di file 3 in lettura e scrittura come connessione TCP a 192.0.2.25, porta 25
echo "exit=$?"Emette il codice di uscita del comando precedente

Il codice di uscita è qui l’informazione essenziale:

uscitaSignificato
0La connessione è attiva, la porta è aperta
124Timeout: i pacchetti vengono scartati, tipico di un firewall con regola DROP
1Rifiuto immediato (RST) oppure rotta mancante

Nella pratica, la differenza tra 124 e 1 è l’indizio più importante in assoluto. Un timeout significa che qualcuno lungo il percorso scarta silenziosamente, ed è quasi sempre una regola firewall. Un RST immediato proviene invece da un sistema che risponde, ma non offre il servizio.

Verificate subito entrambe le porte pertinenti e inoltre un’altra destinazione qualsiasi, per vedere se l’host è autorizzato a stabilire connessioni in uscita:

for t in "192.0.2.25 25" "192.0.2.25 587" "1.1.1.1 443"; do
  set -- $t
  timeout 8 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null
  echo "$1:$2 -> exit=$?"
done
Opzioni spiegate
OpzioneEffetto
set -- $tDivide la coppia di valori sullo spazio nei parametri posizionali $1 (indirizzo IP) e $2 (porta)
timeout 8Interrompe il tentativo di connessione dopo 8 secondi (codice di uscita 124)
bash -c "…"Esegue l’apertura della connessione /dev/tcp in una bash
2>/dev/nullSopprime i messaggi di errore, affinché per ogni destinazione appaia esattamente una riga di risultato

Se fallisce anche il test di controllo, il sistema non ha in generale un accesso diretto in uscita e il traffico deve passare attraverso un relay interno o un proxy. Più avanti vedremo perché questo caso è particolarmente insidioso.

Se manca /dev/tcp, la shell non è bash. In sh, ash o ksh questa funzionalità non esiste, e spesso ciò viene erroneamente interpretato come un problema di rete:

ps -p $$ -o comm= ; echo "BASH_VERSION=${BASH_VERSION:-keine bash}"
Opzioni spiegate
OpzioneEffetto
-p $$Limita l’output al processo con il PID della shell corrente ($$)
-o comm=Emette solo il nome del comando; l’etichetta vuota dopo = sopprime l’intestazione
${BASH_VERSION:-keine bash}Emette la versione di bash oppure il testo sostitutivo se la variabile non è impostata

Passo 3: prima ascoltare, non inviare

Un server SMTP si presenta autonomamente con un banner 220. Il singolo test più significativo consiste quindi nell’aprire una connessione e non fare nulla:

{ exec 3<>/dev/tcp/192.0.2.25/25 && timeout 15 cat <&3; echo "[ende exit=$?]"; }
Opzioni spiegate
OpzioneEffetto
exec 3<>/dev/tcp/192.0.2.25/25Apre il descrittore di file 3 come connessione TCP alla destinazione
timeout 15 cat <&3Per 15 secondi legge tutto ciò che il server invia spontaneamente e lo emette
echo "[ende exit=$?]"Mostra il codice di uscita alla scadenza; 124 significa: per 15 secondi non è arrivato più nulla

Questi pochi caratteri separano due situazioni completamente diverse. Se arriva un 220 mail.example.com ESMTP, la controparte comunica e tutti gli ulteriori errori risiedono nel dialogo. Se non arriva nulla, non dipende da un comando formulato erroneamente da parte vostra, perché non ne avete inviato alcuno.

Il descrittore di file resta poi aperto nella shell. Chiudetelo prima di avviare il test successivo, altrimenti potreste continuare a lavorare con una vecchia connessione non più integra:

exec 3<&- 3>&-
Opzioni spiegate
OpzioneEffetto
3<&-Chiude il lato di lettura del descrittore di file 3
3>&-Chiude il lato di scrittura del descrittore di file 3

Passo 4: il dialogo SMTP a mano

Se il banner è presente, eseguite il dialogo completo. È importante avere un processo di lettura in esecuzione, così da vedere ogni risposta nel momento in cui arriva. Uno script che prima invia tutto e poi legge non mostra nulla in caso di interruzione a metà dialogo:

{
exec 3<>/dev/tcp/192.0.2.25/25
cat <&3 & R=$!
sleep 1; printf 'EHLO host.example.com\r\n' >&3
sleep 2; printf 'MAIL FROM:<absender@example.com>\r\n' >&3
sleep 2; printf 'RCPT TO:<empfaenger@example.net>\r\n' >&3
sleep 2; printf 'DATA\r\n' >&3
sleep 2; printf 'From: absender@example.com\r\nTo: empfaenger@example.net\r\nSubject: Relay-Test\r\n' >&3
printf 'Date: %s\r\nMessage-ID: <%s@example.com>\r\n\r\nTestnachricht\r\n.\r\n' "$(date -R)" "$(date +%s).$" >&3
sleep 3; printf 'QUIT\r\n' >&3
sleep 2; kill $R 2>/dev/null
}
Opzioni spiegate
OpzioneEffetto
exec 3<>/dev/tcp/192.0.2.25/25Apre il descrittore di file 3 come connessione TCP alla destinazione
cat <&3 & R=$!Avvia un lettore in background per il descrittore di file 3 e memorizza il suo PID in R
printf '…\r\n' >&3Invia un comando SMTP con la terminazione di riga CRLF richiesta sulla connessione
sleep nAttende i secondi indicati per la risposta del server prima di inviare il comando successivo
date -RFornisce la data in formato conforme a RFC per l’header Date:
date +%sFornisce l’ora Unix come semplice base univoca per il Message-ID
kill $R 2>/dev/nullTermina il lettore in background; il messaggio di errore viene omesso se è già terminato

Due dettagli determinano il successo o il fallimento. SMTP richiede CRLF come terminazione di riga, quindi printf con \r\n e non echo. Il punto su una riga propria termina la parte del messaggio; deve essere inviato come \r\n.\r\n.

Il flusso previsto: 220 all’apertura della connessione, 250 in risposta a EHLO, 250 2.1.0 a MAIL FROM, 250 2.1.5 a RCPT TO, 354 a DATA e infine 250 2.0.0 Ok: queued as <id>. Annotate l’ID della coda. In questo modo il provider che gestisce il servizio può tracciare il messaggio se non arriva mai al destinatario.

Il nome EHLO merita attenzione: alcuni relay lo verificano tramite DNS diretto e inverso e altrimenti rispondono con 501 o 504. Utilizzate l’FQDN effettivo del sistema mittente, non il nome breve.

Passo 5: STARTTLS e certificato

Per la connessione cifrata, openssl s_client gestisce autonomamente la negoziazione STARTTLS e poi passa il canale allo standard input:

openssl s_client -connect 192.0.2.25:25 -starttls smtp -tls1_2 -brief </dev/null
Opzioni spiegate
OpzioneEffetto
-connect 192.0.2.25:25Host e porta di destinazione della connessione
-starttls smtpEsegue prima il dialogo SMTP in chiaro e poi passa a TLS tramite STARTTLS
-tls1_2Negozia esclusivamente TLS 1.2
-briefRiduce l’output a un breve riepilogo della connessione negoziata
</dev/nullChiude subito lo standard input affinché s_client non attenda in modo interattivo dopo l’handshake

Se vi connettete tramite indirizzo IP perché manca il DNS, la verifica del nome host non può funzionare. Il nome nel certificato non corrisponde quindi all’indirizzo numerico. SNI e nome di verifica possono essere impostati esplicitamente, senza alcuna interrogazione DNS:

openssl s_client -connect 192.0.2.25:25 \
  -servername mail.example.com -verify_hostname mail.example.com \
  -starttls smtp -tls1_2 -brief </dev/null
Opzioni spiegate
OpzioneEffetto
-servername mail.example.comImposta il nome SNI nel ClientHello, indipendentemente dall’indirizzo di connessione
-verify_hostname mail.example.comVerifica il certificato del server rispetto a questo nome anziché rispetto all’indirizzo numerico

Qui compaiono regolarmente due quadri di errore, spesso interpretati erroneamente.

«Didn’t find STARTTLS in server response, trying anyway» significa che il server non ha offerto STARTTLS nella risposta a EHLO. openssl invia comunque un TLS ClientHello, il server lo considera dati di protocollo non validi e la connessione termina con wrong version number o write:errno=32 (EPIPE). Entrambi i messaggi sono errori conseguenti. L’informazione effettiva è: nessun STARTTLS. Con il dialogo in chiaro del passo 4 potete verificare quali capability segnala realmente il server.

Nessun STARTTLS su un hop interno è spesso del tutto corretto. Se un Load Balancer inoltra la connessione a livello 4, non negozia TLS lui, bensì il sistema posto dietro di esso verso la destinazione effettiva. Testare in chiaro sul segmento interno non è quindi una carenza di sicurezza, ma semplicemente l’architettura.

Passo 6: Python come alternativa

Se Python è disponibile, potete evitare la temporizzazione manuale con sleep. È sufficiente la libreria standard, non occorre installare nulla:

#!/usr/bin/env python3
import smtplib, ssl
from email.message import EmailMessage
from email.utils import formatdate, make_msgid

msg = EmailMessage()
msg["From"] = "absender@example.com"
msg["To"] = "empfaenger@example.net"
msg["Subject"] = "Relay-Test"
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid(domain="example.com")
msg.set_content("Testnachricht\n")

ctx = ssl.create_default_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2

s = smtplib.SMTP("192.0.2.25", 25, timeout=30, local_hostname="host.example.com")
s.set_debuglevel(1)
s.ehlo()
if s.has_extn("starttls"):
    s.starttls(context=ctx, server_hostname="mail.example.com")
    s.ehlo()
    print("TLS:", s.sock.version(), s.sock.cipher()[0])
s.send_message(msg)
s.quit()

set_debuglevel(1) registra il dialogo completo, inclusi tutti i codici di risposta, e smtplib legge ogni risposta in modo sincrono. Un’interruzione appare come SMTPServerDisconnected insieme all’ultima riga ricevuta, invece che come una Broken Pipe silenziosa.

Due aspetti spesso causano problemi: server_hostname è indispensabile quando ci si connette tramite indirizzo IP, altrimenti Python verifica il certificato rispetto all’indirizzo numerico. E se disattivate consapevolmente la verifica, check_hostname = False deve precedere verify_mode = ssl.CERT_NONE, altrimenti Python genera un ValueError.

Indirizzo del mittente, SPF e alignment

Un test sorprendentemente spesso non fallisce nel trasporto, ma nell’indirizzo del mittente scelto. Tre aspetti vanno verificati in anticipo.

Il dominio del mittente deve essere un FQDN. Un indirizzo come test@meine-testdomain senza dominio di primo livello viene rifiutato da molti MTA già al MAIL FROM con 501 o 553.

Il dominio deve autorizzare il percorso di invio utilizzato. Uno sguardo al record SPF mostra se l’indirizzo in uscita è coperto:

dig +short TXT example.com | grep spf1
Opzioni spiegate
OpzioneEffetto
+shortEmette solo i valori dei record, senza intestazioni e metadati
TXTTipo di record interrogato
example.comNome interrogato
grep spf1Filtra la riga SPF da più record TXT

E con DMARC attivo, è decisivo l’alignment. Se nel record è presente aspf=s, il dominio nell’envelope (MAIL FROM) e il dominio nell’header From: devono corrispondere esattamente, non essere soltanto correlati:

dig +short TXT _dmarc.example.com
Opzioni spiegate
OpzioneEffetto
+shortEmette solo i valori dei record, senza intestazioni e metadati
TXT _dmarc.example.comTipo di record e nome definito per DMARC sotto il dominio

Con p=reject, un’e-mail di prova con alignment non corrispondente scompare senza commenti presso il destinatario, anche se il relay l’ha accettata con 250 queued. È la causa più frequente dei messaggi che dal lato di invio sono considerati riusciti e tuttavia non arrivano mai.

Quando c’è un Load Balancer nel mezzo

In ambienti più grandi, un’appliance raramente invia direttamente su Internet. È comune un server virtuale su un Load Balancer che accetta la connessione, la riscrive tramite source NAT a un indirizzo definito e solo allora la inoltra verso l’esterno. Questo ha una conseguenza spiacevole per la diagnosi.

Un server virtuale che opera a livello 4 conferma immediatamente l’handshake TCP, prima ancora di aver stabilito lui stesso una connessione alla destinazione. Se questa seconda connessione fallisce, sul client vedete una connessione stabilita con successo e subito dopo resettata: Connection reset by peer, senza alcun banner SMTP. L’errore non è quindi né presso di voi né presso la destinazione, ma nel pool dietro il server virtuale, ad esempio perché un membro è contrassegnato come down oppure l’FQDN configurato non viene risolto.

Questo spiega anche perché un test diretto contro la destinazione Internet deve fallire, se la regola di inoltro accetta solo traffico dall’indirizzo SNAT già riscritto. Le connessioni con l’indirizzo sorgente originale non corrispondono a nessuna regola e vengono scartate. In questi ambienti testate sempre contro il server virtuale previsto, non contro la destinazione effettiva.

Quale indirizzo sorgente utilizza il vostro sistema per una determinata destinazione è indicato da una sola riga. Il valore dopo src è esattamente l’informazione di cui il team di rete ha bisogno per l’abilitazione:

ip route get 192.0.2.25
Opzioni spiegate
OpzioneEffetto
route getChiede al kernel quale rotta sceglierebbe per una destinazione concreta
192.0.2.25Indirizzo di destinazione della connessione simulata

Se il sistema si trova dietro NAT, la controparte non vede questo indirizzo, bensì l’indirizzo pubblico del perimetro. Non è possibile determinarlo dall’interno finché non passa alcun traffico; è indicato nella regola NAT.

Quadri di errore a colpo d’occhio

OsservazioneCausa probabile
Name or service not knownNessuna risoluzione dei nomi sull’host
Timeout, uscita 124Il firewall scarta silenziosamente (DROP)
Connection refusedNessun servizio sulla porta o regola REJECT
Connessione attiva, nessun banner, poi RSTIl Load Balancer accetta, backend non raggiungibile
Didn't find STARTTLSIl server non offre crittografia del trasporto
wrong version number, errno=32Errori conseguenti dopo TLS forzato senza STARTTLS
501 / 553 su MAIL FROMIl dominio del mittente non è un FQDN oppure non è consentito
554 relay access deniedIP sorgente non abilitato sul relay
250 queued, ma nessuna consegnaAlignment SPF, DKIM o DMARC presso il destinatario

Test di carico e limiti di frequenza

Per i test di volume vale una regola spesso trascurata nella quotidianità: il problema non è il numero di messaggi, ma il numero di connessioni. I relay tipici consentono alcune centinaia di connessioni al minuto, ma decine di migliaia di messaggi. Mantenete quindi aperta una sessione e inviate molti envelope attraverso di essa, invece di riconnettervi per ogni messaggio.

In smtplib ciò significa semplicemente riutilizzare più volte lo stesso oggetto di connessione e ristabilire in modo controllato la sessione dopo un numero fisso di messaggi. Chi invece apre una nuova connessione per ogni e-mail supera il limite di connessioni ben prima del limite di messaggi e provoca rifiuti che sembrano un problema della controparte.

Conclusione

Il test SMTP manuale non è un ripiego per ambienti privi di strumenti, ma la diagnosi più precisa disponibile nell’esercizio della posta elettronica. Separa chiaramente risoluzione dei nomi, raggiungibilità, dialogo di protocollo e crittografia, e fornisce un risultato univoco per ciascun livello. Chi prima ascolta soltanto, poi conduce il dialogo a mano e prende sul serio i codici di risposta, ottiene in pochi minuti un riscontro con cui documentare un ticket al team di rete o al provider: con indirizzo sorgente, porta di destinazione, comportamento osservato e codice di uscita.

Fonti

  1. RFC 5321: Simple Mail Transfer Protocol

    Definisce il dialogo SMTP, l’ordine dei comandi e il significato dei codici di risposta.

    https://www.rfc-editor.org/rfc/rfc5321
  2. RFC 3207: SMTP Service Extension for Secure SMTP over TLS

    Descrive STARTTLS come estensione, incluso il comportamento quando il server non la offre.

    https://www.rfc-editor.org/rfc/rfc3207
  3. RFC 7208: Sender Policy Framework (SPF)

    Struttura e valutazione del record SPF per l’autorizzazione dei sistemi mittenti.

    https://www.rfc-editor.org/rfc/rfc7208
  4. RFC 7489: Domain-based Message Authentication, Reporting, and Conformance (DMARC)

    Regola l’alignment tra mittente dell’envelope e dell’header, nonché la valutazione delle policy.

    https://www.rfc-editor.org/rfc/rfc7489
  5. OpenSSL: s_client

    Riferimento per le opzioni utilizzate, tra cui -starttls, -servername e -verify_hostname.

    https://docs.openssl.org/master/man1/openssl-s_client/
  6. Documentazione Python: smtplib

    Libreria standard per sessioni SMTP, incluse STARTTLS e output di debug.

    https://docs.python.org/3/library/smtplib.html
  7. Bash Reference Manual: Redirections

    Documenta /dev/tcp come dispositivo pseudo specifico di bash per le connessioni di rete.

    https://www.gnu.org/software/bash/manual/bash.html#Redirections

Commenti

I commenti vengono caricati da GitHub / Giscus.