Pular para o conteúdo

O ponto de encontro

O degrau 4 do ADR 0022 — furo de NAT — precisa de um serviço minúsculo que apresente as duas máquinas uma à outra. Este documento é sobre ele: o que ele faz, o que ele fica sabendo, e como pôr um seu no ar.

Se você só quer entender por que "não conecta", o documento é o outro: `alcance-pela-internet.md`.

O que ele faz, em uma frase

Ele diz a quem manda um pacote qual é o endereço de onde aquele pacote veio — ou conta isso a um terceiro endereço, quando quem manda pede.

E guarda em memória o endereço em que cada anfitrião disse morar, para dizê-lo a quem perguntar por ele: é o quarto, descrito abaixo. Esse endereço vale 60 segundos desde o último aviso do anfitrião; vencido, deixa de ser dito a quem pergunta, mas não sai da memória na hora: fica lá até o quarto encher e chegar uma marca nova (Quarto::morar), até um aviso novo em nome do mesmo anfitrião o substituir, ou até o ponto reiniciar. Não há mais nada.

Nenhuma máquina atrás de NAT sabe o próprio endereço público: o roteador reescreve isso na saída e o interior nunca vê o resultado. Alguém de fora precisa contar, e furar um NAT é os dois lados mandando pacote ao mesmo tempo depois de saberem para onde. O ponto de encontro é esse "alguém de fora".

Quando a conexão sobe, ele já não participa de nada: o áudio e o texto nunca passam por ele, e o TLS 1.3 e o TOFU do ADR 0003 continuam ponta a ponta.

O que ele fica sabendo

Metadado. Que endereço falou com que endereço, e quando. Isso é real, é o custo que o ADR 0022 nomeia em voz alta, e não há como ter o degrau 4 sem ele.

Nada do que é dito. Ele não vê conteúdo nem chave; não teria o que fazer com eles se visse, porque o que passa por ali são linhas curtas de texto com marcas e endereços.

Quase nada guardado. Não há banco nem arquivo. A resposta a ONDE e a LEVE continua sendo uma função que recebe um datagrama e devolve outro (seele_proto::encontro::responder, sem self e sem estado). MORO e QUEM passam por o quarto, que existe desde 2026-09-03: um mapa de marca → endereço, em memória, com prazo de 60 segundos e teto de 4096 marcas (ADR 0022, «O quarto, e por que a recusa foi revista»). O MORO escreve nele, e a resposta a QUEM sai dele; a função sem estado, sozinha, cala o QUEM e responde o MORO como um ONDE. Os 60 segundos são a validade de uma entrada, contados do último MORO, e não o tempo que ela leva para sumir: vencida, ela deixa de responder ao QUEM, e continua na memória até o quarto encher e chegar uma marca nova (Quarto::morar; aí o que venceu é varrido), um MORO novo da mesma marca a substituir, ou o ponto reiniciar. Nada sobrevive a um reinício, e nada vai a disco em nenhum momento.

O que o operador do ponto de encontro consegue ler por causa do quarto: que uma marca está no ar, ou esteve até a última varredura, e em que endereço. Nada além disso — a marca são os 16 primeiros dos 64 caracteres da impressão digital do servidor (e uma letra), não um nome nem um endereço de e-mail, e quem falou com quem não passa por ali, porque a conversa nunca passou. Por padrão o serviço nem imprime isto — --barulhento liga a impressão para investigar um problema, e avisa na saída o que passou a registrar, e mesmo com ele ligado o quarto não é impresso.

Ele não decide em quem se confia. Quem chega com o bilhete do ponto de encontro (o do link, ou o que a lista de conhecidos guardou) e com uma impressão digital a conferir (a do link, ou a da lista) pergunta ao quarto onde o anfitrião mora hoje, e lê a resposta. Faltando uma das duas, não há pergunta. A resposta traz dois endereços, e cada um tem a sua barreira.

O do servidor entra na frente dos guardados. Um ponto de encontro hostil, ou quem ocupou a marca, consegue mandar essa conexão para o endereço errado, ou não avisar o anfitrião. O que ele não escolhe é a impressão digital: a esperada, a do link ou a da lista, é conferida dentro do aperto de mão TLS, antes de qualquer Hello. Se a chave não confere, o aperto falha ali, e o convite, a senha e o apelido não chegam a quem atendeu com a chave errada. O prejuízo é não entrar. Isso vale também onde esta máquina já fixou uma chave naquele endereço: o endereço que o quarto devolve, quando entra na corrida, é um candidato que a pessoa não escolheu, e ali um pino que confere não passa por cima da impressão esperada (o adendo de 2026-09-29 ao ADR 0003). Se a chave fixada ali mudou, a conexão é recusada do mesmo jeito.

