Saat aplikasi web yang disajikan dari origin public (atau origin local intranet) terhubung ke jaringan lokal pribadi pengguna (seperti printer LAN, hub IoT, atau router di 192.168.1.x atau device.local) atau ke layanan pendamping lokal yang berjalan di komputer pengguna (127.0.0.1, [::1], atau localhost), browser akan membatasi koneksi di balik dialog izin Akses Jaringan Lokal (LNA).
Local Network Access membagi akses menjadi dua izin terperinci dan token Permissions-Policy berdasarkan kisi ruang alamat IP (public → local → loopback), yang menerapkan pemeriksaan hanya saat permintaan berpindah dari ruang alamat yang kurang pribadi ke ruang alamat yang lebih pribadi:
local-network: Diperlukan saat menghubungkan dari originpublicke ruang alamatlocal(rentang IPv4 pribadi RFC 191810.0.0.0/8,172.16.0.0/12,192.168.0.0/16, link-local169.254.0.0/16/fe80::/10, Alamat Lokal Unik IPv6fc00::/7, dan nama host mDNS.local).loopback-network: Diperlukan saat menghubungkan dari originpublicataulocalke ruang alamatloopback(127.0.0.0/8,::1/128, ataulocalhost).
Karena localhost (127.0.0.1 dan [::1]) sudah berada di ruang alamat paling pribadi (loopback), menyajikan aplikasi web Anda dari http://localhost selama pengembangan lokal tidak akan memicu pemeriksaan LNA secara default. Untuk menguji dialog LNA dan status izin secara lokal tanpa men-deploy ke server publik, ganti ruang alamat IPv4 dan IPv6 server pengembangan lokal Anda ke public menggunakan flag Chromium --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public, atau alihkan Jaringan lokal dan Jaringan loopback secara manual antara Ask, Allow, dan Block di Setelan situs browser.
WAJIB: Tayangkan aplikasi web apa pun yang memulai permintaan jaringan lokal atau jaringan loopback dari Konteks Aman (https://). Izin Akses Jaringan Lokal otomatis ditolak dalam konteks tingkat teratas yang tidak aman (http://).
JANGAN mengandalkan header preflight Akses Jaringan Pribadi (PNA) yang tidak digunakan lagi (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true). Akses Jaringan Lokal menerapkan akses melalui dialog izin pengguna sisi klien (local-network dan loopback-network) yang digabungkan dengan header CORS standar (Access-Control-Allow-Origin) saat membaca respons lintas origin. Preflight PNA tidak pernah diterapkan sebagai persyaratan pemblokiran dan tidak diperlukan sebagai penggantian. Server atau firmware perangkat yang sudah menampilkan Access-Control-Allow-Private-Network: true dapat terus melakukannya tanpa membahayakan, tetapi jangan menambahkan penanganan PNA ke kode baru.
Langkah-Langkah Penerapan
1. Pra-Periksa Status Izin Terperinci dan Gerbang pada Gestur Pengguna
Jangan pernah mengaktifkan permintaan jaringan lokal atau loopback tanpa syarat saat pemuatan halaman (DOMContentLoaded) jika status izinnya adalah 'prompt', dan jangan pernah memulai permintaan pemicu izin pertama dari Service Worker atau Shared Worker (yang tidak memiliki Document terlampir dan akan langsung gagal jika izin belum 'granted').
Sebagai gantinya, buat kueri navigator.permissions.query() untuk 'local-network' atau 'loopback-network' terlebih dahulu:
- Jika
state === 'granted', hubungkan secara otomatis. - Jika
state === 'prompt', tampilkan UI yang jelas yang menjelaskan alasan aplikasi memerlukan akses ke perangkat lokal atau aplikasi pendamping, dan picu permintaan dari gestur pengguna yang eksplisit (seperti klik tombol). - Jika
state === 'denied', render UI perbaikan yang dapat ditindaklanjuti yang menginstruksikan pengguna cara mengaktifkan kembali akses Jaringan Lokal atau Loopback di setelan situs 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. Mendeklarasikan targetAddressSpace di fetch() dan Request
Saat halaman https:// meminta endpoint http:// di jaringan lokal atau antarmuka loopback, tentukan targetAddressSpace: 'local' atau targetAddressSpace: 'loopback' di opsi fetch() atau Request.
Menetapkan targetAddressSpace secara eksplisit akan melakukan dua hal penting:
- Pengecualian Konten Campuran: Pemeriksaan Konten Campuran dijalankan sebelum resolusi DNS. Mendeklarasikan
targetAddressSpace: 'local'(atau'loopback') akan memberi tahu browser sebelum resolusi DNS bahwa URLhttp://(termasuk nama host DNS kustom yang me-resolve ke IP lokal) menargetkan endpoint lokal/loopback dan harus dikecualikan dari pemblokiran Konten Campuran setelah pengguna memberikan izin. - Verifikasi Ruang Alamat: Setelah resolusi DNS, browser akan memverifikasi bahwa alamat IP yang di-resolve benar-benar milik
targetAddressSpaceyang dideklarasikan. Jika nama host dengantargetAddressSpace: 'local'secara tidak terduga di-resolve ke alamat IPpublic, browser akan segera membatalkan permintaan dengan error jaringanTypeError.
/**
* 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. Koneksi Real-Time dengan WebSocket dan WebTransport
Pembatasan Akses Jaringan Lokal juga mengatur koneksi WebSocket dan WebTransport:
WebSocket: KonstruktorWebSocket(url, protocols)hanya menerima string atau array subprotokol sebagai argumen keduanya—jangan meneruskan objek opsi{ targetAddressSpace }, yang akan menampilkanSyntaxError. Sebagai gantinya, browser akan memeriksa URLWebSocketsebelum resolusi DNS: Koneksiws://yang dimulai dari halamanhttps://dikecualikan dari pemblokiran Konten Campuran jika host URL dapat dikenali secara sintaksis sebagai lokal atau loopback—khususnya literal IP pribadi atau loopback (ws://192.168.1.100:8080,ws://127.0.0.1:8080,ws://[::1]:8080),localhost(*.localhost), atau nama host mDNS.local(ws://device.local:8080)—dan dibatasi di balik dialog izinlocal-networkatauloopback-network. Jika endpoint lokal ditangani melalui nama host DNS non-.localkustom, hubungkan melaluiwss://.WebTransport: KarenaWebTransportsangat memerlukanhttps://(HTTP/3 melalui QUIC dengan TLS atauserverCertificateHashesuntuk sertifikat yang ditandatangani sendiri secara lokal), protokol ini tidak pernah memicu pemblokiran Konten Campuran dan tidak menerima opsitargetAddressSpace. Sebagai gantinya, browser akan otomatis memverifikasi ruang alamat IP yang di-resolve dan membatasi promisetransport.readydi balik dialog izinlocal-networkatauloopback-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. Mendelegasikan Akses ke Iframe Lintas Origin melalui Permissions-Policy
Secara default, local-network dan loopback-network memiliki daftar yang diizinkan default 'self', yang memblokir sematan <iframe> lintas origin agar tidak membuat permintaan lokal atau loopback. Untuk mengizinkan <iframe> lintas origin yang disematkan (seperti widget penyiapan hardware atau frame SSO yang berkomunikasi dengan agen lokal) memulai permintaan lokal atau loopback, delegasikan izin terperinci secara eksplisit di atribut allow dan/atau header Permissions-Policy HTTP:
<!-- 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>
Kirim header Permissions-Policy pada respons dokumen tingkat teratas (penyematan), bukan pada respons iframe:
Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")
Strategi Penggantian
Jika target Baseline Anda tidak mendukung izin Akses Jaringan Lokal dan opsi targetAddressSpace, terapkan deteksi fitur dengan penggantian progresif:
- Penggantian Kueri Izin: Gabungkan
navigator.permissions.query({ name: permissionName })dalamtry / catch. Jika browser menampilkanTypeErrorkarena'local-network'atau'loopback-network'adalahPermissionNameyang tidak dikenali, anggap statusnya sebagai'prompt'sehingga permintaan tetap berjalan pada klik pengguna eksplisit (upaya koneksi itu sendiri memicu dialog browser tempat LNA didukung). - JANGAN mengkueri nama izin gabungan lama
{ name: 'local-network-access' }. Di Chrome versi lama, kueri ini akan membuat proses perender error, dantry / catchtidak dapat mencegahnya. Versi Chrome yang lebih baru memperlakukan'local-network-access'hanya sebagai alias lama untuk izin terperinci. - Penanganan Penolakan yang Tepat: Selalu gabungkan
fetch(), peristiwa errorWebSocket, dantransport.readydalam pengendali error yang menangkap kegagalan koneksi/TypeErrordan menyajikan langkah-langkah perbaikan yang jelas (memeriksa apakah perangkat lokal diaktifkan, terhubung ke jaringan yang sama, dan diizinkan dalam izin 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';
}
}