← Blog

· 19 min de leitura #rust#mqtt#iot#unikernel#código aberto

urMQTT: construímos um broker MQTT que é também um sistema operativo

Notas de desenvolvimento sobre o urMQTT, um unikernel escrito em Rust que é, ele próprio, um broker MQTT: um único ELF com TCP/IP próprio, TLS 1.3, broker e SQLite, arrancado a partir de um único ISO.

Os dispositivos instalados em veículos, em parques de estacionamento e em armários de eletrónica têm uma característica em comum: enviam mensagens pequenas, raramente, mas são muitos e estão sempre ligados. Há anos que os ligamos a brokers MQTT e há anos que a mesma pergunta nos incomoda. Porque precisamos de um Linux inteiro, do OpenSSL, do runtime de uma linguagem, de SSH, de um gestor de pacotes e só depois de um broker, para algo tão simples? Cada uma dessas camadas traz as suas próprias vulnerabilidades, consome a sua própria memória e acrescenta mais uma coisa que se pode avariar, e uma frota de dispositivos só precisa de três coisas do servidor: manter muitas ligações quase inativas, autenticar cada dispositivo de forma fiável e transportar as mensagens sem perdas.

Foi assim que nasceu o urMQTT: um unikernel que é um broker MQTT. Kernel, TCP/IP, TLS 1.3, MQTT e uma base de dados integrada num único binário Rust, que arranca a partir de um único ISO. Não há processos, nem chamadas de sistema, nem shell. A primeira plataforma em que é construído e testado é aarch64, e o mesmo código caminha para x86_64, por UEFI e por BIOS clássico, e para hardware real: o objetivo é um único ISO que arranque em todos eles. O projeto é de código aberto, sob licença MIT ou Apache 2.0, e neste texto queremos registar onde chegámos, como está organizado por dentro e o que aprendemos pelo caminho.

O que significa exatamente "unikernel"

Um broker habitual é um programa que corre sobre um sistema operativo. O urMQTT não tem sistema operativo por baixo: arranca sozinho através do firmware UEFI e do bootloader Limine, configura as suas próprias tabelas de páginas, programa o controlador de interrupções e o temporizador, encontra os dispositivos virtio através de PCIe, obtém um endereço por DHCP e escuta num único porto. Tudo o que normalmente seria feito pelo kernel, pelos controladores, pela pilha de rede e pela aplicação é aqui um único ELF que corre ao nível EL1.

A comparação com uma instalação típica fala por si:

Broker típico em Linux urMQTT
Serviços expostos SSH, broker, muitas vezes também um porto de métricas ou de administração apenas TCP 8883 (MQTT sobre TLS 1.3 com mTLS)
Código na imagem kernel, userland, libc, OpenSSL, runtime um único ELF em Rust (mais o SQLite incorporado)
Gestão remota shell, ficheiros de configuração, atualizações de pacotes nenhuma. Uma nova versão é um novo ISO
Memória por ligação inativa quilobytes (buffers de socket, objetos TLS, heap) objetivo ≤ 512 B
Modelo multinúcleo threads e locks ou um único ciclo de eventos uma thread por núcleo, sem estado partilhado, processamento até ao fim
Integridade do código são possíveis mapeamentos simultaneamente graváveis e executáveis W^X, tabelas de páginas bloqueadas, PAC/BTI

As prioridades são, por esta ordem: segurança, escalabilidade, desempenho multinúcleo, pequena pegada e instalação imutável. Sempre que dois objetivos entram em conflito, ganha o que está mais acima nesta lista.

Onde estamos agora

