כשאפליקציית אינטרנט שמוצגת ממקור public (או ממקור local באינטראנט) מתחברת לרשת המקומית הפרטית של המשתמש (כמו מדפסת LAN, רכזת IoT או נתב בכתובת 192.168.1.x או device.local) או לשירות נלווה מקומי שפועל במכשיר של המשתמש (127.0.0.1, [::1] או localhost), הדפדפנים חוסמים את החיבור ומציגים בקשת הרשאה לגישה לרשת המקומית (LNA).
הגישה לרשת המקומית מחלקת את הגישה לשתי הרשאות מפורטות ולאסימוני Permissions-Policy על סמך סריג מרחב כתובות ה-IP (public ← local ← loopback), ואוכפת בדיקות רק כשבקשה עוברת ממרחב כתובות פחות פרטי למרחב כתובות יותר פרטי:
-
local-network: נדרש כשמתחברים ממקורpublicלמרחב הכתובותlocal(טווחי IPv4 פרטיים של RFC 191810.0.0.0/8,172.16.0.0/12,192.168.0.0/16, link-local169.254.0.0/16/fe80::/10, כתובות מקומיות ייחודיות של IPv6fc00::/7, ושמות מארחים של mDNS.local). -
loopback-network: נדרש כשמתחברים ממקורpublicאוlocalלמרחב הכתובות שלloopback(127.0.0.0/8,::1/128אוlocalhost).
מכיוון ש-localhost (127.0.0.1 ו-[::1]) כבר נמצא במרחב הכתובות הפרטי ביותר (loopback), הצגת אפליקציית האינטרנט מ-http://localhost במהלך פיתוח מקומי לא תפעיל בדיקות LNA כברירת מחדל. כדי לבדוק את ההודעות וההרשאות של LNA באופן מקומי בלי לפרוס אותן בשרת ציבורי, אפשר לשנות את מרחב הכתובות של IPv4 ו-IPv6 בשרת הפיתוח המקומי ל-public באמצעות הדגל --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public של Chromium, או להחליף באופן ידני את הרשת המקומית ואת רשת הלולאה החוזרת בין Ask, Allow ו-Block בהגדרות האתר בדפדפן.
חובה: צריך להציג כל אפליקציית אינטרנט שיוזמת בקשות לאחזור מהרשת המקומית או מרשת הלולאה החוזרת מהקשר מאובטח (https://). הרשאות גישה לרשת המקומית נדחות באופן אוטומטי בהקשרים לא מאובטחים (http://) ברמה העליונה.
אל תסתמכו על כותרות של בקשות קדם-הפעלה של גישה לרשת פרטית (PNA) שהוצאו משימוש (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true). הגישה לרשת המקומית אוכפת את הגישה באמצעות בקשות להרשאות משתמש בצד הלקוח (local-network ו-loopback-network) בשילוב עם כותרות CORS רגילות (Access-Control-Allow-Origin) כשקוראים תגובות ממקורות שונים. בדיקות מקדימות של PNA אף פעם לא נאכפו כדרישה לחסימה, ואין צורך בהן כחלופה. שרתים קיימים או קושחת מכשיר שכבר מחזירים את הערך Access-Control-Allow-Private-Network: true יכולים להמשיך לעשות זאת בלי לגרום נזק, אבל אין להוסיף טיפול ב-PNA לקוד חדש.
שלבי ההטמעה
1. בדיקה מראש של מצב ההרשאה הספציפית והגבלת הגישה לתנועה של המשתמש
אף פעם אל תפעילו בקשות לרשת מקומית או ללולאה חוזרת (loopback) ללא תנאי בזמן טעינת הדף (DOMContentLoaded) כשמצב ההרשאה הוא 'prompt', ואף פעם אל תפעילו את הבקשה הראשונה שמפעילה הרשאה מ-Service Worker או מ-Shared Worker (שאין להם Document מצורף והם ייכשלו מיד אם ההרשאה היא לא 'granted').
במקום זאת, קודם צריך לשלוח שאילתה ל-navigator.permissions.query() לגבי 'local-network' או 'loopback-network':
- אם
state === 'granted', מתחברים אוטומטית. - אם
state === 'prompt', צריך להציג ממשק משתמש ברור שמסביר למה האפליקציה צריכה גישה למכשיר המקומי או לאפליקציה הנלווית, ולהפעיל את הבקשה באמצעות תנועה מפורשת של המשתמש (כמו לחיצה על כפתור). - אם
state === 'denied', צריך להציג ממשק משתמש עם הוראות לתיקון הבעיה, שמסביר למשתמש איך להפעיל מחדש את הגישה לרשת המקומית או ל-Loopback בהגדרות האתר בדפדפן.
/**
* 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 ב-fetch() וב-Request
כשדף https:// מבקש נקודת קצה (endpoint) מסוג http:// בממשק הרשת המקומית או בממשק הלולאה החוזרת (loopback), צריך לציין את הערך targetAddressSpace: 'local' או targetAddressSpace: 'loopback' באפשרויות fetch() או Request.
הגדרה מפורשת של targetAddressSpace מבצעת שתי פעולות קריטיות:
- פטור מתוכן מעורב: בדיקות של תוכן מעורב פועלות לפני פענוח DNS. הצהרה על
targetAddressSpace: 'local'(או'loopback') מודיעה לדפדפן לפני פענוח ה-DNS שכתובת URL שלhttp://(כולל שם מארח DNS מותאם אישית שמפוענח לכתובת IP מקומית) מטרגטת נקודת קצה מקומית או נקודת קצה מסוג loopback, וצריכה להיות פטורה מחסימת תוכן מעורב אחרי שהמשתמש מעניק את ההרשאה. - אימות מרחב הכתובות: אחרי פענוח ה-DNS, הדפדפן מוודא שכתובת ה-IP המפוענחת באמת שייכת ל-
targetAddressSpaceהמוצהר. אם שם מארח עםtargetAddressSpace: 'local'מפנה באופן בלתי צפוי לכתובת IP שלpublic, הדפדפן מבטל מיד את הבקשה עם שגיאה בחיבור לרשת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. חיבורים בזמן אמת עם WebSocket ו-WebTransport
הגבלות הגישה לרשת המקומית חלות גם על חיבורי WebSocket ו-WebTransport:
-
WebSocket: ה-constructor שלWebSocket(url, protocols)מקבל רק מחרוזת או מערך של תת-פרוטוקול כארגומנט השני שלו – אל תעבירו אובייקט של אפשרויות{ targetAddressSpace }, כי זה יגרום ל-SyntaxError. במקום זאת, הדפדפן בודק את כתובת ה-URL שלWebSocketלפני פענוח ה-DNS: חיבוריws://שנוצרו מדףhttps://פטורים מחסימת תוכן מעורב כשמארח כתובת ה-URL ניתן לזיהוי תחבירי כמקומי או כ-loopback – באופן ספציפי כתובת IP פרטית או כתובת IP של loopback (ws://192.168.1.100:8080,ws://127.0.0.1:8080,ws://[::1]:8080),localhost(*.localhost), או שם מארח mDNS של.local(ws://device.local:8080) – ומוגבלים מאחורי בקשת ההרשאהlocal-networkאוloopback-network. אם נקודת קצה מקומית מקבלת פנייה דרך שם מארח DNS מותאם אישית שאינו.local, צריך להתחבר דרךwss://. WebTransport: מכיוון ש-WebTransportדורש באופן מחמיר אתhttps://(HTTP/3 over QUIC עם TLS אוserverCertificateHashesלאישורים מקומיים בחתימה עצמית), הוא אף פעם לא מפעיל חסימות של תוכן מעורב ולא מקבל אפשרות שלtargetAddressSpace. במקום זאת, הדפדפן מאמת באופן אוטומטי את מרחב כתובות ה-IP שנפתרו, ומגביל את ה-promise שלtransport.readyמאחורי בקשת ההרשאה שלlocal-networkאו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. הענקת גישה ל-iframe ממקורות שונים באמצעות Permissions-Policy
כברירת מחדל, ל-local-network ול-loopback-network יש רשימת היתרים שמוגדרת כברירת מחדל של 'self', שחוסמת הטמעות של <iframe> ממקורות שונים מלשלוח בקשות מקומיות או בקשות לולאה חוזרת (loopback). כדי לאפשר ל-<iframe> מוטמע ממקורות שונים (כמו ווידג'ט להגדרת חומרה או מסגרת SSO שמתקשרת עם סוכן מקומי) לשלוח בקשות מקומיות או בקשות לולאה חוזרת (loopback), צריך להקצות באופן מפורש את ההרשאות המפורטות במאפיין allow ו/או בכותרת ה-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>
שולחים את הכותרת Permissions-Policy בתגובה של המסמך ברמה העליונה (המסמך המוטמע), ולא בתגובה של ה-iframe:
Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")
שיטות חלופיות
אם יעד ה-Baseline שלכם לא תומך בהרשאות גישה לרשת המקומית ובאפשרויות של targetAddressSpace, אתם יכולים להטמיע זיהוי תכונות עם חלופה הדרגתית:
- Permission Query Fallback: Wrap
navigator.permissions.query({ name: permissionName })in atry / catch. אם הדפדפן מחזירTypeErrorכי'local-network'או'loopback-network'הםPermissionNameלא מזוהים, צריך להתייחס למצב כאל'prompt'כדי שהבקשה עדיין תפעל בלחיצה מפורשת של המשתמש (ניסיון החיבור עצמו מפעיל את ההנחיה בדפדפן שבו יש תמיכה ב-LNA). - אל תשלחו שאילתה לשם ההרשאה המשולבת הקודמת
{ name: 'local-network-access' }. בגרסאות ישנות יותר של Chrome, השאילתה הזו גורמת לקריסה של תהליך הרינדור, ו-try / catchלא יכול למנוע את זה. בגרסאות חדשות יותר של Chrome, המערכת מתייחסת ל-'local-network-access'רק ככינוי מדור קודם להרשאות המפורטות. - טיפול תקין בדחיות: תמיד צריך לעטוף את
fetch(), אירועי השגיאה שלWebSocketו-transport.readyב-error handlers שתופסים כשלים ב-TypeErrorאו בחיבור ומציגים שלבים ברורים לפתרון הבעיה (בדיקה שהמכשיר המקומי מופעל, מחובר לאותה רשת ומורשה בהרשאות הדפדפן).
/**
* 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';
}
}