Accesso alla rete locale

Funzionalità web: Accesso alla rete locale

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'origine public allo spazio di indirizzi local (intervalli IPv4 privati 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, indirizzi IPv6 Unique Local fc00::/7 e nomi host mDNS .local).
  • loopback-network: obbligatorio quando ci si connette da un'origine public o local allo spazio di indirizzi loopback (127.0.0.0/8, ::1/128 o localhost).

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:

  1. 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 URL http:// (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.
  2. Verifica dello spazio degli indirizzi: dopo la risoluzione DNS, il browser verifica che l'indirizzo IP risolto appartenga effettivamente all'targetAddressSpace dichiarato. Se un nome host con targetAddressSpace: 'local' viene risolto in modo imprevisto in un indirizzo IP public, il browser interrompe immediatamente la richiesta con un errore di rete 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. Connessioni in tempo reale con WebSocket e WebTransport

Le limitazioni di accesso alla rete locale regolano anche le connessioni WebSocket e WebTransport:

  • WebSocket: il costruttore WebSocket(url, protocols) accetta solo una stringa o un array di sottoprotocolli come secondo argomento. Non passare un oggetto opzioni { targetAddressSpace }, che genera un SyntaxError. Il browser esamina invece l'URL WebSocket prima della risoluzione DNS: le connessioni ws:// avviate da una pagina https:// 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 autorizzazione local-network o loopback-network. Se un endpoint locale viene indirizzato tramite un nome host DNS personalizzato non .local, connettiti tramite wss://.
  • WebTransport: poiché WebTransport richiede rigorosamente https:// (HTTP/3 su QUIC con TLS o serverCertificateHashes per i certificati autofirmati locali), non attiva mai i blocchi di contenuti misti e non accetta un'opzione targetAddressSpace. Invece, il browser verifica automaticamente lo spazio degli indirizzi IP risolto e blocca la promise transport.ready dietro la richiesta di autorizzazione local-network o 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. 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:

  1. Fallback per la query di autorizzazione: inserisci navigator.permissions.query({ name: permissionName }) in un try / catch. Se il browser genera un TypeError perché 'local-network' o 'loopback-network' è un PermissionName non 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).
  2. 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 e try / catch non può impedirlo. Le versioni più recenti di Chrome considerano 'local-network-access' solo come un alias legacy per le autorizzazioni granulari.
  3. Gestione corretta dei rifiuti: inserisci sempre fetch(), gli eventi di errore WebSocket e transport.ready in gestori degli errori che rilevano TypeError/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';
  }
}