O projeto está numa fase pré-alfa, mas já fechámos dezassete marcos de desenvolvimento, de M0 a M17. Hoje o urMQTT serve MQTT 3.1.1 sobre TLS 1.3 com certificados de cliente obrigatórios e guarda o estado em SQLite num disco em bruto. Em resumo, o que já funciona:

  • Kernel: vetores de exceção próprios com relatórios descodificados, tabelas de páginas próprias com W^X imposto, páginas de guarda, PAC e BTI onde o processador os tem, GICv3 e GICv2, temporizador genérico, SMP até 64 núcleos.
  • Deteção de hardware a partir de DTB ou ACPI, numa única descrição da plataforma, com resultado idêntico a partir de ambas as fontes.
  • Controladores: PCIe ECAM, MSI-X, virtio-net, virtio-blk, virtio-scsi e virtio-rng, e um CSPRNG inicializado com entropia de hardware.
  • Rede: DHCPv4, ARP, ICMP e IPv4 estrito próprios, e um TCP próprio apenas do lado do servidor: SYN cookies, roda de temporizadores hierárquica, NewReno, fusão de ACK, sem estado TIME_WAIT.
  • TLS: apenas TLS 1.3, mTLS obrigatório, identidade do dispositivo a partir do CN ou do SAN URI; o handshake é feito pelo rustls e, depois dele, por uma camada de registos própria com AES-128-GCM e ChaCha20-Poly1305. Retoma de sessão com tickets PSK, lista de revogação, controlo de admissão logo no handshake.
  • MQTT: codec sem cópia, testado com fuzzing e com clientes mosquitto reais; QoS 0 e 1, wildcards, mensagens retidas, testamento (LWT), sessões persistentes, ACL limitada a devices/{id}/#, limites de débito por dispositivo.
  • Armazenamento: SQLite dentro do kernel através de um VFS sobre o disco em bruto, em modo WAL, com verificação de integridade no arranque; registo de dispositivos, revogações, mensagens retidas, sessões persistentes com filas de mensagens para dispositivos offline; por núcleo, um diário de telemetria separado com CRC por registo e recuperação após falha.
  • Operação e supervisão: watchdog para os handlers, verificação de saúde, registo de logs limitado, syslog e statsd por UDP, admissão que sobrevive a tempestades de religações.
  • Entrega: compilações bit a bit idênticas verificadas na CI, ISO e imagem de disco GPT em bruto, assinaturas minisign, instruções para KVM, Proxmox e Hetzner.

Cada passo está assinalado com o marco que o trouxe, e o ficheiro docs/PROGRESS.md no repositório é a única fonte de verdade sobre o que realmente funciona.

Arquitetura em três imagens

Todo o desenho está descrito em docs/ARCHITECTURE.md, e as razões de cada decisão importante estão registadas como ADR em docs/DECISIONS.md. Aqui fica uma visão geral.

Camadas

Camadas do urMQTT: do broker, passando pelo codec MQTT, TLS, TCP e rede, até aos controladores virtio e à camada de arquitetura, tudo num único ELF ao nível EL1.
Camadas do urMQTT: do broker, passando pelo codec MQTT, TLS, TCP e rede, até aos controladores virtio e à camada de arquitetura, tudo num único ELF ao nível EL1.

De cima para baixo: o broker (sessões, árvore de tópicos, QoS, mensagens retidas, testamentos, ACL, limites), o codec MQTT, TLS, TCP, a camada de rede e, por fim, os controladores. Ao lado deste caminho principal está o armazenamento (SQLite e diário de telemetria) e, por baixo de tudo, a gestão de memória, a deteção da plataforma e a configuração, que é convertida em constantes no momento da compilação, juntamente com o certificado da CA, o certificado do servidor e a chave incorporados. O hipervisor com os dispositivos virtio é a única coisa que fica fora da imagem.

Uma thread por núcleo, sem estado partilhado

Modelo de execução: cada núcleo de dados tem o seu próprio ciclo e a sua parte do broker, e o núcleo mais alto executa apenas o SQLite. Os núcleos comunicam exclusivamente através de anéis SPSC.
Modelo de execução: cada núcleo de dados tem o seu próprio ciclo e a sua parte do broker, e o núcleo mais alto executa apenas o SQLite. Os núcleos comunicam exclusivamente através de anéis SPSC.

Cada núcleo de dados executa o seu próprio ciclo, com a sua pilha TCP, os seus estados TLS, uma parte do broker (sessões e árvore de tópicos), a cache do heap e a partição de telemetria. Os núcleos não partilham qualquer estado mutável, apenas anéis limitados e sem locks, um para cada par ordenado de núcleos. O núcleo mais alto executa o SQLite e mais nada.

A placa de rede tem hoje um único par de filas, que pertence ao núcleo 0. Este encaminha cada fluxo MQTT para um dos núcleos de trabalho, segundo o hash Toeplitz do respetivo quádruplo, e envia o que os núcleos de trabalho devolvem. A sessão vive no seu núcleo de origem, determinado pelo hash do identificador do cliente, pelo que a tomada de controlo, os testamentos e a persistência nunca atravessam núcleos. Uma ligação que aterre noutro núcleo mantém aí o seu TCP e o seu TLS, mas reencaminha o MQTT simples para o núcleo de origem, com controlo de fluxo baseado em créditos. Cada mensagem encaminhada é também distribuída pelas restantes partes do broker, cada uma das quais mantém uma réplica das mensagens retidas.

