Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

Ciao a tutti! Mi chiamo Dmitry Samsonov, lavoro come amministratore di sistema leader presso Odnoklassniki. Abbiamo più di 7mila server fisici, 11mila container nel nostro cloud e 200 applicazioni, che in varie configurazioni formano 700 cluster diversi. La stragrande maggioranza dei server esegue CentOS 7.
Il 14 agosto 2018 sono state pubblicate informazioni sulla vulnerabilità FragmentSmack
(CVE-2018-5391) e SegmentSmack (CVE-2018-5390). Si tratta di vulnerabilità con un vettore di attacco di rete e un punteggio abbastanza elevato (7.5), che minacciano la negazione del servizio (DoS) a causa dell'esaurimento delle risorse (CPU). All'epoca non era stata proposta una correzione del kernel per FragmentSmack, che inoltre è stata pubblicata molto più tardi rispetto alla pubblicazione delle informazioni sulla vulnerabilità. Per eliminare SegmentSmack, è stato suggerito di aggiornare il kernel. Il pacchetto di aggiornamento stesso è stato rilasciato lo stesso giorno, non restava che installarlo.
No, non siamo affatto contrari all'aggiornamento del kernel! Tuttavia ci sono delle sfumature...

Come aggiorniamo il kernel in produzione

In generale, niente di complicato:

  1. Scarica pacchetti;
  2. Installali su una serie di server (compresi i server che ospitano il nostro cloud);
  3. Assicurati che nulla sia rotto;
  4. Assicurati che tutte le impostazioni standard del kernel siano applicate senza errori;
  5. Aspetta qualche giorno;
  6. Controllare le prestazioni del server;
  7. Passare la distribuzione dei nuovi server al nuovo kernel;
  8. Aggiornare tutti i server per data center (un data center alla volta per ridurre al minimo l'effetto sugli utenti in caso di problemi);
  9. Riavvia tutti i server.

Ripeti per tutti i rami dei chicchi che abbiamo. Al momento è:

  • Stock CentOS 7 3.10 - per la maggior parte dei server regolari;
  • Vaniglia 4.19 - per i nostri nuvole di una nuvola, perché abbiamo bisogno di BFQ, BBR, ecc.;
  • Elrepo kernel-ml 5.2 - per distributori molto carichi, perché 4.19 si comportava in modo instabile, ma sono necessarie le stesse funzionalità.

Come avrai intuito, il riavvio di migliaia di server richiede più tempo. Poiché non tutte le vulnerabilità sono critiche per tutti i server, riavviamo solo quelli direttamente accessibili da Internet. Nel cloud, per non limitare la flessibilità, non colleghiamo contenitori accessibili dall'esterno ai singoli server con un nuovo kernel, ma riavviamo tutti gli host senza eccezioni. Fortunatamente, la procedura è più semplice rispetto ai server normali. Ad esempio, i contenitori stateless possono semplicemente spostarsi su un altro server durante un riavvio.

Tuttavia, c'è ancora molto lavoro e potrebbero volerci diverse settimane e, in caso di problemi con la nuova versione, fino a diversi mesi. Gli aggressori lo capiscono molto bene, quindi hanno bisogno di un piano B.

FragmentSmack/SegmentSmack. Soluzione alternativa

Fortunatamente, per alcune vulnerabilità esiste un piano B chiamato Workaround. Nella maggior parte dei casi si tratta di una modifica alle impostazioni del kernel/dell'applicazione che può ridurre al minimo il possibile effetto o eliminare completamente lo sfruttamento delle vulnerabilità.

Nel caso di FragmentSmack/SegmentSmack è stato proposto questa soluzione alternativa:

«È possibile modificare i valori predefiniti di 4 MB e 3 MB in net.ipv4.ipfrag_high_thresh e net.ipv4.ipfrag_low_thresh (e le loro controparti per ipv6 net.ipv6.ipfrag_high_thresh e net.ipv6.ipfrag_low_thresh) rispettivamente a 256 kB e 192 kB oppure inferiore. I test mostrano cali da piccoli a significativi nell'utilizzo della CPU durante un attacco a seconda dell'hardware, delle impostazioni e delle condizioni. Tuttavia, potrebbe esserci un certo impatto sulle prestazioni a causa di ipfrag_high_thresh=262144 byte, poiché solo due frammenti da 64 KB alla volta possono essere inseriti nella coda di riassemblaggio. Esiste ad esempio il rischio che le applicazioni che funzionano con pacchetti UDP di grandi dimensioni si interrompano'.

I parametri stessi nella documentazione del kernel descritto come segue:

ipfrag_high_thresh - LONG INTEGER
    Maximum memory used to reassemble IP fragments.

ipfrag_low_thresh - LONG INTEGER
    Maximum memory used to reassemble IP fragments before the kernel
    begins to remove incomplete fragment queues to free up resources.
    The kernel still accepts new fragments for defragmentation.

Non abbiamo grandi UDP sui servizi di produzione. Non c'è traffico frammentato sulla LAN; c'è traffico frammentato sulla WAN, ma non significativo. Non ci sono segnali: puoi implementare la soluzione alternativa!

FragmentSmack/SegmentSmack. Primo sangue

Il primo problema che abbiamo riscontrato è stato che i contenitori cloud a volte applicavano le nuove impostazioni solo parzialmente (solo ipfrag_low_thresh) e talvolta non le applicavano affatto: semplicemente si bloccavano all'inizio. Non è stato possibile riprodurre stabilmente il problema (tutte le impostazioni sono state applicate manualmente senza alcuna difficoltà). Anche capire perché il container si blocca all'inizio non è così semplice: non sono stati trovati errori. Una cosa era certa: ripristinare le impostazioni risolve il problema dei crash dei container.

Perché non è sufficiente applicare Sysctl sull'host? Il contenitore vive nel proprio spazio dei nomi di rete dedicato, almeno parte dei parametri Sysctl della rete nel contenitore potrebbe differire dall'host.

Come vengono applicate esattamente le impostazioni Sysctl nel contenitore? Poiché i nostri contenitori non sono privilegiati, non sarai in grado di modificare alcuna impostazione Sysctl accedendo al contenitore stesso: semplicemente non disponi di diritti sufficienti. Per eseguire i container, il nostro cloud a quel tempo utilizzava Docker (ora Podman). I parametri del nuovo contenitore sono stati passati a Docker tramite API, comprese le necessarie impostazioni Sysctl.
Durante la ricerca tra le versioni, si è scoperto che l'API Docker non restituiva tutti gli errori (almeno nella versione 1.10). Quando abbiamo provato ad avviare il container tramite “docker run”, abbiamo finalmente visto almeno qualcosa:

write /proc/sys/net/ipv4/ipfrag_high_thresh: invalid argument docker: Error response from daemon: Cannot start container <...>: [9] System error: could not synchronise with container process.

Il valore del parametro non è valido. Ma perché? E perché non è valido solo qualche volta? Si è scoperto che Docker non garantisce l'ordine in cui vengono applicati i parametri Sysctl (l'ultima versione testata è 1.13.1), quindi a volte ipfrag_high_thresh ha provato a essere impostato su 256K quando ipfrag_low_thresh era ancora 3M, ovvero il limite superiore era inferiore rispetto al limite inferiore, che ha portato all'errore.