O da escuta de avisos vira, só nesta volta, o destino do LEVE, e o LEVE sai antes de qualquer aperto de mão: o ponto repassa a esse destino o endereço público de quem chega (IP e porta), no instante em que ele tenta chegar. A marca da escuta está em todo link, e um anfitrião 0.15.0 nunca a registra. Por isso quem chega só usa a escuta do quarto quando ela mora no mesmo IP que o servidor da mesma resposta: a escuta e o servidor de um anfitrião saem da mesma máquina, pelo mesmo IP público. Fora disso, o LEVE vai ao aviso do link ou da lista, e o seele.log diz por quê. E a lista de conhecidos guarda o bilhete do link ou da lista, e nunca o que o quarto deu.

O que sobra num servidor guardado, que continua no SEELE-ENC/1: quem toma as duas marcas do anfitrião no quarto, a do servidor e a da escuta, ou só a da escuta saindo pelo mesmo IP público que ele, recebe pelo LEVE o IP, a porta e o instante de quem tenta chegar. A marca do servidor só fica livre em três janelas: com o anfitrião fora do ar por mais de 60 segundos; quando ele não se registra naquele ponto (com IPv4 global ele não abre o degrau 4, e SEELE_ENCONTRO desliga o degrau ou troca o ponto, enquanto o bilhete que a lista de conhecidos guardou continua apontando o de antes); e com o ponto recém-reiniciado, antes de o anfitrião se registrar de novo (ele se registra a cada 15 segundos). A da escuta tem as mesmas janelas, e num anfitrião 0.15.0 está sempre livre. O conteúdo, o convite, a senha e o apelido não saem: o TLS recusa o servidor errado.

A sala pessoal, no SEELE-ENC/2 (ADR 0062), não tem essa sobra. Ela se registra num segundo quarto, separado do primeiro, com um MORO2 assinado pela chave da pessoa, e o ponto confere, nesta ordem, que o pedido traz o cookie que ele deu àquele IP (sem ele, o ponto responde um DESAFIO com o cookie e não guarda nada), que a marca sai daquela chave, que a assinatura vale e que o carimbo de hora está a menos de cinco minutos do relógio dele. Ninguém sem a chave ocupa as marcas de uma sala; um IP ocupa no máximo 128 marcas vivas; um MORO2 copiado no caminho e repetido depois do original é recusado quando vem de outro endereço; e uma troca de rede de quem recebe vale no reavivamento seguinte, sem esperar os 60 segundos. Dentro do limite de pedidos, quem pergunta pela sala com QUEM2 ouve sempre uma resposta: o endereço e as dicas, ou NINGUEM. Acima dele, o ponto recusa, e diz a recusa uma vez por segundo.

O que sobra na sala. Quem está no caminho entre a sala e o ponto, e consegue entregar a cópia de um MORO2 antes do original, aponta a marca para si até o reavivamento seguinte, quinze segundos depois; com a marca da escuta, recebe pelo LEVE o IP e a porta de quem liga nesse meio tempo. A mensagem assinada não diz para qual ponto ela é, então ela vale em qualquer um. Quem forja a origem dos pacotes não registra nada em nome de outro IP, nem esvazia o limite de registro dele: ele não recebe o cookie do IP que forja. O que ele ainda faz é esvaziar o limite de perguntas de um IP, com QUEM2 forjados, e quem está naquele IP não consegue procurar ninguém pelo código enquanto isso durar (continua achável); e fazer o ponto mandar um DESAFIO a um IP que não pediu nada, do tamanho do pedido, como o ONDE de sempre. Em nenhum dos casos o servidor errado atende: quem liga confere o código dentro do TLS.

O que a sala conta. Quem tem o seu código consegue saber, sem ligar, se o seu SEELE está aberto, qual é o seu IP público, e as dicas de rede local da sua sala (até dois endereços da rede da sua casa, que é o que deixa duas pessoas na mesma casa se ligarem sem o roteador dar a volta). É a natureza de ser encontrável, e a alavanca é «não receber ligações». O operador do ponto aprende o mesmo, e quem procura quem, enquanto as perguntas passam por ele; nada disso vai a disco.

