Toegang tot lokaal netwerk

Als een web-app die wordt geleverd vanuit een public-oorsprong (of een intranet-local-oorsprong) verbinding maakt met het privé lokale netwerk van een gebruiker (zoals een LAN-printer, IoT-hub of router op 192.168.1.x of device.local) of met een lokale begeleidende service die wordt uitgevoerd op de machine van de gebruiker (127.0.0.1, [::1] of localhost), plaatsen browsers de verbinding achter een prompt voor rechten voor lokale netwerktoegang (Local Network Access, LNA).

Lokale netwerktoegang splitst de toegang op in 2 gedetailleerde rechten en Permissions-Policy tokens op basis van de IP-adresruimtelattice (public → local → loopback). Controles worden alleen afgedwongen als een verzoek van een minder privé adresruimte naar een meer privé adresruimte gaat:

  • local-network: Vereist bij verbinding vanuit een public-oorsprong met de local-adresruimte (RFC 1918 privé IPv4-bereiken 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, link-local 169.254.0.0/16 / fe80::/10, IPv6 Unique Local Addresses fc00::/7 en .local mDNS-hostnamen).
  • loopback-network: Vereist als u verbinding maakt vanuit een public- of local-oorsprong met de loopback-adresruimte (127.0.0.0/8, ::1/128 of localhost).

Omdat localhost (127.0.0.1 en [::1]) zich al in de meest privé (loopback) adresruimte bevindt, worden er standaard geen LNA-controles geactiveerd als je je web-app weergeeft vanuit http://localhost tijdens lokale ontwikkeling. Als je LNA-prompts en rechtenstatussen lokaal wilt testen zonder te implementeren op een openbare server, overschrijf je de IPv4- en IPv6-adresruimte van je lokale dev-server naar public met de Chromium-vlag --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public of schakel je handmatig tussen Lokaal netwerk en Loopback-netwerk tussen Ask, Allow en Block in de Site-instellingen van de browser.

