O que existe
Três programas, e o terceiro é o menos importante.
O host é o Outlink rodando na máquina que vai ser acessada. Captura a tela, codifica, aceita conexões, aplica mouse e teclado.
O viewer é o Outlink rodando na máquina de quem acessa. É o mesmo executável: o programa faz os dois papéis.
O servidor de encontro existe para uma coisa só: transformar um ID de nove dígitos num endereço alcançável, e guardar papéis que os clientes assinaram. Ele não vê a tela, não vê as teclas e não consegue abrir sessão nenhuma.
A sessão é direta entre as duas máquinas. Não há relay: o vídeo não passa pelo servidor.
Linguagem e dependências
Python 3.11+ nos três. Interface em PySide6 (Qt 6). Criptografia pela
biblioteca cryptography (OpenSSL). Captura por DXGI Desktop
Duplication no Windows e por X11 ou portal no Linux. Codificação de vídeo
por FFmpeg, no encoder de hardware da placa presente. Servidor em aiohttp
sobre PostgreSQL 16, via asyncpg.
Modelo de segurança
A pergunta que organiza todo o desenho é esta: o que acontece se alguém tomar o servidor?
Num produto de acesso remoto comum, a resposta é: essa pessoa entra em todos os computadores dos clientes. O servidor guarda a lista de quem pode acessar o quê, e os agentes obedecem à lista. Quem edita a lista, manda.
No Outlink a resposta é: essa pessoa consegue derrubar o serviço, e nada além disso. Ela não entra em máquina nenhuma, não lê tela nenhuma e não fabrica permissão para si mesma.
| Se o servidor for tomado, ele… | por quê |
|---|---|
| não autoriza ninguém | acesso é documento assinado pelo dono, e a máquina confere a assinatura (§5) |
| não se passa por uma máquina | cada máquina prova posse da chave privada dela (§6) |
| não lê as senhas nem as chaves privadas | ele recebe uma prova derivada, e o cofre chega cifrado (§4) |
O que ele consegue, e é honesto dizer: parar de responder, apagar o registro de endereços, e ver metadados — quais IDs existem, de que endereço apareceram e quando, e quem compartilhou acesso com quem. Isso não é pouco. É o preço de ter um serviço que encontra máquinas por um número curto.
Não protege contra quem está com a máquina na mão. Não protege contra malware com privilégio de administrador em qualquer uma das pontas. Não protege contra a pessoa do outro lado tirando foto da tela. Não há defesa técnica para nada disso e não fingimos que há.
Identidade das máquinas
Toda instalação gera, no primeiro uso, um ID de nove
dígitos (418-207-935) — curto o bastante para ser
lido por telefone — e um par Ed25519, a caneta da
máquina.
A chave privada nunca sai do computador. No Windows é protegida por
DPAPI; no Linux, arquivo com permissão 0600.
O ID não é emitido por ninguém — cada máquina sorteia o seu. O registro no servidor é confiança no primeiro uso: quem registrar aquele número primeiro fica com ele, e daí em diante só a chave que registrou pode atualizar o endereço.
Toda requisição do dispositivo vai assinada, com um instante de tempo dentro (janela de 120 segundos). Sem o instante, um heartbeat gravado poderia ser reenviado depois para fingir que a máquina está online, ou apontar o ID para o endereço errado.
Contas e o cofre
A conta existe para administrar máquinas: listar, agrupar, dar acesso. Ela não é obrigatória — a conexão por ID e senha funciona sem cadastro, e esse é o caminho certo para socorrer alguém uma vez pelo telefone.
O que o servidor recebe, e o que ele não recebe
O cliente deriva um segredo mestre da senha e tira dele duas coisas diferentes, com rótulos diferentes:
mestre = scrypt(senha, sal_kdf) N=2^14, r=8, p=1 prova_login = HKDF(mestre, "login") ──▶ vai para o servidor chave_cofre = HKDF(mestre, "cofre") ──▶ NUNCA sai do cliente
O servidor guarda só um hash da prova de login. Com isso ele consegue dizer “essa pessoa sabe a senha”, e não consegue derivar a chave do cofre: HKDF não anda para trás.
O cofre é a chave privada Ed25519 da conta, cifrada em
AES-GCM com a chave_cofre. Ele fica guardado no servidor —
para você entrar de uma máquina nova — e é opaco para ele.
Senha esquecida é cofre perdido. Se houvesse recuperação, o servidor conseguiria abrir, e a propriedade toda cairia. A tela de cadastro diz isso antes de criar a conta, e não depois.
Freio de tentativas
O scrypt encarece cada tentativa, mas ele roda no cliente — quem ataca faz o cálculo na própria máquina, na velocidade da placa dele, e o servidor só compara hashes. Por isso há um freio do lado de cá, contando por duas chaves:
| chave | limite | castigo |
|---|---|---|
| por IP | 10 falhas em 5 min | 15 min |
| por conta | 30 falhas em 5 min | 5 min |
Os números são diferentes de propósito. Iguais, qualquer um que soubesse o e-mail de uma pessoa trancaria essa pessoa por quinze minutos só errando a senha dez vezes — sabotagem de graça. Assim, um IP hostil bate no limite dele muito antes de a conta chegar ao dela: trancar uma conta exige pelo menos três origens diferentes.
Autorizações assinadas
Esta é a peça central. O dono assina um papel, e a máquina só obedece a papel assinado. O servidor entrega, mas não sabe falsificar. É o mesmo princípio dos certificados de SSH.
{
"v": 1,
"dispositivo": "418207935",
"para": "<chave pública de quem recebe>",
"permissoes": ["ver", "controlar"],
"emitido_em": 1755640000,
"expira_em": null,
"emissor": "<chave pública de quem assinou>"
}O JSON é serializado de forma canônica — chaves ordenadas, sem espaços — e assinado. O envelope que trafega leva documento, assinatura e emissor.
As permissões
| permissão | o que libera |
|---|---|
ver | receber a imagem da tela |
controlar | mouse, teclado e área de transferência |
arquivos | a pasta compartilhada |
sem_aprovacao | entrar sem ninguém clicar em “sim” |
São separadas de propósito. Juntar tudo em “acesso” é o que gera abuso: dar suporte a alguém é uma coisa, poder entrar sem ninguém ver é outra.
As âncoras de confiança
A máquina guarda um conjunto de chaves públicas em que confia: a da conta que a pareou, gravada no momento do pareamento, e as dos administradores da organização quando for máquina corporativa — lista que também vem assinada.
O pareamento é o único momento em que dá para estabelecer isso com segurança: a pessoa está sentada naquela máquina e logada nela. Depois, qualquer resposta sobre “em quem confiar” chegaria pela rede, e quem controla a rede escreve a resposta.
Uma máquina que nunca foi pareada não tem âncora, e recusa todo mundo — inclusive papéis legítimos. É o padrão seguro.
O que o Porteiro verifica, na ordem
- A assinatura confere com a chave que diz ter assinado?
- Esse emissor está entre as âncoras desta máquina? É aqui que o servidor perde a partida: entregar o papel não adianta se quem assinou não estiver na lista.
- O papel é para esta máquina mesmo? Sem isso, um papel válido para o computador da recepção serviria no servidor de arquivos.
- Ainda está no prazo?
- Quem está batendo na porta é o destinatário do papel?
- As permissões escritas existem?
Falhou qualquer uma, recusa e diz qual. Nunca devolve “talvez”.
Autorizações se somam: quem recebeu ver em um
papel e arquivos em outro tem as duas.
Grupos e revogação
Uma lista de membros também é um documento assinado. Isso permite liberar um grupo inteiro com uma assinatura só — e é o que impede o servidor de acrescentar alguém ao grupo por conta própria.
Apagar a autorização no servidor faz a máquina deixar de recebê-la na próxima atualização da lista, que acontece a cada 5 minutos. Esse é o atraso máximo entre revogar e a porta fechar.
Falha de rede não limpa a lista carregada: a lista antiga é melhor do que lista nenhuma, e o servidor cair não pode virar um jeito de trancar o dono do lado de fora.
Os dois apertos de mão
Existem dois, para dois casos de uso diferentes. Quem conecta manda oito
bytes de preâmbulo dizendo qual quer — OUTLINK\x02 para senha,
OUTLINK\x03 para conta. Um cliente de versão anterior não manda
nada e espera o cabeçalho; o host desiste de esperar depois de 300 ms e
segue pelo caminho da senha.
Por senha — para suporte avulso
A senha em si nunca trafega. As provas são AES-GCM sob uma chave derivada por HKDF do mestre, tendo o transcrito inteiro como sal — trocar qualquer campo no caminho faz as duas provas deixarem de bater.
O passo 3 não é cortesia. Sem ele, qualquer um poderia se passar pelo host e capturar o que o viewer digitasse.
As chaves de sessão saem da senha e do segredo efêmero X25519, que nunca vai para a rede e é descartado no fim. Isso dá sigilo futuro: quem gravar o tráfego de hoje e descobrir a senha daqui a um ano não abre nada.
Quem capturar o handshake pode testar senhas offline contra a prova. O scrypt encarece cada tentativa e a senha sorteada pelo programa tem 59,5 bits, o que torna isso inviável. Senha fixa curta, escolhida à mão, não.
Por conta — para frota
Aqui ninguém digita nada, e nenhum segredo é compartilhado. Os dois lados provam posse de chave privada.
transcrito = SHA-256( MAGIC_CONTA
‖ desafio_host ‖ desafio_viewer
‖ efêmera_host ‖ efêmera_viewer
‖ id_host ‖ id_viewer )As duas identidades entram no transcrito junto com as efêmeras. Ficassem de fora, uma assinatura feita numa conversa poderia ser reapresentada em outra com as identidades trocadas.
O viewer confere a chave do aparelho contra a que o servidor informou na listagem. É isso que torna o aperto de mão autenticado: sem essa verificação, o viewer aceitaria quem quer que atendesse na porta. Se não bater, a conexão morre ali, antes de o viewer mandar a própria assinatura.
O host não assina antes de o Porteiro autorizar. Assinar e só então recusar entregaria de graça uma assinatura válida da máquina sobre um transcrito escolhido por quem está batendo na porta.
Quem entra por conta recebe só as permissões do papel. A trava está no
host, e não no caps que a interface lê: quem tem apenas
ver recebe um backend de entrada que não faz nada, e a pasta
compartilhada não é montada. Um cliente modificado que ignore a interface
e mande o clique mesmo assim não encontra onde ele caia.
O canal de dados
Depois do aperto de mão, o mesmo socket TCP carrega tudo, multiplexado em canais.
| canal | conteúdo |
|---|---|
| 1 — controle | JSON: estado, mouse, teclado, área de transferência, comandos |
| 2 — vídeo | binário: retângulos JPEG ou unidades H.264/AV1 |
| 3 — arquivos | binário: blocos de transferência |
[ 4 bytes: tamanho ][ AES-GCM( 1 byte canal ‖ carga ) + tag de 16 ]
Cada sentido tem chave e contador próprios. O nonce é o contador de 12 bytes em big-endian, e como as chaves são derivadas por sessão — entram os dois desafios aleatórios — nunca se repete o par (chave, nonce).
Controle da fila de saída
A fila é medida em tempo, não em bytes. Byte não diz nada sozinho: os mesmos 4 MB são 320 ms num link de 100 Mbps e mais de seis segundos num de 5.
O programa aprende a taxa real do link medindo quanto tempo o
drain() leva, com média móvel. Se enfileirar o próximo quadro
passar de 55 ms — pouco mais de três quadros a 60 fps —
o quadro é descartado. Acima de 400 ms, cai até keyframe:
um keyframe pesa cerca de dez vezes um quadro comum, e insistir nele com a
fila afogada enfileira quase um segundo de uma vez.
Vídeo por UDP, com correção de erro
O canal acima carrega tudo, e é o certo para controle, arquivos e área de transferência: são pequenos, precisam chegar todos e em ordem, e essa é a descrição do TCP.
Vídeo é o contrário. Em TCP, um pacote perdido trava todos os seguintes até a retransmissão chegar, e a retransmissão leva uma ida e volta inteira. Num link com 30 ms de latência e 1% de perda, isso é um congelamento de 60 ms várias vezes por minuto — e o quadro que chega 60 ms atrasado já não serve para nada.
Por isso o vídeo tem caminho próprio em UDP, negociado no início da sessão e com queda automática para o TCP quando não vinga.
O segredo viaja pelo TCP porque o TCP já está autenticado e cifrado com sigilo futuro. Dele saem duas chaves, uma por sentido: os dois lados mandam pacote, e cada um tem contador de nonce próprio começando em zero. Com chave única, o pacote 0 de um e o pacote 0 do outro usariam o mesmo par (chave, nonce) — e em AES-GCM isso não vaza só as duas mensagens, vaza a chave de autenticação.
O datagrama de abertura é o que faz o caminho existir: o roteador de quem assiste só aceita o que vier do endereço para o qual ele mandou antes. É também assim que o host descobre para onde mandar, sem ninguém configurar roteador. O host só troca o destino por uma origem que abriu um pacote com a chave da sessão — sem isso, descobrir a porta bastaria para receber a tela de outra pessoa.
A correção de erro
Cada quadro é cortado em fragmentos de 1200 bytes — cabe num datagrama comum sem fragmentação de IP. O fragmento i pertence ao grupo i % 4, e cada grupo ganha um pacote de paridade que é o XOR de todos os membros dele.
Se você sabe A, B e (A xor B), sabe os três. Perdeu B? B = A xor (A xor B)
O intercalamento é a decisão que faz o XOR valer aqui. Em blocos contíguos, uma rajada de quatro perdas cairia toda dentro do mesmo grupo, e grupo com duas perdas não se recupera. Intercalado, quatro perdas seguidas caem em quatro grupos diferentes — uma em cada — e todas voltam. Perda na internet é quase sempre em rajada: o roteador enche e derruba vários seguidos.
O que não recupera: duas perdas no mesmo grupo. Contra isso o Reed-Solomon do Moonlight protegeria e este código não protege.
Por que XOR e não Reed-Solomon
| implementação | custo a 60 fps |
|---|---|
Reed-Solomon em Python puro (reedsolo) | 300% de um núcleo — inviável |
zfec (em C) | não compila no Python 3.14 |
| XOR intercalado com numpy | 0,15% (quadro comum), 0,97% (keyframe) |
Três ordens de grandeza, por uma perda de robustez que o intercalamento quase elimina.
O que isso compra
Medido com 400 quadros de 42 KB e perda sorteada por pacote. A coluna do meio é a fração que chega completa na primeira tentativa — em TCP o resto ainda chega, com uma ida e volta de atraso; em UDP sem correção, se perderia.
| perda | completos de primeira | com correção | banda a mais |
|---|---|---|---|
| 0,5% | 84,5% | 99,8% | 11,4% |
| 1% | 69,2% | 97,2% | 11,4% |
| 2% | 48,0% | 95,5% | 11,4% |
| 3% | 34,2% | 89,8% | 11,4% |
| 5% | 21,0% | 75,0% | 11,4% |
| 10% | 3,0% | 33,5% | 11,4% |
E o caso que motivou o intercalamento — uma rajada de quatro pacotes seguidos perdida em cada quadro: 0% sem correção, 100% com.
Fica no TCP quando o host não tem porta UDP disponível, quando o host é de uma versão anterior e ignora o pedido, ou quando o datagrama de abertura não chega. Em nenhum desses casos a sessão para ou fica sem imagem — o vídeo continua saindo pelo canal de sempre.
Vídeo
Dois modos, e a diferença entre eles não é “qualidade”, é o que cada um otimiza.
Modo Trabalho captura a tela, compara com a anterior e manda só os retângulos que mudaram, em JPEG. Numa planilha ou num editor de texto isso é quase nada de banda, e o texto fica nítido — não passa por compressão com perda temporal.
Modo Jogo é fluxo contínuo, codificado no hardware da placa.
| fabricante | H.264 | AV1 |
|---|---|---|
| NVIDIA | h264_nvenc | av1_nvenc |
| AMD | h264_amf | av1_amf |
| Intel | h264_qsv | av1_qsv |
| Linux (genérico) | h264_vaapi | av1_vaapi |
A escala também é por fabricante, e isso não é detalhe: em NVIDIA a
redução de resolução acontece na GPU (scale_cuda); nas
outras, depois de trazer o quadro para a memória principal. Medido:
captura nativa custa 5% de um núcleo, escala por CUDA 24%, escala em CPU
69%.
AV1 entrega a mesma imagem com menos banda, e só é usado quando as duas pontas têm hardware para ele. O host espera 400 ms pela lista de codecs do viewer antes de começar — sem essa espera ele começava em H.264 e ficava nele com os dois lados capazes de AV1. O padrão é H.264: o padrão seguro é o que todo mundo lê.
Resoluções oferecidas: 2160p, 1440p, 1080p, 900p, 720p, 540p, além da nativa. O teto de quadros por segundo é a taxa real do monitor do host. Pedir mais não produz imagem mais nova: a captura só entrega quadro quando a tela muda, e acima da taxa dela o que sai são repetições que custam banda e não trazem nada.
Som
O que atravessa é o que a outra máquina está tocando — vídeo, alarme, a música que ficou aberta. Não é microfone: o caminho de volta não existe ainda. Desligado por padrão, e só começa quando quem assiste pede.
| sistema | captura |
|---|---|
| Windows | WASAPI em modo loopback — grava a saída sem cabo e sem depender do “Stereo Mix”, que metade das placas não tem |
| Linux | o dispositivo .monitor do PulseAudio ou do PipeWire |
Silêncio não trafega, e isso começou como defeito. No Windows o loopback não entrega nada enquanto o sistema está calado — nem zeros, nada — e a primeira versão travava numa leitura que nunca voltava. Em vez de contornar sintetizando silêncio, o desenho aproveita: sem som tocando, nenhum pacote sai. Do outro lado, buraco no fluxo vira silêncio naturalmente.
Opus a 96 kbps em quadros de 20 ms, pelo PyAV — sem subprocesso e sem contêiner, porque o canal já delimita mensagem. Medido com um tom de 440 Hz: 469 KB de PCM viram 35 KB, e o tom volta em 440 Hz do outro lado.
Vai por TCP, e a razão é diferente da do vídeo: pacote de áudio perdido produz um estalo audível, enquanto quadro de vídeo perdido some sem ninguém notar — e corrigir estalo exige buffer, que é latência.
Som acompanha a permissão ver, sem permissão própria.
Ouvir o que a máquina toca é a mesma observação que ver a tela dela. Uma
permissão separada também não existiria nos papéis já assinados, e o
áudio nunca funcionaria para quem entrou por conta.
Pacote vazio não é pacote ruim: para o decodificador ele significa “acabou o fluxo”. Depois de receber um, todo pacote seguinte falha, e o som morre pelo resto da sessão sem nada na tela dizendo por quê. O decodificador descarta entrada vazia e se reconstrói a cada erro.
Como uma máquina é encontrada
Três caminhos, tentados em ordem:
- Endereço direto — IP,
IP:portaou domínio digitado. Não envolve servidor nenhum. - Broadcast na rede local — o ID é perguntado em UDP; a máquina que o tiver responde com o endereço dela. Funciona sem internet.
- Servidor de encontro — para fora da rede local. O host manda sinal de vida a cada 45 segundos; o servidor considera online quem apareceu nos últimos 120.
O endereço público não vai no corpo da mensagem: quem observa é o servidor, pelo socket. Se o dispositivo pudesse declarar o próprio endereço, daria para apontar um ID alheio para outra máquina.
Opcionalmente o host pede ao roteador que encaminhe a porta por UPnP. É melhor esforço: roteador sem UPnP não impede nada, e o programa avisa quando detecta CGNAT — caso em que não há porta a abrir.
API do servidor
Tudo em JSON sobre HTTPS. Erros voltam como
{"erro": "…"} com o status HTTP correspondente. Três formas
de autenticar, para três tipos de cliente:
- Token de conta —
Authorization: Bearer, para o que a pessoa faz. - Corpo assinado — o dispositivo assina o JSON
canônico com um
instantedentro. - Cabeçalhos assinados —
X-AssinaturaeX-Instante, para o dispositivo em GET.
Contas
| rota | autenticação | o que faz | |
|---|---|---|---|
| POST | /v1/contas | — | cria conta (pode exigir convite) |
| GET | /v1/contas/{email}/sal | — | sal do KDF, necessário para logar |
| POST | /v1/sessoes | — | login; devolve token e cofre cifrado |
| DELETE | /v1/sessoes | token | logout |
| GET | /v1/eu | token | dados da conta |
| DELETE | /v1/eu | token + senha | apaga a conta |
| POST | /v1/contas/chave | token | chave pública de alguém, pelo e-mail |
O sal volta aleatório para e-mail inexistente, e o login dá a mesma resposta para e-mail inexistente e senha errada. Distinguir os dois entregaria uma lista de quem tem conta.
Dispositivos
| rota | autenticação | o que faz | |
|---|---|---|---|
| POST | /v1/registrar | assinado | registra ID e chave |
| POST | /v1/presenca | assinado | sinal de vida com a porta |
| GET | /v1/localizar/{id} | — | endereço atual de um ID |
| POST | /v1/dispositivos/parear | token + assinado | liga máquina à conta |
| GET | /v1/dispositivos | token | os seus e os da organização |
| DELETE | /v1/dispositivos/{id} | token | tira da conta |
O pareamento exige as duas provas ao mesmo tempo: a assinatura do dispositivo, que só quem está na máquina produz, e o token da conta, que só quem sabe a senha tem. É o que garante que pareamento é presencial — quem roubar só a senha não acrescenta uma máquina que não tem na frente.
Organizações e grupos
| rota | autenticação | o que faz | |
|---|---|---|---|
| POST | /v1/organizacoes | token | cria; devolve o código de resgate uma vez |
| GET | /v1/organizacoes | token | as suas |
| DELETE | /v1/organizacoes/{id} | token, admin | apaga; exige o nome digitado de volta |
| GET | …/membros | token, membro | lista |
| POST | …/membros | token, admin | acrescenta |
| DELETE | …/membros/{conta} | token, admin | remove |
| POST | …/grupos | token | grava lista assinada |
| GET | …/grupos | token, membro | lista |
| DELETE | …/grupos/{grupo} | token, admin | apaga |
Do código de resgate o servidor guarda só o hash. Guardá-lo em claro deixaria o servidor recriar a raiz de confiança da organização sozinho. E nunca se remove um administrador se isso deixar a organização com menos de dois: um só significa que perder aquela máquina trava a frota inteira.
Autorizações
| rota | autenticação | o que faz | |
|---|---|---|---|
| POST | /v1/autorizacoes | assinatura do emissor | arquiva um papel |
| GET | /v1/dispositivos/{id}/autorizacoes | cabeçalhos assinados | o dispositivo busca a lista dele |
| GET | /v1/autorizacoes/minhas | token | máquinas compartilhadas comigo |
| DELETE | /v1/autorizacoes/{id} | token | revoga |
Ao arquivar, o servidor confere apenas que a assinatura bate com a chave que o emissor declara — o suficiente para não guardar lixo. Ele não decide se aquele emissor tem direito de autorizar aquele dispositivo. Quem decide é o dispositivo. Essa separação é o que impede o servidor de fabricar acesso.
Distribuição e diagnóstico
| rota | autenticação | o que faz | |
|---|---|---|---|
| GET | /v1/versao | — | versão publicada, tamanho e SHA-256 |
| GET | /baixar/{plataforma} | — | o instalador |
| GET | /v1/saude | — | sinal de vida |
| GET | /v1/numeros | token | contagens |
Banco de dados
PostgreSQL 16, acessado por asyncpg com pool de conexões.
| tabela | guarda |
|---|---|
contas | e-mail, hash da prova, sais, chave pública, cofre cifrado |
sessoes | hash do token, validade |
organizacoes | nome, hash do código de resgate, assentos |
membros | quem está em qual organização, com qual papel |
dispositivos | ID, chave pública, endereço visto, dono, organização |
grupos | documentos de grupo assinados |
autorizacoes | envelopes assinados, destinatário, validade |
Nada de segredo em claro: só hashes, chaves públicas e blobs cifrados que o servidor não consegue abrir.
As datas são epochs em BIGINT, e não
TIMESTAMPTZ, porque é isso que o protocolo troca com os
dispositivos — converter nas bordas criaria duas representações do mesmo
instante para manter em dia.
Apagar uma conta leva sessões e participações junto,
mas não os dispositivos: dispositivos.conta_id é
ON DELETE SET NULL. O programa continua instalado e rodando na
máquina de alguém, e apagar o registro não desligaria nada — só faria o ID
sumir da busca sem avisar ninguém. A mesma lógica vale para apagar uma
organização.
Operação
Serviço systemd, com Restart=always,
ProtectSystem e EnvironmentFile.
O DSN do banco vem do ambiente, nunca do código: ele
carrega a senha do banco, e senha em arquivo versionado é senha publicada.
O .env fica com permissão 600 e é lido pelo systemd. Nos logs
a senha aparece mascarada.
| variável | efeito |
|---|---|
OUTLINK_DSN | conexão do PostgreSQL |
OUTLINK_CONVITE | preenchida, o cadastro só aceita quem souber o código |
--confiar-proxy | passa a ler X-Forwarded-For |
X-Forwarded-For só é lido quando há proxy declarado.
Confiar nele sem proxy na frente deixaria o atacante escolher a própria
chave de freio — bastaria mandar um cabeçalho diferente a cada tentativa.
TLS é obrigatório para o painel: o navegador só libera WebCrypto em contexto seguro.
Atualização
O programa consulta /v1/versao e oferece —
não instala sozinho. Instalar sem perguntar reinicia o programa de alguém
no meio de uma sessão.
A comparação de versão é numérica por componente, e não textual: como
texto, "1.0.9" > "1.0.10", e a versão 10 nunca seria
oferecida a quem está na 9.
O arquivo desce com sufixo .parcial e só recebe o nome
final depois de o SHA-256 conferir. Uma queda no meio não deixa um
instalador quebrado com cara de bom esperando ser executado.
O SHA-256 confere integridade, não origem: ele diz que o arquivo chegou inteiro, e não que veio de nós. Quem controlasse o servidor publicaria um executável e o hash correspondente dele. A defesa contra isso é assinatura de código, que ainda não existe.
Testes
pip install -r requirements-dev.txt python -m pytest # os que conversam com o servidor pulam sozinhos; para rodá-los: OUTLINK_TEST_SERVIDOR=https://seu-servidor:8787 python -m pytest
Eles criam contas de nome sorteado e apagam tudo no fim, inclusive quando o teste falha.
A maioria da suíte é de recusa. O caminho que funciona aparece no uso diário; o caminho que recusa só aparece quando alguém tenta, e é ele que sustenta o que o produto promete. Os handshakes são testados sobre socket de verdade, com host e viewer reais — metade dos erros desse tipo de código está na ordem dos bytes, e objeto falso não pega isso.
Limites conhecidos
Coisas que o Outlink ainda não faz, ditas aqui porque descobrir depois é pior.
- O áudio vai só numa direção, do host para quem assiste. Microfone, ou seja falar de volta, não existe.
- Não há suporte a controle de videogame, nem HDR.
- A correção de erro é XOR, não Reed-Solomon. Duas perdas no mesmo grupo de paridade não se recuperam. O Moonlight protege esse caso; nós protegemos rajada, que é o formato comum de perda.
- Controle e arquivos continuam em TCP. É a escolha certa para eles, mas uma perda no canal de controle ainda custa uma ida e volta.
- A conversão de cor acontece na CPU, e custa cerca de 7,5 ms por quadro. É o maior custo isolado da cadeia de vídeo.
- Não há assinatura de código ainda. O Windows avisa que o editor é desconhecido. A esteira de assinatura está pronta e testada; falta o certificado, que só é emitido mediante verificação de identidade da empresa.
- No Linux, Wayland depende do portal de captura, com as limitações dele.
- Não há cliente móvel.
Outlink 1.3.1 — documentação técnica.