O link fica com o seu endereço público dentro. O bilhete (enc=) carrega o endereço do ponto de encontro e o endereço público da sua escuta de avisos — quem tem o link aprende o seu endereço sem precisar conectar. Quem conecta aprenderia de qualquer forma; um link é para dar a quem se convida.

Como não usar o nosso

Uma variável de ambiente, na máquina que hospeda. O degrau 4 é do HOSPEDAR AQUI do app, e não do seeled: abra o SEELE com a variável no ambiente e aperte HOSPEDAR AQUI.

sh
SEELE_ENCONTRO=encontro.suacasa.exemplo:8384 ./target/release/seele-app

SEELE_ENCONTRO=nao ./target/release/seele-app

O app só vê a variável se nascer com ela: aberto pelo Finder, pelo Dock ou pelo menu Iniciar, ele herda o ambiente do sistema, e não o do seu terminal. Com o app instalado, rode do terminal o executável do app, de dentro da instalação (no macOS, em SEELE.app/Contents/MacOS/, ao lado do seeled).

Quem entra não configura nada: o endereço do ponto de encontro viaja no próprio seele://, dentro do enc=. É isso que faz o serviço ser trocável de verdade — apontar para o seu não exige versão nova de nada, nem que a outra pessoa saiba que ele mudou.

Com SEELE_ENCONTRO=nao, tudo o que funcionava continua funcionando: rede local, IPv6 e porta no roteador não passam por ponto de encontro nenhum.

Subir o seu

Precisa de uma máquina com endereço público — uma VPS de dez reais serve, e sobra. Não precisa de banco, de disco, nem de domínio (um endereço IP no SEELE_ENCONTRO funciona igual).

sh
cargo build --release -p seele-encontro
./target/release/seele-encontro

Ele abre a porta 8384/UDP em IPv4 e IPv6 e fica ali. Opções:

texto
--porta N       em que porta atender (padrão 8384)
--rede-local    também apresentar endereços de rede local (só para experimentar)
--barulhento    imprimir quem falou com quem (é metadado; desligue depois)

A partir da 0.17, o mesmo processo fala também o ENC/3 (o registro e a chamada da 0.17) e responde STUN Binding na mesma porta UDP. O STUN é como o navegador descobre o próprio endereço público, e a página usa o ponto do convite, e só ele: nenhum STUN de terceiro.

O navegador: --sinal, atrás de um proxy TLS

O navegador não fala UDP. Para ele chamar um anfitrião atrás de NAT, o ponto atende um POST /sinal/1 em HTTP:

texto
--sinal 127.0.0.1:8385     o HTTP do navegador, só no loopback
--origem https://seele.sh   quem pode ler a resposta (repetível; este é o padrão)

O ponto não fala TLS, de propósito: ele continua Rust puro, sem certificado e sem ACME dentro dele. Quem termina o HTTPS é um proxy reverso na mesma máquina, no mesmo nome do ponto e na porta

  1. A página chama https://<nome do ponto>/sinal/1, então um ponto escrito como IP no convite não

tem sinal, só o STUN. Se a máquina já tem um servidor em 443, o bloco vai nele (dois não sobem na mesma porta). Com o Caddy, que emite e renova o certificado sozinho, e o 404 dito para todo caminho que não é o sinal:

texto
encontro.suacasa.exemplo {
    handle /sinal/1 {
        reverse_proxy 127.0.0.1:8385
    }
    handle {
        respond 404
    }
}
sh
sudo ufw allow 443/tcp

O ponto confia no X-Forwarded-For só quando a conexão vem do loopback, e lê o último endereço dele. É por esse IP que o limite de chamadas é contado. Sem --sinal, o navegador entra só pelo caminho direto (a rede local, o IPv6 e a porta aberta); o resto continua igual.

O que o sinal fica sabendo: o mesmo que o LEVE sempre soube. Ele vê o endereço público da página e o candidato que ela mandou, e conta à escuta do anfitrião que alguém quer entrar. A conversa não passa por ele: a página escreve a impressão do convite como a do anfitrião, e o navegador recusa outro certificado.

Libere a porta no firewall da máquina:

sh
sudo ufw allow 8384/udp

E, para ele subir junto com a máquina, um serviço de systemd de nove linhas:

ini
[Unit]
Description=SEELE — ponto de encontro
After=network.target

[Service]
ExecStart=/opt/seele/seele-encontro --sinal 127.0.0.1:8385
Restart=always
DynamicUser=yes

[Install]
WantedBy=multi-user.target

DynamicUser=yes porque ele não precisa de usuário, de casa, nem de permissão de escrita em lugar nenhum — não há o que persistir.

Reiniciá-lo no meio de uma apresentação custa a repetição de um datagrama (96 bytes no ENC/1, 384 no ENC/2), e esvazia os dois quartos, que são, com o limite por prefixo, o único estado dele. Cada anfitrião e cada sala voltam ao quarto no reavivamento seguinte, a cada quinze segundos; até lá, quem pergunta por eles não os acha e tenta os endereços que já tinha.

O segredo dos cookies. O ponto sorteia 32 bytes do sistema quando sobe (getrandom), e é com eles que ele faz e confere o cookie de cada IP. Eles moram só na memória do processo: nunca em disco, nunca num arquivo de configuração, nunca no --barulhento, e quem opera o ponto não configura nada. Reiniciar o ponto sorteia outros, e os cookies de antes param de valer: cada sala ouve um DESAFIO no reavivamento seguinte, manda de novo com o cookie novo, e volta ao quarto, que o reinício também esvaziou. Dois processos do ponto atrás do mesmo nome teriam segredos diferentes, e a sala que alternasse entre eles ouviria DESAFIO a cada troca; um ponto é um processo.

O relógio da máquina tem de estar certo. A sala pessoal se registra pelo ENC/2 com um carimbo de hora, assinado, e o ponto o confere contra o próprio relógio, com tolerância de cinco minutos. Um ponto com o relógio torto recusa o registro de toda sala, e cada pessoa lê no seele.log que o relógio dela e o do ponto diferem. Numa VPS com systemd, confira que a hora vem da rede:

sh
timedatectl          # «System clock synchronized: yes» e «NTP service: active»

Conferir que ele está mesmo no ar

systemctl status responde «o processo está de pé», que é uma pergunta diferente de «alguém de fora alcança esta porta». Entre as duas há o firewall do provedor, o firewall da máquina e a regra da porta — e cada um deles já quebrou isto numa máquina de verdade.

De outra máquina:

sh
cargo run -p seele-encontro --example sondar -- <endereço>:8384

Ele fala o protocolo mesmo, e não um ping: um serviço que responde a ICMP e recusa datagrama de 96 bytes está tão quebrado quanto um desligado, e só isto distingue os dois. As duas famílias são tentadas em separado, porque um ponto de encontro que atende IPv4 e não IPv6 apresenta mal justamente os pares que mais precisam dele.

Quando dá certo, ele imprime o seu endereço visto de fora — que é literalmente o serviço que este ponto de encontro presta.

Depois ele confere as duas coisas que um ponto de pé pode não ter, e diz cada uma: o quarto (MORO e QUEM), que um binário anterior a 03/09/2026 não tem, e o ENC/2 (MORO2, QUEM2 e NINGUEM, com uma chave de sondagem nova a cada rodada), que um binário anterior à 0.16 não tem. Sem o ENC/2, ninguém acha ninguém pelo código. No ENC/2 ele também mede o relógio do ponto, e diz de quantos segundos ele difere do da máquina que sonda.

Com --exige-enc2, ele sai com falha se alguma família que respondeu ao ONDE não falar o ENC/2:

sh
cargo run -p seele-encontro --example sondar -- --exige-enc2 <endereço>

Antes de publicar uma versão que depende do ENC/2

A partir da 0.16, a sala pessoal se registra pelo ENC/2, e quem liga pelo código pergunta por ele. O ponto de produção vai antes do app: o binário novo fala as duas versões, então a 0.15 não muda nada com ele, e o app novo sem ele não acha ninguém pelo código. Foi assim com o quarto: o ADR 0047 registra dez dias de recurso morto por um binário velho na VPS, com o app certo do lado de cá.

Na ordem:

  1. reimplantar a VPS com o seele-encontro da versão que vai sair (`cargo build

--release -p seele-encontro e o systemctl restart, como em «Subir o seu»), e conferir que o relógio dela está sincronizado (timedatectl`);

  1. de outra máquina, conferir:

``sh cargo run -p seele-encontro --example sondar -- --exige-enc2 --exige-enc3 rendzvous.seele.sh ``

