2 pontos por GN⁺ 2023-09-11 | 1 comentários | Compartilhar no WhatsApp
  • O NAT IPv4 normalmente distingue o destino das respostas por portas TCP/UDP, mas o ICMP echo do ping não tem portas, então a questão central é qual valor o Linux usa como chave de mapeamento
  • O experimento recria uma configuração em que o tráfego de 192.168.99.0/24 para 10.0.100.0/24 é NATeado usando namespaces de rede com client1, client2, natbox e server, além de iptables MASQUERADE
  • Comparando a RFC 792 com capturas de pacotes, vê-se que o Identifier e o Sequence Number do ICMP echo são usados para correlacionar requisições e respostas, e no caminho ICMP SOCK_DGRAM do Linux a porta local do socket entra como ID
  • Quando dois clientes usam o mesmo ICMP ID 999, o netfilter altera o ICMP ID para um valor aleatório em um dos lados para evitar conflito e, na resposta, restaura o IP e o ID originais do cliente
  • Mesmo no ICMP sem portas, o Linux NAT armazena no conntrack tuples de estado nas direções original e de resposta, usando o ICMP ID como chave manipulável para mapear a resposta ao host interno correto

Ambiente de teste e configuração do NAT

  • Para simular vários dispositivos em uma única máquina Linux, foram usados namespaces de rede
  • Foram criados dois clientes, um natbox atuando como roteador NAT e um servidor, cada um em seu próprio namespace, separando a rede privada da rede do servidor
    • client1: 192.168.99.1/24
    • client2: 192.168.99.2/24
    • Interface interna do natbox: 192.168.99.3/24
    • Interface externa do natbox: 10.0.100.1/24
    • server: 10.0.100.2/24
  • Os testes foram feitos em uma VM Fedora 38 Server com Linux kernel 6.2.9, executando ip, iptables, tcpdump e outros comandos como root
  • Os dois clientes são conectados à bridge br0, e o natbox é ligado tanto à bridge quanto ao par veth do lado do servidor
  • A rota padrão dos clientes foi configurada para 192.168.99.3, fazendo com que o tráfego para o servidor passe pelo natbox
  • No natbox, o encaminhamento de pacotes foi ativado com net.ipv4.ip_forward=1, e uma regra MASQUERADE foi adicionada à cadeia POSTROUTING da tabela nat do iptables
    • ip netns exec natbox iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE

NAT de ICMP visto por captura de pacotes

  • Pacotes ICMP foram capturados nos namespaces client1 e server com tcpdump -n icmp
  • Do lado do cliente, aparecem o echo request 192.168.99.1 > 10.0.100.2 e o echo reply 10.0.100.2 > 192.168.99.1
  • Do lado do servidor, o IP de origem da mesma requisição aparece alterado para 10.0.100.1, confirmando que o NAT reescreveu o endereço de origem para o IP externo do natbox
  • Requisições ICMP de clientes diferentes têm campos id diferentes
    • No exemplo, client1 usa o ID 31428
    • client2 usa o ID 33391
  • Essa observação mostra que o natbox pode usar o campo ID para devolver a resposta ICMP ao cliente interno correto

RFC 792 e o ICMP ID do ping

  • O ICMP é um protocolo antigo definido na RFC 792, publicada em 1981
  • Mensagens ICMP echo e echo reply incluem Type, Code, Checksum, Identifier, Sequence Number e Data
  • Type distingue echo request de echo reply
    • O Type do echo request é 8
    • No trecho citado da RFC, o echo reply aparece como 1
    • O Code é 0
  • A RFC 792 explica que Identifier e Sequence Number podem ser usados para correlacionar echo request e reply
  • O Identifier pode ser usado para identificar a sessão, como as portas em TCP/UDP, e o Sequence Number pode ser incrementado a cada echo request
  • Como a RFC não especifica como o ID deve ser escolhido na prática, é necessário olhar o código-fonte da implementação ping

