Pular para o conteúdo
, , , , ,

Redes no Firecracker: Configurando TAP, NAT e Internet para seu Nano-Lambda [pt.3]

Aprenda a configurar TAP, NAT e iptables no Firecracker. Dê internet às suas microVMs para criar um validador de URLs real, mantendo isolamento total ao bloquear o acesso à rede local.

Avatar de DK
DKTrabalha com Linux e Unix a mais de 23 anos e possui as certificações LPI 3, RHCE, AIX e VIO.

08 dez, 2025
12 min de leitura

Atualizado em 08/06/2026

Série Firecracker:


No artigo anterior, construímos um nano-Lambda que executa funções Python em microVMs isoladas. Gerou QR Codes lindos. Mas tinha uma limitação: a VM era uma ilha. Sem rede, sem internet, sem comunicação com o mundo exterior.

Hoje vamos resolver isso. Vamos dar internet pro nosso nano-Lambda.

E pra provar que funciona de verdade, vamos construir algo útil: um validador de URLs. Você passa uma lista de sites, a microVM acorda, dispara requests em paralelo, e retorna um JSON com o status de cada um. Simples, prático, e impossível de fazer sem rede.

Por que rede em microVMs é diferente?

Quando você roda um container Docker, a rede funciona sozinha. Docker cria bridges, configura NAT, gerencia IPs… tudo automático. Você nem pensa nisso.

Com Firecracker, você pensa. Muito.

Firecracker é minimalista até na rede. Ele suporta um único tipo de dispositivo de rede: virtio-net. E ele espera que você, configure tudo do lado do host. O Firecracker só conecta a VM a uma interface TAP que você criou.

Isso parece trabalhoso (e é), mas tem um lado bom: você entende exatamente o que tá acontecendo. Sem mágica, sem camadas escondidas. E isso é ótimo pra segurança: você controla cada pacote que entra e sai.

O que vamos construir

Nosso objetivo final:

  1. Criar uma interface TAP no host
  2. Configurar NAT pra VM acessar a internet
  3. Configurar DNS dentro da VM
  4. Atualizar o nano-Lambda pra suportar rede
  5. Criar um validador de URLs que funciona de verdade

Bônus: vamos bloquear acesso da VM à rede local, permitindo apenas internet pública. Isolamento de verdade.

Conceitos rápidos: TAP, Bridge e NAT

Antes de meter a mão na massa, um glossário rápido:

TAP: É uma interface de rede virtual. Pensa nela como um “cabo de rede imaginário”. De um lado, a microVM acha que tá conectada numa placa de rede real. Do outro lado, o host Linux vê uma interface de rede normal que pode rotear, filtrar, fazer NAT…

Bridge: Conecta várias interfaces de rede como se fossem um switch. Útil quando você quer que várias VMs conversem entre si.

NAT (Network Address Translation): Permite que a VM use o IP do host pra acessar a internet. A VM tem um IP privado (tipo 172.16.0.2), mas quando ela acessa google.com, o pacote sai com o IP público do host.

Pra nosso caso, vamos usar a configuração mais simples: TAP + NAT. Uma interface TAP conectada diretamente ao host, com masquerading pra internet.

Diagrama de rede do Firecracker mostrando o fluxo da MicroVM para internet e bloqueio da rede local

O diagrama mostra exatamente o que vamos construir: a App Python dentro da MicroVM se comunica via eth0 (172.16.0.2), que conecta na interface tap0 do host. O firewall faz NAT pra internet pública, mas bloqueia acesso à rede local (192.168.x.x). Isolamento de verdade.

Nota sobre firewalls: Neste artigo uso firewalld (padrão no Fedora). Se você usa Ubuntu, o sistema usa iptables. Os scripts do repositório detectam automaticamente qual firewall está ativo e usam os comandos corretos. Se preferir seguir com iptables, os conceitos são os mesmos, só a sintaxe muda.

Passo 1: Criando a interface TAP

Primeiro, precisamos criar uma interface TAP. Isso é feito com o comando ip tuntap:

# Cria a interface TAP
sudo ip tuntap add dev tap0 mode tap

# Atribui um IP pro lado do host
sudo ip addr add 172.16.0.1/24 dev tap0

