
Como funciona NAT em um ESP32: Roteamento e Repetidor Wi-Fi
NAPT não funciona no Arduino Core: o lwIP dele vem sem IP_FORWARD. O sdkconfig do ESP-IDF, a propagação de DNS por DHCP e um repetidor medido rodando.
Requisitos de hardware:
- Placa ESP32-S3 (referência: módulo N16R8)
- Cabo USB-C com transmissão de dados
- Windows 10 ou 11 com acesso à internet
- Roteador Wi-Fi 2,4 GHz e um celular para testar
Baixe o material deste tutorial:
- Projeto completo (ZIP)94 KBFirmware ESP-IDF, sdkconfig com as opções de NAPT, tabela de partições, definição da placa N16R8, README e o manual em PDF.
- Manual em PDF (7 páginas)101 KBPasso a passo em nove etapas, tabela de solução de problemas e checklist final com as medições de referência.
Quer receber novos manuais e esquemas industriais? Entre no nosso canal do Telegram / WhatsApp
O que é NAT num ESP32, e o que ele não é
O NAT é o que permite que vários dispositivos numa rede privada alcancem a internet através de um único endereço. Num ESP32 a variante usada é o NAPT, que traduz endereço e porta ao mesmo tempo — é o que transforma a placa num repetidor Wi-Fi de verdade, e não apenas num ponto de acesso isolado.
A ideia é simples. O ESP32 sobe as duas interfaces ao mesmo tempo: a STA conecta no seu roteador, a AP cria uma rede própria com servidor DHCP. O motor NAPT reescreve o cabeçalho dos pacotes vindos dos clientes do AP e os retransmite pela STA, guardando numa tabela para onde devolver a resposta.
O que quase todo tutorial esquece de dizer é que isso não compila no Arduino Core. A próxima seção explica por quê, e o resto do artigo mostra o caminho que funciona.
Por que o Arduino Core não faz NAT
Você vai encontrar por aí exemplos com ip_napt_enable e ip_portmap_init incluindo lwip/napt.h direto num sketch Arduino. Esse código não funciona nas builds atuais do Arduino Core para ESP32-S3, por três motivos concretos:
A opção CONFIG_LWIP_IP_FORWARD vem desabilitada no sdkconfig pré-compilado do Arduino. Sem encaminhamento de IP, o pacote do cliente chega na placa e morre ali.
A liblwip.a distribuída não expõe o símbolo ip_napt_enable. Dependendo da versão, o link falha ou o código compila com um stub que não faz nada.
E como o sdkconfig do Arduino já vem compilado, não dá para ligar a opção depois. Ela precisa estar ativa no momento em que o lwIP é compilado.
O resultado prático é cruel: em algumas versões o sketch compila, sobe, imprime a mensagem de sucesso, e o cliente conectado no AP simplesmente não navega. Não há erro para investigar.
O caminho correto é firmware ESP-IDF com sdkconfig próprio. É o que este artigo faz.
As quatro opções de sdkconfig que habilitam NAPT
Coloque no seu arquivo sdkconfig.defaults, antes de compilar:
| 1 | CONFIG_LWIP_IP_FORWARD=y |
| 2 | CONFIG_LWIP_IPV4_NAPT=y |
| 3 | CONFIG_LWIP_IPV4_NAPT_PORTMAP=y |
| 4 | CONFIG_LWIP_L2_TO_L3_COPY=y |
IP_FORWARD liga o encaminhamento entre interfaces. IPV4_NAPT liga a tradução de endereço e porta. NAPT_PORTMAP habilita mapeamento de portas, necessário se você quiser expor um serviço interno. E L2_TO_L3_COPY copia o pacote da camada 2 para a 3 antes do roteamento, sem o qual o encaminhamento entre AP e STA se comporta de forma errática.
Depois de mexer aqui, apague a pasta build e recompile do zero. Alterar sdkconfig sem limpar é fonte clássica de comportamento inexplicável.
esp_netif_napt_enable, a API que você deve usar
Esqueça o ip_napt_enable cru do lwIP. O ESP-IDF moderno expõe uma API de mais alto nível, que recebe o handle da interface e cuida dos detalhes:
| 1 | static void enable_nat(void) |
| 2 | { |
| 3 | if (!ap_netif) { |
| 4 | nat_result = ESP_ERR_INVALID_STATE; |
| 5 | nat_enabled = false; |
| 6 | return; |
| 7 | } |
| 8 | if (sta_netif) { |
| 9 | esp_netif_napt_disable(sta_netif); |
| 10 | } |
| 11 | nat_result = esp_netif_napt_enable(ap_netif); |
| 12 | nat_enabled = nat_result == ESP_OK; |
| 13 | ESP_LOGI(TAG, "NAPT enable on AP: %s", esp_err_to_name(nat_result)); |
| 14 | } |
Dois detalhes importam. O NAPT é habilitado na interface AP, que é de onde vem o tráfego a ser traduzido, e não na STA. E vale desabilitar explicitamente na STA antes: se sobrar NAPT ativo do outro lado, o roteamento fica ambíguo.
Guarde o retorno numa variável em vez de descartar. Ele é a diferença entre "achei que ligou" e "ligou".
O DNS é a metade que ninguém conta
Aqui está o motivo real pelo qual a maioria das tentativas de repetidor ESP32 "não funciona".
Com o NAPT ativo, o cliente conecta, recebe IP do DHCP e o roteamento funciona. Mesmo assim o navegador não abre nada. A pessoa conclui que o NAT falhou — quando na verdade ninguém entregou um servidor DNS ao cliente. Sem resolver nomes, a internet parece morta ainda que os pacotes estejam sendo roteados perfeitamente.
O DHCP do AP precisa oferecer o DNS, e a opção só é aceita com o servidor parado. A sequência correta é parar, configurar, iniciar:
| 1 | static void sync_dns_to_ap(void) |
| 2 | { |
| 3 | esp_netif_dns_info_t dns = {0}; |
| 4 | if (esp_netif_get_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns) == ESP_OK) { |
| 5 | if (dns.ip.u_addr.ip4.addr == 0) { |
| 6 | ESP_LOGW(TAG, "DNS do roteador veio vazio, usando 8.8.8.8"); |
| 7 | esp_netif_str_to_ip4("8.8.8.8", &dns.ip.u_addr.ip4); |
| 8 | dns.ip.type = ESP_IPADDR_TYPE_V4; |
| 9 | } |
| 10 | esp_netif_set_dns_info(ap_netif, ESP_NETIF_DNS_MAIN, &dns); |
| 11 | esp_netif_dhcps_stop(ap_netif); |
| 12 | uint8_t offer_router = 1; |
| 13 | uint8_t offer_dns = 1; |
| 14 | esp_netif_dhcps_option(ap_netif, ESP_NETIF_OP_SET, |
| 15 | ESP_NETIF_ROUTER_SOLICITATION_ADDRESS, |
| 16 | &offer_router, sizeof(offer_router)); |
| 17 | esp_netif_dhcps_option(ap_netif, ESP_NETIF_OP_SET, |
| 18 | ESP_NETIF_DOMAIN_NAME_SERVER, |
| 19 | &offer_dns, sizeof(offer_dns)); |
| 20 | esp_netif_dhcps_start(ap_netif); |
| 21 | } |
| 22 | } |
O DNS é lido da interface STA, ou seja, é o mesmo que o seu roteador entregou ao ESP32. Quando ele volta vazio — acontece com alguns provedores — o código cai para 8.8.8.8 em vez de deixar o cliente sem resolução.
As duas flags de oferta precisam ser ligadas explicitamente. ESP_NETIF_DOMAIN_NAME_SERVER faz o DHCP anunciar o DNS, e ESP_NETIF_ROUTER_SOLICITATION_ADDRESS faz ele anunciar o gateway. Faltando a segunda, o cliente recebe IP e DNS mas não sabe para onde mandar o tráfego de saída.
Chame essa função depois que a STA obtiver IP, não antes: o DNS do roteador só existe a partir daí.
Como saber se funcionou de verdade
Não confie em log de boot. Exponha o estado num endpoint HTTP e leia enquanto o repetidor está rodando. Este é o retorno real de um ESP32-S3 N16R8 servindo como repetidor, com os campos que importam:
| 1 | { |
| 2 | "network_mode": "nat", |
| 3 | "ap_ssid": "MeuRepetidor-01", |
| 4 | "ap_ip": "192.168.4.1", |
| 5 | "ap_dhcp_start": "192.168.4.2", |
| 6 | "ap_dhcp_end": "192.168.4.20", |
| 7 | "ap_clients": 1, |
| 8 | "dns": "192.168.15.1", |
| 9 | "dhcp_status": "router:ESP_OK dns:ESP_OK start:ESP_OK", |
| 10 | "nat_enabled": true, |
| 11 | "nat_result": "ESP_OK", |
| 12 | "nat_interface": "ap", |
| 13 | "upstream_http_1_1_1_1": 301, |
| 14 | "upstream_http_neverssl": -1, |
| 15 | "rssi": -39, |
| 16 | "free_heap": 8600128, |
| 17 | "uptime_ms": 13687494 |
| 18 | } |
O campo nat_result em ESP_OK com nat_interface em ap confirma que o NAPT subiu no lugar certo. O dhcp_status mostra as três operações da seção anterior, e as três precisam estar em ESP_OK — se a do meio falhar, o cliente fica sem DNS. E dns precisa mostrar o endereço do seu roteador; se aparecer 8.8.8.8, o fallback entrou em ação e vale investigar por quê.
Diagnóstico: IP puro contra nome
Este é o teste que separa os dois problemas mais confundidos do mundo, e cabe em duas requisições.
Sonde um endereço IP literal e um hostname, ambos em HTTP simples, nunca HTTPS — TLS adiciona uma camada de falha que atrapalha o diagnóstico:
| 1 | static void run_upstream_diagnostics(void) |
| 2 | { |
| 3 | upstream_http_1_1_1_1 = http_probe("http://1.1.1.1", |
| 4 | upstream_error_1_1_1_1, |
| 5 | sizeof(upstream_error_1_1_1_1)); |
| 6 | upstream_http_neverssl = http_probe("http://neverssl.com", |
| 7 | upstream_error_neverssl, |
| 8 | sizeof(upstream_error_neverssl)); |
| 9 | } |
A leitura do resultado é direta. Se o IP puro responde e o nome falha, o roteamento está bom e o problema é resolução de nomes. Se os dois falham, o problema é roteamento ou o link de subida. Se os dois respondem, o caminho está inteiro.
Uma sutileza que custa horas: o resolver do próprio ESP32 e o DNS entregue aos clientes são caminhos diferentes. O cliente pergunta ao servidor anunciado pelo DHCP e essa consulta atravessa o NAPT como qualquer outro pacote. Já a sondagem acima usa a pilha interna da placa.
Isso não é teoria. No dispositivo medido para este artigo, a sondagem por nome falhava com ESP_ERR_HTTP_CONNECT enquanto a sondagem por IP puro respondia normalmente — e, ao mesmo tempo, o celular conectado ao AP resolvia domínios e navegava sem qualquer problema, conforme o teste de IP público descrito adiante.
Ou seja: o repetidor estava perfeitamente funcional para os clientes, e o autodiagnóstico da própria placa acusava falha de DNS. Se você cair nesse cenário, investigue o resolver interno do firmware e não o NAT. E desconfie do painel: um diagnóstico que mede só o IP puro vai exibir tudo em ordem enquanto o usuário não abre um site sequer.
Vale também rodar esse diagnóstico periodicamente, e não só quando a STA pega IP. Um valor medido no boot e exibido horas depois como se fosse atual é pior que não exibir nada.
Medições de um repetidor real
Os números abaixo são de um ESP32-S3 N16R8 rodando o firmware descrito aqui, monitorado por polling a cada dez segundos.
O NAPT subiu com nat_result em ESP_OK na interface AP, e o DHCP propagou o DNS com as três operações em ESP_OK, entregando o endereço do roteador local em vez do fallback.
Um celular Android conectou no AP e o contador ap_clients passou de 0 para 1, permanecendo estável ao longo de oito minutos de leituras contínuas, sem reassociação.
Como provar o roteamento sem depender de boa fé
Cliente associado não é cliente navegando, e abrir o portal em 192.168.4.1 também não prova nada: esse endereço é o próprio ESP32, então o tráfego não atravessa a tradução. Dizer "desliguei os dados móveis, confia" também não serve como evidência.
Existe um teste que resolve isso de forma objetiva, e ele cabe em uma única imagem: compare o endereço IP público visto pelo cliente com o do seu link.
Descubra primeiro qual é o IPv4 público da sua rede, a partir de qualquer máquina cabeada:
| 1 | curl -4 https://api.ipify.org |
Depois, no celular conectado ao AP do ESP32, abra api.ipify.org no navegador. Se o endereço exibido for o mesmo do seu link, o tráfego saiu pela sua banda larga — ou seja, atravessou o repetidor. Se for outro, o celular escapou pela rede móvel e o teste não vale.
A elegância do método é que ele não depende de configuração declarada. O IP mostrado identifica sozinho por onde o pacote passou. E como você digita um nome de domínio em vez de um IP, o mesmo print prova três coisas de uma vez: o DNS resolveu, o TCP saiu e voltou, e a rota foi pelo seu link.
No teste deste artigo o celular exibiu o mesmo endereço IPv4 do link de casa, com o ap_clients registrando 1 em vinte leituras consecutivas cobrindo o instante da captura, sem nenhuma leitura zerada no intervalo. Repetidor validado de ponta a ponta.
Um detalhe que pode confundir: o NAPT do ESP32 é somente IPv4. Se o endereço exibido vier no formato IPv6, começando com algo como 2804, o celular saiu por IPv6 nativo e não usou o NAT. Force o teste em IPv4.
O detalhe mais interessante apareceu na memória. O menor heap livre registrado caiu de 8.590.580 para 8.527.300 bytes depois que o cliente conectou, uma diferença de cerca de 63 KB. Essa queda é a tabela NAPT e o rastreamento de conexões sendo alocados: é a assinatura de tráfego realmente atravessando a tradução, e não apenas de um cliente associado sem trafegar.
O dispositivo acumulava 3 horas e 48 minutos de operação contínua, com sinal de −39 dBm no canal 11. Vale notar que os 8,6 MB de heap livre só existem porque a PSRAM octal está ativa: o ESP32-S3 tem cerca de 320 KB de RAM interna, e sem PSRAM a tabela NAPT compete com o resto do firmware por um espaço bem mais apertado.
Tutorial: do computador zerado ao repetidor funcionando
A partir daqui é passo a passo, sem pular etapa. O ponto de partida é um Windows sem nada instalado e uma placa ESP32-S3 na caixa. O destino é um celular navegando através dela.
Reserve de 40 a 60 minutos na primeira vez. A maior parte é download: o ESP-IDF baixa o compilador Xtensa inteiro.
Passo 1: instalar o ESP-IDF
O Arduino não serve aqui, conforme a segunda seção deste artigo. Baixe o instalador oficial do ESP-IDF para Windows no site da Espressif e escolha a versão 5.x ou superior.
Durante a instalação, aceite o caminho padrão e deixe marcada a opção de criar os atalhos. O instalador baixa o Python, o Git e a toolchain de compilação — é normal demorar.
Ao terminar, procure no menu iniciar por ESP-IDF PowerShell. Esse atalho abre um terminal com todas as variáveis de ambiente já configuradas. Use sempre ele, e não o PowerShell comum: fora dele o comando idf.py não existe.
Confirme que funcionou:
| 1 | idf.py --version |
Passo 2: criar o projeto
Ainda no ESP-IDF PowerShell, vá até a pasta onde você guarda projetos e gere o esqueleto:
| 1 | cd C:\\projetos |
| 2 | idf.py create-project esp32-repetidor |
| 3 | cd esp32-repetidor |
Isso cria a estrutura mínima, com a pasta main e o CMakeLists.txt. O código do repetidor vai no arquivo main dentro dessa pasta, usando as funções mostradas nas seções anteriores.
Passo 3: escrever o sdkconfig.defaults
Este é o passo que define se o NAT vai existir ou não. Crie um arquivo chamado sdkconfig.defaults na raiz do projeto, ao lado do CMakeLists.txt, com o conteúdo abaixo. As quatro primeiras linhas são as que habilitam o NAPT, e as demais ligam a PSRAM octal e a flash de 16 MB de uma placa N16R8:
| 1 | CONFIG_LWIP_IP_FORWARD=y |
| 2 | CONFIG_LWIP_IPV4_NAPT=y |
| 3 | CONFIG_LWIP_IPV4_NAPT_PORTMAP=y |
| 4 | CONFIG_LWIP_L2_TO_L3_COPY=y |
| 5 | CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y |
| 6 | CONFIG_SPIRAM=y |
| 7 | CONFIG_SPIRAM_MODE_OCT=y |
| 8 | CONFIG_SPIRAM_SPEED_80M=y |
| 9 | CONFIG_SPIRAM_USE_MALLOC=y |
Se a sua placa não tiver PSRAM, remova as quatro linhas de SPIRAM. Se a flash for de 8 MB, troque o valor de FLASHSIZE. As quatro primeiras nunca mudam.
Passo 4: escolher o alvo e compilar
Diga ao ESP-IDF qual chip você está usando. Esse comando precisa vir antes da primeira compilação, porque ele é quem monta o sdkconfig a partir do seu sdkconfig.defaults:
| 1 | idf.py set-target esp32s3 |
| 2 | idf.py build |
A primeira compilação é demorada, porque compila o ESP-IDF inteiro junto. As seguintes levam segundos.
Se você editar o sdkconfig.defaults depois, apague a pasta build e rode o set-target de novo. Alterar as opções sem limpar é a causa mais comum de "mudei a configuração e nada aconteceu".
Passo 5: descobrir a porta e gravar
Conecte a placa pelo cabo USB. Numa ESP32-S3 com duas portas, use a marcada UART.

