Dostęp przez sieć lokalną

Funkcje internetowe: Dostęp do sieci lokalnej

Gdy aplikacja internetowa udostępniana ze źródła public (lub źródła intranetowego local) łączy się z prywatną siecią lokalną użytkownika (np. drukarką LAN, centrum IoT lub routerem pod adresem 192.168.1.x lub device.local) albo z lokalną usługą towarzyszącą działającą na komputerze użytkownika (127.0.0.1, [::1] lub localhost), przeglądarki blokują połączenie, wyświetlając prośbę o przyznanie uprawnień dostępu do sieci lokalnej (LNA).

Dostęp do sieci lokalnej dzieli dostęp na 2 szczegółowe uprawnienia i tokeny Permissions-Policy na podstawie siatki przestrzeni adresów IP (public → local → loopback), wymuszając kontrole tylko wtedy, gdy żądanie przechodzi z mniej prywatnej przestrzeni adresowej do bardziej prywatnej:

  • local-network: wymagane podczas nawiązywania połączenia z miejsca pochodzenia public do przestrzeni adresowej local (prywatne zakresy adresów IPv4 RFC 1918 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, adresy link-local 169.254.0.0/16 / fe80::/10, unikalne lokalne adresy IPv6 fc00::/7 i nazwy hostów mDNS .local).
  • loopback-network: wymagany podczas nawiązywania połączenia z źródła public lub local do przestrzeni adresowej loopback (127.0.0.0/8, ::1/128 lub localhost).

Ponieważ adres localhost (127.0.0.1 i [::1]) znajduje się już w najbardziej prywatnej przestrzeni adresowej (loopback), udostępnianie aplikacji internetowej z adresu http://localhost podczas lokalnego programowania nie spowoduje domyślnie uruchomienia kontroli LNA. Aby przetestować prośby i stany uprawnień LNA lokalnie bez wdrażania na serwerze publicznym, zastąp przestrzeń adresową IPv4 i IPv6 lokalnego serwera deweloperskiego wartością public za pomocą flagi Chromium --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public lub ręcznie przełącz Sieć lokalną i Sieć sprzężenia zwrotnego między Ask, Allow i Block w Ustawieniach witryny w przeglądarce.