# Sobe a interface
sudo ip link set tap0 up

Pronto. Agora temos uma interface tap0 com IP 172.16.0.1. A VM vai usar 172.16.0.2.

Pra verificar:

ip addr show tap0

Deve mostrar algo como:

tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 ...
    inet 172.16.0.1/24 scope global tap0

Passo 2: Configurando NAT no host

Agora precisamos que o host faça NAT, ou seja, traduza os pacotes da VM pro IP do host quando eles saem pra internet.

# Habilita IP forwarding (permite que o host roteie pacotes)
sudo sysctl -w net.ipv4.ip_forward=1

# Adiciona a interface TAP à zona trusted (permite todo tráfego dela)
sudo firewall-cmd --zone=trusted --add-interface=tap0

# Habilita masquerading (NAT) pra saída
sudo firewall-cmd --add-masquerade

O firewalld cuida do resto. A zona trusted permite todo tráfego da TAP, e o masquerade faz o NAT automático.

Dica: Pra descobrir sua interface de saída, use ip route | grep default. A interface aparece depois de “dev”.

Bloqueando acesso à rede local (segurança)

Aqui tá o ouro do isolamento. Queremos que a VM acesse a internet pública, mas NÃO a sua rede local. Imagina se alguém mandar um código malicioso que tenta escanear sua rede interna?

Tem um detalhe que pega muita gente aqui. O tráfego que sai da VM passa por dois caminhos diferentes no firewall:

  • O que é destinado ao próprio host (o IP do seu Fedora na rede local) cai na cadeia INPUT.
  • O que a VM tenta rotear pra outras máquinas da rede local cai na cadeia FORWARD.

As rich rules da zona trusted só filtram a cadeia INPUT. Ou seja, elas bloqueiam a VM de falar com o host, mas deixam ela alcançar todo o resto da sua rede. Pra fechar o buraco de verdade, a gente precisa das duas coisas:

# Bloqueia a VM de falar com o proprio host (cadeia INPUT)
sudo firewall-cmd --zone=trusted --add-rich-rule='rule family=ipv4 source address=172.16.0.0/24 destination address=10.0.0.0/8 drop'
sudo firewall-cmd --zone=trusted --add-rich-rule='rule family=ipv4 source address=172.16.0.0/24 destination address=192.168.0.0/16 drop'

# Bloqueia a VM de rotear pra rede local (cadeia FORWARD)
# Libera primeiro a propria subnet da VM, depois derruba as redes privadas
sudo firewall-cmd --direct --add-rule ipv4 filter FORWARD 0 -i tap0 -d 172.16.0.0/24 -j ACCEPT
sudo firewall-cmd --direct --add-rule ipv4 filter FORWARD 1 -i tap0 -d 10.0.0.0/8 -j DROP
sudo firewall-cmd --direct --add-rule ipv4 filter FORWARD 1 -i tap0 -d 172.16.0.0/12 -j DROP
sudo firewall-cmd --direct --add-rule ipv4 filter FORWARD 1 -i tap0 -d 192.168.0.0/16 -j DROP

Agora a VM pode acessar google.com, mas não consegue tocar em 192.168.1.1 (seu roteador), nem em nenhuma outra máquina da sua rede, nem no próprio host.

Por que não bastam as rich rules? Se você usar só elas, a VM continua alcançando todo mundo na rede local, menos o host. O bloqueio fica falso. As regras na cadeia FORWARD são o que de fato impede a VM de escanear a sua rede.

Versão com iptables (Ubuntu) Se você usa Ubuntu ou prefere iptables: “`bash # Habilita IP forwarding sudo sysctl -w net.ipv4.ip_forward=1 # Configura masquerading (NAT) – substitua eth0 pela sua interface sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE # Permite tráfego da TAP pro mundo sudo iptables -A FORWARD -i tap0 -o eth0 -j ACCEPT sudo iptables -A FORWARD -i eth0 -o tap0 -m state –state RELATED,ESTABLISHED -j ACCEPT # Bloqueia acesso a redes privadas sudo iptables -I FORWARD -i tap0 -d 10.0.0.0/8 -j DROP sudo iptables -I FORWARD -i tap0 -d 172.16.0.0/12 -j DROP sudo iptables -I FORWARD -i tap0 -d 192.168.0.0/16 -j DROP # Mas permite a própria subnet da VM sudo iptables -I FORWARD -i tap0 -d 172.16.0.0/24 -j ACCEPT “`