Ele sai com sucesso só se toda família que responde ao ONDE também falar o ENC/2, com o relógio do ponto a menos de um minuto do da máquina que sonda, e diz de quantos segundos é a diferença. Com --exige-enc3 (a 0.17), também o QUEM3 do ENC/3 e o STUN Binding em toda família: um ponto da 0.16 passa no ENC/2 e reprova aqui. O ENC/3 é provado de ponta a ponta numa sala efêmera (o MORO3 com o cookie do DESAFIO, o QUEM3, e um CHAMA de um segundo socket atendido pela escuta), e não só por uma consulta; e o sinal do navegador, pelo HTTPS de verdade e com a origem do web app (a resposta tem de ser a linha SEELE-SINAL/1 NINGUEM, e o Access-Control-Allow-Origin tem de ser https://seele.sh; um 200 de outra página não conta):

``sh corpo=$(cargo run -q -p seele-encontro --example sondar -- --corpo-do-sinal) curl --fail -sS --proto '=https' -D - -H 'Origin: https://seele.sh' \ -H 'Content-Type: text/plain' --data-binary "$corpo" https://rendzvous.seele.sh/sinal/1 \ | tee /dev/stderr | sed '1,/^\r\{0,1\}$/d' \ | cargo run -q -p seele-encontro --example sondar -- --resposta-do-sinal ``

  1. publicar. O empacotar/publicar.sh roda a mesma conferência antes de

qualquer passo com efeito, e para se o ponto não falar o ENC/2. O --sem-ponto pula a conferência, para a emergência que não depende do ponto, e o corpo do release diz que ela foi pulada.

Reiniciar o ponto esvazia os dois quartos por uns quinze segundos: cada anfitrião e cada sala voltam no reavivamento seguinte.

O que ele não resolve

NAT simétrico dos dois lados não fura. Nesse caso o mapeamento do roteador muda a cada destino, então o endereço que o ponto de encontro viu não é o endereço por onde o outro lado chegaria. É por isso que a frase do degrau 4 diz "deve funcionar" e não "funciona", e por isso a escada continua caindo para os degraus de baixo com as saídas de sempre — encaminhar a porta à mão, ou uma VPN de rede entre os dois.

A resposta a esse caso seria retransmissão: o tráfego passando pela máquina de um terceiro. O ADR 0022 põe isso fora de escopo por decisão, e não é falta de tempo — é o que separa este produto do que ele existe para não ser.

Se você for mexer no código

O protocolo — quatro verbos que chegam ao ponto (ONDE, LEVE, MORO e QUEM), a resposta AQUI, e uma função sem estado que decide o ONDE e o LEVE — está em crates/seele-proto/src/encontro.rs, e a versão 2 ao lado dele (MORO2 e QUEM2, as respostas MOROU, DESAFIO, AQUI2, NINGUEM e RECUSA, ADR 0062). O quarto, que guarda o MORO e responde ao QUEM, o quarto assinado do ENC/2, o limite por prefixo e o serviço estão em crates/seele-encontro/. O lado de quem hospeda, em crates/seele-server/src/alcance/encontro.rs; o de quem entra, em crates/seele-core/src/encontro.rs. O bilhete que viaja no link é o enc= de crates/seele-proto/src/uri.rs.

Duas propriedades que o código guarda de propósito, e que valem ser mantidas se alguém mexer:

resposta é montada campo a campo. O único pedaço do pedido que reaparece é a marca, que é alfanumérica e curta justamente por isso.

pedido e resposta, sempre do mesmo tamanho.** Um refletor que responde mais do que recebe é uma arma apontada para quem nunca ouviu falar deste projeto. O enchimento é o que mantém o ganho em 1:1, e um datagrama de qualquer outro tamanho é calado.

(IPv6), contra um pedido de 20 ou mais: um ganho de até 2,2:1. Ele é contido pelo balde próprio por prefixo, pela recusa de origem em porta abaixo de 1024 e pela recusa de qualquer pedido que não seja um Binding Request bem formado (crates/seele-encontro/src/stun.rs).

a uma escuta registrada no ENC/3, com o tamanho do CHAMA que o causou e com um id de 128 bits que só ela recebe. O CHAMA gasta um balde próprio por prefixo, e as chamadas pendentes têm teto (crates/seele-encontro/src/enc3.rs).