OBOWIĄZKOWO: udostępniaj każdą aplikację internetową, która inicjuje żądania sieci lokalnej lub sieci sprzężenia zwrotnego, z bezpiecznego kontekstu (https://). Uprawnienia dostępu do sieci lokalnej są automatycznie odrzucane w niebezpiecznych (http://) kontekstach najwyższego poziomu.

NIE polegaj na wycofanych nagłówkach procesu wstępnego dostępu do sieci prywatnej (PNA) (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true). Dostęp do sieci lokalnej wymusza dostęp za pomocą próśb o zgodę użytkownika po stronie klienta (local-network i loopback-network) w połączeniu ze standardowymi nagłówkami CORS (Access-Control-Allow-Origin) podczas odczytywania odpowiedzi ze współdzielonych domen. Wstępne żądania PNA nigdy nie były wymagane jako blokujące i nie są potrzebne jako rozwiązanie zastępcze. Istniejące serwery lub oprogramowanie układowe urządzeń, które już zwracają Access-Control-Allow-Private-Network: true, mogą nadal to robić bez szkody, ale nie dodawaj obsługi PNA do nowego kodu.

Etapy wdrażania

1. Wstępne sprawdzanie stanu szczegółowych uprawnień i bramki w gestach użytkownika

Nigdy nie uruchamiaj bezwarunkowo żądań sieci lokalnej ani żądań sprzężenia zwrotnego podczas wczytywania strony (DOMContentLoaded), gdy stan uprawnień to 'prompt', i nigdy nie inicjuj pierwszego żądania wywołującego uprawnienia z poziomu skryptu service worker ani skryptu shared worker (które nie mają dołączonego elementu Document i natychmiast zakończą się niepowodzeniem, jeśli uprawnienia nie są już ustawione na 'granted').

Zamiast tego najpierw wyślij zapytanie do navigator.permissions.query() o 'local-network' lub 'loopback-network':

  • Jeśli state === 'granted', połącz automatycznie.
  • Jeśli state === 'prompt', wyświetl przejrzysty interfejs wyjaśniający, dlaczego aplikacja potrzebuje dostępu do urządzenia lokalnego lub aplikacji towarzyszącej, i wywołaj prośbę za pomocą wyraźnego gestu użytkownika (np. kliknięcia przycisku).
  • Jeśli state === 'denied', wyświetl interfejs z instrukcjami, jak ponownie włączyć dostęp do sieci lokalnej lub sprzężenia zwrotnego w ustawieniach witryny w przeglądarce.
/**
 * 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. Zadeklaruj targetAddressSpace w usługach fetch() i Request

Gdy strona https:// żąda punktu końcowego http:// w sieci lokalnej lub interfejsie pętli zwrotnej, określ targetAddressSpace: 'local' lub targetAddressSpace: 'loopback' w opcjach fetch() lub Request.

Jawne ustawienie targetAddressSpace powoduje 2 ważne rzeczy:

  1. Wyjątek dotyczący treści mieszanych: sprawdzanie treści mieszanych odbywa się przed rozpoznawaniem nazw DNS. Zadeklarowanie targetAddressSpace: 'local' (lub 'loopback') informuje przeglądarkę przed rozpoznaniem nazw DNS, że adres URL http:// (w tym niestandardowa nazwa hosta DNS, która jest rozpoznawana jako lokalny adres IP) jest kierowany na lokalny punkt końcowy lub punkt końcowy pętli zwrotnej i powinien zostać wyłączony z blokowania treści mieszanych, gdy użytkownik udzieli uprawnień.
  2. Weryfikacja przestrzeni adresowej: po rozpoznaniu nazw DNS przeglądarka sprawdza, czy rozpoznany adres IP rzeczywiście należy do zadeklarowanej domeny targetAddressSpace. Jeśli nazwa hosta z targetAddressSpace: 'local' nieoczekiwanie zostanie rozpoznana jako adres IP public, przeglądarka natychmiast przerwie żądanie i wyświetli błąd sieci 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. Połączenia w czasie rzeczywistym z usługami WebSocket i WebTransport

Ograniczenia dostępu do sieci lokalnej dotyczą też połączeń WebSocket i WebTransport:

  • WebSocket: konstruktor WebSocket(url, protocols) akceptuje tylko ciąg znaków lub tablicę subprotokołu jako drugi argument – nie przekazuj obiektu opcji { targetAddressSpace }, który wywołuje błąd SyntaxError. Zamiast tego przeglądarka sprawdza adres URL WebSocket przed rozpoznaniem nazw DNS: połączenia ws:// inicjowane ze strony https:// są wyłączone z blokowania treści mieszanych, gdy host adresu URL jest rozpoznawalny składniowo jako lokalny lub pętli zwrotnej – w szczególności prywatny lub pętli zwrotnej adres IP (ws://192.168.1.100:8080, ws://127.0.0.1:8080, ws://[::1]:8080), localhost (*.localhost) lub nazwa hosta mDNS .local (ws://device.local:8080) – i jest chroniony przez prośbę o przyznanie uprawnień local-network lub loopback-network. Jeśli lokalny punkt końcowy jest adresowany za pomocą niestandardowej nazwy hosta DNS innej niż .local, połącz się przez wss://.
  • WebTransport: ponieważ WebTransport bezwzględnie wymaga https:// (HTTP/3 przez QUIC z protokołem TLS lub serverCertificateHashes w przypadku lokalnych certyfikatów podpisanych samodzielnie), nigdy nie wywołuje blokad treści mieszanych i nie akceptuje opcji targetAddressSpace. Zamiast tego przeglądarka automatycznie weryfikuje przestrzeń adresów IP i blokuje obietnicę transport.ready za pomocą prośby o przyznanie uprawnień local-network lub 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. Przekazywanie dostępu do elementów iframe ze współdzieleniem za pomocą Permissions-Policy

Domyślnie local-network i loopback-network mają domyślną listę dozwolonych 'self', która blokuje osadzone elementy <iframe> typu współdzielenie przed wysyłaniem żądań lokalnych lub żądań sprzężenia zwrotnego. Aby umożliwić osadzonemu elementowi współdzielenia <iframe> (np. widżetowi konfiguracji sprzętu lub ramce logowania jednokrotnego, która komunikuje się z lokalnym agentem) inicjowanie żądań lokalnych lub żądań sprzężenia zwrotnego, wyraźnie przekaż uprawnienia szczegółowe w atrybucie allow lub nagłówku 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>

Wyślij nagłówek Permissions-Policy w odpowiedzi dokumentu najwyższego poziomu (osadzonego), a nie w odpowiedzi elementu iframe:

Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")

Strategie zastępcze

Jeśli Twój cel Baseline nie obsługuje uprawnień dostępu do sieci lokalnej i opcji targetAddressSpace, zaimplementuj wykrywanie funkcji z progresywnym przełączaniem awaryjnym:

  1. Zastępcze zapytanie o uprawnienia: umieść navigator.permissions.query({ name: permissionName }) w try / catch. Jeśli przeglądarka zgłasza błąd TypeError, ponieważ 'local-network' lub 'loopback-network' jest nierozpoznanym PermissionName, traktuj stan jako 'prompt', aby żądanie nadal było uruchamiane po wyraźnym kliknięciu przez użytkownika (sama próba połączenia powoduje wyświetlenie monitu przeglądarki, jeśli obsługiwany jest dostęp do sieci lokalnej).
  2. NIE wysyłaj zapytania o starszą nazwę uprawnień połączonych { name: 'local-network-access' }. W starszych wersjach Chrome to zapytanie powoduje awarię procesu renderowania, czemu try / catch nie może zapobiec. Nowsze wersje Chrome traktują 'local-network-access' tylko jako starszy alias uprawnień szczegółowych.
  3. Prawidłowa obsługa odrzucenia: zawsze umieszczaj zdarzenia błędów fetch(), WebSocket i transport.ready w modułach obsługi błędów, które wychwytują błędy TypeError i błędy połączenia oraz przedstawiają jasne instrukcje rozwiązania problemu (sprawdzenie, czy urządzenie lokalne jest włączone, połączone z tą samą siecią i ma uprawnienia w przeglądarce).
/**
 * 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';
  }
}