Acesso à rede local

Recursos da Web: Acesso à rede local

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 origem public ao espaço de endereço local (intervalos IPv4 privados RFC 1918 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, link-local 169.254.0.0/16 / fe80::/10, endereços locais exclusivos IPv6 fc00::/7 e nomes de host mDNS .local).
  • loopback-network: obrigatório ao se conectar de uma origem public ou local ao espaço de endereço loopback (127.0.0.0/8, ::1/128 ou localhost).

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:

  1. 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 URL http:// (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.
  2. 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 targetAddressSpace declarado. Se um nome de host com targetAddressSpace: 'local' for resolvido inesperadamente para um endereço IP public, o navegador vai cancelar imediatamente a solicitação com um erro de rede TypeError.
/**
 * 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 construtor WebSocket(url, protocols) só aceita uma string ou matriz de subprotocolo como segundo argumento. Não transmita um objeto de opções { targetAddressSpace }, que gera um SyntaxError. Em vez disso, o navegador inspeciona o URL WebSocket antes da resolução de DNS: as conexões ws:// iniciadas em uma página https:// 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ão local-network ou loopback-network. Se um endpoint local for endereçado por um nome de host DNS personalizado que não seja .local, conecte-se por wss://.
  • WebTransport: como o WebTransport exige estritamente o https:// (HTTP/3 sobre QUIC com TLS ou serverCertificateHashes para certificados autoassinados locais), ele nunca aciona blocos de conteúdo misto e não aceita uma opção targetAddressSpace. Em vez disso, o navegador verifica automaticamente o espaço de endereço IP resolvido e bloqueia a promessa transport.ready atrás do comando de permissão local-network ou loopback-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:

  1. Substituto para consulta de permissão: envolva navigator.permissions.query({ name: permissionName }) em um try / catch. Se o navegador gerar um TypeError porque 'local-network' ou 'loopback-network' é um PermissionName nã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).
  2. 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 o try / catch nã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.
  3. Tratamento de rejeição adequado: sempre envolva fetch(), eventos de erro WebSocket e transport.ready em gerenciadores de erros que capturem falhas de conexão/TypeError e 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';
  }
}