Passo 3: Configurando o Firecracker pra usar a TAP

Quando iniciar o Firecracker, precisamos dizer pra ele usar a interface TAP. Isso é feito via API:

curl --unix-socket /tmp/firecracker.socket -X PUT \
  "http://localhost/network-interfaces/eth0" \
  -H "Content-Type: application/json" \
  -d '{
    "iface_id": "eth0",
    "guest_mac": "AA:FC:00:00:00:01",
    "host_dev_name": "tap0"
  }'

Isso conecta a interface eth0 dentro da VM à interface tap0 do host.

Passo 4: Configurando a rede dentro da VM

Agora a parte que pega muita gente: configurar a rede DENTRO da microVM. O Firecracker só conecta o “cabo”. Quem configura IP, gateway e DNS é o sistema dentro da VM.

Vamos ver o que precisa mudar no rootfs. No Passo 5 o script de build faz tudo isso automático, então aqui a ideia é entender o que acontece por baixo. Pra espiar, monta a imagem do artigo anterior:

# Cria um diretório temporário para montar
sudo mkdir -p /mnt/rootfs

# Monta a imagem ext4 do artigo anterior
sudo mount rootfs-python.ext4 /mnt/rootfs

A primeira coisa que muda é o DNS. Sem ele, ping google.com falha mesmo com a rede funcionando, porque a VM não sabe onde perguntar “qual o IP de google.com?”. Como o arquivo fica num diretório montado como root, a gente escreve com sudo tee. Um sudo cat > não funciona aqui, porque quem faz o redirecionamento > é o seu shell normal, fora do sudo, e ele esbarra na permissão:

sudo tee /mnt/rootfs/etc/resolv.conf > /dev/null << 'EOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF

Atualizando o script de boot

A segunda coisa que muda é a interface em si. Em vez de depender de um serviço de rede, a gente sobe a eth0 na unha, direto no script de boot, antes de rodar a função. Atualize o /mnt/rootfs/run-function.sh:

#!/bin/sh
echo "Configurando rede"

# Sobe a interface de rede
ip link set eth0 up
ip addr add 172.16.0.2/24 dev eth0
ip route add default via 172.16.0.1

echo "Testando conectividade..."
ping -c 1 8.8.8.8 > /dev/null 2>&1 && echo "Internet OK" || echo "Sem internet"

echo ""
echo "nano-Lambda executando..."
echo ""

if [ -f /functions/handler.py ]; then
    cd /functions
    python3 handler.py
    RETVAL=$?
    echo ""
    echo "Execução finalizada (exit: $RETVAL)"
else
    echo "ERRO: handler.py não encontrado"
fi

echo ""
sync
sleep 1
# reboot -f (nao poweroff): com reboot=k nos boot_args o kernel faz um reset
# que o Firecracker intercepta e encerra na hora. poweroff -f deixaria o
# Firecracker travado ate o timeout.
reboot -f

Esses ip addr add e ip route add no boot são o que de fato dão rede pra VM. Não precisa de /etc/network/interfaces nem de serviço de rede nenhum.

Depois de espiar, desmonte o rootfs:

# Desmonta o rootfs
sudo umount /mnt/rootfs

Passo 5: O build-rootfs-network.sh

O build-rootfs-network.sh é o build do artigo anterior com as configurações de rede já embutidas. Ele monta o resolv.conf, o run-function.sh com o setup de rede, e instala os pacotes de rede no rootfs:

# Adiciona pacotes de rede
apk add --root /rootfs --no-cache \
    python3 \
    py3-requests \
    py3-urllib3 \
    ca-certificates

O ca-certificates é importante pra HTTPS funcionar. Sem ele, requests.get('https://google.com') vai reclamar de certificado inválido.

Esse build gera um arquivo novo, o rootfs-network.ext4. O rootfs-python.ext4 do artigo anterior fica intacto, ele não é usado aqui. Pra construir:

sudo ./build-rootfs-network.sh

Importante: Esse rootfs precisa das dependências de rede (requests, ca-certificates). Se você tentar reusar o rootfs-python.ext4 do artigo anterior, a função falha com ModuleNotFoundError. Use sempre o rootfs-network.ext4.

Passo 6: O Validador de URLs

Agora a função que vai provar que tudo funciona. Salve como exemplo-validador/handler.py:

#!/usr/bin/env python3
"""
Função nano-Lambda: Validador de URLs

Lê uma lista de URLs de /functions/input.txt e retorna o status HTTP de cada uma.
Usa requests em paralelo pra performance.
"""

import json
import sys
import ssl
import socket
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    import urllib.request
    import urllib.error
    HAS_REQUESTS = False

def check_url_requests(url):
    """Verifica URL usando requests."""
    try:
        resp = requests.head(url, timeout=10, allow_redirects=True)
        return {
            "url": url,
            "status": resp.status_code,
            "ok": resp.ok
        }
    except requests.exceptions.SSLError as e:
        return {"url": url, "status": "SSL_ERROR", "ok": False, "error": str(e)}
    except requests.exceptions.ConnectionError:
        return {"url": url, "status": "CONNECTION_ERROR", "ok": False}
    except requests.exceptions.Timeout:
        return {"url": url, "status": "TIMEOUT", "ok": False}
    except Exception as e:
        return {"url": url, "status": "ERROR", "ok": False, "error": str(e)}

def check_url_urllib(url):
    """Verifica URL usando urllib (fallback)."""
    try:
        req = urllib.request.Request(url, method='HEAD')
        with urllib.request.urlopen(req, timeout=10) as resp:
            return {
                "url": url,
                "status": resp.getcode(),
                "ok": 200 <= resp.getcode() < 400
            }
    except urllib.error.HTTPError as e:
        return {"url": url, "status": e.code, "ok": False}
    except urllib.error.URLError as e:
        return {"url": url, "status": "URL_ERROR", "ok": False, "error": str(e.reason)}
    except Exception as e:
        return {"url": url, "status": "ERROR", "ok": False, "error": str(e)}

