Pular para o conteúdo
Decoding Chat API

Atrás do seu próprio proxy reverso

O Decoding Chat roda atrás de qualquer proxy que termine o TLS e encaminhe upgrades de WebSocket.

A stack inclui o Caddy e o executa por padrão nas portas 80 e 443. Um arquivo Compose extra interrompe isso e publica apenas uma única porta HTTP simples. Essa porta já roteia todos os caminhos para o serviço interno correto, então o proxy na frente precisa de uma regra de roteamento: enviar tudo para ela.

docker-compose.proxy.yml vem junto com o docker-compose.yml e é aplicado sobre ele. Passe ambos os arquivos em todos os comandos:

Janela do terminal
docker compose -f docker-compose.yml -f docker-compose.proxy.yml up -d

Ou defina COMPOSE_FILE no .env uma vez e continue digitando comandos simples do docker compose:

COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml

Em uma primeira instalação, sh install.sh --tls proxy escreve essa linha e FLUXER_EDGE_BIND no .env para você, então não há nada para adicionar manualmente. Sinalizadores do instalador o lista com o resto.

A porta se liga a 127.0.0.1:8080 por padrão. FLUXER_EDGE_BIND define a interface e a porta do host. Mova a porta quando outra coisa no host já ocupar a 8080:

FLUXER_EDGE_BIND=127.0.0.1:8081

O lado do contêiner permanece 8080 independentemente do que você definir. Um proxy no host então aponta para 127.0.0.1:8081, e um proxy na própria rede Docker da stack aponta para edge:8080. Vincule um endereço roteável apenas quando o proxy for executado em outra máquina e configure um firewall na porta para essa máquina:

FLUXER_EDGE_BIND=0.0.0.0:8080

Confirme se a porta responde antes de configurar qualquer coisa na frente dela:

Janela do terminal
curl -i http://127.0.0.1:8080/_health

/_health retorna 200 OK da própria borda sem alcançar um upstream, então esta é a verificação de integridade para dar ao seu proxy.

Com FLUXER_PUBLIC_SCHEME=https, o cookie CSRF de administração é __Host-csrf_token, que os navegadores aceitam apenas via HTTPS.

