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 pochodzeniapublicdo przestrzeni adresowejlocal(prywatne zakresy adresów IPv4 RFC 191810.0.0.0/8,172.16.0.0/12,192.168.0.0/16, adresy link-local169.254.0.0/16/fe80::/10, unikalne lokalne adresy IPv6fc00::/7i nazwy hostów mDNS.local).loopback-network: wymagany podczas nawiązywania połączenia z źródłapubliclublocaldo przestrzeni adresowejloopback(127.0.0.0/8,::1/128lublocalhost).
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:
- 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 URLhttp://(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ń. - 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 ztargetAddressSpace: 'local'nieoczekiwanie zostanie rozpoznana jako adres IPpublic, przeglądarka natychmiast przerwie żądanie i wyświetli błąd sieciTypeError.
/**
* 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: konstruktorWebSocket(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łądSyntaxError. Zamiast tego przeglądarka sprawdza adres URLWebSocketprzed rozpoznaniem nazw DNS: połączeniaws://inicjowane ze stronyhttps://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-networklubloopback-network. Jeśli lokalny punkt końcowy jest adresowany za pomocą niestandardowej nazwy hosta DNS innej niż.local, połącz się przezwss://.WebTransport: ponieważWebTransportbezwzględnie wymagahttps://(HTTP/3 przez QUIC z protokołem TLS lubserverCertificateHashesw przypadku lokalnych certyfikatów podpisanych samodzielnie), nigdy nie wywołuje blokad treści mieszanych i nie akceptuje opcjitargetAddressSpace. Zamiast tego przeglądarka automatycznie weryfikuje przestrzeń adresów IP i blokuje obietnicętransport.readyza pomocą prośby o przyznanie uprawnieńlocal-networklubloopback-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:
- Zastępcze zapytanie o uprawnienia: umieść
navigator.permissions.query({ name: permissionName })wtry / catch. Jeśli przeglądarka zgłasza błądTypeError, ponieważ'local-network'lub'loopback-network'jest nierozpoznanymPermissionName, 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). - NIE wysyłaj zapytania o starszą nazwę uprawnień połączonych
{ name: 'local-network-access' }. W starszych wersjach Chrome to zapytanie powoduje awarię procesu renderowania, czemutry / catchnie może zapobiec. Nowsze wersje Chrome traktują'local-network-access'tylko jako starszy alias uprawnień szczegółowych. - Prawidłowa obsługa odrzucenia: zawsze umieszczaj zdarzenia błędów
fetch(),WebSocketitransport.readyw modułach obsługi błędów, które wychwytują błędyTypeErrori 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';
}
}