Para descobrir qual porta COM apareceu:
| 1 | idf.py -p COM3 flash |
Troque COM3 pelo número que apareceu no Gerenciador de Dispositivos do Windows, em Portas. Se a gravação falhar com uma mensagem de falha ao conectar, segure o botão BOOT da placa, toque o RESET, solte o BOOT e rode o comando de novo.
Passo 6: acompanhar pelo monitor serial
Ainda com a placa conectada, abra o monitor:
| 1 | idf.py -p COM3 monitor |
Você deve ver o log de boot e, logo depois que a placa subir, a linha do NAPT. Ela é a primeira confirmação de que deu certo:
| 1 | I (2841) AnalictY-NAT: NAPT enable on AP: ESP_OK |
Se aparecer qualquer coisa diferente de ESP_OK, volte ao passo 3: as opções de sdkconfig não entraram. Para sair do monitor, use Ctrl mais o colchete direito.
Passo 7: informar as credenciais da sua rede
Agora a placa está rodando, mas ainda não sabe a senha da sua rede. E aqui vale um aviso que economiza tempo: o portal do firmware NAT é um painel de status, não um formulário. Ele mostra o estado do repetidor e oferece apenas atualizar e reiniciar — não tem campo de SSID nem botão de varredura de redes.
Isso é decisão de projeto, não limitação. Varrer redes exige colocar o rádio em modo de varredura, o que interrompe momentaneamente as duas interfaces e desestabiliza justamente o roteamento que o repetidor precisa manter de pé. Por isso o provisionamento com busca de redes vive num firmware separado, e é assunto do tutorial do portal de configuração — o complemento natural deste: um configura, o outro roteia.
Para informar a rede a esta placa, envie as credenciais direto ao endpoint de configuração, de um computador na mesma rede ou conectado ao ponto de acesso dela:
| 1 | curl -X POST http://192.168.4.1/api/config -H "Content-Type: application/json" -d "{\\"wifi_ssid\\":\\"MinhaRede-2.4G\\",\\"wifi_pass\\":\\"minha-senha\\"}" |
As credenciais são gravadas na NVS e sobrevivem a reinício e a nova gravação de firmware.
Um detalhe que economiza tempo: o ESP32-S3 não conecta em redes de 5 GHz. Se a sua rede aparece com o sufixo 5G, use a de 2,4 GHz.
Passo 8: validar o repetidor
Com a placa conectada na sua rede, ela ganha um endereço na rede de casa. Descubra qual no monitor serial ou na lista de dispositivos do seu roteador, e abra esse endereço no navegador do computador.
O painel mostra o estado completo, e é onde você confirma o que interessa:

Confira três campos antes de comemorar. O nat_result precisa estar em ESP_OK. O dhcp_status precisa mostrar as três operações em ESP_OK. E o dns precisa trazer o endereço do seu roteador.
O teste final é o mais simples de todos: mantenha o celular conectado na rede da placa e abra qualquer site. Se carregar, o contador de clientes vai para 1 e o repetidor está validado de ponta a ponta.
Erros comuns e como resolver
O cliente conecta mas não navega, e o NAPT está em ESP_OK. É DNS. Confira o dhcp_status: as três operações precisam estar em ESP_OK, e a flag ESP_NETIF_DOMAIN_NAME_SERVER precisa estar ligada.
O cliente recebe IP e DNS mas nada sai. Falta anunciar o gateway. Ligue ESP_NETIF_ROUTER_SOLICITATION_ADDRESS junto com a de DNS.
esp_netif_napt_enable retorna erro. As opções de sdkconfig não entraram. Confirme as quatro e recompile com a pasta build apagada.
Compilei no Arduino e não funciona. Não vai funcionar. Migre para ESP-IDF, conforme a segunda seção.
Funciona por alguns minutos e degrada. A tabela NAPT tem limite de entradas. Num chip sem PSRAM, poucos clientes com muitas conexões simultâneas esgotam o espaço rápido.
O ESP32 acusa falha de DNS mas os clientes navegam. Comportamento esperado, conforme explicado no diagnóstico: são caminhos distintos. Investigue o resolver interno, não o NAT.