Três regras mantêm este modelo coeso. Processamento até ao fim: um pacote recebido é tratado por inteiro num único núcleo, do TCP, passando pela decifragem TLS no local, pelo parsing de MQTT e pelo encaminhamento, até à resposta cifrada, sem filas de tarefas e sem cópias para buffers da aplicação. Polling adaptativo: sob carga, as filas de receção são consultadas ativamente e, em repouso, o núcleo regressa às interrupções e a WFI; os handlers de interrupção apenas confirmam a interrupção e marcam o trabalho. Watchdog: um tique de 10 ms por núcleo apanha os handlers que excedem o seu orçamento.

A vida de um PUBLISH

O percurso de uma mensagem PUBLISH com QoS 1: do registo TLS na placa de rede, passando pela decifragem, verificação de ACL e encaminhamento, até ao PUBACK fundido com o ACK de TCP num único segmento.
O percurso de uma mensagem PUBLISH com QoS 1: do registo TLS na placa de rede, passando pela decifragem, verificação de ACL e encaminhamento, até ao PUBACK fundido com o ACK de TCP num único segmento.

O dispositivo envia um registo TLS. A placa de rede entrega-o ao ciclo do núcleo através de um descritor, sem cópia. O ciclo verifica o TCP, decifra o TLS no local e faz o parsing do MQTT a partir do buffer emprestado; depois verifica em memória a ACL, o limite de débito e o caminho pela árvore de tópicos. Se outro núcleo estiver interessado nesse tópico, recebe uma mensagem de encaminhamento com uma referência ao conteúdo e o identificador do tópico; a base de dados recebe um registo de telemetria ou a atualização de uma mensagem retida, em lote. A resposta PUBACK é cifrada diretamente para o buffer de saída e fundida com o ACK de TCP, pelo que ao dispositivo chega um único segmento. A mensagem custa a outro núcleo uma cifragem por subscritor, no momento do envio.

Quanto custa uma ligação

O objetivo é no máximo 512 bytes por ligação TLS inativa, com a ambição de chegar a 256. Enquanto a ligação está em repouso, nada é retido para ela em buffers: os dados recebidos são processados diretamente a partir da trama de rede, e os buffers de envio, de registos e de MQTT só existem enquanto houver algo por concluir ou por confirmar. O kernel mede-o ele próprio: contadores exatos do heap, em bytes pedidos e em blocos ocupados, escritos de dez em dez segundos como heap acima do nível base por ligação aberta.

Componente Tamanho Como
Bloco de controlo TCP 96 B flags compactadas, RTT/RTO de 16 bits, posição na roda de temporizadores, sem buffers em repouso
Posição na tabela TCP 4–8 B apenas o índice da ligação, tabela no máximo meio cheia
Ligação e estado de registos TLS 1.3 152 B segredo e sequência por sentido (40 B); chave e IV derivados por registo
Ligação MQTT 48 B identidade e testamento no heap apenas enquanto existirem
Sessão MQTT, forma quente 64 B uma subscrição em linha, identificadores de nós internados
Subscrição devices/<id>/cmd/# 0 nós da árvore guardada de forma relativa ao identificador da sessão, comum a todos os dispositivos

Medido sob HVF com quatro núcleos virtuais, três deles de trabalho, em que cada dispositivo mantém uma ligação mTLS e uma subscrição com QoS 1, incluindo o arredondamento do alocador:

Ligações Bytes por ligação
960 531 B
9 922 494 B
10 197 474 B

O conjunto de tramas de rede é fixo, com cerca de 2 MiB, o que equivale a aproximadamente 200 B por ligação com dez mil ligações e a cerca de 2 B com um milhão. Não conseguimos medir mais do que isso: a rede de utilizador do QEMU no macOS deixou de aceitar ligações às 10 197, aproximadamente no limite de 10 240 descritores do processo QEMU. O valor desce à medida que as partes fixas se repartem por mais ligações, pelo que não cresce com o seu número.

Modelo de segurança

