Quando un'applicazione web pubblicata da un'origine public (o un'origine intranet local) si connette alla rete locale privata di un utente (ad esempio una stampante LAN, un hub IoT o un router all'indirizzo 192.168.1.x o device.local) o a un servizio companion locale in esecuzione sulla macchina dell'utente (127.0.0.1, [::1] o localhost), i browser bloccano la connessione dietro una richiesta di autorizzazione di accesso alla rete locale (LNA).
L'accesso alla rete locale suddivide l'accesso in due autorizzazioni granulari e token Permissions-Policy in base al reticolo dello spazio degli indirizzi IP (public → local → loopback), applicando i controlli solo quando una richiesta passa da uno spazio degli indirizzi meno privato a uno spazio degli indirizzi più privato:
local-network: obbligatorio per la connessione da un'originepublicallo spazio di indirizzilocal(intervalli IPv4 privati RFC 191810.0.0.0/8,172.16.0.0/12,192.168.0.0/16, link-local169.254.0.0/16/fe80::/10, indirizzi IPv6 Unique Localfc00::/7e nomi host mDNS.local).loopback-network: obbligatorio quando ci si connette da un'originepublicolocalallo spazio di indirizziloopback(127.0.0.0/8,::1/128olocalhost).
Poiché localhost (127.0.0.1 e [::1]) si trova già nello spazio di indirizzi più privato (loopback), la pubblicazione della tua app web da http://localhost durante lo sviluppo locale non attiverà i controlli LNA per impostazione predefinita. Per testare i prompt e gli stati delle autorizzazioni LNA localmente senza eseguire il deployment su un server pubblico, sostituisci lo spazio degli indirizzi IPv4 e IPv6 del tuo server di sviluppo locale con public utilizzando il flag Chromium --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public oppure attiva/disattiva manualmente Rete locale e Rete di loopback tra Ask, Allow e Block nelle Impostazioni sito del browser.
OBBLIGATORIO: pubblica qualsiasi applicazione web che avvia richieste di rete locali o di loopback da un contesto sicuro (https://). Le autorizzazioni di accesso alla rete locale vengono negate automaticamente nei contesti di primo livello non sicuri (http://).
NON fare affidamento sulle intestazioni preflight di Private Network Access (PNA) deprecate (Access-Control-Request-Private-Network: true/Access-Control-Allow-Private-Network: true). L'accesso alla rete locale applica l'accesso tramite prompt di autorizzazione dell'utente lato client (local-network e loopback-network) combinati con intestazioni CORS standard (Access-Control-Allow-Origin) durante la lettura delle risposte multiorigine. I preflight PNA non sono mai stati applicati come requisito di blocco e non sono necessari come opzione di riserva. I server o il firmware dei dispositivi esistenti che restituiscono già Access-Control-Allow-Private-Network: true possono continuare a farlo senza problemi, ma non aggiungere la gestione di PNA al nuovo codice.
Procedura di implementazione
1. Pre-Check Granular Permission State and Gate on User Gesture
Non attivare mai richieste di rete locale o di loopback in modo incondizionato al caricamento pagina (DOMContentLoaded) quando lo stato dell'autorizzazione è 'prompt' e non avviare mai la prima richiesta di attivazione dell'autorizzazione da un service worker o da un worker condiviso (che non hanno un Document collegato e non andranno a buon fine immediatamente se l'autorizzazione non è già 'granted').
Esegui invece prima una query su navigator.permissions.query() per 'local-network' o 'loopback-network':
- Se
state === 'granted', connettiti automaticamente. - Se
state === 'prompt', mostra un'interfaccia utente chiara che spieghi perché l'app ha bisogno di accedere al dispositivo locale o all'app complementare e attiva la richiesta da un'attivazione dall'utente esplicita (ad esempio un clic su un pulsante). - Se
state === 'denied', esegui il rendering della UI di correzione che indica all'utente come riattivare l'accesso alla rete locale o al loopback nelle impostazioni del sito del browser.
/**
* 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. Dichiara targetAddressSpace in fetch() e Request
Quando una pagina https:// richiede un endpoint http:// sulla rete locale o sull'interfaccia di loopback, specifica targetAddressSpace: 'local' o targetAddressSpace: 'loopback' nelle opzioni fetch() o Request.
L'impostazione esplicita di targetAddressSpace ha due effetti fondamentali:
- Esenzione per contenuti misti: i controlli dei contenuti misti vengono eseguiti prima della risoluzione DNS. La dichiarazione di
targetAddressSpace: 'local'(o'loopback') informa il browser prima della risoluzione DNS che un URLhttp://(incluso un nome host DNS personalizzato che si risolve in un IP locale) ha come target un endpoint locale/di loopback e deve essere esentato dal blocco dei contenuti misti una volta che l'utente concede l'autorizzazione. - Verifica dello spazio degli indirizzi: dopo la risoluzione DNS, il browser verifica che l'indirizzo IP risolto appartenga effettivamente all'
targetAddressSpacedichiarato. Se un nome host contargetAddressSpace: 'local'viene risolto in modo imprevisto in un indirizzo IPpublic, il browser interrompe immediatamente la richiesta con un errore di reteTypeError.
/**
* 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. Connessioni in tempo reale con WebSocket e WebTransport
Le limitazioni di accesso alla rete locale regolano anche le connessioni WebSocket e WebTransport:
WebSocket: il costruttoreWebSocket(url, protocols)accetta solo una stringa o un array di sottoprotocolli come secondo argomento. Non passare un oggetto opzioni{ targetAddressSpace }, che genera unSyntaxError. Il browser esamina invece l'URLWebSocketprima della risoluzione DNS: le connessioniws://avviate da una paginahttps://sono esenti dal blocco dei contenuti misti quando l'host dell'URL è sintatticamente riconoscibile come locale o di loopback, in particolare un valore letterale IP privato o di loopback (ws://192.168.1.100:8080,ws://127.0.0.1:8080,ws://[::1]:8080),localhost(*.localhost) o un nome host mDNS.local(ws://device.local:8080), e sono protette dal prompt di autorizzazionelocal-networkoloopback-network. Se un endpoint locale viene indirizzato tramite un nome host DNS personalizzato non.local, connettiti tramitewss://.WebTransport: poichéWebTransportrichiede rigorosamentehttps://(HTTP/3 su QUIC con TLS oserverCertificateHashesper i certificati autofirmati locali), non attiva mai i blocchi di contenuti misti e non accetta un'opzionetargetAddressSpace. Invece, il browser verifica automaticamente lo spazio degli indirizzi IP risolto e blocca la promisetransport.readydietro la richiesta di autorizzazionelocal-networkoloopback-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. Delega l'accesso agli iframe multiorigine tramite Permissions-Policy
Per impostazione predefinita, local-network e loopback-network hanno una lista consentita predefinita di 'self', che impedisce agli incorporamenti <iframe> multiorigine di effettuare richieste locali o di loopback. Per consentire a un <iframe> multiorigine incorporato (ad esempio un widget di configurazione hardware o un frame SSO che comunica con un agente locale) di avviare richieste locali o di loopback, delega esplicitamente le autorizzazioni granulari nell'attributo allow e/o nell'intestazione 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>
Invia l'intestazione Permissions-Policy nella risposta del documento di primo livello (incorporato), non nella risposta dell'iframe:
Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")
Strategie di fallback
Se il tuo target Baseline non supporta le autorizzazioni di accesso alla rete locale e le opzioni targetAddressSpace, implementa il rilevamento delle funzionalità con fallback progressivo:
- Fallback per la query di autorizzazione: inserisci
navigator.permissions.query({ name: permissionName })in untry / catch. Se il browser genera unTypeErrorperché'local-network'o'loopback-network'è unPermissionNamenon riconosciuto, considera lo stato come'prompt'in modo che la richiesta venga comunque eseguita con un clic esplicito dell'utente (il tentativo di connessione stesso attiva il prompt del browser in cui è supportato l'accesso alla rete locale). - NON eseguire query sul nome dell'autorizzazione combinata legacy
{ name: 'local-network-access' }. Nelle versioni precedenti di Chrome, questa query causa l'arresto anomalo del processo di rendering etry / catchnon può impedirlo. Le versioni più recenti di Chrome considerano'local-network-access'solo come un alias legacy per le autorizzazioni granulari. - Gestione corretta dei rifiuti: inserisci sempre
fetch(), gli eventi di erroreWebSocketetransport.readyin gestori degli errori che rilevanoTypeError/errori di connessione e presentano passaggi di correzione chiari (verifica che il dispositivo locale sia acceso, connesso alla stessa rete e consentito nelle autorizzazioni del browser).
/**
* 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';
}
}