Como o ID é definido no ping do iputils

  • O comando ping faz parte do pacote iputils
  • Um comentário perto de ping4_send_probe explica que, ao criar um ICMP echo request, o campo ID é um número aleatório e o Sequence Number é um inteiro crescente
  • Internamente, o ping tem o campo ident em struct ping_rts
    • O valor padrão é -1
    • Ele pode ser sobrescrito pela opção de CLI -e com um valor entre 0 e IDENTIFIER_MAX, que é 0xFFFF
  • Se rts->ident == -1, o ping faz bind do socket com tipo SOCK_DGRAM e protocolo IPPROTO_ICMP
  • Segundo a descrição dos sockets IPPROTO_ICMP no Linux, o cabeçalho ICMP é verificado e ajustado em send(), e o id é definido como o número da porta local do socket
  • Como o ping não especifica uma porta de origem, o kernel Linux escolhe uma porta livre aleatoriamente, e essa porta passa a ser usada como ID do pacote ICMP

Quando há conflito com o mesmo ICMP ID

  • Nos dois clientes, foi usado ping -e 999 para enviar ping ao servidor com o mesmo ICMP ID 999
  • Na captura do servidor, a requisição de um cliente mantém o ID 999, mas a do outro aparece com o ID 30218
  • O dispositivo NAT altera o ID de um dos lados para evitar conflito na combinação entre o mesmo IP externo e o mesmo ICMP ID
  • Para localizar onde esse conflito é tratado, foi examinado no diretório net/netfilter do Linux o código que usa o campo ICMP id

O papel de netfilter, conntrack e NAT

  • O subsistema do kernel que implementa as regras do iptables é o netfilter
  • Como a regra MASQUERADE executa o NAT, a implementação do NAT para ICMP também fica dentro do netfilter
  • Em nf_nat_core.c, nf_nat_setup_info chama get_unique_tuple, que por sua vez leva a nf_nat_l4proto_unique_tuple
  • nf_nat_l4proto_unique_tuple tem um caso para IPPROTO_ICMP e faz referência a tuple->src.u.icmp.id
  • Em nf_nat_proto.c, nf_nat_manip_pkt passa por nf_nat_ipv4_manip_pkt e l4proto_manip_pkt, e no caso de ICMP chama icmp_manip_pkt
  • icmp_manip_pkt grava o ICMP ID real no pacote com hdr->un.echo.id = tuple->src.u.icmp.id

Como o ICMP é representado em tuples do conntrack

  • No netfilter, uma connection não significa apenas uma conexão TCP, mas também o estado que relaciona pacotes de saída e de entrada em protocolos sem conexão, como UDP ou ICMP
  • nf_conn contém tuplehash[IP_CT_DIR_MAX]
    • IP_CT_DIR_ORIGINAL: direção dos pacotes de saída
    • IP_CT_DIR_REPLY: direção da resposta de entrada
  • Cada nf_conntrack_tuple_hash contém um nf_conntrack_tuple que identifica a connection
  • O tuple é dividido em src, que pode ser manipulado, e dst, que é imutável
    • src contém o endereço IP e campos específicos do protocolo
    • No ICMP, o campo específico do protocolo é __be16 id
    • dst contém o endereço IP que não muda e o type e code do ICMP
  • O NAT armazena na connection como o pacote de saída foi alterado e desfaz essa alteração no pacote de resposta

Caminho de código que escolhe o ICMP ID

  • Quando o natbox recebe um ICMP echo, nf_nat_setup_info cria uma nova connection e decide se precisa alterar o IP de origem e o ICMP ID
  • Depois disso, para cada pacote ICMP, nf_nat_manip_pkt define o IP de origem ou destino e o ICMP ID com base nos valores armazenados na connection
  • get_unique_tuple é o caminho central para escolher um tuple NAT disponível
    • find_best_ips_proto reescreve o endereço IP de origem
    • nf_nat_used_tuple verifica se o tuple já está em uso e, se não estiver, retorna o tuple atual sem mudanças
    • Por isso, se os ICMP IDs dos dois clientes forem diferentes, o ID é preservado também nos pacotes após NAT
    • Se o tuple já estiver em uso, nf_nat_l4proto_unique_tuple é chamado para executar o NAT específico do protocolo
  • No ICMP, tuple->src.u.icmp.id é escolhido como a chave a ser NATeada
  • find_free_id gera um ID aleatório com get_random_u16(), ajusta-o para a faixa válida de ICMP ID e verifica se já está em uso
  • A faixa padrão de ID cobre todo o espaço de IDs, mas é possível definir um intervalo como 100-200 usando --to-ports na regra MASQUERADE do iptables
  • Se não for encontrado um tuple livre, um ID duplicado permanece na connection, e depois __nf_conntrack_confirm detecta a duplicidade e descarta o pacote

