Olá pessoal! Meu nome é Dmitry Samsonov, trabalho como administrador de sistemas líder em Odnoklassniki. Temos mais de 7 mil servidores físicos, 11 mil contêineres em nossa nuvem e 200 aplicativos, que em diferentes configurações formam 700 clusters diferentes. A grande maioria dos servidores está executando o CentOS 7.
Informações sobre a vulnerabilidade do FragmentSmack lançada em 14 de agosto de 2018
(
CVE-2018-5391 ) e SegmentSmack (
CVE-2018-5390 ). Essas são vulnerabilidades com um vetor de ataque à rede e uma classificação bastante alta (7.5), que ameaça com negação de serviço (DoS) devido à exaustão de recursos (CPU). Uma correção no kernel do FragmentSmack não foi proposta naquele momento; além disso, saiu muito depois da publicação de informações sobre a vulnerabilidade. Para eliminar o SegmentSmack, foi proposto atualizar o kernel. O pacote de atualização em si foi lançado no mesmo dia, tudo o que restava era instalá-lo.
Não, não somos contra a atualização do kernel! No entanto, existem nuances ...
Como atualizamos o núcleo do produto
Em geral, nada complicado:
- Download de pacotes
- Instale-os em vários servidores (incluindo servidores que hospedam nossa nuvem);
- Certifique-se de que nada esteja quebrado;
- Verifique se todas as configurações padrão do kernel se aplicam sem erros;
- Aguarde alguns dias;
- Verifique o desempenho do servidor;
- Alterne a implantação de novos servidores para um novo kernel;
- Atualize todos os servidores por data centers (um data center por vez para minimizar o efeito para os usuários em caso de problemas);
- Reinicie todos os servidores.
Repita o procedimento para todos os ramos dos núcleos que temos. No momento, isso é:
- Stock CentOS 7 3.10 - para a maioria dos servidores comuns;
- Vanilla 4.19 é para a nossa nuvem única, porque precisamos de BFQ, BBR, etc;
- O Elrepo kernel-ml 5.2 é para distribuidores altamente carregados , porque o 4.19 costumava se comportar de maneira instável, e os recursos precisam dos mesmos.
Como você deve ter adivinhado, reiniciar milhares de servidores leva mais tempo. Como nem todas as vulnerabilidades são críticas para todos os servidores, apenas reiniciamos aquelas diretamente acessíveis pela Internet. Na nuvem, para não limitar a flexibilidade, não vinculamos contêineres acessíveis externamente a servidores individuais com um novo núcleo, mas reinicializamos todos os hosts sem exceção. Felizmente, o procedimento é mais fácil lá do que nos servidores regulares. Por exemplo, contêineres sem estado podem simplesmente mudar para outro servidor durante a reinicialização.
No entanto, ainda há muito trabalho, e pode levar várias semanas e, em caso de problemas com a nova versão - até vários meses. Os atacantes estão bem cientes disso, então o plano B é necessário.
FragmentSmack / SegmentSmack. Solução alternativa
Felizmente, para algumas vulnerabilidades, esse plano "B" existe e é chamado de Solução alternativa. Na maioria das vezes, essa é uma alteração nas configurações do kernel / aplicativo, que podem minimizar o possível efeito ou eliminar completamente a exploração de vulnerabilidades.
No caso do FragmentSmack / SegmentSmack
, a seguinte solução alternativa foi proposta:
“ Você pode alterar os valores padrão de 4 MB e 3 MB em net.ipv4.ipfrag_high_thresh e net.ipv4.ipfrag_low_thresh (e seus análogos para ipv6 net.ipv6.ipfrag_high_thresh e net.ipv6.ipfrag_low_thresh) por 256 kB e 192 kB, respectivamente. Os testes mostram uma queda ligeira a significativa no uso da CPU durante um ataque, dependendo do equipamento, configurações e condições. No entanto, pode haver algum impacto no desempenho devido a ipfrag_high_thresh = 262144 bytes, pois apenas dois fragmentos de 64K podem caber na fila de reconstrução por vez. Por exemplo, existe o risco de que aplicativos que trabalham com pacotes UDP grandes sejam interrompidos . ”
Os próprios parâmetros
na documentação do kernel são descritos a seguir:
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.
Não temos UDP grande em serviços de produção. Não há tráfego fragmentado na LAN; há, mas não significativo, tráfego na WAN. Nada é um mau presságio - você pode rolar a solução alternativa!
FragmentSmack / SegmentSmack. Primeiro sangue
O primeiro problema que encontramos foi que os contêineres na nuvem às vezes aplicavam apenas parcialmente as novas configurações (apenas ipfrag_low_thresh) e às vezes eles não usavam de jeito nenhum - eles apenas travavam no início. Não foi possível reproduzir o problema de forma estável (manualmente, todas as configurações foram aplicadas sem dificuldades). Entender por que o contêiner cai no início também não é tão simples: nenhum erro foi encontrado. Uma coisa era certa: reverter as configurações resolve o problema de derrubar contêineres.
Por que não é suficiente usar o Sysctl no host? O contêiner vive em seu Namespace de rede dedicado, portanto, pelo menos
parte dos parâmetros Sysctl da rede no contêiner podem diferir do host.
Como exatamente as configurações de Sysctl no contêiner se aplicam? Como temos contêineres sem privilégios, a alteração de qualquer configuração do Sysctl entrando no próprio contêiner falhará - simplesmente não haverá direitos suficientes. Na época, nossa nuvem usava o Docker (agora
Podman ) para lançar contêineres. A janela de encaixe via API passou os parâmetros do novo contêiner, incluindo as configurações necessárias do Sysctl.
No decorrer da enumeração das versões, descobriu-se que a API do Docker não gerou todos os erros (pelo menos na versão 1.10). Ao tentar iniciar o contêiner através do "docker run", finalmente vimos pelo menos algo:
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.
O valor do parâmetro não é válido. Mas porque? E por que não é válido apenas às vezes? Aconteceu que o Docker não garantiu a ordem na qual os parâmetros Sysctl foram usados (a versão mais recente testada foi a 1.13.1); portanto, às vezes o ipfrag_high_thresh tentava definir-se para 256K quando o ipfrag_low_thresh ainda era de 3M, ou seja, o limite superior era mais baixo que o inferior, o que causava um erro.
Naquela época, já usamos nosso próprio mecanismo para reconfigurar o contêiner após iniciar (congelar o contêiner através do
cgroup freezer e executar comandos no espaço de nomes do contêiner via
ip netns ) e também adicionamos parâmetros Sysctl a esta parte. O problema foi resolvido.
FragmentSmack / SegmentSmack. Primeiro sangue 2
Antes de sabermos como usar a solução alternativa na nuvem, começaram a chegar as primeiras queixas raras dos usuários. Nesse momento, várias semanas se passaram desde o início da solução alternativa nos primeiros servidores. A investigação inicial mostrou que foram recebidas reclamações sobre serviços individuais, e nem todos os servidores desses serviços. O problema recuperou um caráter extremamente vago.
Antes de tudo, é claro, tentamos reverter as configurações do Sysctl, mas isso não deu nenhum efeito. Várias manipulações com as configurações do servidor e do aplicativo também não ajudaram. Reinicialização ajudou. A reinicialização para Linux é tão antinatural quanto era uma condição normal para trabalhar com o Windows nos velhos tempos. No entanto, isso ajudou e escrevemos tudo para uma “falha no kernel” ao aplicar as novas configurações no Sysctl. Quão frívolo era ...
Três semanas depois, o problema voltou a ocorrer. A configuração desses servidores era bastante simples: Nginx no modo proxy / balancer. O tráfego é um pouco. Novo introdutório: o número de erros 504 (
Gateway Timeout ) está aumentando todos os dias nos clientes. O gráfico mostra o número de 504 erros por dia para este serviço:
Todos os erros são sobre o mesmo back-end - sobre o que está na nuvem. O gráfico do consumo de memória para fragmentos de pacotes nesse back-end foi o seguinte:
Essa é uma das manifestações mais marcantes do problema nos gráficos do sistema operacional. Na nuvem, ao mesmo tempo, outro problema de rede foi corrigido com as configurações de QoS (Controle de Tráfego). No gráfico do consumo de memória para fragmentos de pacotes, parecia exatamente o mesmo:
A suposição era simples: se eles parecem iguais nos gráficos, têm o mesmo motivo. Além disso, quaisquer problemas com esse tipo de memória são extremamente raros.
A essência do problema corrigido foi que usamos o sheduler fq packet com configurações padrão em QoS. Por padrão, para uma conexão, ele permite adicionar 100 pacotes à fila e algumas conexões em uma situação de falta de canal começaram a entupir a fila com falha. Nesse caso, os pacotes caem. Nas estatísticas tc (tc -s qdisc), isso pode ser visto da seguinte maneira:
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" são os pacotes descartados devido a exceder o limite da fila de uma conexão e "464545 descartado" é a soma de todos os pacotes descartados deste sheduler. Depois de aumentar o comprimento da fila para 1 mil e reiniciar os contêineres, o problema deixou de aparecer. Você pode sentar em uma cadeira e tomar um smoothie.
FragmentSmack / SegmentSmack. Último sangue
Primeiro, alguns meses após o anúncio de vulnerabilidades no kernel, finalmente uma correção para o FragmentSmack apareceu (lembro que, com o anúncio em agosto, uma correção foi lançada apenas para o SegmentSmack), que nos deu a chance de abandonar a solução alternativa, o que causou muitos problemas. Alguns dos servidores durante esse período já conseguimos transferir para um novo kernel e agora tivemos que começar do início. Por que atualizamos o kernel sem esperar pela correção do FragmentSmack? O fato é que o processo de proteção contra essas vulnerabilidades coincidiu (e se fundiu) com o processo de atualização do próprio CentOS (que leva ainda mais tempo do que atualizar apenas o kernel). Além disso, o SegmentSmack é uma vulnerabilidade mais perigosa, e uma correção para ela apareceu imediatamente, de modo que o argumento era o mesmo. No entanto, não pudemos simplesmente atualizar o kernel no CentOS, porque a vulnerabilidade FragmentSmack, que apareceu durante o CentOS 7.5, foi corrigida apenas na versão 7.6, por isso tivemos que parar a atualização para 7.5 e começar tudo de novo com a atualização para 7.6. E assim é.
Em segundo lugar, as reclamações raras dos usuários sobre problemas retornaram para nós. Agora já sabemos com certeza que todos eles estão conectados ao download de arquivos de clientes para alguns de nossos servidores. E através desses servidores, houve um número muito pequeno de uploads da massa total.
Como lembramos da história acima, a reversão do Sysctl não ajudou. A reinicialização ajudou, mas temporariamente.
As suspeitas com o Sysctl não foram levantadas, mas desta vez foi necessário coletar o máximo de informações possível. Além disso, havia uma extrema falta de capacidade de reproduzir o problema com o upload do cliente para examinar com mais precisão o que estava acontecendo.
A análise de todas as estatísticas e logs disponíveis não nos aproximou da compreensão do que estava acontecendo. Havia uma falta aguda da capacidade de reproduzir o problema para "sentir" uma conexão específica. Por fim, os desenvolvedores da versão especial do aplicativo conseguiram obter uma reprodução estável de problemas no dispositivo de teste quando conectado via Wi-Fi. Este foi um avanço na investigação. O cliente conectado ao Nginx, procurou proxy para o back-end, que era nosso aplicativo Java.
A caixa de diálogo com problemas foi a seguinte (corrigida no lado do proxy Nginx):
- Cliente: solicitação de informações sobre o download de um arquivo.
- Servidor Java: resposta.
- Cliente: POST com arquivo.
- Servidor Java: erro.
Ao mesmo tempo, o servidor Java grava no log que 0 bytes de dados foram recebidos do cliente e no proxy Nginx que a solicitação levou mais de 30 segundos (30 segundos é o tempo limite do aplicativo cliente). Por que tempo limite e por que 0 bytes? Do ponto de vista do HTTP, tudo funciona como deveria, mas o POST com o arquivo parece desaparecer da rede. E desaparece entre o cliente e o Nginx. É hora de se armar com o Tcpdump! Mas primeiro você precisa entender a configuração de rede. O proxy nginx está por trás do balanceador
N3ware L3. O encapsulamento é usado para entregar pacotes do balanceador L3 para o servidor, que adiciona seus cabeçalhos aos pacotes:
Ao mesmo tempo, a rede chega a esse servidor na forma de tráfego marcado por Vlan, que também adiciona seus campos aos pacotes:
E esse tráfego pode ser fragmentado (a porcentagem muito pequena de tráfego fragmentado de entrada de que falamos ao avaliar os riscos da solução alternativa), o que também altera o conteúdo dos cabeçalhos:
Mais uma vez: os pacotes são encapsulados por uma tag Vlan, encapsulados por um túnel, fragmentados. Para entender melhor como isso acontece, vamos rastrear a rota do pacote do cliente para o proxy Nginx.
- O pacote chega ao balanceador L3. Para o roteamento correto dentro do data center, o pacote é encapsulado no túnel e enviado para a placa de rede.
- Como os cabeçalhos de pacotes + túneis não se encaixam na MTU, o pacote é cortado em fragmentos e enviado à rede.
- O switch após o balanceador L3 ao receber o pacote adiciona uma tag Vlan e a envia ainda mais.
- O switch antes do proxy Nginx vê (de acordo com as configurações da porta) que o servidor está esperando um pacote encapsulado em Vlan, para que ele o envie como está, sem remover a tag Vlan.
- O Linux recebe fragmentos de pacotes individuais e os cola em um pacote grande.
- Em seguida, o pacote chega à interface Vlan, onde a primeira camada é removida - o encapsulamento Vlan.
- O Linux então envia para a interface Tunnel, onde outra camada é removida - encapsulamento do Tunnel.
A dificuldade é passar tudo isso como parâmetros para o tcpdump.
Vamos começar do final: existem pacotes IP limpos (sem cabeçalhos extras) de clientes com o encapsulamento de vlan e túnel removido?
tcpdump host <ip >
Não, não havia esses pacotes no servidor. Portanto, o problema deve ser anterior. Existem pacotes com apenas o encapsulamento Vlan removido?
tcpdump ip[32:4]=0xx390x2xx
0xx390x2xx é o endereço IP do cliente em formato hexadecimal.
32: 4 - endereço e comprimento do campo em que o IP do SCR é gravado no pacote de túnel.
O endereço do campo teve que ser selecionado por força bruta, já que a Internet grava cerca de 40, 44, 50, 54, mas não havia endereço IP. Você também pode observar um dos pacotes em hexadecimal (o parâmetro -xx ou -XX em tcpdump) e calcular em qual endereço o IP é conhecido.
Existe algum fragmento de pacote sem o encapsulamento Vlan e Tunnel removido?
tcpdump ((ip[6:2] > 0) and (not ip[6] = 64))
Essa mágica nos mostrará todos os fragmentos, incluindo o último. Provavelmente, o mesmo pode ser filtrado por IP, mas não tentei, porque não existem muitos desses pacotes, e os que eu precisava foram facilmente encontrados no fluxo geral. Aqui estão elas:
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 .faEE.....@.2.m.
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.....j Ex
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 ..............
Estes são dois fragmentos de um pacote (o mesmo ID 53652) com uma fotografia (a palavra Exif é visível no primeiro pacote). Devido ao fato de haver pacotes nesse nível, mas não colados em lixões, o problema está claramente na montagem. Finalmente, há evidências documentais disso!
O decodificador de pacotes não revelou nenhum problema que impediu a montagem. Tentei aqui:
hpd.gasmi.net . A princípio, ao tentar empinar algo lá, o decodificador não gosta do formato de pacote. Aconteceu que havia dois octetos extras entre Srcmac e Ethertype (não relacionados a informações de fragmentos). Depois de removê-los, o decodificador funcionou. No entanto, ele não mostrou problemas.
Diga o que quiser, exceto para aqueles muito Sysctl, mais nada foi encontrado. Faltava encontrar uma maneira de identificar servidores problemáticos para entender a escala e decidir sobre outras ações. Rapidamente, encontrei o contador certo:
netstat -s | grep "packet reassembles failed”
Está no snmpd sob OID = 1.3.6.1.2.1.4.31.1.1.16.1 (
ipSystemStatsReasmFails ).
"O número de falhas detectadas pelo algoritmo de remontagem de IP (por qualquer motivo: tempo limite excedido, erros, etc.)."
Entre o grupo de servidores em que o problema foi estudado, em dois esse contador aumentou mais rapidamente, em dois - mais lento e em dois não aumentou. Uma comparação da dinâmica desse contador com a dinâmica dos erros HTTP no servidor Java revelou uma correlação. Ou seja, o contador pode ser configurado para monitoramento.
Ter um indicador confiável de problemas é muito importante para que você possa determinar com precisão se a reversão do Sysctl ajuda, pois sabemos pela história anterior que isso não está imediatamente claro no aplicativo. Este indicador permitiria identificar todas as áreas problemáticas da produção antes que os usuários a descobrissem.
Após a reversão do Sysctl, os erros de monitoramento foram interrompidos, assim a causa dos problemas foi comprovada e o fato de a reversão ajudar.
Revertemos as configurações de fragmentação em outros servidores, onde um novo monitoramento pegou fogo e, em algum lugar, alocamos ainda mais memória para os fragmentos do que antes, por padrão (isso era udp-statistics, cuja perda parcial não era perceptível no cenário geral).
As perguntas mais importantes
Por que os pacotes se fragmentam em nosso balanceador L3? A maioria dos pacotes que chegam dos usuários aos balanceadores são SYN e ACK. Os tamanhos dessas sacolas são pequenos. Porém, como o compartilhamento de tais pacotes é muito grande, não observamos a presença de pacotes grandes que começaram a se fragmentar.
O motivo foi o
script de configuração de advmss corrompidos em servidores com interfaces Vlan (havia muito poucos servidores com tráfego marcado na produção naquele momento). O Advmss permite transmitir ao cliente informações de que os pacotes em nossa direção devem ser menores para que, após colar os cabeçalhos do túnel neles, eles não precisem ser fragmentados.
Por que a reversão do Sysctl não ajudou, mas a reinicialização? A reversão do Sysctl alterou a quantidade de memória disponível para colar pacotes. Ao mesmo tempo, aparentemente, o próprio fato de excesso de memória para fragmentos levou à inibição das conexões, o que levou ao fato de que os fragmentos estavam atrasados na fila por um longo tempo. Ou seja, o processo está em loop.
A refutação anulou a memória e tudo estava em ordem.
Você poderia fazer sem solução alternativa? Sim, mas há um grande risco de deixar os usuários sem vigilância em caso de ataque. Obviamente, o uso da solução alternativa, como resultado, levou a vários problemas, incluindo a inibição de um dos serviços pelos usuários, mas, no entanto, acreditamos que as ações foram justificadas.
Muito obrigado a Andrei Timofeev (
atimofeyev ) por ajudar na investigação e a Alexei Krenev (
devicex ) pelo trabalho titânico de atualizar Centos e kernels nos servidores. O processo, que neste caso teve que ser iniciado várias vezes desde o início, por causa do qual se arrastou por muitos meses.