Prima di leggere: ricostruisci una spiegazione
Nella lezione sul firewall hai misurato una regola che accettava il traffico sulla porta 8080, contava i pacchetti, e non faceva funzionare la connessione. La spiegazione era che i pacchetti di ritorno arrivano con una porta di destinazione diversa. Ricostruiscila senza rileggere: quale porta hanno, chi la sceglie, e perché cambia a ogni connessione?
In questa lezione quella spiegazione non resta una spiegazione: la vedrai, pacchetto per pacchetto, con i numeri di porta sullo schermo. È il senso di una lezione sulla diagnostica. Gli strumenti non servono a sapere più comandi, servono a rendere visibile ciò che finora hai dovuto dedurre.
Il problema: «non funziona» non è una diagnosi
Ti arriva una segnalazione: il servizio non risponde. Potresti provare quindici cose, e in genere è quello che si fa: si riavvia il servizio, si guarda il firewall, si pinga qualcosa, si riavvia la macchina. A volte funziona, e quando funziona non sai perché, quindi la volta dopo ricominci da zero.
L'abilità che allenerai qui è un'altra: dividere la catena in anelli e provare un anello alla volta, scegliendo ogni volta lo strumento che risponde a quella domanda e nient'altro. Si chiama pratica deliberata perché ogni passaggio ha un obiettivo singolo e un modo di dire se è riuscito, e perché migliorare significa correggere una debolezza precisa e non «fare più esperienza».
Gli anelli di una connessione sono quattro: il nome diventa un indirizzo, l'indirizzo è raggiungibile, la porta è aperta, il servizio risponde correttamente. Quattro domande, quattro strumenti.
Primo anello: il nome. E due strumenti che danno risposte diverse
La domanda è «questo nome a quale indirizzo corrisponde», e lo strumento ovvio è dig.
Lo strumento giusto però dipende da chi fa la domanda nella realtà, ed è qui che si annida l'errore
più comune della diagnostica DNS. Guarda queste due risposte, raccolte sulla stessa macchina a
pochi secondi di distanza:
$ dig +noall +answer example.com A
example.com. 70 IN A 172.66.147.243
example.com. 70 IN A 104.20.23.154
$ getent hosts example.com
2606:4700:10::ac42:93f3 example.com
2606:4700:10::6814:179a example.com
Due comandi, due famiglie di indirizzi completamente diverse. Non è un errore di nessuno dei due:
dig interroga il DNS e gli ho chiesto i record A, cioè IPv4;
getent usa la libreria di risoluzione del sistema, la stessa che usano i programmi, e
quella ha restituito gli indirizzi IPv6 perché li preferisce. Il tuo programma si collegherà a un
indirizzo IPv6, e tu stavi diagnosticando un indirizzo IPv4.
La regola che ne segue: dig per capire che cosa dice il DNS, getent hosts
per capire che cosa farà il programma. Se le due risposte divergono, la divergenza
è il problema, e di solito la causa sta in /etc/hosts o nella
preferenza fra le due famiglie di indirizzi.
Un terzo dato serve a sapere chi sta rispondendo. Su questa macchina
/etc/resolv.conf contiene nameserver 127.0.0.53, che non è un server DNS
vero ma un intermediario locale, e resolvectl status lo conferma con
resolv.conf mode: stub. Significa che le domande passano da un servizio locale prima di
uscire, e che una cache intermedia può spiegare una risposta vecchia.
Secondo e terzo anello: la strada e la porta
Per la strada hai già lo strumento: ip route get della lezione sulle interfacce, che
dice se una destinazione è raggiungibile e da dove. Per la porta lo strumento è ss, e il
suo valore sta nei filtri, perché su una macchina reale l'elenco completo è illeggibile.
# ss -ltn "sport = :8080"
State Recv-Q Send-Q Local Address:Port Peer Address:Port
# ss -ltnp "sport = :8080"
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:* users:(("python3",pid=1350567,fd=3))
Prima e dopo l'avvio del servizio. Le lettere valgono la pena: -l solo quelli in
ascolto, -t solo TCP, -n numeri invece di nomi di servizio, -p
il processo. L'ultima colonna è la più utile di tutte, perché nomina il programma e il suo
identificativo: risponde a «chi tiene occupata questa porta», che è la domanda che si fa davvero.
E c'è un dettaglio nella colonna dell'indirizzo che spiega metà delle segnalazioni di servizi
irraggiungibili: 127.0.0.1:8080 significa che il servizio ascolta
solo sull'interfaccia di anello, quindi nessuno da fuori lo raggiungerà, qualunque
cosa dica il firewall. Se la colonna dicesse 0.0.0.0:8080 ascolterebbe su tutte le
interfacce. È una riga di configurazione del servizio, non un problema di rete, e distinguere le due
cose evita di passare un'ora sul firewall.
Gli strumenti che potresti non avere
Due strumenti che ogni guida alla diagnostica di rete nomina non sono installati su questa
macchina, e vale la pena dirlo invece di dare per scontato che ci siano. Misurato il 2026-10-30:
command -v traceroute non restituisce nulla, e
apt-cache policy traceroute dichiara Installato: (nessuno) con candidato
1:2.1.6-1build1. Lo stesso vale per nmap.
Al loro posto ci sono due alternative presenti di serie. tracepath fa un lavoro
analogo a traceroute e non richiede privilegi, e misura anche la dimensione massima dei
pacchetti lungo il percorso. mtr combina il tracciamento del percorso con una
misurazione continua, e serve quando il problema è intermittente e un singolo tentativo non lo
cattura. La lezione da portarsi via non è quale dei due usare, è che la prima cosa da fare su una
macchina nuova è verificare quali strumenti ci sono: una diagnosi che comincia con
l'installazione di un pacchetto su un server in avaria è una diagnosi che comincia male.
Esempio svolto: tre guasti, tre codici di uscita
Il problema: hai un indirizzo e una porta e la connessione non riesce. Vuoi sapere a quale anello è rotta la catena, senza accedere al server e senza chiedere a nessuno.
L'informazione disponibile è che curl non lascia un codice di uscita generico: ne lascia
uno diverso per famiglia di guasto. Il ragionamento: se i codici sono diversi, allora il codice è
una diagnosi, e provare lo stesso comando contro tre guasti costruiti su misura mi dà una tabella di
riferimento che userò ogni volta.
$ curl -sS --max-time 3 http://questo-nome-non-esiste.invalid/
curl: (6) Could not resolve host: questo-nome-non-esiste.invalid
$ curl -sS --max-time 3 http://127.0.0.1:8081/
curl: (7) Failed to connect to 127.0.0.1 port 8081 after 0 ms: Could not connect to server
$ curl -s -o /dev/null --max-time 3 http://127.0.0.1:8080/ # con una regola di drop attiva
tempo: 3.00 s
exit di curl=28
Codice 6: il primo anello è rotto, è un problema di nome, e il firewall non c'entra
niente. Codice 7 immediato: la strada c'è, la porta risponde con un rifiuto, quindi non
c'è nessuno in ascolto oppure c'è un reject. Codice 28 con l'attesa che
arriva fino al limite: qualcuno sta buttando i pacchetti in silenzio, e questo è il firewall.
La decisione che prendo: davanti a una segnalazione di servizio irraggiungibile, il primo comando è
curl con un limite di tempo breve, e il codice di uscita decide dove guardare.
L'alternativa che ho scartato è cominciare dal ping, per due motivi misurati. Il primo
è che ping usa un protocollo diverso da quello del tuo servizio, e tantissime reti lo
bloccano senza bloccare il resto: un ping che non risponde non dice quasi nulla. Il
secondo è che nella lezione sulle interfacce ho visto un ping riuscire verso un
indirizzo locale senza attraversare nessun cavo.
La verifica che chiude il ragionamento è il quarto anello, e si fa guardando i pacchetti. Questo è lo scambio completo di una singola richiesta HTTP, catturato nel laboratorio isolato:
# tcpdump --no-promiscuous-mode -Z root -i lo -n -c 8 "tcp port 8080"
IP 127.0.0.1.54674 > 127.0.0.1.8080: Flags [S], seq 1849229325, length 0
IP 127.0.0.1.8080 > 127.0.0.1.54674: Flags [S.], seq 2504559539, ack 1849229326, length 0
IP 127.0.0.1.54674 > 127.0.0.1.8080: Flags [.], ack 1, length 0
IP 127.0.0.1.54674 > 127.0.0.1.8080: Flags [P.], seq 1:79, ack 1, length 78: HTTP: GET / HTTP/1.1
IP 127.0.0.1.8080 > 127.0.0.1.54674: Flags [.], ack 79, length 0
IP 127.0.0.1.8080 > 127.0.0.1.54674: Flags [P.], seq 1:157, ack 79, length 156: HTTP: HTTP/1.0 200 OK
Guarda i numeri di porta e ritrova la spiegazione del firewall. Il client parte dalla porta
54674, scelta dal kernel al momento, verso la 8080. Le risposte del
server vanno dalla 8080 alla 54674. Una regola che autorizza la porta di
destinazione 8080 copre tre di questi sei pacchetti e non gli altri tre: non è una teoria, sono le
righe qui sopra.
Le tre lettere fra parentesi quadre sono il dialogo di apertura. [S] è la richiesta di
connessione, [S.] è la conferma, il punto da solo è la conferma della conferma, e
[P.] porta dati. Se vedi partire [S] e non arrivare mai
[S.], il problema è prima del servizio. Se vedi [R], qualcuno ha rifiutato
attivamente. L'errore che avrei potuto commettere è lanciare tcpdump senza un filtro: su
una macchina reale l'uscita scorre troppo in fretta per essere letta, e la diagnosi si perde nel
rumore.
Esercizio 1, guidato: costruisci i tre guasti
Nel laboratorio isolato, costruisci deliberatamente i tre guasti dell'esempio e raccogli per
ciascuno il codice di uscita di curl e il tempo trascorso. Il primo guasto ti serve una
precisazione: dentro il namespace non c'è risoluzione dei nomi, quindi il codice 6 lo
ottieni comunque, ma per una ragione diversa. Scrivi quale, e come lo verifichi.
Criterio: hai una tabella di tre righe con guasto, codice e tempo, e per ogni riga il comando che
distingue quel guasto dagli altri due.
Esercizio 2, trasferimento: la stessa diagnosi su un servizio diverso
Prendi un servizio di tipo diverso, non HTTP: un database, un server di posta, una cache. Non puoi
usare curl come prima. Riprogetta i quattro anelli per quel servizio: quale strumento
risponde a ciascuna delle quattro domande, e che differenza fa il fatto che il protocollo non sia
HTTP? Criterio di successo: hai quattro comandi, uno per anello, e per almeno uno di essi sai dire
perché lo strumento HTTP non sarebbe servito.
Esercizio 3, autonomo: la porta occupata
Sulla tua macchina, trova un servizio che ascolta su una porta e stabilisci tre cose con un solo comando: quale programma è, su quali interfacce ascolta, e se sarebbe raggiungibile da un'altra macchina della rete. Criterio: la risposta alla terza domanda viene da una colonna dell'output, non da una prova di connessione.
Soluzioni commentate
Esercizio 1, la precisazione sul primo guasto. Dentro il namespace il codice
6 arriva perché non esiste una strada verso il server DNS, non perché il nome non
esista. Sono due guasti diversi che producono lo stesso codice, e distinguerli è esattamente il tipo
di raffinamento che la diagnostica richiede. Il comando che li separa è dig: con un nome
inesistente e un DNS raggiungibile, dig risponde con stato NXDOMAIN, che è
una risposta; con un DNS irraggiungibile, dig non risponde affatto e scade. Quindi la
riga della tua tabella per il codice 6 va divisa in due, e il comando della colonna
«come lo distinguo» è dig.
Sugli altri due, l'errore più comune è leggere il codice e non il tempo. Il codice 7 può
arrivare subito oppure dopo un'attesa, e la differenza conta: subito significa rifiuto attivo, dopo
un'attesa significa che il tentativo di connessione è andato a vuoto. Per questo nella tabella la
colonna del tempo non è decorativa, e per questo nell'esempio ho messo
--max-time su ogni invocazione: senza un limite dichiarato, il tempo non è un dato
confrontabile.
Esercizio 2. I primi tre anelli non cambiano, e questa è la risposta più importante:
getent hosts, ip route get e ss funzionano identici per
qualunque servizio, perché stanno sotto il protocollo applicativo. Cambia solo il quarto anello, e
cambia in un modo che vale la pena capire: curl parla HTTP, quindi su un database non
può dirti se il servizio risponde correttamente, al massimo ti dice che la porta accetta connessioni.
Per il quarto anello hai due strade. La prima è il client nativo del servizio, che è la risposta
giusta quando esiste e quando hai le credenziali. La seconda, utile quando non hai né l'uno né le
altre, è ss più tcpdump: se vedi la connessione stabilita in
ss ma nessun dato scambiato in tcpdump, il servizio accetta e non parla, che
è un sintomo diverso da «la porta è chiusa». Perché telnet sulla porta non basta più
come una volta: apre la connessione, quindi prova l'anello tre, e per i protocolli che aspettano che
sia il server a parlare primo ti dà un'impressione di successo senza aver verificato niente.
Esercizio 3. Il comando è ss -ltnp, e le tre risposte stanno tutte
nella sua uscita: la colonna Process dà il programma, la colonna
Local Address:Port dà l'interfaccia. Sulla terza domanda, il criterio è quello che hai
visto nella spiegazione: se l'indirizzo locale è 127.0.0.1, il servizio non è
raggiungibile da fuori e nessuna regola di firewall lo renderebbe tale; se è 0.0.0.0 o
un indirizzo di un'interfaccia vera, lo è, e il firewall diventa l'unica cosa che lo protegge.
Perché la prova di connessione sarebbe una risposta più debole. Provare da un'altra macchina
risponde «sì» o «no» e non dice perché: un «no» può venire dall'indirizzo di ascolto, dal firewall
locale, da un firewall di rete in mezzo, o dal fatto che il servizio era appena riavviato. La colonna
di ss risponde a una sola di quelle cause e la risponde con certezza, e in diagnostica
una risposta certa su un anello vale più di una risposta incerta su tutta la catena.
Che cosa adesso sai fare
Sai dividere una connessione in quattro anelli e provarne uno alla volta, con lo strumento che
risponde a quella domanda: getent hosts e dig per il nome,
ip route get per la strada, ss -ltnp per la porta e per il processo che la
tiene, tcpdump con un filtro per lo scambio. Sai leggere il codice di uscita di
curl come una diagnosi, insieme al tempo trascorso.
Sai che dig e getent possono dare risposte diverse sullo stesso nome, e sai
che quando divergono la divergenza è il problema. E hai visto, nei numeri di porta di una cattura
vera, perché una regola di firewall sulla porta di destinazione copre metà di una connessione. La
verifica concreta: esegui ss -ltnp sulla tua macchina e trova un servizio che ascolta su
tutte le interfacce senza che tu lo volessi.