وقتی برنامه وبی که از مبدأ public (یا مبدأ اینترانت local) ارائه میشود به شبکه محلی خصوصی کاربر (مثل چاپگر LAN، هاب اینترنت اشیا، یا رهیاب در 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، IPv6 Unique Local Addressesfc00::/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 سرور توسعه محلیتان را بااستفاده از پرچم Chromium --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public به public لغو کنید، یا شبکه محلی و شبکه حلقه بازگشتی را بهصورت دستی بین 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 را به کد جدید اضافه نکنید.
مراحل پیادهسازی
۱. بررسی اولیه وضعیت اجازه جزئی و دروازه در اشاره کاربر
هرگز درخواستهای شبکه محلی یا حلقه برگشتی را بدون قیدوشرط در بارگیری صفحه (DOMContentLoaded) زمانی که وضعیت اجازه 'prompt' است، فعال نکنید و هرگز اولین درخواست فعالکننده اجازه را از Service Worker یا Shared Worker (که فاقد Document پیوستشده هستند و اگر اجازه ازقبل 'granted' نباشد، فوراً ناموفق خواهند بود) شروع نکنید.
درعوض، ابتدا navigator.permissions.query() را برای 'local-network' یا 'loopback-network' پرسمان کنید:
- اگر
state === 'granted'، بهطور خودکار متصل شود. - اگر
state === 'prompt'، رابط کاربری واضحی نمایش دهید که توضیح دهد چرا برنامه به دسترسی به دستگاه محلی یا برنامه همراه نیاز دارد، و درخواست را ازطریق اشاره صریح کاربر (مثل کلیک کردن روی دکمه) راهاندازی کنید. - اگر
state === 'denied'، میانای کاربر اصلاحی قابلاجرا را ارائه دهید که به کاربر آموزش دهد چگونه دسترسی به «شبکه محلی» یا «لوپبک» را در تنظیمات سایت مرورگر دوباره فعال کند.
/**
* 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';
}
}
۲. اعلام targetAddressSpace در fetch() و Request
وقتی صفحه https:// درخواست نقطه پایانی http:// در شبکه محلی یا میانای برگشت حلقه میکند، targetAddressSpace: 'local' یا targetAddressSpace: 'loopback' را در گزینههای fetch() یا Request مشخص کنید.
تنظیم صریح targetAddressSpace دو کار مهم انجام میدهد:
- معافیت محتوای مختلط: بررسیهای محتوای مختلط قبلاز وضوح DNS اجرا میشود. اعلام
targetAddressSpace: 'local'(یا'loopback') پیشاز وضوح ساناد به مرورگر اطلاع میدهد که نشانی وبhttp://(ازجمله نام میزبان ساناد سفارشی که به IP محلی تبدیل میشود) نقطه پایانی محلی/ حلقه برگشتی را هدف قرار میدهد و پساز اینکه کاربر اجازه را اعطا کرد باید از مسدود کردن «محتوای مختلط» معاف شود. - درستیسنجی فضای نشانی: پساز وضوح ساناد، مرورگر درستیسنجی میکند که نشانی IP وضوحیافته واقعاً به
targetAddressSpaceاعلامشده تعلق دارد. اگر نام میزبانی باtargetAddressSpace: 'local'بهطور غیرمنتظرهای به نشانی IPpublicتبدیل شود، مرورگر بلافاصله درخواست را با خطای شبکه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();
}
۳. اتصالهای همزمان با WebSocket و WebTransport
محدودیتهای «دسترسی به شبکه محلی» بر اتصالات WebSocket و WebTransport نیز حاکم است:
WebSocket: سازندهWebSocket(url, protocols)فقط رشته یا آرایه زیرپروتکل را بهعنوان آرگومان دوم میپذیرد—شیء گزینههای{ targetAddressSpace }را ارسال نکنید، چون باعث ایجادSyntaxErrorمیشود. درعوض، مرورگر نشانی وبWebSocketرا قبلاز وضوح ساناد بررسی میکند: اتصالاتws://که از صفحهhttps://شروع شدهاند، وقتی میزبان نشانی وب ازنظر نحوی بهعنوان محلی یا حلقه برگشتی قابلتشخیص باشد (بهویژه IP تحتاللفظی خصوصی یا حلقه برگشتی (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قرار داشته باشد، از مسدود کردن «محتوای مختلط» معاف میشوند. اگر نقطه پایانی محلی ازطریق نام میزبان ساناد سفارشی غیر.localآدرسدهی شده است، ازطریقwss://متصل شوید.WebTransport: چونWebTransportاکیداً بهhttps://(HTTP/3 روی QUIC با TLS یاserverCertificateHashesبرای گواهیهای خودامضاشده محلی) نیاز دارد، هرگز مسدود کردن «محتوای مختلط» را راهاندازی نمیکند و گزینهtargetAddressSpaceرا نمیپذیرد. درعوض، مرورگر بهطور خودکار فضای نشانی IP حلشده را درستیسنجی میکند و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;
}
۴. Delegate Access to Cross-Origin Iframes via Permissions-Policy
بهطور پیشفرض، local-network و loopback-network دارای فهرست مجاز پیشفرض 'self' هستند که جاسازیهای <iframe> میانمبدئی را از ایجاد درخواستهای محلی یا برگشتحلقهای مسدود میکند. برای اینکه به <iframe> میانمبدئی جاسازیشده (مثل ابزاره تنظیم سختافزار یا قاب SSO که با نماینده محلی ارتباط برقرار میکند) اجازه دهید درخواستهای محلی یا برگشتبهعقب را آغاز کند، اجازههای جزئی را بهطور صریح در مشخصه allow و/یا سرایند 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>
سرایند Permissions-Policy را در پاسخ سند سطح بالا (جاسازی) ارسال کنید، نه در پاسخ iframe:
Permissions-Policy: local-network=(self "https://setup.partner.example.com"), loopback-network=(self "https://setup.partner.example.com")
راهبردهای جایگزین
اگر هدف «خط پایه» شما از اجازههای «دسترسی به شبکه محلی» و گزینههای 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را در مدیریتکنندههای خطا قرار دهید که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';
}
}