Verificando o comportamento do kernel com bpftrace

  • Para confirmar o funcionamento do netfilter, foi usado bpftrace
  • As funções do kernel rastreadas foram nf_nat_setup_info e nf_nat_manip_pkt
  • kprobe rastreia o momento da chamada da função, e kretprobe rastreia o momento em que ela retorna
  • Como em kretprobe não é possível acessar diretamente os argumentos da função, os argumentos são salvos em um mapa BPF na entrada e lidos novamente no retorno
  • struct sk_buff é a estrutura usada pelo kernel Linux para representar um pacote
  • bswap é usado para converter a ordem de bytes de rede, big endian, para little endian
  • ntop converte endereços IP em string
  • Graças ao BPF Type Format (BTF) dos kernels Linux mais recentes, programas BPF conseguem referenciar estruturas do kernel como sk_buff e nf_conn sem incluir headers
  • Esse programa em bpftrace foi testado no Linux kernel 6.2.9, e o funcionamento pode variar em outras versões do kernel

Resultados do rastreamento e conclusão

  • Quando dois clientes enviam ping com o mesmo ICMP ID 999, nf_nat_setup_info é chamado uma vez para cada cliente
  • O primeiro cliente, 192.168.99.1, mantém o ICMP ID 999 tanto no original tuple quanto no reply tuple
  • O segundo cliente, 192.168.99.2, tem o ICMP ID do reply tuple reescrito para 32809
  • nf_nat_manip_pkt altera o IP de origem para 10.0.100.1 com NF_NAT_MANIP_SRC no echo request e, na resposta, restaura o IP de destino para o IP original do cliente com NF_NAT_MANIP_DST
  • O ICMP ID do pacote de resposta também é restaurado do valor NATeado para o valor originalmente enviado pelo cliente
  • O timeout padrão do conntrack para ICMP pode ser visto em /proc/sys/net/netfilter/nf_conntrack_icmp_timeout, e o valor padrão observado foi 30 segundos
  • Se o cliente ficar mais de 30 segundos sem enviar pacotes, nf_nat_setup_info será chamado novamente no próximo ping
  • O comportamento do Linux ao fazer NAT de ping também está documentado no Netfilter Hacking HOWTO, e o ponto central são os tuples do conntrack e a reescrita do ICMP ID

