Quando um aplicativo da Web veiculado de uma origem public (ou uma origem de intranet local) se conecta à rede local privada de um usuário (como uma impressora LAN, um hub de IoT ou um roteador em 192.168.1.x ou device.local) ou a um serviço complementar local em execução na máquina do usuário (127.0.0.1, [::1] ou localhost), os navegadores bloqueiam a conexão atrás de uma solicitação de permissão de acesso à rede local (LNA, na sigla em inglês).
O acesso à rede local divide o acesso em duas permissões granulares e tokens Permissions-Policy com base na rede de espaço de endereço IP (public → local → loopback), aplicando verificações somente quando uma solicitação passa de um espaço de endereço menos privado para um espaço de endereço mais privado:
local-network: obrigatório ao se conectar de uma origempublicao espaço de endereçolocal(intervalos IPv4 privados RFC 191810.0.0.0/8,172.16.0.0/12,192.168.0.0/16, link-local169.254.0.0/16/fe80::/10, endereços locais exclusivos IPv6fc00::/7e nomes de host mDNS.local).loopback-network: obrigatório ao se conectar de uma origempublicoulocalao espaço de endereçoloopback(127.0.0.0/8,::1/128oulocalhost).
Como localhost (127.0.0.1 e [::1]) já está no espaço de endereço mais particular (loopback), veicular seu app da Web de http://localhost durante o desenvolvimento local não vai acionar as verificações de LNA por padrão. Para testar os comandos e os estados de permissão da LNA localmente sem implantar em um servidor público, substitua o espaço de endereço IPv4 e IPv6 do servidor de desenvolvimento local por public usando a flag --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public do Chromium ou alterne manualmente entre Rede local e Rede de loopback entre Ask, Allow e Block nas Configurações do site do navegador.
OBRIGATÓRIO: disponibilize qualquer aplicativo da Web que inicie solicitações de rede local ou de loopback em um contexto seguro (https://). As permissões de acesso à rede local são negadas automaticamente em contextos de nível superior não seguros (http://).
NÃO dependa de cabeçalhos de simulação de acesso à rede privada (PNA) descontinuados (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true). O acesso à rede local aplica o acesso usando solicitações de permissão do usuário do lado do cliente (local-network e loopback-network) combinadas com cabeçalhos CORS padrão (Access-Control-Allow-Origin) ao ler respostas entre origens. As simulações de PNA nunca foram aplicadas como um requisito de bloqueio e não são necessárias como substituto. Os servidores ou o firmware do dispositivo que já retornam Access-Control-Allow-Private-Network: true podem continuar fazendo isso sem problemas, mas não adicione o processamento de PNA ao novo código.
Etapas de implementação
1. Pré-verificar o estado de permissão granular e o portão na interação do usuário
Nunca dispare solicitações de rede local ou de loopback incondicionalmente no carregamento de página (DOMContentLoaded) quando o estado da permissão for 'prompt' e nunca inicie a primeira solicitação de acionamento de permissão de um Service Worker ou Shared Worker (que não têm um Document anexado e vão falhar imediatamente se a permissão ainda não for 'granted').
Em vez disso, consulte navigator.permissions.query() para 'local-network' ou 'loopback-network' primeiro:
- Se
state === 'granted', conecte-se automaticamente. - Se for
state === 'prompt', mostre uma interface clara explicando por que o app precisa de acesso ao dispositivo local ou ao app complementar e acione a solicitação com uma interação explícita do usuário (como o clique em um botão). - Se for
state === 'denied', renderize uma interface de correção acionável que instrua o usuário sobre como reativar o acesso à rede local ou ao loopback nas configurações do site do navegador.
/**
* Queries the granular Local Network Access permission for the target space.
* @param {'local-network' | 'loopback-network'} permissionName
*/
export async function checkNetworkPermission(permissionName) {
const status = await navigator.permissions.query({ name: permissionName });
// Keep UI synchronized if the user changes permission in browser site settings
status.addEventListener('change', () => {
updatePermissionUI(permissionName, status.state);
});
updatePermissionUI(permissionName, status.state);
return status.state;
}
function updatePermissionUI(permissionName, state) {
const statusBadge = document.querySelector(`[data-permission-status="${permissionName}"]`);
const remediationBox = document.querySelector(`[data-remediation="${permissionName}"]`);
if (statusBadge) {
statusBadge.textContent = state;
}
if (remediationBox) {
// Show remediation instructions whenever permission is explicitly denied
remediationBox.hidden = state !== 'denied';
}
}
2. Declare targetAddressSpace em fetch() e Request
Quando uma página https:// solicita um endpoint http:// na rede local ou na interface de loopback, especifique targetAddressSpace: 'local' ou targetAddressSpace: 'loopback' nas opções fetch() ou Request.
Definir targetAddressSpace explicitamente faz duas coisas importantes:
- Isenção de conteúdo misto: as verificações de conteúdo misto são executadas antes da resolução de DNS. Declarar
targetAddressSpace: 'local'(ou'loopback') informa ao navegador, antes da resolução de DNS, que um URLhttp://(incluindo um nome de host DNS personalizado que resolve para um IP local) tem como destino um endpoint local/de loopback e precisa ser isento do bloqueio de conteúdo misto depois que o usuário conceder a permissão. - Verificação do espaço de endereço: após a resolução de DNS, o navegador verifica se o endereço IP resolvido realmente pertence ao
targetAddressSpacedeclarado. Se um nome de host comtargetAddressSpace: 'local'for resolvido inesperadamente para um endereço IPpublic, o navegador vai cancelar imediatamente a solicitação com um erro de redeTypeError.
/**
* Connects to a local network device (e.g., LAN printer or IoT hub) over HTTP.
* Example endpoint URL is illustrative; replace with your device's address.
*/
export async function fetchLocalDeviceStatus(deviceUrl = 'http://192.168.1.100:8080/api/status') {
const request = new Request(deviceUrl, {
method: 'GET',
mode: 'cors',
// MANDATORY: Declare 'local' when targeting RFC 1918 / .local LAN addresses
targetAddressSpace: 'local',
});
const response = await fetch(request);
if (!response.ok) {
throw new Error(`Device responded with HTTP ${response.status}`);
}
return response.json();
}
/**
* Connects to a local companion daemon running on the user's loopback interface.
* Example loopback port is illustrative; replace with your service's port.
*/
export async function fetchLoopbackDaemon(daemonUrl = 'http://127.0.0.1:45678/health') {
const request = new Request(daemonUrl, {
method: 'GET',
mode: 'cors',
// MANDATORY: Declare 'loopback' when targeting 127.0.0.1, [::1], or localhost
targetAddressSpace: 'loopback',
});
const response = await fetch(request);
if (!response.ok) {
throw new Error(`Companion app responded with HTTP ${response.status}`);
}
return response.json();
}
3. Conexões em tempo real com WebSocket e WebTransport
As restrições de acesso à rede local também regem as conexões WebSocket e WebTransport:
WebSocket: o construtorWebSocket(url, protocols)só aceita uma string ou matriz de subprotocolo como segundo argumento. Não transmita um objeto de opções{ targetAddressSpace }, que gera umSyntaxError. Em vez disso, o navegador inspeciona o URLWebSocketantes da resolução de DNS: as conexõesws://iniciadas em uma páginahttps://são isentas do bloqueio de conteúdo misto quando o host do URL é sintaticamente reconhecível como local ou de loopback, especificamente um IP literal privado ou de loopback (ws://192.168.1.100:8080,ws://127.0.0.1:8080,ws://[::1]:8080),localhost(*.localhost) ou um nome de host mDNS.local(ws://device.local:8080), e controlado pelo prompt de permissãolocal-networkouloopback-network. Se um endpoint local for endereçado por um nome de host DNS personalizado que não seja.local, conecte-se porwss://.WebTransport: como oWebTransportexige estritamente ohttps://(HTTP/3 sobre QUIC com TLS ouserverCertificateHashespara certificados autoassinados locais), ele nunca aciona blocos de conteúdo misto e não aceita uma opçãotargetAddressSpace. Em vez disso, o navegador verifica automaticamente o espaço de endereço IP resolvido e bloqueia a promessatransport.readyatrás do comando de permissãolocal-networkouloopback-network.
/**
* Opens a WebSocket connection to a local network device (IP literal, .local, or localhost).
*/
export function openLocalWebSocket(wsUrl = 'ws://192.168.1.100:8080/stream') {
// Pass only subprotocol string(s) as the second argument (never an options object).
// Browsers exempt ws:// from Mixed Content when the host is a local/loopback IP literal,
// .local, or localhost, and gate the connection on 'local-network' or 'loopback-network'.
const socket = new WebSocket(wsUrl, ['v1.telemetry']);
return socket;
}
/**
* Opens a WebTransport session to a local network endpoint using a pinned self-signed certificate hash.
*/
export async function openLocalWebTransport(wtUrl = 'https://192.168.1.100:4433/wt', certSha256Bytes) {
const transport = new WebTransport(wtUrl, {
serverCertificateHashes: [
{
algorithm: 'sha-256',
value: certSha256Bytes,
},
],
});
// transport.ready resolves only after the user grants the 'local-network' permission
// and rejects with WebTransportError if the permission is denied.
await transport.ready;
return transport;
}
4. Delegar acesso a iframes entre origens usando Permissions-Policy
Por padrão, local-network e loopback-network têm uma lista de permissões padrão de 'self', impedindo que incorporações de <iframe> entre origens façam solicitações locais ou de loopback. Para permitir que um <iframe> entre origens incorporado (como um widget de configuração de hardware ou um frame de SSO que se comunica com um agente local) inicie solicitações locais ou de loopback, delegue explicitamente as permissões granulares no atributo allow e/ou no cabeçalho HTTP Permissions-Policy:
<!-- Delegate granular local-network and loopback-network permissions to the embedded iframe -->
<iframe
src="https://setup.partner.example.com/device-bridge"
allow="local-network; loopback-network"
title="Local Device Bridge">
</iframe>
Envie o cabeçalho Permissions-Policy na resposta do documento de nível superior (incorporado), não na resposta do iframe:
Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")
Estratégias de fallback
Se o destino do Baseline não for compatível com as permissões de acesso à rede local e as opções de targetAddressSpace, implemente a detecção de recursos com substituição progressiva:
- Substituto para consulta de permissão: envolva
navigator.permissions.query({ name: permissionName })em umtry / catch. Se o navegador gerar umTypeErrorporque'local-network'ou'loopback-network'é umPermissionNamenão reconhecido, trate o estado como'prompt'para que a solicitação ainda seja executada em um clique explícito do usuário (a tentativa de conexão aciona o prompt do navegador em que o LNA é compatível). - NÃO consulte o nome da permissão combinada legada
{ name: 'local-network-access' }. Em versões mais antigas do Chrome, essa consulta causa uma falha no processo do renderizador, e otry / catchnão pode impedir isso. As versões mais recentes do Chrome tratam'local-network-access'apenas como um alias legado para as permissões granulares. - Tratamento de rejeição adequado: sempre envolva
fetch(), eventos de erroWebSocketetransport.readyem gerenciadores de erros que capturem falhas de conexão/TypeErrore apresentem etapas de correção claras (verificar se o dispositivo local está ligado, conectado à mesma rede e permitido nas permissões do navegador).
/**
* Cross-browser permission check for the granular LNA permissions.
* Never falls back to querying 'local-network-access', which crashes older Chrome versions.
* @param {'local-network' | 'loopback-network'} permissionName
*/
export async function queryLnaPermissionSafe(permissionName) {
if (!navigator.permissions?.query) {
return 'prompt';
}
try {
const status = await navigator.permissions.query({ name: permissionName });
return status.state;
} catch {
// Unrecognized permission name; proceed on user gesture
return 'prompt';
}
}