Em projetos de IoT baseados em ESP32, é comum precisar de um Hub central — um servidor local responsável por receber dados, exibir dashboards e coordenar a comunicação entre diversos módulos sensores e atuadores.
Entretanto, quando esse Hub está atrás de um roteador doméstico, surge uma limitação: como acessá-lo remotamente pela Internet sem depender de IP fixo ou configurações complexas de NAT/port forwarding?
Soluções tradicionais como DDNS ou VPNs podem funcionar, mas envolvem custos, dependências externas ou configurações avançadas.
Com o Cloudflare Tunnel, e em particular o Quick Tunnel, torna-se possível publicar o servidor local com pouco esforço, sem expor a rede doméstica e sem exigir autenticação prévia com a conta Cloudflare.
Cada inicialização gera automaticamente uma URL pública temporária no domínio trycloudflare.com.
Para tornar essa arquitetura realmente prática, o projeto acrescenta um segundo componente essencial: o serviço de notificação ntfy.sh, que envia a nova URL pública diretamente ao usuário, sempre que o sistema é reiniciado.
A motivação deste trabalho surgiu da necessidade de monitorar e controlar múltiplas estações ESP32 de forma remota, utilizando um Hub central com interface web local (via Nginx e HTML).
Nos testes iniciais, verificou-se que o Quick Tunnel atende perfeitamente para uso doméstico, laboratorial e educacional, porém sua principal limitação é a não persistência da URL pública — ela muda a cada reinicialização.
A solução foi criar um serviço automatizado (systemd) que:
Assim, o sistema torna-se autônomo e resiliente:
cada vez que o servidor Linux ou Raspberry Pi é ligado, a URL é recriada, validada e comunicada ao administrador — permitindo acesso remoto imediato ao Hub e aos dashboards ESP32 sem intervenção manual.
Antes de iniciar a configuração do túnel e das notificações automáticas, é importante garantir que o ambiente de base esteja pronto e funcional.
A seguir, são listados os componentes de hardware e software necessários, bem como as etapas iniciais para acesso remoto via mDNS — um recurso que facilita o uso do Putty a partir de outro computador na mesma rede local.
O projeto foi testado em:
Ambos oferecem suporte nativo ao systemd, essencial para a automação dos serviços cloudflared e cf-url-notify.
Partiremos do princípio de que o leitor já possui:
Para referência, tutoriais de instalação do Raspberry Pi OS podem ser encontrados no site oficial:
🔗 https://www.raspberrypi.com/software/
Durante o desenvolvimento, o acesso remoto foi realizado a partir de um computador Windows, utilizando o Putty.
Para evitar depender de endereços IP dinâmicos na LAN, configuramos o serviço mDNS (Multicast DNS) no Linux, o que permite o acesso pelo nome do host seguido de .local.
Por exemplo:
ssh dailton@domlinux.local
Instalação e ativação do mDNS
No Linux, execute:
sudo apt update
sudo apt install avahi-daemon avahi-utils -y
sudo systemctl enable --now avahi-daemon
No Raspberry PI, na geração da imagem pelo utilitário oficial, é possível definir o hostname e habilitar o SSH, eliminando dois passos no procedimento (instalar mDNS e habilitar SSH). Veja o link a seguir:
https://www.raspberrypi.com/documentation/computers/getting-started.html
Verifique se o serviço está ativo:
systemctl status avahi-daemon
Se tudo estiver correto, o comando deve indicar active (running).
Configure o hostname desejado
sudo hostnamectl set-hostname <hostname>
Substitua <hostname>pelo nome de sua escolha (por exemplo, meu-hub ou raspberrypi).
Atualize o arquivo /etc/hosts (opcional, mas recomendado)
sudo nano /etc/hosts
Verifique se existe uma linha semelhante a:
127.0.1.1 <hostname>
Caso não exista, adicione-a logo após a linha 127.0.0.1 localhost.
Reinicie o serviço Avahi para aplicar as alterações:
sudo systemctl restart avahi-daemon
Teste o acesso pelo Windows com PuTTY ou terminal:
ssh <user>@<hostname>.local
Se tudo estiver correto, o PuTTY (ou qualquer cliente SSH) resolverá automaticamente o nome <hostname>.local via mDNS, conectando ao seu Linux sem precisar saber o IP.
💡 Dica:
O mDNS também funciona entre máquinas Linux e macOS — basta que estejam na mesma rede local e com o Avahi (Linux) ou Bonjour (macOS/Windows) instalados e ativos.
Além do avahi-daemon, serão necessários os seguintes pacotes:
sudo apt install nginx curl systemd net-tools -y
Esses componentes cobrem:
Para o envio das notificações, é necessário ter um tópico público ou privado criado no serviço ntfy.sh.
O projeto usará o tópico de exemplo:
https://ntfy.sh/dom_07c2_07e9_alerts
Esse endereço pode ser acessado diretamente no navegador ou pelo aplicativo ntfy no celular, onde as notificações aparecerão automaticamente sempre que o túnel for recriado.
A figura a seguir apresenta a arquitetura geral da solução, destacando o papel de cada componente e o fluxo de comunicação entre eles:
Figura 1 – Componentes da Arquitetura o Sistema
🌐 Visão geral
O sistema foi projetado para permitir acesso remoto seguro ao Hub ESP32 hospedado em um servidor Linux ou Raspberry Pi, utilizando o Cloudflare Quick Tunnel como ponte entre a rede local e a Internet.
Todo o processo ocorre automaticamente a cada inicialização do sistema, garantindo que o Hub permaneça acessível mesmo que o IP local ou público varie.
Componentes principais
🖥️ 1. Servidor Linux / Raspberry Pi (Hub Local)
É o núcleo da solução.
Nele residem:
Esse servidor opera como ponto de concentração dos dados coletados pelos nós ESP32 (coletoras), além de disponibilizar a visualização e controle remoto.
☁️ 2. Cloudflare Quick Tunnel
O Quick Tunnel é a ponte entre o Hub local e o mundo externo.
Ao ser iniciado, o cloudflared estabelece uma conexão segura com os servidores da Cloudflare e publica um endereço público dinâmico no domínio trycloudflare.com, permitindo que o Hub seja acessado sem IP fixo ou configurações de roteador.
A cada reinicialização, o túnel gera uma nova URL, por exemplo:
https://josh-commerce-ann-humidity.trycloudflare.com
Limitações dos Quick Tunnels
É importante observar que os Quick Tunnels do Cloudflare possuem restrições que podem impactar determinadas arquiteturas de IoT ou aplicações com alta taxa de requisições. Cada túnel está limitado a 200 requisições simultâneas (in-flight), e ao atingir esse teto o Cloudflare retornará HTTP 429 (Too Many Requests) para novas conexões. Além disso, Quick Tunnels não suportam Server-Sent Events (SSE), o que inviabiliza o uso desse mecanismo para atualizações contínuas de dados. Projetos que dependam de streaming em tempo real ou alto volume de acessos devem considerar o uso de tunnels permanentes (com domínio próprio) ou outras soluções mais robustas.
Observação:
Ao iniciar os testes, avaliamos também a opção de utilizar um Named Tunnel do Cloudflare. No entanto, essa modalidade exige que o usuário tenha um domínio próprio registrado e delegado ao Cloudflare, além de ativar recursos do Zero Trust — etapas que, no Brasil, frequentemente solicitam um cartão de crédito mesmo no plano gratuito. Como o objetivo deste projeto é oferecer uma solução prática, acessível e replicável por qualquer leitor sem custos adicionais, optamos pelo Quick Tunnel. Essa opção não exige domínio, conta ou configurações adicionais, funcionando imediatamente e permitindo que a URL pública seja obtida automaticamente a cada inicialização. Com a automação criada neste guia, o Quick Tunnel se torna simples, funcional e perfeitamente adequado ao propósito de expor o HubESP32 pela Internet.
🔔 3. Serviço ntfy.sh
O ntfy.sh é um sistema de notificações via HTTP e MQTT que permite o envio instantâneo de mensagens para o celular ou navegador.
Neste projeto, ele é usado para notificar o administrador sobre a nova URL pública criada pelo túnel.
A mensagem enviada contém um link direto para o acesso remoto do Hub.
Exemplo de notificação recebida:
[Hub ESP32 - Cloudflare Tunnel]
Nova URL: https://josh-commerce-ann-humidity.trycloudflare.com
📡4. Usuário e Acesso Remoto
O administrador, ao receber a notificação, pode clicar diretamente no link informado e acessar a interface web do Hub, mesmo que o servidor esteja por trás de NAT ou firewall residencial.
O acesso pode ser feito a partir de qualquer dispositivo conectado à Internet — notebook, smartphone ou desktop.
🔌 5. Dispositivos ESP32 (Coletoras e Sensores)
As estações ESP32 comunicam-se com o Hub via rede local (Wi-Fi ou Ethernet).
Cada uma possui um proxy configurado no servidor (via Nginx) que permite redirecionar requisições e monitorar status individualmente.
Assim, o Hub centraliza dados de sensores, gráficos e comandos de controle — enquanto o túnel expõe apenas a interface do Hub, mantendo os dispositivos internos protegidos.
🔁 6. Fluxo de inicialização resumido
🧩Acesso remoto ao servidor (via PuTTY e SSH/mDNS)
Para administrar o servidor Linux (ou o Raspberry Pi 4) onde o Hub será instalado, é prático usar o acesso remoto via SSH. Assim, todo o processo de configuração pode ser feito a partir do seu computador Windows, sem necessidade de teclado, mouse ou monitor conectados ao Raspberry.
🔹 Instalando e usando o PuTTY (Windows)
login as: <seu usuário>
password: ******
(informe o usuário configurado no seu Linux/Raspberry Pi)