A quel tempo, utilizzavamo già il nostro meccanismo per riconfigurare il contenitore dopo l'avvio (congelando il contenitore dopo congelatore di gruppo ed eseguendo comandi nello spazio dei nomi del contenitore tramite reti ip), e in questa parte abbiamo anche aggiunto la scrittura dei parametri Sysctl. Il problema è stato risolto.

FragmentSmack/SegmentSmack. Primo sangue 2

Prima che avessimo il tempo di comprendere l'uso di Workaround nel cloud, iniziarono ad arrivare i primi rari reclami da parte degli utenti. A quel tempo erano trascorse diverse settimane dall'inizio dell'utilizzo di Workaround sui primi server. Dall'indagine iniziale è emerso che sono stati ricevuti reclami contro singoli servizi e non contro tutti i server di tali servizi. Il problema è tornato ad essere estremamente incerto.

Prima di tutto, ovviamente, abbiamo provato a ripristinare le impostazioni Sysctl, ma ciò non ha avuto alcun effetto. Anche varie manipolazioni con le impostazioni del server e dell'applicazione non sono state d'aiuto. Il riavvio ha aiutato. Riavviare Linux è tanto innaturale quanto lo era per Windows ai vecchi tempi. Tuttavia, ha aiutato e lo abbiamo attribuito a un "problema tecnico del kernel" durante l'applicazione delle nuove impostazioni in Sysctl. Quanto era frivolo...

