Voltar ao site
Outlink

Outlink por dentro

O protocolo, o modelo de segurança e a arquitetura do Outlink — acesso remoto em que a sessão é direta entre as duas máquinas e o servidor não consegue abrir nenhuma.

versão 1.3.1 Python 3.11+ Ed25519 · X25519 · AES-GCM PostgreSQL 16
01

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.

02

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.

O QUE A CRIPTOGRAFIA AQUI NÃO RESOLVE

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á.

03

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.

04

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.

O PREÇO

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:

chavelimitecastigo
por IP10 falhas em 5 min15 min
por conta30 falhas em 5 min5 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.

05

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ãoo que libera
verreceber a imagem da tela
controlarmouse, teclado e área de transferência
arquivosa pasta compartilhada
sem_aprovacaoentrar 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

  1. A assinatura confere com a chave que diz ter assinado?
  2. 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.
  3. O papel é para esta máquina mesmo? Sem isso, um papel válido para o computador da recepção serviria no servidor de arquivos.
  4. Ainda está no prazo?
  5. Quem está batendo na porta é o destinatário do papel?
  6. 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.

06

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

HOSTQUEM CONECTA
01
host ──▶ MAGIC, sal, desafio e chave efêmera
◀── viewer desafio, chave efêmera e prova
02
03
host ──▶ prova de que o host também conhece a senha

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.

LIMITE CONHECIDO

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.

HOSTQUEM CONECTA
01
host ──▶ desafio, efêmera e chave pública do aparelho
◀── viewer desafio, efêmera, chave pública da conta, assinatura do transcrito e autorizações assinadas
02
03
host ──▶ assinatura do transcrito
transcrito = SHA-256( MAGIC_CONTA
                    ‖ desafio_host   ‖ desafio_viewer
                    ‖ efêmera_host   ‖ efêmera_viewer
                    ‖ id_hostid_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.

07

O canal de dados

Depois do aperto de mão, o mesmo socket TCP carrega tudo, multiplexado em canais.

canalconteúdo
1 — controleJSON: estado, mouse, teclado, área de transferência, comandos
2 — vídeobinário: retângulos JPEG ou unidades H.264/AV1
3 — arquivosbiná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.

HOSTQUEM CONECTA
◀── viewer quero UDP
01
02
host ──▶ minha porta é N, e o segredo é este
◀── viewer datagrama de abertura, a cada 10 s
03
04
host ──▶ aprende o endereço de retorno e manda o vídeo por lá

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çãocusto 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 numpy0,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.

perdacompletos de primeiracom correçãobanda 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.

QUANDO ELE NÃO É USADO

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.

08

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.

fabricanteH.264AV1
NVIDIAh264_nvencav1_nvenc
AMDh264_amfav1_amf
Intelh264_qsvav1_qsv
Linux (genérico)h264_vaapiav1_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.

sistemacaptura
WindowsWASAPI em modo loopback — grava a saída sem cabo e sem depender do “Stereo Mix”, que metade das placas não tem
Linuxo 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.

UM ERRO QUE FALHA CALADO

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.

09

Como uma máquina é encontrada

Três caminhos, tentados em ordem:

  • Endereço direto — IP, IP:porta ou 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.

10

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 contaAuthorization: Bearer, para o que a pessoa faz.
  • Corpo assinado — o dispositivo assina o JSON canônico com um instante dentro.
  • Cabeçalhos assinadosX-Assinatura e X-Instante, para o dispositivo em GET.

Contas

rotaautenticaçãoo que faz
POST/v1/contascria conta (pode exigir convite)
GET/v1/contas/{email}/salsal do KDF, necessário para logar
POST/v1/sessoeslogin; devolve token e cofre cifrado
DELETE/v1/sessoestokenlogout
GET/v1/eutokendados da conta
DELETE/v1/eutoken + senhaapaga a conta
POST/v1/contas/chavetokenchave 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

rotaautenticaçãoo que faz
POST/v1/registrarassinadoregistra ID e chave
POST/v1/presencaassinadosinal de vida com a porta
GET/v1/localizar/{id}endereço atual de um ID
POST/v1/dispositivos/pareartoken + assinadoliga máquina à conta
GET/v1/dispositivostokenos seus e os da organização
DELETE/v1/dispositivos/{id}tokentira 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

rotaautenticaçãoo que faz
POST/v1/organizacoestokencria; devolve o código de resgate uma vez
GET/v1/organizacoestokenas suas
DELETE/v1/organizacoes/{id}token, adminapaga; exige o nome digitado de volta
GET…/membrostoken, membrolista
POST…/membrostoken, adminacrescenta
DELETE…/membros/{conta}token, adminremove
POST…/grupostokengrava lista assinada
GET…/grupostoken, membrolista
DELETE…/grupos/{grupo}token, adminapaga

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

rotaautenticaçãoo que faz
POST/v1/autorizacoesassinatura do emissorarquiva um papel
GET/v1/dispositivos/{id}/autorizacoescabeçalhos assinadoso dispositivo busca a lista dele
GET/v1/autorizacoes/minhastokenmáquinas compartilhadas comigo
DELETE/v1/autorizacoes/{id}tokenrevoga

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

rotaautenticaçãoo que faz
GET/v1/versaoversão publicada, tamanho e SHA-256
GET/baixar/{plataforma}o instalador
GET/v1/saudesinal de vida
GET/v1/numerostokencontagens
11

Banco de dados

PostgreSQL 16, acessado por asyncpg com pool de conexões.

tabelaguarda
contase-mail, hash da prova, sais, chave pública, cofre cifrado
sessoeshash do token, validade
organizacoesnome, hash do código de resgate, assentos
membrosquem está em qual organização, com qual papel
dispositivosID, chave pública, endereço visto, dono, organização
gruposdocumentos de grupo assinados
autorizacoesenvelopes 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.

12

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ávelefeito
OUTLINK_DSNconexão do PostgreSQL
OUTLINK_CONVITEpreenchida, o cadastro só aceita quem souber o código
--confiar-proxypassa 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.

13

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 QUE O HASH NÃO PROVA

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.

14

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.

15

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.