def check_ssl_expiry(url):
    """Verifica se o certificado SSL vai expirar em 30 dias."""
    try:
        hostname = url.replace("https://", "").replace("http://", "").split("/")[0]
        context = ssl.create_default_context()

        with socket.create_connection((hostname, 443), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()

        not_after = cert['notAfter']
        expire_date = datetime.strptime(not_after, '%b %d %H:%M:%S %Y %Z')
        days_left = (expire_date - datetime.now()).days

        return {
            "expires": not_after,
            "days_left": days_left,
            "warning": days_left < 30
        }
    except Exception as e:
        return {"error": str(e)}

def main():
    try:
        with open('/functions/input.txt', 'r') as f:
            content = f.read().strip()
    except FileNotFoundError:
        print("ERRO: /functions/input.txt não encontrado")
        sys.exit(1)

    # Parseia URLs (uma por linha ou separadas por vírgula)
    urls = [u.strip() for u in content.replace(',', '\n').split('\n') if u.strip()]

    if not urls:
        print("ERRO: Nenhuma URL fornecida")
        sys.exit(1)

    print(f"Validando {len(urls)} URLs...")

    # Escolhe função de verificação
    check_func = check_url_requests if HAS_REQUESTS else check_url_urllib

    results = []
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = {executor.submit(check_func, url): url for url in urls}

        for future in as_completed(futures):
            result = future.result()

            if result["url"].startswith("https://") and result.get("ok"):
                result["ssl"] = check_ssl_expiry(result["url"])

            results.append(result)
            print(f"  {result['url']}: {result['status']}")

    report = {
        "timestamp": datetime.now().isoformat(),
        "total": len(urls),
        "ok": sum(1 for r in results if r.get("ok")),
        "failed": sum(1 for r in results if not r.get("ok")),
        "results": results
    }

    print("")
    print("JSON_RESULT_START")
    print(json.dumps(report, indent=2))
    print("JSON_RESULT_END")

if __name__ == '__main__':
    main()

Essa função:

  1. Lê URLs do input (uma por linha ou separadas por vírgula)
  2. Verifica cada uma em paralelo usando concurrent.futures
  3. Pra URLs HTTPS, também verifica se o certificado SSL expira em menos de 30 dias
  4. Retorna um JSON estruturado com os resultados

Passo 7: O nano-lambda-network.py

Agora vamos criar a versão com rede do nano-Lambda, o nano-lambda-network.py. Ele é o nano-lambda.py do artigo anterior com duas adições: configurar a rede do host antes de iniciar a VM, e conectar a interface da VM na TAP. O setup_network() automatiza o que fizemos à mão nos Passos 1 e 2.

Cuidado com duplicação de regras: Se você rodar o script 10 vezes, não queremos 10 regras idênticas poluindo o firewall. A solução é verificar se a interface já existe antes de configurar:

def setup_network(self):
    """Configura a interface TAP, o NAT e o isolamento no host."""
    # Evita refazer tudo se a TAP já existe
    if os.path.exists("/sys/class/net/tap0"):
        print("[*] Rede já configurada, pulando setup...")
        return

    print("[*] Configurando rede...")

    subprocess.run(["ip", "tuntap", "add", "dev", "tap0", "mode", "tap"], check=True)
    subprocess.run(["ip", "addr", "add", "172.16.0.1/24", "dev", "tap0"], check=True)
    subprocess.run(["ip", "link", "set", "tap0", "up"], check=True)
    subprocess.run(
        ["sysctl", "-w", "net.ipv4.ip_forward=1"],
        check=True, capture_output=True
    )

    # NAT (firewalld no Fedora)
    subprocess.run(["firewall-cmd", "--zone=trusted", "--add-interface=tap0"], check=False)
    subprocess.run(["firewall-cmd", "--add-masquerade"], check=False)

    # Isolamento: bloqueia a VM de rotear pra rede local (cadeia FORWARD)
    subprocess.run(["firewall-cmd", "--direct", "--add-rule", "ipv4", "filter",
                    "FORWARD", "0", "-i", "tap0", "-d", "172.16.0.0/24", "-j", "ACCEPT"],
                   check=False)
    for net in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"):
        subprocess.run(["firewall-cmd", "--direct", "--add-rule", "ipv4", "filter",
                        "FORWARD", "1", "-i", "tap0", "-d", net, "-j", "DROP"],
                       check=False)

def configure_vm(self):
    """Configura a microVM via API."""

    print("[*] Configurando rede da VM...")
    self._call_api("PUT", "/network-interfaces/eth0", {
        "iface_id": "eth0",
        "guest_mac": "AA:FC:00:00:00:01",
        "host_dev_name": "tap0"
    })

Nota: O script completo no repositório também adiciona as rich rules que bloqueiam o host e detecta automaticamente se o sistema usa firewalld ou iptables. Se você usa Ubuntu, ele usa iptables sem você precisar mudar nada.

Testando!

Com tudo configurado:

# Cria arquivo de input com URLs pra testar
cat > urls.txt << 'EOF'
https://google.com
https://fogonacaixadagua.com.br
https://github.com
https://site-que-nao-existe.xyz
EOF

# Executa o validador
sudo python3 nano-lambda-network.py exemplo-validador/handler.py "$(cat urls.txt)"

Se tudo funcionar, você verá:

==================================================
nano-Lambda Network: Funcao em microVM com internet
==================================================
Funcao: exemplo-validador/handler.py
Input: https://google.com
https://fogonacaixadagua.com.br...

[*] Configurando rede do host...
    Firewall: firewalld
    Rede configurada
[*] Copiando rootfs template...
[*] Montando rootfs temporario...
[*] Copiando funcao: exemplo-validador/handler.py
[*] Iniciando Firecracker...
[*] Configurando kernel...
[*] Configurando rootfs...
[*] Configurando recursos (1 vCPU, 256MB RAM)...
[*] Configurando rede da VM...
[*] Iniciando microVM...
[*] Aguardando execucao (timeout: 60s)...
[*] Limpando...

==================================================
Resultado:
==================================================
{
  "timestamp": "2026-06-09T05:11:42.562082",
  "total": 4,
  "ok": 3,
  "failed": 1,
  "results": [
    {
      "url": "https://site-que-nao-existe.xyz",
      "status": "CONNECTION_ERROR",
      "ok": false
    },
    {
      "url": "https://github.com",
      "status": 200,
      "ok": true,
      "ssl": {
        "expires": "Aug  2 23:59:59 2026 GMT",
        "days_left": 54,
        "warning": false
      }
    },
    {
      "url": "https://google.com",
      "status": 200,
      "ok": true,
      "ssl": {
        "expires": "Aug 10 18:35:20 2026 GMT",
        "days_left": 62,
        "warning": false
      }
    },
    {
      "url": "https://fogonacaixadagua.com.br",
      "status": 200,
      "ok": true,
      "ssl": {
        "expires": "Jul 30 08:46:10 2026 GMT",
        "days_left": 51,
        "warning": false
      }
    }
  ]
}

A execução inteira leva uns 6 segundos. As URLs são checadas em paralelo, então a ordem no resultado pode sair diferente da ordem de entrada. Os campos days_left são calculados na hora, então vão variar conforme a data.

Conferindo o isolamento

A parte de segurança não dá pra ver pelo resultado do validador, então vamos checar direto no firewall. As regras de FORWARD que bloqueiam a rede local têm que estar lá:

sudo firewall-cmd --direct --get-all-rules

Saída esperada:

ipv4 filter FORWARD 0 -i tap0 -d 172.16.0.0/24 -j ACCEPT
ipv4 filter FORWARD 1 -i tap0 -d 10.0.0.0/8 -j DROP
ipv4 filter FORWARD 1 -i tap0 -d 172.16.0.0/12 -j DROP
ipv4 filter FORWARD 1 -i tap0 -d 192.168.0.0/16 -j DROP

Se as três regras DROP estão na lista, a VM consegue sair pra internet, mas não alcança nenhuma máquina da sua rede local.

O que aprendemos

Configurar rede no Firecracker é mais trabalhoso que no Docker, mas você ganha:

  1. Controle total: Você sabe exatamente o que entra e sai
  2. Isolamento real: A VM pode acessar internet mas não sua rede local
  3. Segurança: Código malicioso não consegue escanear sua rede interna

Esse é o tipo de isolamento que faz sentido pra executar código não confiável. A VM pode baixar o que quiser da internet, mas tá completamente bloqueada de acessar qualquer coisa na sua rede.

Troubleshooting

VM não consegue pingar nada:

  • Verifique se net.ipv4.ip_forward=1 está ativo: sysctl net.ipv4.ip_forward
  • Verifique se masquerading está ativo: firewall-cmd --query-masquerade (ou iptables -t nat -L -n no Ubuntu)
  • Verifique se a TAP está na zona trusted: firewall-cmd --zone=trusted --list-interfaces

DNS não funciona (ping IP funciona, ping domínio não):

  • Verifique /etc/resolv.conf dentro da VM
  • Tente com DNS diferente: nameserver 8.8.8.8
  • Em redes corporativas ou VPNs, a porta 53 pode ser bloqueada pra fora. Use o DNS da sua rede local

HTTPS falha com erro de certificado:

  • Verifique se ca-certificates está instalado no rootfs
  • Tente update-ca-certificates dentro da VM

Próximos passos

Agora seu nano-Lambda tem acesso à internet e pode fazer coisas úteis de verdade. Algumas ideias:

  • Web scraper isolado: Baixa páginas, extrai dados, retorna JSON
  • Verificador de APIs: Testa endpoints e mede latência
  • Downloader seguro: Baixa arquivos suspeitos em ambiente isolado

No próximo artigo, vamos explorar snapshots e restore: a tecnologia que permite o Lambda da AWS iniciar em milissegundos. Spoiler: envolve tirar “fotos” da memória da VM.

Até lá!


Este artigo faz parte da série “Firecracker: MicroVMs na Prática”. Todo o código está disponível em GitHub.

Avatar de DK

Comentários

Comentários fechados para visitantes. Entre ou registre-se para comentar.

Ir para