1 comentários

 
GN⁺ 2023-09-11
Comentários do Hacker News
  • Talvez você se interesse por https://samy.pl/pwnat/
    Quando o servidor é iniciado, ele começa a enviar pacotes fixos de solicitação de eco ICMP para o endereço fixo 3.3.3.3, esperando que esses pacotes não retornem
    3.3.3.3 não é um host acessível nem um alvo a ser falsificado. Em vez disso, quando o cliente tenta se conectar, ele sabe o IP do servidor e envia ao servidor um pacote ICMP Time Exceeded. Dentro desse pacote ICMP está o pacote fixo “original” que o servidor estava enviando para 3.3.3.3, e esse pacote hardcoded funciona como identificador do pwnat
    O cliente basicamente finge ser um hop na internet e informa que a “ICMP echo request” original do servidor não pôde ser encaminhada. O NAT vê que o pacote dentro do ICMP Time Exceeded corresponde ao pacote enviado pelo servidor e o encaminha para o servidor atrás do NAT; como isso inclui todo o cabeçalho IP do cliente, o servidor consegue saber o endereço IP do cliente
    • Em resumo, o truque de dar ping em 3.3.3.3 é uma forma de permitir que o servidor atrás de NAT descubra o endereço IP do cliente atrás de NAT, sem precisar de um servidor sem NAT como https://ifconfig.co
      O funcionamento central dessa ferramenta depois é criar um túnel UDP entre cliente e servidor
      Porém, pelo que vi rapidamente, parece assumir que o NAT não reescreve a porta de origem UDP, então acho que não funciona em todos os roteadores. STUN, usado em WebRTC etc., implementa técnicas mais sofisticadas, mas quando mesmo assim não funciona é preciso usar TURN, que é um relay
      É bem provável que o mesmo problema se aplique ao truque do ping para 3.3.3.3. Se o NAT reescrever o identificador do ping, como no artigo, esse truque quebra
  • Quando um dispositivo na rede local envia um ping para um dispositivo na internet, o roteador que faz NAT troca o endereço de origem do ping pelo seu próprio IP público e reescreve o campo ID do pacote ICMP com um valor único
    Ao receber a resposta, o roteador usa esse valor único de ID para encaminhar a resposta ao dispositivo correto na rede local
    • Para entender melhor, basta pensar em como o sistema operacional diferencia diferentes conversas ICMP destinadas ao mesmo destino
      Dá para verificar isso só com um computador e Wireshark/tcpdump
      O artigo em si é bom e pode ser revelador para quem não tinha nenhum entendimento de redes. Mas, no fundo, parece mais uma forma de montar um laboratório de redes adequado e fuçar o código-fonte do que de simplesmente raciocinar sobre o assunto
    • Se levar essa ideia um pouco mais adiante, é como transformar um protocolo sem estado em um protocolo com estado
    • De qualquer forma, o ping também precisa dessas informações de estado para associar requisições e respostas
    • Fico curioso por que não usar o IP privado de origem em vez de um “valor único”
    • Fico curioso se esse ID fica no cabeçalho ICMP ou se pertence à parte de IP
  • É bom ver textos do tipo “como funciona” descendo pelas camadas de abstração até chegar ao código-fonte. A explicação é boa e traz bastante informação
    • Eu também vim dizer isso. Roteamento e redes ainda me confundem, e os textos sobre o tema em geral parecem abstratos demais
      Exemplos assim, que dá para acompanhar na prática, são muito valiosos, e pretendo seguir o passo a passo
      Quase o único outro texto sobre esse tema que consegui entender bem foi este da Tailscale. Ele tem muitos “exemplos destrinchados”, o que deixa claro como tudo se encaixa
      https://tailscale.com/blog/how-nat-traversal-works/
  • Bom artigo
    Por coincidência, neste fim de semana eu estava brigando com o Netfilter para ativar um proxy transparente em um roteador OpenWRT
    Os materiais básicos de referência ao olhar para o Netfilter são https://wiki.nftables.org/wiki-nftables/index.php/Main_Page e https://www.netfilter.org/projects/nftables/manpage.html
  • Como ICMP não tem portas, não é preciso lidar com o problema de o NAT devolver a resposta de eco ICMP para a porta correta
    Mas uma solicitação de eco ICMP tem um ID, que na prática cumpre o papel de um número de porta de origem
    Para fazer NAT corretamente em eco ICMP, o ID precisa ser remapeado nos dois sentidos, assim como se remapeia a porta de origem no UDP
    Isso porque, se uma máquina atrás do NAT receber pings de dois hosts ao mesmo tempo e esses dois hosts por acaso usarem o mesmo número de requisição, haverá ambiguidade
    Outra possibilidade é não reescrever o identificador e manter uma lista de máquinas remotas associadas a cada ID. Se houver colisão de ID, a lista terá mais de um endereço IP remoto; quando a resposta vier da máquina atrás do NAT, o NAT pode escolher um da lista, enviar a resposta para essa máquina e remover a entrada
  • NAT é uma abstração realmente suja. O IPv4 deveria desaparecer
    • Na minha internet de casa há dispositivos em várias sub-redes da faixa 192.168. Pouco tempo atrás mudei de ISP, o AS ao qual minha casa pertence mudou e recebi um novo endereço IPv4, mas bastou atualizar, no roteador WAN, o encaminhamento do tráfego que chega para o novo IP
      Se fosse IPv6, eu teria de mudar todos os nós da rede e também atualizar o DNS interno
      Em teoria, eu poderia ter meu próprio /48 portável, mas o novo ISP teria de anunciá-lo, e, embora o ISP atual talvez fizesse isso, não é algo comum
      Uma semana atrás, quando a linha telefônica caiu, peguei um MiFi 5G e movi a conexão WAN para ele; nessa interface bastou aplicar um masquerade simples. O sinal era fraco, então não era ideal, mas funcionou
      O problema é que, mesmo com IPv6, ainda seria preciso operar dual stack ou usar a abstração suja de NAT. Só aumenta meu trabalho sem me trazer benefício
      No trabalho é a mesma coisa. Veículos usando sub-redes internas 172.16/12 se conectam e fazem roteamento entre si, e se ligam ao exterior por várias conexões VPN. Como muitas vezes ficam estacionados no subsolo e quase não têm sinal, a arquitetura é torcer para que pelo menos uma das várias opções funcione

