Wenn eine Webanwendung, die von einem public-Ursprung (oder einem Intranet-local-Ursprung) bereitgestellt wird, eine Verbindung zum privaten lokalen Netzwerk eines Nutzers (z. B. einem LAN-Drucker, IoT-Hub oder Router unter 192.168.1.x oder device.local) oder zu einem lokalen Begleitdienst herstellt, der auf dem Computer des Nutzers ausgeführt wird (127.0.0.1, [::1] oder localhost), blockieren Browser die Verbindung hinter einer Aufforderung zur Erteilung der Berechtigung für den lokalen Netzwerkzugriff (LNA).
Der Zugriff auf das lokale Netzwerk wird in zwei detaillierte Berechtigungen und Permissions-Policy-Tokens aufgeteilt, die auf dem IP-Adressbereichsgitter (public → local → loopback) basieren. Prüfungen werden nur dann erzwungen, wenn eine Anfrage von einem weniger privaten Adressbereich in einen privateren Adressbereich wechselt:
local-network: Erforderlich, wenn eine Verbindung von einempublic-Ursprung zumlocal-Adressraum hergestellt wird (RFC 1918-private IPv4-Bereiche10.0.0.0/8,172.16.0.0/12,192.168.0.0/16, Link-Local169.254.0.0/16/fe80::/10, IPv6 Unique Local Addressesfc00::/7und.localmDNS-Hostnamen).loopback-network: Erforderlich, wenn eine Verbindung von einempublic- oderlocal-Ursprung zumloopback-Adressraum (127.0.0.0/8,::1/128oderlocalhost) hergestellt wird.
Da sich localhost (127.0.0.1 und [::1]) bereits im privatesten Adressraum (loopback) befindet, werden bei der Bereitstellung Ihrer Web-App über http://localhost während der lokalen Entwicklung standardmäßig keine LNA-Prüfungen ausgelöst. Wenn Sie LNA-Aufforderungen und Berechtigungsstatus lokal testen möchten, ohne sie auf einem öffentlichen Server bereitzustellen, können Sie entweder den IPv4- und IPv6-Adressbereich Ihres lokalen Entwicklungsservers mit dem Chromium-Flag --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public auf public überschreiben oder in den Website-Einstellungen des Browsers manuell zwischen Ask, Allow und Block für Lokales Netzwerk und Loopback-Netzwerk wechseln.
OBLIGATORISCH: Alle Webanwendungen, die lokale oder Loopback-Netzwerkanfragen initiieren, müssen aus einem sicheren Kontext (https://) bereitgestellt werden. In unsicheren (http://) Kontexten auf oberster Ebene werden Berechtigungen für den Zugriff auf das lokale Netzwerk automatisch verweigert.
Verlassen Sie sich NICHT auf die veralteten Preflight-Header für den Zugriff auf private Netzwerke (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true). Der Zugriff auf lokale Netzwerke wird über clientseitige Aufforderungen zur Nutzerberechtigung (local-network und loopback-network) in Kombination mit Standard-CORS-Headern (Access-Control-Allow-Origin) beim Lesen von ursprungsübergreifenden Antworten erzwungen. PNA-Preflights wurden nie als blockierende Anforderung erzwungen und sind nicht als Fallback erforderlich. Bestehende Server oder Gerätefirmware, die bereits Access-Control-Allow-Private-Network: true zurückgeben, können dies weiterhin tun, aber fügen Sie neuen Code keine PNA-Verarbeitung hinzu.
Implementierungsschritte
1. Detaillierten Berechtigungsstatus vorab prüfen und Nutzergeste abfragen
Senden Sie niemals bedingungslos Anfragen an das lokale Netzwerk oder Loopback-Anfragen beim Seitenaufbau (DOMContentLoaded), wenn der Berechtigungsstatus 'prompt' ist. Initiieren Sie die erste Anfrage, die eine Berechtigung auslöst, niemals über einen Service Worker oder Shared Worker, da diese keinen angehängten Document haben und sofort fehlschlagen, wenn die Berechtigung nicht bereits 'granted' ist.
Fragen Sie stattdessen zuerst navigator.permissions.query() für 'local-network' oder 'loopback-network' ab:
- Bei
state === 'granted'automatisch verbinden. - Wenn
state === 'prompt', zeige eine klare Benutzeroberfläche an, die erklärt, warum die App Zugriff auf das lokale Gerät oder die Companion-App benötigt, und löse die Anfrage über eine explizite Nutzergeste aus (z. B. einen Klick auf einen Button). - Wenn
state === 'denied', wird eine interaktive Benutzeroberfläche zur Fehlerbehebung gerendert, die den Nutzer anleitet, wie er den Zugriff auf das lokale Netzwerk oder Loopback in den Website-Einstellungen des Browsers wieder aktivieren kann.
/**
* 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 in fetch() und Request deklarieren
Wenn eine https://-Seite einen http://-Endpunkt im lokalen Netzwerk oder auf der Loopback-Schnittstelle anfordert, geben Sie targetAddressSpace: 'local' oder targetAddressSpace: 'loopback' in den Optionen fetch() oder Request an.
Das explizite Festlegen von targetAddressSpace hat zwei wichtige Auswirkungen:
- Ausnahme für gemischte Inhalte: Die Prüfung auf gemischte Inhalte erfolgt vor der DNS-Auflösung. Durch die Deklaration von
targetAddressSpace: 'local'(oder'loopback') wird der Browser vor der DNS-Auflösung darüber informiert, dass einehttp://-URL (einschließlich eines benutzerdefinierten DNS-Hostnamens, der in eine lokale IP-Adresse aufgelöst wird) auf einen lokalen/Loopback-Endpunkt verweist und von der Blockierung gemischter Inhalte ausgenommen werden sollte, sobald der Nutzer die Berechtigung erteilt. - Adressraumprüfung: Nach der DNS-Auflösung prüft der Browser, ob die aufgelöste IP-Adresse tatsächlich zum deklarierten
targetAddressSpacegehört. Wenn ein Hostname mittargetAddressSpace: 'local'unerwartet in einepublic-IP-Adresse aufgelöst wird, bricht der Browser die Anfrage sofort mit dem NetzwerkfehlerTypeErrorab.
/**
* 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. Echtzeitverbindungen mit WebSocket und WebTransport
Die Einschränkungen für den Zugriff auf das lokale Netzwerk gelten auch für WebSocket- und WebTransport-Verbindungen:
WebSocket: DerWebSocket(url, protocols)-Konstruktor akzeptiert nur einen Subprotokoll-String oder ein Subprotokoll-Array als zweites Argument. Übergeben Sie kein{ targetAddressSpace }-Optionsobjekt, da dies einenSyntaxErrorauslöst. Stattdessen prüft der Browser dieWebSocket-URL vor der DNS-Auflösung:ws://-Verbindungen, die von einerhttps://-Seite initiiert werden, sind von der Blockierung gemischter Inhalte ausgenommen, wenn der URL-Host syntaktisch als lokal oder Loopback erkennbar ist – insbesondere ein privates oder Loopback-IP-Literal (ws://192.168.1.100:8080,ws://127.0.0.1:8080,ws://[::1]:8080),localhost(*.localhost) oder ein.local-mDNS-Hostname (ws://device.local:8080) – und hinter der Berechtigungsaufforderunglocal-networkoderloopback-networkverborgen ist. Wenn ein lokaler Endpunkt über einen benutzerdefinierten DNS-Hostnamen, der nicht.localist, adressiert wird, stellen Sie eine Verbindung überwss://her.WebTransport: DaWebTransportzwingendhttps://(HTTP/3 über QUIC mit TLS oderserverCertificateHashesfür lokale selbst signierte Zertifikate) erfordert, werden nie Blöcke für gemischte Inhalte ausgelöst und die OptiontargetAddressSpacewird nicht akzeptiert. Stattdessen prüft der Browser automatisch den aufgelösten IP-Adressbereich und schränkt dastransport.ready-Promise hinter der Berechtigungsaufforderunglocal-networkoderloopback-networkein.
/**
* 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. Zugriff auf ursprungsübergreifende iFrames über Permissions-Policy delegieren
Standardmäßig haben local-network und loopback-network eine Standard-Zulassungsliste von 'self', wodurch ursprungsübergreifende <iframe>-Einbettungen daran gehindert werden, lokale oder Loopback-Anfragen zu stellen. Wenn Sie einem eingebetteten ursprungsübergreifenden <iframe> (z. B. einem Widget für die Hardwareeinrichtung oder einem SSO-Frame, der mit einem lokalen Agent kommuniziert) erlauben möchten, lokale oder Loopback-Anfragen zu initiieren, delegieren Sie die detaillierten Berechtigungen explizit im Attribut allow und/oder im 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>
Sende den Permissions-Policy-Header in der Antwort des übergeordneten (einbettenden) Dokuments, nicht in der Antwort des iFrames:
Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")
Fallback-Strategien
Wenn Ihr Baseline-Ziel die Berechtigungen für den Zugriff auf das lokale Netzwerk und die targetAddressSpace-Optionen nicht unterstützt, implementieren Sie die Funktionserkennung mit progressivem Fallback:
- Fallback für Berechtigungsabfrage: Verpacken Sie
navigator.permissions.query({ name: permissionName })in eintry / catch. Wenn der Browser einenTypeErrorausgibt, weil'local-network'oder'loopback-network'ein nicht erkannterPermissionNameist, behandeln Sie den Status als'prompt', damit die Anfrage weiterhin bei einem expliziten Nutzerklick ausgeführt wird. Der Verbindungsversuch selbst löst die Browseraufforderung aus, wenn LNA unterstützt wird. - Fragen Sie NICHT den alten kombinierten Berechtigungsnamen
{ name: 'local-network-access' }ab. In älteren Chrome-Versionen führt diese Abfrage zum Absturz des Renderer-Prozesses undtry / catchkann dies nicht verhindern. In neueren Chrome-Versionen wird'local-network-access'nur als Legacy-Alias für die detaillierten Berechtigungen behandelt. - Ordnungsgemäße Ablehnungsbehandlung: Verpacke
fetch(),WebSocket-Fehlerereignisse undtransport.readyimmer in Fehler-Handler, dieTypeError- / Verbindungsfehler abfangen und klare Schritte zur Behebung vorschlagen (prüfen, ob das lokale Gerät eingeschaltet, mit demselben Netzwerk verbunden und in den Browserberechtigungen zugelassen ist).
/**
* 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';
}
}