Blog ·
Come un agente sceglie il server edge giusto
DNS con più record A, edge di livello 4 e PROXY protocol v2, con le misure: che cosa fa un client con più indirizzi e perché tornare è la metà difficile.
Di Provibr
Se fa girare un agente sull’hardware dei suoi clienti, l’indirizzo che esso compone vive in due posti scomodi: nel DNS pubblico e in qualunque file di credenziali sia stato consegnato al momento dell’arruolamento. Entrambi sono difficili da cambiare in seguito, e uno dei due dice a tutta Internet dove si trova il suo piano di controllo.
Davanti al nostro abbiamo messo uno strato di server edge. Ecco che cosa è servito, comprese le parti emerse soltanto quando ci sono passati agenti veri.
Il problema è l’indirizzo, non il traffico
Il nome host del gateway si risolveva nell’origine. Ogni agente arruolato lo conosceva, e lo conosceva anche chiunque sapesse digitare dig. Un’inondazione volumetrica puntata su quell’indirizzo mette fuori uso il piano di controllo per tutti, e a cose fatte non c’è nulla di astuto da fare.
L’obiettivo era ristretto, ed enunciarlo mantiene onesto il progetto: l’indirizzo dell’origine sparisce dal percorso che gli agenti usano, così un’inondazione colpisce una macchina sostituibile in dieci minuti. Non era affatto un obiettivo che gli edge proteggessero il contenuto. Il TLS mutuo lo fa già da un capo all’altro, e un edge in grado di ispezionare il traffico sarebbe un peggioramento.
Livello quattro, perché terminare il TLS romperebbe ciò che conta
Il progetto ovvio è un reverse proxy che termina il TLS all’edge. Qui è quello sbagliato, per due ragioni specifiche di questo tipo di sistema.
Il gateway autentica gli agenti sul loro certificato client, ancorato a un’impronta conservata per ciascun agente. Terminare il TLS all’edge chiude lì l’handshake mutuo, quindi l’origine dovrebbe fidarsi di qualunque cosa l’edge affermi. E significherebbe copiare la chiave privata del gateway su diverse macchine virtuali economiche il cui unico scopo è essere sacrificabili.
Perciò ogni edge è un semplice inoltratore TCP. Vede testo cifrato, non detiene segreti e non esegue alcuna applicazione, superficie abbastanza piccola perché «sostituiscilo» risponda a quasi ogni problema.
Costa una cosa sola. Ogni connessione sembra ora provenire dall’edge, quindi l’origine non sa più chi stia chiamando.
PROXY protocol v2, e la differenza fra un’affermazione e una misura
La correzione standard è il PROXY protocol: l’inoltratore antepone a ciascuna connessione una piccola intestazione binaria che porta l’indirizzo originale. Il gateway la interpreta e rimette l’indirizzo vero nei propri registri.
La decisione di progetto che vale la pena copiare è tenere due valori anziché uno.
- L’indirizzo che viene dall’intestazione è ciò che l’altro capo dice che il client sia. È un’affermazione.
- L’indirizzo del socket esterno, la macchina da cui i byte sono arrivati davvero, è qualcosa che abbiamo misurato noi.
Un’intestazione contraffatta può far mentire il primo. Il secondo resta vero. Tenerli come campi separati significa che una riga di registro si può leggere anni dopo senza chiedersi quale dei due sia, e rende facile la risposta onesta a «possiamo fidarci dell’IP»: non ne dipende nulla se non una riga di registro, perché l’identità dipende dal certificato del peer.
Due note di implementazione che ci hanno risparmiato dei guai:
Annusare, non esigere. Una migrazione al PROXY protocol non è atomica. Se i primi byte somigliano a un ClientHello TLS anziché alla firma del protocollo, li si rimette nel flusso e si inoltra la connessione intatta. Questo rende il rilascio del parser e la sua attivazione sugli inoltratori due passi indipendenti.
Tenere una mappa indicizzata sulla connessione interna. Una volta che i byte raggiungono una libreria gRPC, l’indirizzo del peer è una porta di loopback, e tanto l’agente quanto l’edge sono diventati introvabili. Indicizzare il peer reale sulla porta di origine della connessione interna, e cancellarlo alla chiusura, è l’unico posto in cui quell’informazione sopravvive.
Lì dentro si nasconde anche una piccola vittoria operativa: una connessione che si apre e si chiude senza inviare nulla non produce alcuna riga di registro. I controlli di stato lo fanno di continuo, e non scriverli ha tolto decine di migliaia di righe inutili al giorno.
Scoperta: l’insieme dei record A è l’elenco
Come fa l’agente a sapere che esiste più di un edge? Le opzioni erano record TXT con un carico strutturato, record SRV, oppure semplicemente diversi record A sullo stesso nome.
I record A multipli hanno vinto su un argomento solo. L’insieme dei record A è già l’elenco dei posti a cui connettersi. Un record TXT sarebbe una seconda fonte di verità per lo stesso fatto, e le due possono contraddirsi. SRV aggiungerebbe pesi di cui non abbiamo bisogno, perché l’agente misura per conto proprio anziché farsi dire quale preferire. Il TTL è breve, quindi rimuovere un edge è una modifica DNS più un TTL di pazienza.
Che cosa fa davvero un client con diversi record A
Prima di costruire qualunque logica di selezione, valeva la pena misurare che cosa accada gratis. Un client HTTP moderno prova gli indirizzi dell’insieme di record e distribuisce fra di essi il proprio timeout di connessione. Abbiamo messo per primo un indirizzo notoriamente morto e misurato:
| Comportamento dell’indirizzo morto | Tempo per connettersi a quello vivo |
|---|---|
| Connessione rifiutata | 7 ms |
| Nessuna rotta verso l’host | 7,5 ms |
| SYN scartato in silenzio, timeout di connessione di 25 s | 12,5 s |
| SYN scartato in silenzio, timeout di connessione di 10 s | 5,0 s |
Lo schema è abbastanza chiaro da poterci progettare contro: i rifiuti netti sono gratuiti, e un buco nero costa all’incirca il timeout di connessione diviso per il numero di indirizzi. Il failover quindi funzionava già senza alcuna modifica all’agente, così il passaggio del DNS ha potuto uscire prima della logica di selezione. Significa anche che il suo timeout di connessione è ora una funzione di quanti record pubblica, cosa non ovvia finché non si ha questa tabella davanti.
Il failover non è la selezione
Il failover gratuito sceglie l’indirizzo che risponde per primo, che non è lo stesso del migliore. Un agente ad Amsterdam è rimasto per giorni su un edge in un altro paese, perché quello aveva risposto per primo e la sessione poi è vissuta per una settimana.
Perciò l’agente misura. I dettagli che hanno contato:
Sondare con un handshake TLS completo, non con una connessione TCP. Un edge può accettare una connessione e non inoltrare nulla, che è esattamente il guasto che la sonda deve intercettare. Una nuda connessione viene esaudita altrettanto allegramente da un inoltratore rotto e da uno funzionante.
Dare a ciascuna sonda il proprio budget, in parallelo. Un edge che accetta e poi ammutolisce può lasciare un handshake appeso oltre il timeout di connessione, quindi il budget per indirizzo gli sta accanto anziché farvi affidamento.
Rendere persistente l’intera classifica, non il vincitore. L’indirizzo scelto e l’elenco ordinato finiscono nel file di stato dell’agente, insieme all’insieme di indirizzi da cui sono stati misurati. Questo rende economico un riavvio, e significa che un failover sopravvive a un riavvio invece di tornare allegramente all’indirizzo che ha appena fallito.
Invalidare sull’insieme, non sulla scelta. Se un edge viene aggiunto o rimosso, la misura è vecchia anche se l’indirizzo scelto è ancora presente.
Non fare nulla quando non c’è nulla da scegliere. Con meno di due candidati l’intero meccanismo viene saltato, che è ciò che accade in sviluppo, dove il nome si risolve in un unico indirizzo.
E la regola che governa tutto questo: la selezione non deve mai diventare una nuova modalità di guasto. Il DNS rifiuta, non risponde nessuno, il file di stato non si può scrivere: ognuno di questi percorsi termina in «nessuna scelta», e l’agente si connette esattamente come faceva prima che tutto ciò esistesse.
Tornare indietro è più difficile del failover
Il failover è facile. La metà difficile è che poi l’agente resta sull’edge più lento per tutta la durata di quella sessione, che può essere di settimane.
Perciò accanto alla sessione viva gira una sorveglianza che fa due cose e nient’altro. Prova gli indirizzi classificati sopra quello corrente, usando lo stesso handshake completo, e rimisura tutto quando l’ultima misura è vecchia. I numeri, e la ragione di ciascuno:
- Intervallo di cinque minuti con jitter pieno, così le sonde di una flotta non si sincronizzano. Ogni minuto costa cinque volte tanto e non guadagna nulla, perché l’isteresi qui sotto fa comunque richiedere due giri a ogni ritorno.
- Una serie di due prima di tornare indietro. Una sonda riuscita è un edge che ha appena finito di riavviarsi.
- Due soglie per la rimisurazione periodica: una assoluta di 20 ms e una relativa del 25 %. Devono essere soddisfatte entrambe. Quella assoluta da sola approverebbe uno spostamento da 100 ms a 79 ms; quella relativa da sola approverebbe da 4 ms a 3 ms. Due edge nello stesso data center differiscono di meno di un millisecondo, e senza questo si scambierebbero di posto un giorno sì e uno no.
Il passaggio è una riconnessione pulita con una propria motivazione, che sta accanto agli altri termini di sessione pianificati anziché essere modellata come un errore. Conta più di quanto sembri: un errore alimenterebbe la scala di attesa esponenziale, quindi un agente che torna al proprio edge preferito verrebbe punito con un ritardo crescente per aver fatto la cosa giusta. La cassetta dei messaggi in uscita ritrasmette e l’archivio di deduplicazione fa sì che nulla venga eseguito due volte.
Guardarlo dall’altro lato
La piattaforma sorveglia la stessa cosa dalla propria estremità, e lì due dettagli meritano di essere rubati.
Risolvere il nome nel modo in cui lo fa l’agente, attraverso il resolver di sistema anziché con un’interrogazione DNS diretta. Un’interrogazione diretta è fallita del tutto su una macchina il cui resolver puntava a sé stessa e, cosa più importante, il resolver di sistema è quello che il traffico reale usa, quindi restituisce lo stesso elenco.
Prendere due numeri da un unico socket: la connessione TCP, che è il viaggio di andata e ritorno fino all’edge stesso, e l’handshake TLS completo, che deve attraversare il tunnel fino all’origine e tornare. Lo stato di salute dipende dal secondo. Un inoltratore con un backend morto risponde perfettamente al primo.
L’ultima lezione riguarda il firewall. Docker aggira completamente il firewall dell’host. Le regole per cui solo gli edge possono raggiungere le porte del gateway devono vivere nella catena che Docker stesso consulta, altrimenti sono decorative pur sembrando attive. E quella catena vede il traffico in entrambe le direzioni, quindi le nostre stesse sonde in uscita dall’origine verso gli edge corrispondevano alla stessa porta di destinazione e venivano scartate, facendo sì che il monitoraggio segnalasse ogni edge come giù mentre ogni agente stava benissimo.
Che cosa non risolve
Gli edge non proteggono il contenuto, e sarebbe un errore iniziare a trattarli come un confine di sicurezza. Un indirizzo che era giù durante una misurazione completa viene riscoperto soltanto dalla successiva, lacuna nota e accettata. E il TTL del DNS è un limite inferiore alla rapidità con cui un edge esce dalla rotazione, che è il prezzo di lasciare che l’insieme dei record A sia l’elenco.
Ecco come l’agente raggiunge la piattaforma in Provibr, dove si connette solo verso l’esterno e conserva le credenziali sull’host del cliente.