Tre settimane dopo il problema si è ripresentato. La configurazione di questi server è stata abbastanza semplice: Nginx in modalità proxy/bilanciatore. Non molto traffico. Nuova nota introduttiva: il numero degli errori 504 sui client aumenta ogni giorno (Gateway Timeout). Il grafico mostra il numero di 504 errori al giorno per questo servizio:

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

Tutti gli errori riguardano lo stesso backend, ovvero quello che si trova nel cloud. Il grafico del consumo di memoria per i frammenti di pacchetto su questo backend era simile al seguente:

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

Questa è una delle manifestazioni più evidenti del problema nei grafici del sistema operativo. Nel cloud, proprio contemporaneamente, è stato risolto un altro problema di rete con le impostazioni QoS (controllo del traffico). Sul grafico del consumo di memoria per i frammenti di pacchetto, sembrava esattamente lo stesso:

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

Il presupposto era semplice: se appaiono uguali sui grafici, allora hanno la stessa ragione. Inoltre, eventuali problemi con questo tipo di memoria sono estremamente rari.

L'essenza del problema risolto era che abbiamo utilizzato lo scheduler dei pacchetti fq con le impostazioni predefinite in QoS. Per impostazione predefinita, per una connessione, consente di aggiungere 100 pacchetti alla coda e alcune connessioni, in situazioni di carenza di canale, hanno iniziato a intasare la coda fino alla capacità. In questo caso i pacchetti vengono scartati. Nelle statistiche tc (tc -s qdisc) può essere visto in questo modo:

qdisc fq 2c6c: parent 1:2c6c limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 3028 initial_quantum 15140 refill_delay 40.0ms
 Sent 454701676345 bytes 491683359 pkt (dropped 464545, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
  1024 flows (1021 inactive, 0 throttled)
  0 gc, 0 highprio, 0 throttled, 464545 flows_plimit

“464545 Flows_plimit” indica i pacchetti eliminati a causa del superamento del limite della coda di una connessione e “dropped 464545” è la somma di tutti i pacchetti eliminati di questo scheduler. Dopo aver aumentato la lunghezza della coda a 1 mila e aver riavviato i contenitori, il problema ha smesso di verificarsi. Puoi sederti e bere un frullato.

FragmentSmack/SegmentSmack. Ultimo sangue

Innanzitutto, diversi mesi dopo l'annuncio delle vulnerabilità nel kernel, è finalmente apparsa una correzione per FragmentSmack (vi ricordo che insieme all'annuncio di agosto è stata rilasciata una correzione solo per SegmentSmack), che ci ha dato la possibilità di abbandonare Workaround, il che ci ha causato non pochi problemi. Durante questo periodo eravamo già riusciti a trasferire alcuni server sul nuovo kernel e ora dovevamo ricominciare dall'inizio. Perché abbiamo aggiornato il kernel senza attendere la correzione di FragmentSmack? Il fatto è che il processo di protezione da queste vulnerabilità coincide (e si fonde) con il processo di aggiornamento stesso di CentOS (che richiede ancora più tempo rispetto all'aggiornamento del solo kernel). Inoltre, SegmentSmack è una vulnerabilità più pericolosa e una soluzione è apparsa immediatamente, quindi aveva comunque senso. Tuttavia non potevamo semplicemente aggiornare il kernel su CentOS perché la vulnerabilità FragmentSmack, comparsa durante CentOS 7.5, è stata risolta solo nella versione 7.6, quindi abbiamo dovuto interrompere l'aggiornamento alla 7.5 e ricominciare tutto da capo con l'aggiornamento alla 7.6. E succede anche questo.