A segurança é a primeira prioridade e ganha todos os conflitos com os restantes objetivos. O modelo tem várias camadas:

  • Um só porto. TCP 8883, apenas TLS 1.3, certificados de cliente obrigatórios assinados pela CA incorporada na imagem. O identificador do cliente MQTT tem de ser igual ao identificador do dispositivo constante do certificado.
  • Sem entrada. Sem shell, sem entrada pela consola, sem SSH, sem API de administração, sem código dinâmico, sem SQL acessível pela rede e sem sistema de ficheiros.
  • Segurança de memória. Rust, com forbid(unsafe_code) nos pacotes de protocolo; cada bloco unsafe tem um comentário // SAFETY: verificado por lint, e perante entrada externa não há panic: o clippy, nos pacotes do caminho de dados, proíbe unwrap, expect, indexação e panic!.
  • Mitigação de exploração. O W^X é imposto pela codificação dos descritores e pelo bit SCTLR_EL1.WXN, e é auditado em cada arranque. Depois do arranque, as tabelas de páginas são só de leitura, sem alias gravável. A metade inferior do espaço de endereçamento está excluída. Cada pilha tem páginas de guarda e as exceções correm numa pilha separada. PAC e BTI são ativados quando o processador os suporta, o que é verificado pelos smoke tests.
  • Resistência ao abuso. SYN cookies; a admissão recusa novas ligações, antes de qualquer criptografia, quando o heap está baixo, quando a base de dados está atrasada, quando há demasiados handshakes em curso por núcleo ou por endereço de origem, ou quando o ritmo de handshakes é demasiado elevado. Limites de débito por dispositivo, filas limitadas, janela TCP ligada à contrapressão do broker, registo de logs limitado, watchdog e verificação de saúde.
  • Imutabilidade. A configuração, a CA e a chave do servidor estão incorporadas na imagem. Alterar seja o que for implica uma nova imagem.

O modelo de ameaças está registado em docs/SECURITY.md.

Armazenamento sem sistema de ficheiros

Os dados vivem num disco separado, virtio-blk ou virtio-scsi, no qual não existe sistema de ficheiros. A disposição é mínima e própria:

LBA 0      [ superbloco: magia · versão da disposição · uuid · tabela de regiões · CRC-32C ]
LBA 8, 16  [ tabela de tamanhos dos ficheiros, posições A/B (vence a mais recente válida) ]
LBA 64     [ base principal SQLite ½ ][ WAL do SQLite ⅛ ][ diário 1 MiB ][ diário de telemetria: 8 partições … ]
últimos 4 KiB [ espaço para a autoverificação do dispositivo de blocos no arranque ]

O SQLite (3.54.1 incorporado, com verificação do hash na compilação) é compilado para dentro do kernel com o clang, sem biblioteca padrão, e usa um heap memsys5 fixo. O seu VFS mapeia três ficheiros fixos para regiões do disco. Funciona em modo WAL com locking_mode=EXCLUSIVE, pelo que não precisa de um índice em memória partilhada, e com synchronous=FULL; se o quick_check no arranque encontrar um erro, o sistema pára em segurança. Na base de dados estão o registo de dispositivos, as revogações, as mensagens retidas e as sessões persistentes, com filas de mensagens QoS 1 para dispositivos que não estão ligados.

O núcleo da base de dados, o de número mais alto, executa o SQLite e mais nada. O plano de dados envia-lhe as alterações de estado através de um anel SPSC sem locks; este agrupa-as numa única transação a cada 5 ms ou a cada 512 operações e devolve as confirmações por outro anel. Um disco vazio é formatado no primeiro arranque; tudo o resto nunca é formatado por cima.

A telemetria segue outro caminho. Cada PUBLISH aceite em tópicos de storage.telemetry_topics vai para a partição do diário de telemetria do seu núcleo: um anel de segmentos com CRC por registo, salgado com a sequência do segmento, de modo que as passagens antigas do anel nunca voltam a aparecer. Os núcleos entregam os registos à base de dados através de um anel próprio, sem cópia, e esta escreve-os a cada 5 ms ou a cada 256 KiB. No arranque, a recuperação mantém todos os registos escritos, corta a cauda incompleta e continua num novo segmento. A retenção é por tamanho, através do anel, e por idade, através de retention_days. Verificámo-lo com um milhão de registos e com kill -9 a meio da escrita.