A conexão do cliente é em /gateway e a sinalização de voz é em /livekit/*. Ambos abrem como upgrades de WebSocket. Verifique se o handshake é concluído através do que quer que esteja na frente da instância, com o seu próprio hostname no lugar de chat.example.com:

Janela do terminal
curl -sS -i --http1.1 --max-time 5 \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==' \
'https://chat.example.com/gateway?v=1&encoding=json' | head -1

A resposta deve ser HTTP/1.1 101 Switching Protocols. Confirme se um cliente real também se conecta, o que cobre todo o caminho, incluindo a string de consulta.

Substituir X-Forwarded-For pelo endereço real do cliente

Seção intitulada “Substituir X-Forwarded-For pelo endereço real do cliente”

Defina o cabeçalho a partir do endereço de conexão e descarte o que o visitante enviou. Limites de taxa, banimentos de IP e detecção de abuso aplicam-se ao endereço que chega nele.

Aceitar um corpo de solicitação acima do limite de anexo

Seção intitulada “Aceitar um corpo de solicitação acima do limite de anexo”

Os uploads passam pelo proxy. Aumente qualquer limite de corpo padrão, como o de 1 MB do nginx, para acima do limite de anexo da instância.

O soquete do Gateway permanece aberto durante a vida de uma sessão de cliente, portanto, dê aos tempos limite ociosos e de leitura o valor de uma hora.

Os navegadores o enviam, e o painel de administração recusa uma solicitação de alteração com um valor de site cruzado.

A instância envia sua própria política, e seu nonce é o que permite que a aplicação web inicie. Os navegadores impõem todas as políticas que recebem.

Todas as configurações detalhadas abaixo atendem a todos esses requisitos.

As variáveis abaixo dizem à instância o endereço que os navegadores usam. O Decoding Chat não lê nem X-Forwarded-Proto nem X-Forwarded-Host, então você os mantém corretos manualmente:

FLUXER_DOMAIN=chat.example.com
FLUXER_PUBLIC_SCHEME=https
FLUXER_PUBLIC_PORT=443

Eles permanecem https na 443 mesmo que a própria instância fale HTTP simples na 8080. Os clientes leem cada URL base a partir do documento de descoberta que a API constrói a partir desses valores.

Servir em uma porta diferente de 443 significa alterar uma dessas variáveis:

FLUXER_PUBLIC_PORT=8443

O seu proxy detém a porta pública, e o overlay fixa a borda em HTTP simples na 8080 seja qual for essa porta, então nada mais no .env muda com isso.

Deixe FLUXER_PUBLIC_ORIGIN comentado, ou mantenha-o consistente com todas as três configurações. Endereços conflitantes podem quebrar o login e a configuração. Veja A origem pública.

O bloco map faz com que Connection siga Upgrade. O trecho aumenta o limite do corpo e o tempo limite de leitura acima do próprio limite de 1 MB e 60 segundos do nginx.

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name chat.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name chat.example.com;
ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
client_max_body_size 512m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_request_buffering off;
proxy_buffering off;
}
}

Defina X-Forwarded-For a partir de $remote_addr. $proxy_add_x_forwarded_for anexa o que o cliente enviou, o que coloca um endereço escolhido por um invasor na frente do endereço real.

Um Caddy externo precisa de um bloco de site. Ele encaminha upgrades de WebSocket nativamente, solicita o próprio certificado e não aplica nenhum limite de corpo de solicitação nem tempo limite de leitura próprio. Por padrão, ele ignora os cabeçalhos de entrada X-Forwarded-* de clientes não confiáveis e escreve os seus próprios, portanto, não há linha de cabeçalho a ser adicionada.

chat.example.com {
reverse_proxy 127.0.0.1:8080
}

O Caddy não confia em nenhum proxy por padrão. Se outro proxy ou CDN estiver na frente dele, configure a opção global trusted_proxies do Caddy e os proxies confiáveis do Decoding Chat para que as solicitações retenham o endereço real do cliente.

O Traefik encaminha upgrades de WebSocket nativamente e, por padrão, remove os cabeçalhos X-Forwarded-* de entrada de clientes não confiáveis e escreve os seus próprios. Deixe forwardedHeaders como está, a menos que outro proxy esteja na frente do Traefik.

Configuração estática:

entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: 0s
idleTimeout: 3600s
providers:
file:
filename: /etc/traefik/dynamic.yml
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /etc/traefik/acme.json
httpChallenge:
entryPoint: web

readTimeout tem o padrão de 60 segundos e cobre a leitura de toda a solicitação, incluindo o corpo, então 0s remove o limite e permite que um grande upload termine. idleTimeout tem o padrão de 180 segundos, então aumente para uma hora para manter o soquete do Gateway aberto.

Configuração dinâmica em /etc/traefik/dynamic.yml:

http:
routers:
fluxer:
rule: Host(`chat.example.com`)
entryPoints:
- websecure
service: fluxer
tls:
certResolver: letsencrypt
services:
fluxer:
loadBalancer:
passHostHeader: true
servers:
- url: http://127.0.0.1:8080

O Traefik atinge o contêiner por meio de uma rede Docker compartilhada, portanto, a porta publicada não é usada. Mantenha o overlay aplicado de qualquer maneira, ou a instância pegará as portas 80 e 443 do Traefik.

Coloque isso em traefik.compose.yml junto com a stack:

services:
edge:
networks:
- fluxer
- traefik
labels:
traefik.enable: "true"
traefik.docker.network: traefik
traefik.http.routers.fluxer.rule: Host(`chat.example.com`)
traefik.http.routers.fluxer.entrypoints: websecure
traefik.http.routers.fluxer.tls.certresolver: letsencrypt
traefik.http.services.fluxer.loadbalancer.server.port: "8080"
networks:
traefik:
external: true

Em seguida, liste todos os arquivos para que todos os comandos os captem:

COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml:traefik.compose.yml

Um frontend, um backend e os tempos limite:

defaults
mode http
log stdout format raw local0
option httplog
timeout connect 5s
timeout client 1h
timeout server 1h
timeout tunnel 1h
frontend fluxer
bind :80
bind :443 ssl crt /etc/haproxy/certs/chat.example.com.pem alpn h2,http/1.1
http-request redirect scheme https unless { ssl_fc }
http-request set-header X-Forwarded-For %[src]
default_backend fluxer_edge
backend fluxer_edge
server edge 127.0.0.1:8080

timeout client e timeout server deixam de ser aplicados assim que uma conexão é atualizada, portanto, timeout tunnel é o que mantém o soquete do Gateway e a sinalização LiveKit ativos. set-header substitui qualquer cabeçalho que o cliente tenha enviado. option forwardfor anexa a ele. Dê nomes diferentes para o frontend e o backend. O HAProxy avisa sobre um nome compartilhado e deixará de oferecer suporte a ele na versão 3.3.

Esta configuração precisa desses módulos: mod_proxy, mod_proxy_http e mod_headers. Desde a versão 2.4.47, mod_proxy_http lida com o upgrade do WebSocket em si, então mod_proxy_wstunnel não é necessário.

<VirtualHost *:443>
ServerName chat.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/chat.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/chat.example.com/privkey.pem
LimitRequestBody 536870912
ProxyPreserveHost On
ProxyTimeout 3600
RequestHeader set X-Forwarded-For "expr=%{REMOTE_ADDR}"
ProxyPass / http://127.0.0.1:8080/ upgrade=websocket timeout=3600
ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>

upgrade=websocket é a configuração inteira do WebSocket na versão 2.4.47 e posteriores.

RequestHeader set substitui o que o cliente enviou, e as aspas ao redor de "expr=%{REMOTE_ADDR}" são exigidas pelo analisador de expressões. ProxyAddHeaders está ativado por padrão e anexa o endereço de conexão após esse valor. A borda lê o endereço real de qualquer forma.

LimitRequestBody é 0 por padrão, o que significa sem limite, mas pacotes de distribuição e blocos <Directory> geralmente o definem como menor. O 536870912 acima corresponde a 512 MiB. ProxyTimeout e o parâmetro timeout=3600 limitam a conexão upstream, e uma hora cobre o soquete do Gateway.

Uma regra de entrada para o hostname, com o requisito abrangente que o cloudflared necessita abaixo dela:

tunnel: fluxer
credentials-file: /etc/cloudflared/fluxer.json
ingress:
- hostname: chat.example.com
service: http://127.0.0.1:8080
originRequest:
connectTimeout: 30s
- service: http_status:404

Quando o cloudflared for executado como um contêiner na rede Docker da stack, aponte-o para http://edge:8080. A porta publicada então nunca será usada. O Compose prefixa o nome da rede com o nome do projeto, que a stack define como fluxer, então a rede é fluxer_fluxer e uma declaração externa a nomeia por completo:

networks:
fluxer:
external: true
name: fluxer_fluxer

A Cloudflare anexa o endereço do visitante a qualquer X-Forwarded-For que o visitante enviou, para que o cabeçalho possa chegar com um valor forjado na frente do endereço real. A borda lê o endereço mais à direita que não está em FLUXER_EDGE_TRUSTED_PROXIES, que é o que a Cloudflare anexou. Uma Regra de Transformação que define X-Forwarded-For para cf.connecting_ip remove a ambiguidade.

A Cloudflare limita o corpo da solicitação a 100 MB nos planos Free e Pro, 200 MB no Business e 500 MB no Enterprise, e responde com 413 acima do limite. O limite se aplica ao tráfego do túnel, e nenhuma configuração do cloudflared o aumenta. Mantenha o limite max_attachment_file_size abaixo da capacidade de seu plano, no painel de administração em configuração de limites. O padrão é 26214400 bytes para uma conta não premium e 524288000 bytes para uma conta premium, que está acima de todos os limites abaixo do Enterprise.

Um túnel tem a aplicação web, API, Gateway, painel de administração, rotas de mídia e sinalização LiveKit, mas nenhuma mídia LiveKit.

Adicione um host de proxy com estes valores:

CampoValor
Domain NamesO hostname chat.example.com
Schemehttp simples
Forward Hostname / IP127.0.0.1, ou o contêiner edge quando ambos rodarem no Docker
Forward PortPorta 8080
Websockets SupportAtivado, e obrigatório
Block Common ExploitsDesativado, porque a sua filtragem de solicitações rejeita tráfego legítimo de API
SSLSolicite um certificado, depois ative Force SSL e HTTP/2 Support

Na guia Advanced:

client_max_body_size 512m;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

O Nginx Proxy Manager anexa ao X-Forwarded-For, de modo que o próprio valor do visitante chega na frente do endereço real. A borda pega a entrada mais à direita que não está em FLUXER_EDGE_TRUSTED_PROXIES, que é a que o Nginx Proxy Manager anexou. Os usuários em uma LAN ou uma VPN dentro das private_ranges padrão são a exceção e se resolvem para a entrada forjada, portanto, restrinja FLUXER_EDGE_TRUSTED_PROXIES ao endereço do proxy nesse layout.

Encaminhe cada caminho e string de consulta inalterados. A borda lida com o roteamento:

Caminho públicoPropósito
/_healthVerificação de integridade da borda
/gateway, /gateway/*Conexões e integridade do Gateway
/api/*API HTTP
/media/*Mídia e entrega de anexos
/livekit/*Sinalização de voz
/admin, /admin/*Painel de administração
/web/*, /emoji/*, /libs/*, /avatars/*, /badges/*, /desktop/*, /embeds/*Ativos estáticos
/.well-known/fluxerDescoberta de instância
/.well-known/apple-app-site-association, /apple-app-site-associationAssociação de aplicativos da Apple
/.well-known/assetlinks.jsonAssociação de aplicativos do Android
/version.jsonMetadados de versão do cliente

Todos os outros caminhos servem a aplicação web.

Passe a string de consulta em /gateway intacta. Os clientes sempre enviam ?v=, ?encoding=, ?compress= e ?stream=, e 1 é a única versão que o Gateway aceita.

A Apple e o Google exigem os arquivos de associação naqueles caminhos fixos para preenchimento automático de senhas salvas e links de aplicativos. Um proxy que encaminha todos os caminhos não precisa de regras extras. Inclua-os explicitamente se você usar uma lista de permissões de caminhos.

/_metrics na API, Media Proxy e Gateway, além de /_health/ready, /_health/drain e /_health/undrain no Gateway, são restritos ao loopback e inacessíveis por meio de qualquer proxy. As sondagens que funcionam através de um proxy são /_health, /api/_health, /gateway/_health e /media/_health.

FLUXER_EDGE_TRUSTED_PROXIES nomeia os endereços de pares cujo X-Forwarded-For a borda acredita. O padrão é private_ranges, que o Caddy expande para 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8, 127.0.0.1/8, fd00::/8 e ::1, então um proxy no mesmo host ou na mesma rede Docker não precisa de alteração.

A borda resolve um endereço de cliente por solicitação e reescreve X-Forwarded-For para ele em cada salto upstream, então nenhum serviço lê o que um visitante enviou. Um par fora da lista se torna o endereço do cliente, e a borda descarta o cabeçalho que ele enviou. Para um par dentro da lista, a borda usa a entrada do cabeçalho mais à direita que não é ela própria confiável. Portanto, um proxy que anexa ao cabeçalho ainda passa o endereço real do cliente.

Um proxy que acessa a instância a partir de um endereço público não é confiável, então a borda descarta o seu cabeçalho e atribui todas as solicitações ao próprio proxy. Isso coloca todos os seus usuários no mesmo balde de limite de taxa e na mesma geolocalização. Adicione o endereço e liste as private_ranges também se você ainda precisar:

FLUXER_EDGE_TRUSTED_PROXIES=private_ranges 203.0.113.10/32

As private_ranges não cobrem 100.64.0.0/10, então um proxy acessando a instância por meio de um endereço Tailscale ou outra tailnet CGNAT não é confiável e reduz todos os visitantes àquele único endereço. Nomeie o endereço da tailnet do proxy:

FLUXER_EDGE_TRUSTED_PROXIES=private_ranges 100.101.102.103/32

A borda aceita o que um par confiável enviar. Qualquer pessoa que consiga abrir uma conexão TCP a partir de um endereço confiável pode definir X-Forwarded-For para qualquer valor e ser registrado como esse endereço, o que derrota banimentos e limites de taxa e polui a detecção de abusos. Mantenha a lista restrita ao endereço único do qual seu proxy chega. Essa também é a única lista correta em uma implantação de LAN ou VPN, onde um visitante cujo próprio endereço se enquadra em uma lista mais ampla é registrado sob o endereço do proxy.

Encontre o endereço que a borda vê a chegada de um proxy do lado do host:

Janela do terminal
docker network inspect fluxer_fluxer -f '{{(index .IPAM.Config 0).Gateway}}'

A borda lê a variável no início do contêiner, então aplique uma alteração com docker compose up -d.

Leituras de mídia são o único tráfego que vale a pena fazer cache, e a instância não espera nenhum cache na frente dela. Toda regra abaixo é uma regra de correção em vez de uma escolha de ajuste, e um cache que não consiga seguir uma não deve armazenar mídia de forma alguma.

Deixe os parâmetros de assinatura fora da chave de cache

Seção intitulada “Deixe os parâmetros de assinatura fora da chave de cache”

ex, is, hm e uc são os parâmetros de URLs de anexos assinadas. Uma chave que os mantém armazena uma nova entrada toda vez que o mesmo objeto for assinado novamente, o que esvazia o cache de qualquer coisa útil. Remova todos os quatro e mantenha todos os outros parâmetros, pois os restantes selecionam a representação.

Somente os remova se o seu proxy também atender às próximas duas regras. Uma chave sem eles é a mesma chave para uma solicitação assinada e uma não assinada, de modo que a verificação de assinatura é a única coisa entre uma solicitação não assinada e o objeto.

Decodifique os nomes de parâmetros e pegue o último valor

Seção intitulada “Decodifique os nomes de parâmetros e pegue o último valor”

O Decoding Chat analisa uma string de consulta com decodificação de URL de formulário, onde %73ize é o nome size e + é um espaço. Quando um nome se repete, o valor final ganha. Um cache que compara bytes brutos ou que mantém o primeiro valor pode receber %73ize=64 ou size=64&size=4096 e ser levado a armazenar uma renderização sob a chave para outra. Atenda às duas regras ou recuse o armazenamento de uma solicitação cuja string de consulta você não consiga analisar dessa forma.

Um acerto de cache nunca chega à instância, então a verificação de assinatura do próprio Media Proxy nunca é executada nela. Sob a política enforce de URLs de anexos assinadas, verifique a assinatura no proxy antes de ele consultar o cache. Um access_by_lua_block do nginx é executado na fase de acesso, que ocorre antes da pesquisa do cache. Um proxy que não consiga fazer isso deve deixar /attachments/ fora do cache, ou ele servirá uma URL expirada ou forjada de uma entrada ativa.

Verifique o mesmo caminho a partir do qual a chave do cache é construída

Seção intitulada “Verifique o mesmo caminho a partir do qual a chave do cache é construída”

Apenas ser executado antes da pesquisa no cache não é suficiente. A verificação e a chave devem ler a mesma grafia do caminho, ou uma solicitação ignorada pela verificação ainda atinge a entrada nomeada pela chave.

No nginx, essa grafia é $uri, a qual o nginx já decodificou por porcentagem, mesclou barras repetidas e resolveu . e ... $request_uri são os bytes brutos que o cliente enviou. Uma verificação com escopo em $request_uri nunca vê //attachments/..., /attachments%2F..., /attachments/./... ou /x/../attachments/... como uma leitura de anexo, enquanto $uri e a chave de cache tornam todos os quatro o mesmo objeto. Leia $uri para o teste de prefixo e para a chave de armazenamento, extraia a consulta de $request_uri e recuse um caminho cujo $uri não seja uma chave de armazenamento utilizável em vez de deixá-lo passar.

A assinatura cobre a chave de armazenamento decodificada, que é $uri sem sua barra inicial.

Não armazene em cache uma solicitação de intervalo sem suporte de fatias

Seção intitulada “Não armazene em cache uma solicitação de intervalo sem suporte de fatias”

O nginx remove Range de uma solicitação que pode armazenar em cache, a menos que o módulo de fatias seja incorporado, de modo que buscar um pedaço de um vídeo atrai o objeto inteiro. Defina proxy_cache_max_range_offset 0. Uma solicitação de intervalo então chega intacta à instância e não é armazenada, enquanto uma entrada que já está ativa ainda responde com intervalos pelo cache.

Configure a limpeza (purge) e limite a vida útil da entrada de qualquer forma

Seção intitulada “Configure a limpeza (purge) e limite a vida útil da entrada de qualquer forma”

Todo retorno 200 de mídia é public, max-age=31536000 sem ETag e sem Last-Modified, então um cache nunca revalida e um objeto deletado permanece até que sua entrada acabe. Aponte FLUXER_CACHE_PURGE_HTTP_ENDPOINT para um receptor que limpe seu cache e leia o contrato que ele deve cumprir em Limpeza de cache. Dê às mídias armazenadas uma vida útil menor e configurada por você, pois uma limpeza que for perdida nunca é enviada novamente.

FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE é a chave da instância, e um proxy que verifica as assinaturas possui a sua própria. Ambos os lados nomeiam os mesmos três estados, off, report e enforce. Ambos leem suas configurações uma vez no início, então aplique uma mudança na instância com docker compose up -d media-proxy, que substitui o contêiner. O comando docker compose restart media-proxy reaproveita o ambiente antigo e não altera nada.

Ative a política nesta ordem e pare sempre que aparecer uma recusa que você não esperava:

  1. Dê a api, worker e media-proxy a mesma FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64 e dê ao proxy a mesma lista. Toda URL que a instância retornar é assinada a partir daqui, e nada verifica ainda.
  2. Deixe assim até que os clientes detentores de URLs não assinadas emitidas antes do passo 1 as tenham substituído. Um cliente atualiza uma URL cuja validade ex está ausente ou expira em menos de uma hora, e aquele que está aberto desde antes do passo 1 mantém suas URLs não assinadas até ser recarregado.
  3. Coloque ambos os lados em report e leia os logs. Em report, nada é recusado, e cada leitura que o enforce recusaria é registrada com seu motivo no lado que a enxergou.
  4. Mova o proxy para enforce quando o report ficar silencioso.
  5. Mova a instância para enforce.

O proxy é o primeiro porque um acerto no cache nunca alcança a instância. Reverter os passos 4 e 5 deixa toda entrada ativa legível sem uma assinatura pelo tempo que ela viver.

Do passo 3 em diante, a instância limita o cabeçalho Cache-Control em uma leitura cuja assinatura seja válida no momento de envio dessa assinatura, onde o valor off retornaria um ano. A opção report também faz essa limitação, para que as entradas armazenadas durante a simulação (dry run) não durem mais que as URLs que as originaram. Uma leitura que report recusaria ainda retorna um ano de duração de cache, pois não há assinatura para delimitá-lo. Entradas armazenadas antes do passo 3 mantêm a validade de um ano que lhes foi dada.

ProxyInstânciaO que uma leitura de anexo recebe
offoffServido. Nenhum dos lados lê uma assinatura
offreportServido. A instância registra o que o enforce recusaria
offenforceRecusado, a menos que uma entrada ativa responda sem chegar à instância
reportoffServido. O proxy registra o que o seu próprio enforce recusaria
reportreportServido. Ambos os lados registram
reportenforceRecusado, a menos que uma entrada ativa responda sem chegar à instância
enforceoffRecusado no proxy, antes da pesquisa de cache
enforcereportRecusado no proxy. A instância não registra nada, pois nunca é consultada
enforceenforceRecusado no proxy

As duas linhas que servem uma leitura que a instância recusaria são aquelas em que a instância força as regras (enforce) sozinha. A instância está certa e o cache está obsoleto, e a entrada atende a chamadas até expirar, então vincule uma alteração originada na instância com um expurgo do /attachments/.

Uma mudança no FLUXER_MEDIA_PROXY_CORS_MODE apresenta o mesmo problema e não tem uma ordem de passos que a evite. Uma entrada armazenada enquanto a política estava off contém Access-Control-Allow-Origin: * e um ano de Cache-Control, e o proxy reproduz os dois valores em todo acerto no cache sem perguntar à instância. A própria verificação de assinatura do proxy não ajuda, pois ele lê a assinatura e nunca lê o Origin, fazendo com que uma página externa portando uma única URL assinada e válida ainda consiga ler os bytes em contexto de origem cruzada a partir de uma entrada de cache ativa. Expurge o diretório /attachments/ ao mudar a política de CORS para enforce, seguindo a mesma instrução de expurgo contida no passo 5.

O parâmetro enforce apenas no proxy alcança toda e qualquer requisição que passa por ele. Tudo aquilo que alcançar a instância por outras vias passará sem ser verificado até que a instância ative a imposição em sua extremidade: um segundo nível de cache, uma sonda (probe) de saúde oriunda de fora do proxy, um serviço interno que leia URLs diretamente dos anexos, ou uma infraestrutura operando totalmente sem a camada de proxy. O passo 5 cuida dessas eventuais deficiências, embora possa causar interrupções.

O passo 4 também interrompe a apresentação das evidências provenientes da instância. Uma leitura recusada pelo proxy nunca atinge a instância, o que significa que, uma vez que a política seja forçada (enforce) no proxy, a instância não registrará nada via report, e seu stream vai permanecer vazio, independentemente do que teria recusado. O passo 5 deve ser executado com base nos achados apurados durante o passo 3, quando ambos os componentes ainda encontravam-se no modo de relatório (report).

Ambos os lados devem manter a mesma lista secreta de chaves. O lado que estiver sem a chave correspondente recusará as URLs rubricadas com ela; portanto, adicione sempre a nova chave a ambos os lados antes de colocar a instância para assinar URLs com base nessa nova referência, e conserve uma chave aposentada nos dois locais até expirar a validade estipulada pela última URL associada à assinatura antiga. Uma URL padrão vence no prazo limite de um dia. A URL do pacote de dados nunca expira e é a remoção da chave usada para firmar sua autenticidade a única forma cabível de invalidá-la.

Desprovidos dessa lista de referência sigilosa, api e worker devolverão URLs desacompanhadas de assinatura, forçando um dos lados configurados para a verificação rigorosa do parâmetro enforce a inviabilizar toda tentativa de leitura do anexo. Nunca mude um lado para enforce antes de a assinatura encontrar-se ativada.

A sinalização do LiveKit passa pelo /livekit/* assim como todo o resto. As mídias do WebRTC não tocam no proxy em momento algum:

  • 7882/udp é mídia.
  • 7881/tcp é o fallback quando o UDP está bloqueado.

Ambas as portas são publicadas diretamente pela stack e devem alcançar o host. Um proxy ou túnel na frente da porta 443 não faz nada por elas.

Hospedar o LiveKit em um hostname diferente de FLUXER_DOMAIN significa expandir o Content-Security-Policy sob o qual a aplicação web é executada. A linha vai no .env, ao lado de FLUXER_DOMAIN:

FLUXER_CSP_EXTRA_CONNECT_SRC=wss://livekit.example.com:7881

O app-proxy constrói a política e lê seu ambiente no início do contêiner, então aplique a alteração com docker compose up -d app-proxy. O comando docker compose restart app-proxy reutiliza o contêiner existente com seu ambiente antigo. Content Security Policy tem as outras variáveis.