Migrar para IPv6 significaria ter que migrar os /48 de novo. Além disso, esses veículos recebem internet em vários estádios esportivos, muitos dos quais têm dificuldade até para desativar MITM/443 ou liberar o bloqueio de UDP. Isso não funciona em um ambiente em que se chega às 10h da manhã de sábado e tudo precisa estar funcionando 2 horas depois
Não vejo qual seria o benefício de negócio de migrar para dual stack, dobrando o trabalho e o risco

  • Não está claro que o IPv6 vá resolver esse problema. Tecnicamente sim, mas os grandes provedores já estão dando apenas /64 para usuários residenciais e cobrando caro por /48 “empresarial”, o que já está levando a NAT em IPv6 ou à subdivisão adicional de /64. Não era para ser assim
  • Então você vai passar a odiar ainda mais o CG-NAT
  • É preciso levar em conta o efeito Lindy. É a observação de que a expectativa de vida futura de algo não perecível, como uma tecnologia ou ideia, é proporcional à sua idade atual; como o IPv4 já é antigo, é bem provável que ainda fique por aí por bastante tempo
    https://en.wikipedia.org/wiki/Lindy_effect
  • IPv6 também deveria desaparecer. Teve tempo suficiente para se tornar dominante, mas continua patinando
  • Fico pensando se daria para abusar do ping para enviar mensagens curtas em redes P2P baseadas em UDP enquanto se lida com travessia de NAT sem um servidor central. Parece que alguém já descobriu a parte das mensagens
    https://stackoverflow.com/questions/31857419/how-to-send-a-m...
    Infelizmente, o ping é tratado pelo sistema operacional, então os aplicativos no IP do par não conseguem ler as mensagens
    Talvez esteja na hora de oferecer hooks em espaço de usuário para alguns desses serviços, permitindo P2P de verdade quando ambos os lados estão atrás de NAT. Nem que seja algo como um stream de eventos somente leitura. Agora parece que todas as barreiras que impedem isso são artificiais
    • Uma pequena correção técnica: ping não é UDP, é ICMP
      Ainda assim, já vi estratégias de exfiltração de dados ou outros métodos de comunicação usando ping. Hoje em dia, a maioria das configurações padrão de firewall descarta silenciosamente todo ICMP, incluindo ping, então acho que seria praticamente inviável para uso P2P
    • Ideia interessante. O id parece ser, na prática, equivalente a (sport, dport), mas tem 16 bits, então o espaço é muito menor que 32 bits
      Mas acho que o problema central do NAT hole punching é que, para criar a conexão, é preciso haver atividade dos dois lados. Por isso sempre é necessário um servidor de coordenação para avisar T que o nó S quer falar com ele
      Mesmo assim, dá o que pensar. Fico imaginando se haveria um jeito usando mensagens de roteamento ICMP, como unreachable ou TTL expired. Quando você faz traceroute para algum IP, recebe pacotes de volta de vários outros IPs arbitrários, e isso geralmente atravessa NAT
      Dá para imaginar um host T que queira receber conexões de entrada escolhendo um endereço IP “dummy” aleatório, publicando (IP do roteador, IP dummy) como seu identificador e enviando periodicamente pacotes para esse IP dummy. Um host S que queira falar com T poderia enviar ao roteador de T um ICMP TTL-expired referente a esse endereço dummy, e o roteador talvez o encaminhasse para T
      Claro, isso depende de os endereços IP dentro dos campos ICMP sofrerem filtragem de ingresso como os endereços no cabeçalho IP
      Edição: já apareceu um comentário de topo apontando para uma implementação dessa ideia
    • Já existe: https://samy.pl/pwnat/
    • Não é exatamente o que eu estava procurando, mas falar em abusar do ping me lembrou do pingfs. Ele dá uma definição totalmente nova para computação em nuvem
      [1] - https://github.com/yarrick/pingfs
    • Com uma adoção maior de IPv6, todos terão IPs publicamente roteáveis e poderão evitar NAT por completo, então esse tipo de problema deve diminuir
  • É irritante como é difícil, ao escrever posts de blog desse tipo, linkar para uma linha específica de código e fazer com que esse link continue vivo e útil ao longo do tempo
    No GitHub, dá para fixar em uma combinação de hash de commit específico, nome de arquivo e número de linha, mas, se a base de código mudar bastante, isso deixa de ser muito útil. Em visualizadores web de git menos usados, como git.blender.org, isso não funcionou bem
  • Em resumo, dentro de um pacote ICMP existe um campo id, e o Netfilter reconhece pacotes ou frames ICMP como um “caso especial”