In secondo luogo, ci sono stati restituiti rari reclami da parte degli utenti relativi a problemi. Ora sappiamo già con certezza che sono tutti legati al caricamento di file dai client ad alcuni dei nostri server. Inoltre, attraverso questi server è passato un numero molto limitato di caricamenti della massa totale.

Come ricordiamo dalla storia sopra, il rollback di Sysctl non ha aiutato. Il riavvio ha aiutato, ma temporaneamente.
I sospetti su Sysctl non sono stati fugati, ma questa volta era necessario raccogliere quante più informazioni possibili. C'era anche un'enorme incapacità di riprodurre il problema del caricamento sul client per studiare più precisamente cosa stava succedendo.

L'analisi di tutte le statistiche e i registri disponibili non ci ha portato più vicino alla comprensione di ciò che stava accadendo. C'era un'acuta mancanza di capacità di riprodurre il problema per “sentire” una connessione specifica. Alla fine, gli sviluppatori, utilizzando una versione speciale dell'applicazione, sono riusciti a ottenere una riproduzione stabile dei problemi sul dispositivo di prova quando connesso tramite Wi-Fi. Questa è stata una svolta nelle indagini. Il client si connetteva a Nginx, che faceva da proxy al backend, che era la nostra applicazione Java.

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

La finestra di dialogo per i problemi era così (risolta sul lato proxy Nginx):

  1. Client: richiesta di ricevere informazioni sul download di un file.
  2. Server Java: risposta.
  3. Cliente: POST con file.
  4. Server Java: errore.

