Como o Linux processa ping através de NAT?
(devnonsense.com)- O NAT IPv4 normalmente distingue o destino das respostas por portas TCP/UDP, mas o ICMP echo do
pingnã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,natboxeserver, além deiptablesMASQUERADE - 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_DGRAMdo 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
natboxatuando como roteador NAT e um servidor, cada um em seu próprio namespace, separando a rede privada da rede do servidorclient1:192.168.99.1/24client2: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,tcpdumpe outros comandos como root - Os dois clientes são conectados à bridge
br0, e onatboxé 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 pelonatbox - No
natbox, o encaminhamento de pacotes foi ativado comnet.ipv4.ip_forward=1, e uma regra MASQUERADE foi adicionada à cadeiaPOSTROUTINGda tabelanatdoiptablesip 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
client1eservercomtcpdump -n icmp - Do lado do cliente, aparecem o echo request
192.168.99.1 > 10.0.100.2e o echo reply10.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 donatbox - Requisições ICMP de clientes diferentes têm campos id diferentes
- No exemplo,
client1usa o ID31428 client2usa o ID33391
- No exemplo,
- Essa observação mostra que o
natboxpode 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
- O Type do echo request é
- 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
pingfaz parte do pacote iputils - Um comentário perto de
ping4_send_probeexplica que, ao criar um ICMP echo request, o campo ID é um número aleatório e o Sequence Number é um inteiro crescente - Internamente, o
pingtem o campoidentemstruct ping_rts- O valor padrão é
-1 - Ele pode ser sobrescrito pela opção de CLI
-ecom um valor entre0eIDENTIFIER_MAX, que é0xFFFF
- O valor padrão é
- Se
rts->ident == -1, opingfaz bind do socket com tipoSOCK_DGRAMe protocoloIPPROTO_ICMP - Segundo a descrição dos sockets
IPPROTO_ICMPno Linux, o cabeçalho ICMP é verificado e ajustado emsend(), e o id é definido como o número da porta local do socket - Como o
pingnã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 999para 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 ID30218 - 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/netfilterdo Linux o código que usa o campo ICMPid
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_infochamaget_unique_tuple, que por sua vez leva anf_nat_l4proto_unique_tuple nf_nat_l4proto_unique_tupletem um caso paraIPPROTO_ICMPe faz referência atuple->src.u.icmp.id- Em
nf_nat_proto.c,nf_nat_manip_pktpassa pornf_nat_ipv4_manip_pktel4proto_manip_pkt, e no caso de ICMP chamaicmp_manip_pkt icmp_manip_pktgrava o ICMP ID real no pacote comhdr->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_conncontémtuplehash[IP_CT_DIR_MAX]IP_CT_DIR_ORIGINAL: direção dos pacotes de saídaIP_CT_DIR_REPLY: direção da resposta de entrada
- Cada
nf_conntrack_tuple_hashcontém umnf_conntrack_tupleque identifica a connection - O tuple é dividido em
src, que pode ser manipulado, edst, que é imutávelsrccontém o endereço IP e campos específicos do protocolo- No ICMP, o campo específico do protocolo é
__be16 id dstcontém o endereço IP que não muda e otypeecodedo 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
natboxrecebe um ICMP echo,nf_nat_setup_infocria 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_pktdefine 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ívelfind_best_ips_protoreescreve o endereço IP de origemnf_nat_used_tupleverifica 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_idgera um ID aleatório comget_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-200usando--to-portsna regra MASQUERADE doiptables - Se não for encontrado um tuple livre, um ID duplicado permanece na connection, e depois
__nf_conntrack_confirmdetecta 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_infoenf_nat_manip_pkt kproberastreia o momento da chamada da função, ekretproberastreia o momento em que ela retorna- Como em
kretprobenã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 pacotebswapé usado para converter a ordem de bytes de rede, big endian, para little endianntopconverte 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_buffenf_connsem incluir headers - Esse programa em
bpftracefoi 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 ID999tanto no original tuple quanto no reply tuple - O segundo cliente,
192.168.99.2, tem o ICMP ID do reply tuple reescrito para32809 nf_nat_manip_pktaltera o IP de origem para10.0.100.1comNF_NAT_MANIP_SRCno echo request e, na resposta, restaura o IP de destino para o IP original do cliente comNF_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_infoserá 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
Comentários do Hacker News
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
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
Ao receber a resposta, o roteador usa esse valor único de ID para encaminhar a resposta ao dispositivo correto na rede local
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
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/
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
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
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
https://en.wikipedia.org/wiki/Lindy_effect
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
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
idparece ser, na prática, equivalente a(sport, dport), mas tem 16 bits, então o espaço é muito menor que 32 bitsMas 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 TClaro, 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
[1] - https://github.com/yarrick/pingfs
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
https://elixir.bootlin.com/linux/latest/source