Os números

A ferramenta de carga em tools/loadgen faz três coisas: uma tempestade de handshakes TLS 1.3 com mTLS e CONNECT, ligações de longa duração com PINGREQ que publicam telemetria a cada segundo, e tempestades de religações. O make bench num Apple M4 Pro sob HVF dá:

Núcleos de trabalho handshakes/s no convidado escalabilidade através da rede de utilizador do QEMU (ligações/s)
1 862 1,00× 789
2 1 724 2,00× 636
4 3 438 3,99× 536

O valor no convidado, em que o cliente e o servidor estão dentro da mesma máquina virtual e cada núcleo de trabalho funciona por si, mostra que o plano de dados escala linearmente. O valor de rede é limitado pela rede de utilizador de uma só thread do QEMU, não pelo convidado; isto também está registado, como ADR-032.

As tempestades de religações testamo-las fazendo com que todos os dispositivos se liguem ao mesmo tempo, repitam as tentativas recusadas com atraso e depois caiam todos sem DISCONNECT. O parâmetro heap_limit_mb= no modo de desenvolvimento faz com que a admissão considere o heap mais pequeno do que é, pelo que uma tempestade maior do que o heap pode ser testada com poucos sockets. Numa ronda destas, com 3 000 dispositivos, ligaram-se 1 368 em 36 segundos e 5 619 tentativas foram devidamente recusadas, com um limite de 8 MiB; a memória voltou ao normal depois da tempestade.

Como trabalhamos

O projeto é conduzido por uma especificação escrita, PROMPT.md, com o plano de marcos, e cada marco termina com o mesmo ritual: make check e make smoke a verde, um commit com a etiqueta M<n>, uma entrada em docs/PROGRESS.md e um ADR para cada decisão que não seja óbvia. As regras são curtas e não se negoceiam: sem panic perante entrada externa, unsafe apenas nos pacotes de hardware e FFI e sempre com comentário, sem alocações no caminho quente, um comentário junto de cada ordenação de operações atómicas, cada limite vem da configuração, e as partes provisórias intencionais falham de forma ruidosa.

A CI faz mais do que conseguiríamos fazer à mão: fmt, clippy para o host e para aarch64-unknown-none, typos, actionlint e shellcheck; testes em Linux e em macOS em arm64; compilação do kernel para as configurações de desenvolvimento e de produção, com o tamanho do ELF no resumo; arranque sob QEMU com todos os cenários do smoke test e guardando os logs de série; cargo-deny para avisos, licenças e fontes; um minuto de fuzzing em cada um dos onze alvos e, durante a noite, dez minutos por alvo com corpus em cache. Além disso, CodeQL, OpenSSF Scorecard, Dependabot e publicação com proveniência SLSA. Cada action de terceiros está fixada ao SHA completo do commit.

Levamos a reprodutibilidade à letra: o make repro compila o HEAD duas vezes em árvores de trabalho limpas e compara o ELF, o ISO e a imagem em bruto byte a byte. As versões publicadas são assinadas com minisign, e os artefactos podem ser verificados com gh attestation verify. A imagem de produção contém a chave privada do servidor, pelo que o próprio ISO é um segredo; as imagens que acompanham as versões no GitHub são de desenvolvimento, com chaves temporárias, e servem apenas para experimentar.

O que aprendemos

Algumas lições que mudaram o plano:

  • Afinal, o TCP próprio compensou. O protótipo funcionava com o smoltcp e serviu-nos bem até ao M13. Mas um servidor que mantém centenas de milhares de ligações não precisa de um TCP de cliente: precisa de SYN cookies, de uma roda de temporizadores e de um estado de 96 bytes. No M14 substituímos o smoltcp por uma pilha própria apenas do lado do servidor, com testes de perda simulada e fuzzing de segmentos.
  • Meçam a memória por dentro. As estimativas por ligação eram inúteis até o kernel começar a contar ele próprio o heap em blocos do alocador, e não em bytes pedidos. Só então vimos para onde vão realmente os 512 bytes e baixámos para 494.
  • A ferramenta de teste faz parte do produto. A maior limitação não foi o kernel, mas a rede de utilizador do QEMU e o número de portos efémeros no computador de desenvolvimento. Para cem mil ligações ou mais é preciso vmnet-shared no macOS ou Linux com KVM, interface tap e vários endereços de cliente.
  • SQLite sem sistema operativo é possível, mas só em condições estritas: heap fixo, bloqueio exclusivo sem memória partilhada, um VFS que desconhece ficheiros e uma admissão que não deixa a base de dados atrasar-se.
  • As tempestades de religações são o estado normal, não a exceção. Quando a rede móvel cai e volta, milhares de dispositivos batem à porta ao mesmo tempo. Por isso a admissão recusa a ligação antes de qualquer criptografia, e cada ligação recusada custa quase nada.