Allo stesso tempo, il server Java scrive nel registro che sono stati ricevuti 0 byte di dati dal client e il proxy Nginx scrive che la richiesta ha richiesto più di 30 secondi (30 secondi è il timeout dell'applicazione client). Perché il timeout e perché 0 byte? Dal punto di vista HTTP tutto funziona come dovrebbe, ma il POST con il file sembra scomparire dalla rete. Inoltre, scompare tra il client e Nginx. È ora di armarsi di Tcpdump! Ma prima devi capire la configurazione della rete. Il proxy Nginx è dietro il bilanciatore L3 NFware. Il tunneling viene utilizzato per consegnare i pacchetti dal bilanciatore L3 al server, che aggiunge le sue intestazioni ai pacchetti:

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

In questo caso, la rete arriva a questo server sotto forma di traffico con tag Vlan, che aggiunge anche i propri campi ai pacchetti:

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

E questo traffico può anche essere frammentato (quella stessa piccola percentuale di traffico frammentato in entrata di cui abbiamo parlato valutando i rischi di Workaround), il che cambia anche il contenuto delle intestazioni:

Attenzione alle vulnerabilità che portano a giri di lavoro. Parte 1: FragmentSmack/SegmentSmack

Ancora una volta: i pacchetti sono incapsulati con un tag Vlan, incapsulati con un tunnel, frammentati. Per comprendere meglio come ciò avvenga, tracciamo il percorso del pacchetto dal client al proxy Nginx.

  1. Il pacchetto raggiunge il bilanciatore L3. Per un corretto instradamento all'interno del data center, il pacchetto viene incapsulato in un tunnel e inviato alla scheda di rete.
  2. Poiché le intestazioni del pacchetto e del tunnel non rientrano nella MTU, il pacchetto viene tagliato in frammenti e inviato alla rete.
  3. Lo switch dopo il bilanciatore L3, quando riceve un pacchetto, gli aggiunge un tag Vlan e lo invia.
  4. Lo switch davanti al proxy Nginx vede (in base alle impostazioni della porta) che il server si aspetta un pacchetto incapsulato Vlan, quindi lo invia così com'è, senza rimuovere il tag Vlan.
  5. Linux prende frammenti di singoli pacchetti e li unisce in un unico grande pacchetto.
  6. Successivamente, il pacchetto raggiunge l'interfaccia Vlan, dove viene rimosso il primo strato: l'incapsulamento Vlan.
  7. Linux quindi lo invia all'interfaccia Tunnel, dove viene rimosso un altro livello: l'incapsulamento del tunnel.

La difficoltà sta nel passare tutto questo come parametri a tcpdump.
Cominciamo dalla fine: ci sono pacchetti IP puliti (senza intestazioni non necessarie) dai client, con vlan e incapsulamento del tunnel rimossi?

tcpdump host <ip клиента>

No, non c'erano pacchetti di questo tipo sul server. Quindi il problema deve esserci già da prima. Sono presenti pacchetti con il solo incapsulamento Vlan rimosso?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx è l'indirizzo IP del client in formato esadecimale.
32:4 — indirizzo e lunghezza del campo in cui è scritto l'IP SCR nel pacchetto Tunnel.

L'indirizzo del campo doveva essere selezionato con la forza bruta, poiché su Internet scrivono circa 40, 44, 50, 54, ma lì non c'era l'indirizzo IP. Puoi anche guardare uno dei pacchetti in formato esadecimale (il parametro -xx o -XX in tcpdump) e calcolare l'indirizzo IP che conosci.

Sono presenti frammenti di pacchetti senza l'incapsulamento Vlan e Tunnel rimosso?

tcpdump ((ip[6:2] > 0) and (not ip[6] = 64))

Questa magia ci mostrerà tutti i frammenti, compreso l'ultimo. Probabilmente, la stessa cosa può essere filtrata per IP, ma non ci ho provato, perché non ci sono molti pacchetti di questo tipo e quelli di cui avevo bisogno si trovavano facilmente nel flusso generale. Eccoli:

14:02:58.471063 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 1516: (tos 0x0, ttl 63, id 53652, offset 0, flags [+], proto IPIP (4), length 1500)
    11.11.11.11 > 22.22.22.22: truncated-ip - 20 bytes missing! (tos 0x0, ttl 50, id 57750, offset 0, flags [DF], proto TCP (6), length 1500)
    33.33.33.33.33333 > 44.44.44.44.80: Flags [.], seq 0:1448, ack 1, win 343, options [nop,nop,TS val 11660691 ecr 2998165860], length 1448
        0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
        0x0010: 4500 05dc d194 2000 3f09 d5fb 0a66 387d E.......?....f8}
        0x0020: 1x67 7899 4500 06xx e198 4000 3206 6xx4 [email protected].
        0x0030: b291 x9xx x345 2541 83b9 0050 9740 0x04 .......A...P.@..
        0x0040: 6444 4939 8010 0257 8c3c 0000 0101 080x dDI9...W.......
        0x0050: 00b1 ed93 b2b4 6964 xxd8 ffe1 006a 4578 ......ad.....jEx
        0x0060: 6966 0000 4x4d 002a 0500 0008 0004 0100 if..MM.*........

14:02:58.471103 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 63, id 53652, offset 1480, flags [none], proto IPIP (4), length 40)
    11.11.11.11 > 22.22.22.22: ip-proto-4
        0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
        0x0010: 4500 0028 d194 00b9 3f04 faf6 2x76 385x E..(....?....f8}
        0x0020: 1x76 6545 xxxx 1x11 2d2c 0c21 8016 8e43 .faE...D-,.!...C
        0x0030: x978 e91d x9b0 d608 0000 0000 0000 7c31 .x............|Q
        0x0040: 881d c4b6 0000 0000 0000 0000 0000 ..............

Si tratta di due frammenti di un pacchetto (stesso ID 53652) con una fotografia (la parola Exif è visibile nel primo pacchetto). Dato che ci sono pacchetti a questo livello, ma non nella forma unita nei dump, il problema riguarda chiaramente l'assembly. Finalmente ci sono prove documentali di ciò!