VERPLICHT: Lever elke web-app die lokale of loopback-netwerkverzoeken start vanuit een beveiligde context (https://). Rechten voor lokale netwerktoegang worden automatisch geweigerd in onbeveiligde (http://) contexten op het hoogste niveau.

Vertrouw NIET op beëindigde pre-flightheaders voor toegang tot privénetwerken (PNA) (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true). Lokale netwerktoegang dwingt toegang af via prompts voor gebruikerstoestemming aan de clientzijde (local-network en loopback-network) in combinatie met standaard CORS-headers (Access-Control-Allow-Origin) bij het lezen van cross-origin-reacties. PNA-preflights zijn nooit afgedwongen als blokkeringsvereiste en zijn niet nodig als fallback. Bestaande servers of apparaatfirmware die al Access-Control-Allow-Private-Network: true retourneren, kunnen dit zonder problemen blijven doen, maar voeg geen PNA-verwerking toe aan nieuwe code.

Stapsgewijze implementatie

1. Gedetailleerde rechtenstatus vooraf controleren en blokkeren bij gebruikersactivatie

Activeer nooit onvoorwaardelijk lokale netwerk- of loopback-verzoeken bij het laden van de pagina (DOMContentLoaded) als de rechtenstatus 'prompt' is en start nooit het eerste verzoek dat rechten activeert vanuit een Service Worker of Shared Worker (die geen gekoppelde Document hebben en onmiddellijk mislukken als de rechten niet al 'granted' zijn).

Voer in plaats daarvan eerst een query uit op navigator.permissions.query() voor 'local-network' of 'loopback-network':

  • Als state === 'granted', maak dan automatisch verbinding.
  • Als state === 'prompt', toon je een duidelijke UI waarin wordt uitgelegd waarom de app toegang nodig heeft tot het lokale apparaat of de bijbehorende app en activeer je het verzoek via een expliciete gebruikersactivatie (zoals een klik op een knop).
  • Als state === 'denied', geef je een UI voor herstelacties weer waarin de gebruiker wordt uitgelegd hoe die de toegang tot het lokale netwerk of loopback opnieuw aanzet in de site-instellingen van de 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. targetAddressSpace aangeven in fetch() en Request

Als een https://-pagina een http://-eindpunt aanvraagt op het lokale netwerk of de loopback-interface, voer je targetAddressSpace: 'local' of targetAddressSpace: 'loopback' in bij de opties fetch() of Request.

Als u targetAddressSpace expliciet instelt, gebeuren er 2 belangrijke dingen:

  1. Uitzondering voor gemengde content: Controles op gemengde content worden vóór DNS-omzetting uitgevoerd. Als je targetAddressSpace: 'local' (of 'loopback') declareert, laat je de browser vóór de DNS-omzetting weten dat een http://-URL (waaronder een aangepaste DNS-hostnaam die wordt omgezet in een lokaal IP-adres) een lokaal/loopback-eindpunt target en moet worden vrijgesteld van de blokkering van gemengde content nadat de gebruiker toestemming heeft gegeven.
  2. Verificatie van adresruimte: Na de DNS-omzetting verifieert de browser of het omgezette IP-adres daadwerkelijk bij de gedeclareerde targetAddressSpace hoort. Als een hostnaam met targetAddressSpace: 'local' onverwacht wordt omgezet in een public IP-adres, breekt de browser het verzoek onmiddellijk af met een TypeError netwerkfout.
/**
 * 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. Realtime verbindingen met WebSocket en WebTransport

Beperkingen voor toegang tot lokaal netwerk gelden ook voor WebSocket- en WebTransport-verbindingen:

  • WebSocket: De WebSocket(url, protocols)-constructor accepteert alleen een subprotocoltekenreeks of -matrix als tweede argument. Geef geen { targetAddressSpace }-optieobject door, omdat dit een SyntaxError veroorzaakt. In plaats daarvan inspecteert de browser de WebSocket-URL vóór de DNS-omzetting: ws://-verbindingen die worden gestart vanaf een https://-pagina, zijn vrijgesteld van de blokkering van gemengde content als de URL-host syntactisch herkenbaar is als lokaal of loopback, met name een letterlijk privé- of loopback-IP-adres (ws://192.168.1.100:8080, ws://127.0.0.1:8080, ws://[::1]:8080), localhost (*.localhost) of een .local mDNS-hostnaam (ws://device.local:8080), en worden afgeschermd achter de local-network- of loopback-network-toestemmingsprompt. Als een lokaal eindpunt wordt geadresseerd via een aangepaste niet-.local DNS-hostnaam, maak je verbinding via wss://.
  • WebTransport: Omdat WebTransport strikt https:// vereist (HTTP/3 via QUIC met TLS of serverCertificateHashes voor lokale zelfondertekende certificaten), worden er nooit blokkeringen voor gemengde content geactiveerd en wordt de optie targetAddressSpace niet geaccepteerd. In plaats daarvan verifieert de browser automatisch de opgeloste IP-adresruimte en blokkeert de transport.ready-promise achter de prompt voor local-network- of loopback-network-rechten.
/**
 * 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. Toegang machtigen tot cross-origin iframes via Permissions-Policy

Standaard hebben local-network en loopback-network een standaard toelatingslijst van 'self', waardoor cross-origin <iframe>-insluitingen geen lokale of loopback-verzoeken kunnen doen. Als je een ingesloten cross-origin <iframe> (zoals een widget voor hardware-instellingen of een SSO-frame dat communiceert met een lokale agent) wilt toestaan om lokale of loopback-verzoeken te starten, delegeer je de gedetailleerde rechten expliciet in het kenmerk allow en/of de HTTP-header 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>

Stuur de Permissions-Policy-header in de reactie van het document op het hoogste niveau (insluiting), niet in de reactie van het iframe:

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

Reservestrategieën

Als je Baseline-doel geen rechten voor lokale netwerktoegang en targetAddressSpace-opties ondersteunt, implementeer je functiedetectie met progressieve fallback:

  1. Permission Query Fallback: Verpak navigator.permissions.query({ name: permissionName }) in een try / catch. Als de browser een TypeError genereert omdat 'local-network' of 'loopback-network' een niet-herkende PermissionName is, behandel je de status als 'prompt', zodat het verzoek nog steeds wordt uitgevoerd bij een expliciete gebruikersklik (de verbindingspoging zelf activeert de browserprompt waar LNA wordt ondersteund).
  2. Voer GEEN query uit voor de verouderde gecombineerde rechtennaam { name: 'local-network-access' }. In oudere Chrome-versies crasht deze query het weergaveproces en kan try / catch dit niet voorkomen. Nieuwere Chrome-versies behandelen 'local-network-access' alleen als een verouderde alias voor de gedetailleerde rechten.
  3. Correcte afhandeling van afwijzingen: Verpak fetch(), WebSocket-foutgebeurtenissen en transport.ready altijd in fouthandlers die TypeError/verbindingsfouten opvangen en duidelijke herstelstappen bieden (controleren of het lokale apparaat aanstaat, verbonden is met hetzelfde netwerk en is toegestaan in de browserrechten).
/**
 * 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';
  }
}