Figura 2 – Tela Inicial do Putty

Figura 3 – Tela de Login do Putty

Figura 4 – Tela do Putty Logado
🔹 Ativando o SSH e o mDNS no Linux/Raspberry Pi
Habilitar o serviço SSH
sudo systemctl enable ssh
sudo systemctl start ssh
Após isso, você poderá acessar:
ssh usuario@meu-linux.local
ou, pelo PuTTY:
Host: meu-linux.local
Port: 22
💡 No Raspberry Pi OS, o SSH já vem instalado (basta habilitá-lo via raspi-config)
e o mDNS já está ativo por padrão.
Em outras distribuições (Debian, Ubuntu Server, etc.), pode ser necessário habilitar manualmente conforme acima.
🔹 Testando o mDNS e SSH
No terminal do seu computador (Windows PowerShell ou Linux):
ping raspberrypi.local
ssh usuario@raspberrypi.local
Se responder, o serviço está corretamente configurado.
🔹 Dica opcional – acesso remoto com chave SSH
Para evitar digitar senhas:
ssh-keygen -t ed25519
ssh-copy-id usuario@meu-linux.local
Assim, o login pelo PuTTY (ou VS Code SSH) será automático e mais seguro.
🔹 Resumo rápido
|
Função |
Comando principal |
|
Instalar mDNS |
sudo apt install avahi-daemon -y |
|
Habilitar SSH |
sudo systemctl enable –now ssh |
|
Testar mDNS |
ping raspberrypi.local |
|
Acessar via PuTTY |
Host: raspberrypi.local, Port: 22, Type: SSH |
|
Login |
usuario / senha do Linux |
Na configuração do Raspberry PI para o Putty enxergar as acentuações: Vá ao Menu Preferences | Localisation | Locale e deve-se definir:
Language : pt (portuguese)
Country: BR (Brazil)
Character Set: UTF-8
Nesta seção faremos a configuração do Hub para rodar em um Linux ou Raspberry PI fazendo o apontamento para duas aplicações rodando em ESP32’s diferentes na mesma rede local. As duas aplicações ficarão disponíveis para o acesso direto através da Internet sem ter IP público, sem domínio próprio registrado, sem liberação de portas pelo provedor de Internet, sem DNS dinâmico, etc.
As duas aplicações já foram publicadas no Blog da Eletrogate. Sugerimos ao leitor dar uma olhada em cada projeto mencionado para melhor entendimento do tutorial aqui apresentado. São elas:
Importante: Foi necessário adaptar os javascripts dos html’s originais para não usar referências absolutas, mas sim referências relativas. Isso foi necessário, pois no Hub, os html’s estão sob Proxies e não diretamente em uma rede local.
Exemplo da mudança no primeiro html: tivemos que criar uma função pathBase() e, na rotina inicializaWebSocket(), comentamos as duas linhas iniciais e substituímos por outras três linhas.
function pathBase() {
// se a URL tiver /d/algumaCoisa no começo, preserve; senão, vazio
const m = location.pathname.match(/^\/d\/\d+(?=\/|$)/);
return m ? m[0] : ''; // ex.: "/d/1" ou ""
}
function inicializaWebSocket() {
//const proto = location.protocol === 'https:' ? 'wss' : 'ws';
//ws = new WebSocket(`${proto}://${location.host}/ws`);
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const base = pathBase(); // "/d/1" no túnel, "" na LAN
ws = new WebSocket(`${proto}://${location.host}${base}/ws`);
ws.onmessage = (ev)=>{
let msg;
try { msg = JSON.parse(ev.data); } catch(e){ return; }
if (msg.type === 'snapshot') {
applySnapshot(msg.nodes);
} else if (msg.type === 'update') {
renderCard(msg);
} else if (msg.type === 'remove') {
removeCard(msg.mac);
}
};
}
Com o ambiente básico preparado e a arquitetura compreendida, passamos agora à configuração prática do servidor local.
Nesta etapa, instalaremos e validaremos os componentes que formam o núcleo da comunicação: o servidor Nginx, o serviço Cloudflared, e os testes de conectividade local e remota.
🧩 1. Instalação e configuração do Nginx
O Nginx será o servidor web local responsável por hospedar o painel do Hub ESP32.
Ele também atuará como ponto de entrada para os proxies que redirecionam as requisições para cada ESP32 coletor da rede local.
Instale com:
sudo apt update
sudo apt install nginx -y
Verifique o status do serviço:
sudo systemctl status nginx
Se tudo estiver correto, o serviço deve aparecer como active (running).
🗂️ 2. Estrutura de diretórios do Hub
A seguir, criaremos uma estrutura simples e organizada para os arquivos do Hub:
sudo mkdir -p /var/www/hubesp32
sudo chown -R $USER:$USER /var/www/hubesp32
Crie um arquivo index.html inicial e mais simples como opção 1:
cat <<'EOF' > /var/www/hubesp32/index.html <!doctype html> <html lang="pt-br"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> <title>Hub ESP32 - Teste</title> </head> <body> <h2>Hub ESP32 ativo!</h2> <p>Este é o painel principal hospedado no Nginx local.</p> <!-- ESP1 --> <a class="card" href="/d/1/">Rede ESP-NOW — ESP01</a> <br><br> <!-- ESP2 --> <a class="card" href="/d/2/">Notificações via ntfy.sh — ESP02</a> </body> </html> EOF
Ou crie um arquivo index.html inicial mais elaborado como opção 2:
cat <<'EOF' > /var/www/hubesp32/index.html
<!doctype html>
<html lang="pt-BR" data-theme="system">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Hub ESP32</title>
<style>
/* ---------- Tema (variáveis) ---------- */
:root{
--bg:#0b0c10; --card:#11151b; --text:#e6e8eb; --muted:#9aa4b2; --accent:#00d2ff;
--radius:18px; --shadow:0 8px 24px rgba(0,0,0,.25); --size:18px;
}
@media (prefers-color-scheme: light){
:root{ --bg:#f7f7fb; --card:#ffffff; --text:#262933; --muted:#5d6677; --accent:#0077ff; --shadow:0 8px 24px rgba(0,0,0,.08); }
}
/* Força claro/escuro quando usuário escolhe */
[data-theme="light"]{
--bg:#f7f7fb; --card:#ffffff; --text:#262933; --muted:#5d6677; --accent:#0077ff; --shadow:0 8px 24px rgba(0,0,0,.08);
}
[data-theme="dark"]{
--bg:#0b0c10; --card:#11151b; --text:#e6e8eb; --muted:#9aa4b2; --accent:#00d2ff; --shadow:0 8px 24px rgba(0,0,0,.25);
}
/* ---------- Layout ---------- */
html,body{height:100%;margin:0;}
body{
font-family: system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Arial,sans-serif;
background:var(--bg); color:var(--text); font-size:var(--size);
display:flex; align-items:center; justify-content:center; padding:24px;
}
.wrap{ width:100%; max-width:720px; }
.card{
background:var(--card); border-radius:var(--radius); box-shadow:var(--shadow);
padding:20px 18px; position:relative;
}
.title{ display:flex; align-items:center; gap:12px; margin:6px 6px 14px; }
.title .logo{ width:36px; height:36px; display:grid; place-items:center;
border-radius:12px; background:linear-gradient(135deg,var(--accent),#7f5af0); color:white; font-weight:800; }
h1{ font-size:1.4rem; line-height:1.2; margin:0; }
p.sub{ margin:2px 0 0; color:var(--muted); font-size:.95rem; }
.grid{ display:grid; gap:14px; grid-template-columns:1fr; margin:12px 6px 4px; }
@media (min-width:560px){ .grid{ grid-template-columns:1fr 1fr; } }
.app{
border:1px solid rgba(127,127,127,.18); border-radius:16px; padding:14px 12px;
display:flex; flex-direction:column; gap:10px;
background:linear-gradient(180deg, color-mix(in oklab, var(--card), #000 2%), var(--card));
}
.app h2{ margin:0; font-size:1.05rem; }
.badges{ display:flex; gap:8px; flex-wrap:wrap; color:var(--muted); font-size:.9rem;}
.btn{
appearance:none; -webkit-appearance:none; cursor:pointer; text-decoration:none;
display:inline-flex; align-items:center; justify-content:center; gap:.6em;
padding:12px 14px; border-radius:12px; font-weight:600; border:0;
color:#fff; background:linear-gradient(135deg,var(--accent),#7f5af0);
box-shadow:0 6px 16px rgba(0,0,0,.25);
}
.btn:active{ transform:translateY(1px); }
.footer{ text-align:center; color:var(--muted); font-size:.9rem; margin-top:14px; }
/* ---------- Alternador de tema ---------- */
.theme-toggle{
position:absolute; top:12px; right:12px;
display:flex; align-items:center; gap:8px;
background:transparent; border:1px dashed rgba(127,127,127,.35);
color:var(--muted); border-radius:12px; padding:6px 10px; cursor:pointer;
font-weight:600;
}
.theme-toggle:hover{ border-style:solid; }
.theme-toggle .dot{ width:8px; height:8px; border-radius:999px; background:var(--accent); }
/* Tamanho bom em telas pequenas */
@media (max-width:400px){
:root{ --size:17px; }
.btn{ padding:14px 16px; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<button id="themeBtn" class="theme-toggle" aria-label="Alternar tema">
<span class="dot"></span><span id="themeLabel">🖥️ Sistema</span>
</button>
<div class="title">
<div class="logo">ESP</div>
<div>
<h1>Hub ESP32</h1>
<p class="sub">Selecione uma aplicação</p>
</div>
</div>
<div class="grid">
<div class="app">
<h2>Rede ESP-NOW de ESP01</h2>
<div class="badges">
<span>WebSocket ✔</span>
<span>Painéis dinâmicos</span>
</div>
<a class="btn" href="/d/1/">Abrir aplicação</a>
</div>
<div class="app">
<h2>Notificações via Ntfy.sh</h2>
<div class="badges">
<span>Formulário de envio</span>
<span>Proxy /send</span>
</div>
<a class="btn" href="/d/2/">Abrir aplicação</a>
</div>
</div>
<div class="footer">Protegido por autenticação • Cloudflare Quick Tunnel</div>
</div>
</div>
<script>
// -------- Alternador de tema --------
const root = document.documentElement;
const btn = document.getElementById('themeBtn');
const lab = document.getElementById('themeLabel');
// Estados possíveis: 'system' | 'light' | 'dark'
function applyTheme(mode){
root.setAttribute('data-theme', mode);
localStorage.setItem('hubTheme', mode);
lab.textContent = mode === 'light' ? '☀️ Claro' : mode === 'dark' ? '🌙 Escuro' : '🖥️ Sistema';
}
// Inicializa com preferência salva (ou 'system')
applyTheme(localStorage.getItem('hubTheme') || 'system');
// Alterna ciclicamente: system -> light -> dark -> system...
btn.addEventListener('click', ()=>{
const now = root.getAttribute('data-theme') || 'system';
applyTheme(now === 'system' ? 'light' : now === 'light' ? 'dark' : 'system');
});
// Ajuste fino para telas com densidade alta
if (window.devicePixelRatio && devicePixelRatio > 2) {
document.documentElement.style.setProperty('--size','19px');
}
</script>
</body>
</html>
EOF
cat <<'EOF' > /var/www/hubesp32/index.html
<!doctype html>
<html lang="pt-br">
<head>
<meta charset="utf-8">
<title>Hub ESP32 - Teste</title>
</head>
<body>
<h2>Hub ESP32 ativo!</h2>
<p>Este é o painel principal hospedado no Nginx local.</p>
</body>
</html>
EOF
🌐 3. Configuração do servidor virtual (site do Hub)
Crie o arquivo de configuração do Nginx:
sudo nano /etc/nginx/sites-available/hubesp32
E adicione o conteúdo abaixo:
map $http_upgrade $connection_upgrade { default upgrade; "" close; }
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
auth_basic "Hub ESP32";
auth_basic_user_file /etc/nginx/.htpasswd;
root /var/www/hubesp32;
index index.html;
# Garante que a raiz SEMPRE cai no nosso index.html
location = / { try_files /index.html =404; }
location / { try_files $uri /index.html; }
# Proxy ESP32 #1 (WebSocket/HTTP)
location ^~ /d/1/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
rewrite ^/d/1/(.*)$ /$1 break;
proxy_pass http://192.168.18.15:80/;
}
# Proxy ESP32 #2 (ajuste o IP)
location ^~ /d/2/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
rewrite ^/d/2/(.*)$ /$1 break;
proxy_pass http://192.168.18.33:80/;
}
# Rota exata /d/2/send -> envia POST direto ao ESP32-2
location = /d/2/send {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.18.33:80/send;
}
# Rota exata /send (quando o HTML usa action="/send")
location = /send {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.18.33:80/send;
}
location ~* \.(js|css|png|jpg|ico)$ {
add_header Cache-Control "public, max-age=3600";
}
}
Ative o site e reinicie o Nginx:
sudo ln -s /etc/nginx/sites-available/hubesp32 /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Teste localmente:
curl -I http://127.0.0.1/
Se tudo estiver correto, você verá algo como:
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
☁️ 4. Instalação e teste do Cloudflare Quick Tunnel
Agora instalaremos o Cloudflared, cliente oficial da Cloudflare. Uma atenção especial deve ser tomada para a questão da arquitetura do sistema para selecionar a versão adequada do Cloudflare.
Para o Linux com arquitetura amd64:
sudo mkdir -p /etc/cloudflared
cd /usr/local/bin
sudo wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
sudo chmod +x cloudflared
Para o Raspberry Pi na arquitetura arm64:
cd ~
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -o cloudflared
chmod +x cloudflared
sudo mv cloudflared /usr/local/bin/cloudflared
Teste o funcionamento do túnel localmente:
cloudflared tunnel --url http://127.0.0.1:80
Após alguns segundos, deve aparecer uma URL pública no formato a seguir. Não dê CTRL-C e procure a URL subindo a tela do terminal.
https://valor-nomeado-aleatorio.trycloudflare.com
Acesse essa URL em outro dispositivo conectado à Internet.
Se a página “Hub ESP32 ativo!” aparecer, o túnel está funcionando corretamente.
Pressione Ctrl+C para encerrar o teste.
⚙️ 5. Configuração do serviço systemd para o Cloudflared
Crie o arquivo de serviço:
sudo nano /etc/systemd/system/cloudflared.service
Conteúdo:
[Unit] Description=cloudflared (Quick Tunnel - 127.0.0.1:80) After=network.target [Service] ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --url http://127.0.0.1:80 Restart=always RestartSec=10 User=root [Install] WantedBy=multi-user.target
Ative e inicie o serviço:
sudo systemctl daemon-reload sudo systemctl enable --now cloudflared
Verifique o status:
sudo systemctl status cloudflared
E veja a URL pública gerada:
journalctl -u cloudflared -n 30 --no-pager | grep trycloudflare
🧩 6. Definição de Credenciais do Hub
Como o Hub vai ser acessado direto pela Internet é fundamental definir credenciais para acesso ao Hub. Estamos definindo a autenticação BASIC mas dentro do contexto https e do tunnel seguro da Cloudflare.
As credenciais do Hub (usuário/senha) são definidas no arquivo de autenticação usado pelo Nginx:
/etc/nginx/.htpasswd
Esse arquivo é criado com o comando (exemplo):
sudo htpasswd -c /etc/nginx/.htpasswd <nome_usuario>
Ele grava o usuário e uma hash da senha.
Se este comando htpasswd não estiver disponível, será necessário instalar o pacote a seguir:
sudo apt update
sudo apt install apache2-utils
Para adicionar outro usuário, sem apagar o existente:
sudo htpasswd /etc/nginx/.htpasswd <novo_usuario>
Para ver os usuários registrados:
cat /etc/nginx/.htpasswd
(⚠️ vai mostrar apenas as hashes — não há como recuperar as senhas originais)
Essas credenciais são referenciadas dentro da configuração do Nginx:
Para ver o arquivo:
cat /etc/nginx/sites-available/hubesp32
Observe as linhas a seguir no arquivo:
auth_basic "Hub ESP32";
auth_basic_user_file /etc/nginx/.htpasswd;
Ou seja:
Com essa configuração, o Hub ESP32 já estará acessível via Internet por meio de uma URL pública temporária — e o túnel será recriado automaticamente em cada inicialização do sistema.
No próximo passo, implementaremos a automação completa com notificações via ntfy.sh, para que o administrador receba a nova URL pública a cada boot sem precisar consultar logs manualmente.
O Quick Tunnel oferece acesso instantâneo à rede local, mas sua URL muda a cada inicialização.
Para evitar a necessidade de consultar manualmente os logs, criaremos um serviço auxiliar (cf-url-notify) que:
Passo 1 : Criar o serviço cloudflared.service
Isso vai deixar o Quick Tunnel subindo automaticamente no boot:
sudo tee /etc/systemd/system/cloudflared.service >/dev/null <<'EOF' [Unit] Description=cloudflared (Quick Tunnel - 127.0.0.1:80) After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/cloudflared tunnel --url http://127.0.0.1:80 Restart=always RestartSec=5 User=root [Install] WantedBy=multi-user.target EOF
Recarregue o systemd e já habilite o serviço:
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared.service
sudo systemctl status cloudflared.service --no-pager -n 20
Se o status mostrar active (running) e no log aparecer uma URL https://...trycloudflare.com, o túnel está ok.
Passo 2: Criando o script de monitoramento e notificação
Crie o arquivo:
sudo nano /usr/local/bin/cf-url-notify.sh
Conteúdo completo:
#!/bin/bash
# cf-url-notify.sh - Monitora logs do Cloudflared e envia a URL pública via ntfy.sh
# Autor: Dailton de Oliveira Menezes (projeto Hub ESP32 com Cloudflare Tunnel)
set -e
TOPIC="dom_07c2_07e9_alerts" # Tópico de destino no ntfy.sh
SERVICE="cloudflared.service"
TMPFILE="/tmp/cf_url_current"
NTFY_URL="https://ntfy.sh/${TOPIC}"
echo "[cf-url-notify] Aguardando URL pública do serviço ${SERVICE} (boot atual)..."
# Aguarda até o Cloudflared gerar a URL
URL=""
for i in {1..60}; do
URL=$(journalctl -u ${SERVICE} -b --no-pager | grep -o 'https://[a-zA-Z0-9.-]*\.trycloudflare\.com' | tail -n1)
if [ -n "$URL" ]; then
echo "[cf-url-notify] URL detectada: $URL"
break
fi
sleep 5
done
if [ -z "$URL" ]; then
echo "[cf-url-notify] Nenhuma URL detectada após o tempo limite."
exit 1
fi
# Evita reenvio da mesma URL
if [ -f "$TMPFILE" ] && grep -q "$URL" "$TMPFILE"; then
echo "[cf-url-notify] Mesma URL já enviada neste boot: $URL (nada a fazer)"
exit 0
fi
# Testa a disponibilidade da URL
for i in {1..10}; do
if curl -s --max-time 3 -I "$URL" | grep -q "200"; then
echo "[cf-url-notify] URL acessível."
break
fi
echo "[cf-url-notify] Aguardando URL ficar acessível..."
sleep 10
done
# Envia notificação
MESSAGE="Nova URL pública do Hub ESP32 disponível:"
curl -s -X POST \
-H "Title: 🌐 Hub ESP32 Online" \
-H "Priority: high" \
-H "Tags: globe_with_meridians,rocket" \
-d "${MESSAGE} ${URL}" \
"${NTFY_URL}"
echo "$URL" > "$TMPFILE"
echo "[cf-url-notify] Notificação enviada: $URL"
Permita execução:
sudo chmod +x /usr/local/bin/cf-url-notify.sh
Passo 3: Criando o serviço systemd cf-url-notify
Crie o arquivo de serviço:
sudo nano /etc/systemd/system/cf-url-notify.service
Conteúdo:
[Unit] Description=Enviar URL do Quick Tunnel para ntfy.sh After=cloudflared.service Requires=cloudflared.service [Service] ExecStart=/usr/local/bin/cf-url-notify.sh Type=oneshot User=root RemainAfterExit=true [Install] WantedBy=multi-user.target
Passo 4: Ativando e testando o serviço
Ative e rode manualmente para testar:
sudo systemctl daemon-reload
sudo systemctl enable cf-url-notify.service
sudo systemctl start cf-url-notify.service
Verifique o log:
journalctl -u cf-url-notify.service -n 30 --no-pager
Você deverá ver algo como:
[cf-url-notify] Aguardando URL pública do serviço cloudflared (boot atual)...
[cf-url-notify] URL detectada: https://lucky-wavey-puma.trycloudflare.com
[cf-url-notify] URL acessível.
[cf-url-notify] Notificação enviada: https://lucky-wavey-puma.trycloudflare.com
E pouco depois, a notificação deve aparecer no aplicativo ou painel web do ntfy.sh.
Passo 5: Teste completo após reboot
Por fim, teste a automação completa:
sudo reboot
Após o sistema reiniciar:
Com essa automação, o ambiente se torna autônomo e resiliente, garantindo que o acesso remoto esteja sempre disponível e notificado — mesmo após quedas de energia, reboots ou mudanças de IP.
Validação e Testes do Sistema
Com todos os componentes devidamente configurados — Nginx, Cloudflared, e o serviço de notificação automática via ntfy.sh —, é hora de realizar uma sequência de testes para validar o comportamento do sistema e confirmar que ele está pronto para operação contínua.
🧠 Objetivo da validação
A validação garante que:
🧩 1. Verificação do servidor web local
Primeiro, verifique se o Nginx está respondendo normalmente na rede local:
curl http://127.0.0.1
Deve retornar o conteúdo da página inicial criada anteriormente:
<h2>Hub ESP32 ativo!</h2>
Também é possível acessar pelo navegador de outro dispositivo conectado à mesma rede local:
http://<IP_DO_LINUX_LOCAL>
Exemplo:
http://192.168.18.18
Se a página aparecer corretamente, o servidor Nginx está funcional.
☁️ 2. Verificação do túnel Cloudflare
Liste os logs recentes do serviço Cloudflared:
journalctl -u cloudflared -n 30 --no-pager
Procure por uma linha semelhante a:
INF | https://josh-commerce-ann-humidity.trycloudflare.com
Acesse essa URL em outro dispositivo conectado à Internet.
Se a página “Hub ESP32 ativo!” for exibida, o túnel está operando normalmente.
📬 3. Verificação da notificação automática (cf-url-notify)
Agora verifique se o serviço de notificação foi executado corretamente:
journalctl -u cf-url-notify.service -n 20 --no-pager
A saída esperada será algo como:
[cf-url-notify] URL detectada: https://josh-commerce-ann-humidity.trycloudflare.com
[cf-url-notify] URL acessível.
[cf-url-notify] Notificação enviada: https://josh-commerce-ann-humidity.trycloudflare.com
Logo após, uma mensagem deverá aparecer no aplicativo ntfy.sh, contendo o link direto para o Hub.
🔁 4. Teste de persistência após reboot
Para garantir o funcionamento contínuo, reinicie o sistema:
sudo reboot
Após o reinício:
A mudança da URL confirma que o Quick Tunnel foi recriado e o serviço cf-url-notify executou com sucesso.
⚙️ 5. Teste de reconexão de rede
Este teste simula uma interrupção de Internet:
sudo journalctl -u cloudflared -n 20 --no-pager
O serviço Cloudflared deve reativar automaticamente a conexão com o túnel, sem necessidade de reiniciar o sistema.
🔍 6. Dicas de diagnóstico
| Problema observado | Possível causa | Ação recomendada |
| ❌ Sem resposta do túnel público | Cloudflared não inicializou corretamente | Verificar systemctl status cloudflared |
| 🔕 Notificação não chegou ao ntfy.sh | Falha de rede ou token incorreto | Testar manualmente com curl e revisar tópico do ntfy |
| 🌐 Página do Hub inacessível | Erro no Nginx ou conflito de porta | Verificar sudo nginx -t e logs em /var/log/nginx/error.log |
| 🚫 URL antiga enviada | Script cf-url-notify.sh não detectou novo boot | Verificar data e conteúdo de /tmp/cf_url_current |
🧾 7. Resultado esperado
Após esses testes, o comportamento esperado do sistema é:
Para adicionar mais ESP32 ao Hub, basicamente, será necessário alterar os arquivos de configuração do Hub e o index.html.
Para ver o arquivo do Hub:
cat /etc/nginx/sites-available/hubesp32
Padrão de nomenclatura (recomendado)
Caminhos estáveis no Hub:
https://<URL_PUBLICA>/d/1/ → encaminha para http://192.168.18.15/
https://<URL_PUBLICA>/d/2/ → encaminha para http://192.168.18.33/
(adicione quantos precisar: /d/3/, /d/4/ …)
Mantemos o sufixo / no final para facilitar “caminhos relativos” das páginas do ESP.
Dica: fixe IPs dos ESP32 via DHCP reservation no seu roteador para não perder o mapeamento.
Configuração do Nginx (reverse proxy + WebSocket)
Edite o arquivo do site do Hub:
sudo nano /etc/nginx/sites-available/hubesp32
Dentro do bloco server { ... }, adicione um location por ESP
Exemplo para ESP3 (IP local 192.168.18.50), adicione o bloco a seguir ao arquivo :
# Proxy ESP32 #3 (ajuste o IP)
location ^~ /d/3/ {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
rewrite ^/d/3/(.*)$ /$1 break;
proxy_pass http://192.168.18.50:80/;
}
Altere o index.html para adicionar ao html:
sudo nano /var/www/hubesp32/index.html
Adicione a referência ao novo ESP3, algo do tipo:
<!-- ESP3 -->
<a class="card" href="/d/3/">Nova Aplicação — ESP03</a>
Teste e recarregue:
sudo nginx -t
sudo systemctl reload nginx
Neste ponto a referência ao novo ESP3 já estará disponível.
Suporte a outras rotas que o ESP possa ter
Como a aplicação o ESP está sob um PROXY, uma atenção especial deve ser tomada para outras rotas que por acaso a aplicação utiliza.
Se a aplicação do ESP faz, por exemplo, fetch('/send', {method: 'POST', ...}) e o ESP está sob o PROXY, a requisição vai ser passada para o PROXY e não para o ESP dando o erro 404 ou 405 porque o servidor web do Hub só conhece a entrada de cada ESP. Portanto, precisamos criar uma regra adicional para cada requisição que o ESP envie informando ao PROXY para devolver a requisição para o ESP.
No nosso exemplo, a aplicação ESP2 envia a requisição /send para o envio da notificação e por isso criamos uma regra adicional para garantir que a requisição retorne do PROXY para o ESP. Veja como ficou o bloco:
# Rota exata /send (quando o HTML usa action="/send")
location = /send {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.18.33:80/send;
}
Boas práticas e pegadinhas
Caminhos relativos no ESP: com proxy_pass http://IP/; o ESP “acha” que está na raiz.
Evite hardcode de caminhos absolutos; prefira relativos.
WebSocket no ESP: padronize caminho (/ws) e não force wss:// no código do ESP; deixe o Nginx/Cloudflare fazer TLS.
CORS: como o Hub e o proxy usam o mesmo host/porta, normalmente não precisa de CORS.
Se for chamar APIs externas, aí sim ajuste CORS no Nginx ou no fetch.
Tempo de leitura: dashboards com SSE/WS podem precisar de proxy_read_timeout 3600s.
IPs fixos: reserve IP dos ESP32 no DHCP para que /espN/ nunca quebre.
Checklist para “mais um ESP”
Fixe o IP do novo ESP (ex.: 192.168.18.53);
Duplique um bloco location /d/x/ { ... } no /etc/nginx/sites-available/hubesp32 trocando IP e caminho;
sudo nginx -t && sudo systemctl reload nginx;
Adicione o card correspondente em /var/www/hubesp32/index.html;
Teste curl -I http://127.0.0.1/d/x/ e, por fim, pela URL pública do Cloudflare.
Checagens rápidas (sanity checks)
A) Nginx local
# Teste de sintaxe sudo nginx -t # Está ativo? systemctl is-active nginx && systemctl is-enabled nginx # Responde na 80? curl -I --max-time 3 http://127.0.0.1/
B) Cloudflared (Quick Tunnel)
# Status do serviço sudo systemctl status cloudflared --no-pager -n 30 # Últimas linhas de log (boot atual) journalctl -b -u cloudflared -n 80 --no-pager # Ver URL pública atual (a partir do log) journalctl -b -u cloudflared --no-pager \ | grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' \ | tail -1
C) Proxies para ESPs
# Deve retornar HTTP 200/401/301 (dependendo do seu setup)
curl -I --max-time 3 http://127.0.0.1/d/1/
curl -I --max-time 3 http://127.0.0.1/d/2/
Se o Hub tem Basic Auth, use -u usuario:senha no curl.
Logs úteis (onde olhar e o que procurar)
A) Nginx
# Acessos sudo tail -n 200 /var/log/nginx/access.log # Erros (proxy, upstream, WebSocket, etc.) sudo tail -n 200 /var/log/nginx/error.log # Filtrar um ESP específico sudo grep '/esp1/' -n /var/log/nginx/access.log | tail -n 50
B) Cloudflared
journalctl -b -u cloudflared -n 200 --no-pager
C) Automação da URL (timer opcional, se você usa)
systemctl list-timers | grep cf-url-notify
journalctl -u cf-url-notify.service -n 80 --no-pager
Browser (DevTools)
Network → WS: confirme que o upgrade do WebSocket virou 101 Switching Protocols.
Frames: verifique se chegam mensagens snapshot/update.
Console: erros de CORS, Mixed Content, TypeError: ws is undefined, etc.
Erros comuns e correções
|
Sintoma / Log |
Causa provável |
Correção |
|
405 Not Allowed vindo do Hub |
Método não roteado no proxy encurtador (ex.: POST /esp2/send) |
Crie location = /esp2/send { proxy_pass http://IP/send; } ou deixe o ESP aceitar o caminho “sem encurtador” via location /esp2/ |
|
502/504 no Nginx |
ESP desligado/fora do ar; IP mudou; timeout curto |
Verifique IP (reserva DHCP), aumente proxy_read_timeout e proxy_send_timeout |
|
WS não conecta (fica “pending”) |
Upgrade/Connection não propagados |
Garanta no server do Nginx: proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade, proxy_set_header Connection $connection_upgrade |
|
WS fecha após alguns segundos |
Timeout do proxy |
Aumente proxy_read_timeout 3600s; |
|
Mixed Content no navegador |
Página HTTPS chamando ws:// explícito |
No front use const proto = location.protocol===’https:’?’wss’:’ws’ |
|
401 Unauthorized no Hub |
Basic Auth ativo |
No teste use curl -u user:senha … (ou desative na fase de debug) |
|
URL pública “antiga” no push |
Captura antes do Cloudflared “subir” |
Espere alguns segundos e leia a última ocorrência via `journalctl … |
Sequência segura de restart
# 1) Nginx sudo nginx -t && sudo systemctl restart nginx # 2) Cloudflared sudo systemctl restart cloudflared sleep 5 journalctl -b -u cloudflared --no-pager \ | grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' | tail -1 # 3) ESP(s) — se necessário # (reinicie o dispositivo físico ou via OTA)
Testes de ponta a ponta
A) Pela URL pública
PUB=$(journalctl -b -u cloudflared --no-pager \ | grep -Eo 'https://[a-z0-9-]+\.trycloudflare\.com' | tail -1) # Hub (página inicial) curl -I --max-time 6 "$PUB/" # ESP1 via Hub curl -I --max-time 6 "$PUB/esp1/" # Envio POST (se tiver encurtador) curl -i --max-time 6 -X POST "$PUB/esp2/send" -d 't=1'
B) WebSocket pelo navegador
Abra https://URL_PUBLICA/d/1/, pressione F12 > Network > WS, confirme 101 e frames chegando.
Hardening rápido (opcional, mas recomendado)
Basic Auth já configurado? Trocar/sincronizar credenciais:
# (se estiver usando htpasswd)
sudo htpasswd /etc/nginx/.htpasswd seu_usuario
sudo systemctl reload nginx
Checklist “deu ruim, e agora?”
O Hub abre em http://127.0.0.1/?
A URL pública aparece no journalctl -b -u cloudflared?
O card do ESP abre localmente (curl -I http://127.0.0.1/esp1/)?
O mesmo card abre pela URL pública?
Para WS: status 101 e frames?
POSTs retornam 2xx?
Logs do Nginx/Cloudflared mostram erro? Qual?
IPs dos ESPs estão fixos e respondendo na LAN?

Figura 5 – Tela do Hub no Desktop

Figura 6 – Tela do Hub no Celular

Figura 7 – Tela Simples do Hub no Celular

Figura 8 – Tela App1 do Hub no Desktop

Figura 9 – Tela App1 do Hub no Celular

Figura 10 – Tela App2 do Hub no Desktop

Figura 11 – Tela App2 do Hub no Celular

Figura 12 – Tela Web do Serviço de Notificação Ntfy.sh

Figura 13- Tela do App do Serviço de Notificação Ntfy.sh
meu-linux.local em vez de IP.Este projeto buscou demonstrar que é possível construir uma infraestrutura segura e totalmente automatizada para hospedar dashboards e serviços locais de IoT sem depender de IPs válidos na Internet, ou domínio registrado, ou roteamento avançado ou conta cadastrada na Cloudflare com fornecimento de cartão de crédito. A combinação entre o Quick Tunnel da Cloudflare, o servidor Nginx e o sistema de notificações ntfy.sh fornece uma base extremamente flexível, especialmente para ambientes dinâmicos com endereços públicos variáveis.
Embora o Quick Tunnel não ofereça persistência de endereço como o Named Tunnel, o uso de um serviço no Linux para monitorar a criação da URL e notificar automaticamente o administrador compensa essa limitação com eficácia. O resultado é um ambiente totalmente funcional, gratuito, que pode ser inicializado, atualizado e acessado remotamente com mínimo esforço.
Além disso, a estrutura modular adotada — com o hub local servindo como ponto central de acesso aos módulos ESP32 — facilita futuras expansões, como a integração de novos dispositivos, sensores ou painéis de controle. Essa arquitetura distribui a complexidade, mas mantém o controle dentro da rede local.
Buscamos também demonstrar que os mesmos princípios poderão ser aplicados, sem alterações significativas, para o Raspberry Pi 4. Isso demonstra a portabilidade e a escalabilidade da solução, tornando-a ideal para laboratórios, sistemas de automação residencial e projetos didáticos.
Por fim, o projeto mostra como tecnologias de código aberto podem ser combinadas para entregar resultados profissionais. Espera-se que este guia sirva de base para novas ideias e inspire a criação de soluções ainda mais inteligentes e acessíveis.
Com poucos recursos e um pouco de curiosidade, é possível transformar um simples servidor local em uma janela segura para o acesso de qualquer lugar do mundo.
|
|
A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.
Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!