Il decodificatore di pacchetti non ha rivelato alcun problema che ne impedisca la compilazione. Provato qui: hpd.gasmi.net. All’inizio, quando provi a inserire qualcosa lì, al decodificatore non piace il formato del pacchetto. Si è scoperto che c'erano altri due ottetti tra Srcmac ed Ethertype (non correlati alle informazioni sui frammenti). Dopo averli rimossi, il decoder ha iniziato a funzionare. Tuttavia, non ha mostrato problemi.
Qualunque cosa si possa dire, non è stato trovato nient'altro oltre a quelli Sysctl. Tutto ciò che restava da fare era trovare un modo per identificare i server problematici per comprenderne la portata e decidere ulteriori azioni. Il contatore richiesto è stato trovato abbastanza rapidamente:

netstat -s | grep "packet reassembles failed”

È anche in snmpd sotto OID=1.3.6.1.2.1.4.31.1.1.16.1 (ipSystemStatsReasmFails).

"Il numero di errori rilevati dall'algoritmo di riassemblaggio IP (per qualsiasi motivo: timeout, errori, ecc.)."

Nel gruppo di server su cui è stato studiato il problema, su due questo contatore è aumentato più velocemente, su due più lentamente e su altri due non è aumentato affatto. Confrontando la dinamica di questo contatore con la dinamica degli errori HTTP sul server Java è emersa una correlazione. Cioè, il contatore potrebbe essere monitorato.

Avere un indicatore affidabile dei problemi è molto importante in modo da poter determinare con precisione se il rollback di Sysctl aiuta, poiché dalla storia precedente sappiamo che questo non può essere compreso immediatamente dall'applicazione. Questo indicatore ci consentirebbe di identificare tutte le aree problematiche della produzione prima che gli utenti le scoprano.
Dopo il rollback di Sysctl gli errori di monitoraggio si sono interrotti, quindi è stata dimostrata la causa dei problemi e il fatto che il rollback aiuta.

Abbiamo ripristinato le impostazioni di frammentazione su altri server, dove è entrato in gioco il nuovo monitoraggio, e da qualche parte abbiamo allocato ancora più memoria per i frammenti rispetto a quella predefinita (si trattava di statistiche UDP, la cui perdita parziale non era evidente nel contesto generale) .

Le domande più importanti

Perché i pacchetti sono frammentati sul nostro bilanciatore L3? La maggior parte dei pacchetti che arrivano dagli utenti ai bilanciatori sono SYN e ACK. Le dimensioni di questi pacchetti sono piccole. Ma poiché la quota di tali pacchetti è molto ampia, sullo sfondo non abbiamo notato la presenza di pacchetti di grandi dimensioni che hanno iniziato a frammentarsi.

Il motivo era uno script di configurazione non funzionante advmss su server con interfacce Vlan (all'epoca c'erano pochissimi server con traffico taggato in produzione). Advmss ci permette di trasmettere al client l'informazione che i pacchetti nella nostra direzione dovrebbero essere di dimensioni più piccole in modo che dopo aver collegato loro le intestazioni del tunnel non debbano essere frammentati.

Perché il rollback di Sysctl non ha aiutato, ma il riavvio ha funzionato? Il rollback di Sysctl ha modificato la quantità di memoria disponibile per l'unione dei pacchetti. Allo stesso tempo, a quanto pare, il fatto stesso dell'overflow della memoria per i frammenti ha portato a un rallentamento delle connessioni, che ha portato i frammenti a rimanere a lungo in coda. Cioè, il processo è andato in cicli.
Il riavvio ha cancellato la memoria e tutto è tornato in ordine.

Era possibile fare a meno di Workaround? Sì, ma il rischio di lasciare gli utenti senza servizio in caso di attacco è elevato. Naturalmente, l'utilizzo di Workaround ha comportato diversi problemi, tra cui il rallentamento di uno dei servizi per gli utenti, ma riteniamo comunque che le azioni fossero giustificate.

Mille grazie ad Andrey Timofeev (atimofeev) per aver contribuito allo svolgimento delle indagini, nonché Alexey Krenev (dispositivox) - per il lavoro titanico di aggiornamento di Centos e kernel sui server. Un processo che in questo caso ha dovuto essere ricominciato più volte dall’inizio, motivo per cui si è trascinato per molti mesi.

Fonte: habr.com

Aggiungi un commento