O que se segue

O trabalho depois do M17 está planeado em três direções, e a ordem entre elas está registada em docs/PLAN.md. Nada disto está ainda construído.

  • Um ISO para várias plataformas. O mesmo ficheiro urmqtt-<env>-<versão>.iso e uma única imagem GPT em bruto que arrancam em UEFI aarch64, UEFI x86_64 e BIOS x86_64, em máquinas virtuais e em hardware real, começando por um PC Intel. Continua a ser um aparelho com tudo lá dentro, reprodutível e assinado.
  • Disco em bruto, sem sistema de ficheiros. Especificação do formato ao nível do byte, uma ferramenta de host urmqtt-disk que partilha os parsers com o kernel, alinhamento das regiões a 1 MiB, crescimento do disco com os dados, discard para discos de provisionamento dinâmico, snapshots A/B da base de dados, verificação de bits e, opcionalmente, cifragem AES-256-XTS com AES por hardware.
  • Comprovadamente seguro. Revogação e lista de dispositivos permitidos em tempo de execução através de um canal de controlo assinado, sem nova imagem; testes diferenciais e de conformidade do TLS e do TCP próprios; KASLR, proteção da pilha, kCFI, MTE e um alocador reforçado; SQLite isolado ao nível EL0; Secure Boot; SBOM e cargo-vet; e, por fim, um pacote para auditoria externa.

As plataformas que oferecem apenas dispositivos não virtio, como o AWS ENA ou o Azure MANA, não são suportadas por agora. O Raspberry Pi 4 e 5 como anfitrião KVM, o Hetzner CAX e o Oracle Ampere A1 estão documentados e suportados no código, mas ainda não foram experimentados num serviço real.

Experimente

O repositório está em github.com/zeljan-alduk/urMQTT. Com o Rust com a toolchain nightly fixada e o QEMU, make certs, make run e um cliente mosquitto bastam para o primeiro PUBLISH sobre mTLS no seu próprio computador. O aviso do início vale também no fim: isto é pré-alfa, e tudo o que funciona está em docs/PROGRESS.md. Relatórios de erros, perguntas sobre o desenho e comunicações de segurança, seguindo as instruções em SECURITY.md, são bem-vindos. Voltaremos a falar aqui de cada marco seguinte.

Comentários

Regras dos comentários

Regras dos comentários

Os comentários servem para partilhar experiências e perguntas sobre os temas do blog. Estas regras mantêm a discussão útil para todos.

  1. Cortesia Sem insultos, ameaças, assédio, discurso de ódio ou discriminação por qualquer motivo. Critique o argumento, não a pessoa.
  2. Dentro do tema Um comentário deve dizer respeito ao artigo ou à discussão que o segue.
  3. Sem publicidade nem spam Sem anúncios, correntes de mensagens ou ligações sem contexto. Ligações e imagens são bem-vindas quando contribuem para a discussão.
  4. Privacidade dos outros Não publique dados pessoais de terceiros: nomes completos de particulares, moradas, telefones, e-mails, fotos sem consentimento.
  5. Direitos de autor Publique apenas conteúdo que possa partilhar. Ao citar, indique a fonte.
  6. Uma só identidade Use uma só alcunha e não se faça passar por outra pessoa.
  7. Confirmação por e-mail Um comentário só é publicado depois de confirmar a ligação enviada por e-mail. O endereço não é mostrado nem usado para mais nada; comentários não confirmados são apagados após 48 horas.
  8. Moderação Comentários que violem as regras são ocultados ou apagados sem aviso, e uma discussão pode ser bloqueada. As decisões dos moderadores são definitivas.
  9. Responsabilidade O autor é responsável pelo conteúdo do seu comentário. Os comentários não refletem as posições da ALDO TECH.
  10. Denúncias Denuncie conteúdo impróprio para info@aldo.tech.