Yerel Ağ Erişimi

Web özellikleri: Yerel ağ erişimi

public kaynağından (veya intranet local kaynağından) sunulan bir web uygulaması, kullanıcının özel yerel ağına (ör. 192.168.1.x veya device.local adresindeki LAN yazıcısı, IoT hub'ı ya da yönlendirici) veya kullanıcının makinesinde çalışan yerel bir tamamlayıcı hizmete (127.0.0.1, [::1] veya localhost) bağlandığında tarayıcılar, bağlantıyı Yerel Ağ Erişimi (LNA) izin istemiyle kısıtlar.

Yerel ağ erişimi, erişimi IP adresi alanı kafesine (public → local → loopback) dayalı olarak iki ayrıntılı izne ve Permissions-Policy jetona böler. Kontroller yalnızca bir istek daha az gizli bir adres alanından daha gizli bir adres alanına geçtiğinde uygulanır:

  • local-network: public kaynaklı bir adresten local adres alanına (RFC 1918 özel IPv4 aralıkları 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, yerel bağlantı 169.254.0.0/16 / fe80::/10, IPv6 Benzersiz Yerel Adresleri fc00::/7 ve .local mDNS ana makine adları) bağlanırken gereklidir.
  • loopback-network: public veya local kaynağından loopback adres alanına (127.0.0.0/8, ::1/128 veya localhost) bağlanırken gereklidir.

localhost (127.0.0.1 ve [::1]) zaten en gizli (loopback) adres alanında olduğundan, yerel geliştirme sırasında web uygulamanızı http://localhost adresinden yayınlamak varsayılan olarak LNA kontrollerini tetiklemez. LNA istemlerini ve izin durumlarını herkese açık bir sunucuya dağıtmadan yerel olarak test etmek için --ip-address-space-overrides=127.0.0.1:8080=public,[::1]:8080=public Chromium işaretini kullanarak yerel geliştirme sunucunuzun IPv4 ve IPv6 adres alanını public olarak geçersiz kılın veya tarayıcının Site ayarları bölümünde Yerel ağ ve Geri döngü ağı'nı Ask, Allow ve Block arasında manuel olarak değiştirin.

ZORUNLU: Yerel veya geri döngü ağ isteklerini başlatan tüm web uygulamalarını Güvenli Bağlam'dan (https://) sunun. Yerel ağ erişimi izinleri, güvenli olmayan (http://) üst düzey bağlamlarda otomatik olarak reddedilir.

Kullanımdan kaldırılan Özel Ağ Erişimi (PNA) ön kontrol başlıklarına (Access-Control-Request-Private-Network: true / Access-Control-Allow-Private-Network: true) GÜVENMEYİN. Yerel Ağ Erişimi, kaynaklar arası yanıtları okurken standart CORS başlıklarıyla (Access-Control-Allow-Origin) birlikte istemci tarafı kullanıcı izni istemleri (local-network ve loopback-network) aracılığıyla erişimi zorunlu kılar. PNA ön kontrol istekleri hiçbir zaman engelleyici bir şart olarak uygulanmadı ve yedek olarak gerekli değildir. Halihazırda Access-Control-Allow-Private-Network: true döndüren mevcut sunucular veya cihaz donanım yazılımları, bunu zararsız bir şekilde yapmaya devam edebilir ancak yeni koda PNA işleme eklemeyin.

Uygulama Adımları

1. Ayrıntılı İzin Durumunu Önceden Kontrol Etme ve Kullanıcı Hareketine Göre Geçit Oluşturma

İzin durumu 'prompt' olduğunda sayfa yüklenirken (DOMContentLoaded) hiçbir zaman koşulsuz olarak yerel ağ veya loopback istekleri göndermeyin ve hiçbir zaman hizmet çalışanından veya paylaşılan çalışandan (ekli Document içermeyen ve izin zaten 'granted' değilse hemen başarısız olan) ilk izni tetikleyen isteği başlatmayın.

Bunun yerine, önce 'local-network' veya 'loopback-network' için navigator.permissions.query() sorgusu yapın:

  • state === 'granted' ise otomatik olarak bağlan.
  • state === 'prompt' ise uygulamanın yerel cihaza veya tamamlayıcı uygulamaya neden erişmesi gerektiğini açıklayan net bir kullanıcı arayüzü gösterin ve isteği açık bir kullanıcı hareketiyle (ör. düğme tıklaması) tetikleyin.
  • state === 'denied' ise kullanıcıya tarayıcı site ayarlarında yerel ağ veya loopback erişimini nasıl yeniden etkinleştireceğini gösteren, işlem yapılabilir bir düzeltme kullanıcı arayüzü oluşturun.
/**
 * 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. fetch() ve Request ürünlerinde targetAddressSpace beyan edin

Bir https:// sayfası, yerel ağda veya loopback arayüzünde bir http:// uç noktası istediğinde fetch() veya Request seçeneklerinde targetAddressSpace: 'local' ya da targetAddressSpace: 'loopback' değerini belirtin.

targetAddressSpace değerini açıkça ayarlamak iki önemli işlevi yerine getirir:

  1. Karma İçerik Muafiyeti: Karma İçerik kontrolleri, DNS çözümlemesinden önce çalıştırılır. targetAddressSpace: 'local' (veya 'loopback') bildirmek, tarayıcıya DNS çözümlemesinden önce bir http:// URL'nin (yerel IP'ye çözümlenen özel bir DNS ana makine adı dahil) yerel/loopback uç noktasını hedeflediğini ve kullanıcı izin verdikten sonra karma içerik engellemesinden muaf tutulması gerektiğini bildirir.
  2. Adres Alanı Doğrulaması: DNS çözümlemesinden sonra tarayıcı, çözümlenen IP adresinin gerçekten beyan edilen targetAddressSpace adresine ait olduğunu doğrular. targetAddressSpace: 'local' içeren bir ana makine adı beklenmedik bir şekilde public IP adresine çözümlenirse tarayıcı, isteği hemen TypeError ağ hatasıyla iptal eder.
/**
 * 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 ve WebTransport ile Gerçek Zamanlı Bağlantılar

Yerel ağ erişimi kısıtlamaları, WebSocket ve WebTransport bağlantılarını da yönetir:

  • WebSocket: WebSocket(url, protocols) oluşturucusu, ikinci bağımsız değişkeni olarak yalnızca bir alt protokol dizesini veya dizisini kabul eder. SyntaxError hatasına neden olan bir { targetAddressSpace } seçenek nesnesi göndermeyin. Bunun yerine tarayıcı, DNS çözümlemesinden önce WebSocket URL'sini inceler: https:// sayfasından başlatılan ws:// bağlantıları, URL ana makinesi söz dizimi açısından yerel veya loopback olarak tanınabiliyorsa (özellikle özel veya loopback IP değişmezi (ws://192.168.1.100:8080, ws://127.0.0.1:8080, ws://[::1]:8080), localhost (*.localhost) ya da .local mDNS ana makine adı (ws://device.local:8080)) ve local-network veya loopback-network izin isteminin arkasında yer alıyorsa karma içerik engellemesinden muaf tutulur. Yerel bir uç noktaya özel bir .local dışı DNS ana makine adı üzerinden erişiliyorsa wss:// üzerinden bağlanın.
  • WebTransport: WebTransport, https://'yi (TLS ile QUIC üzerinden HTTP/3 veya yerel olarak kendinden imzalı sertifikalar için serverCertificateHashes) kesinlikle gerektirdiğinden hiçbir zaman karma içerik engellemelerini tetiklemez ve targetAddressSpace seçeneğini kabul etmez. Bunun yerine tarayıcı, çözümlenen IP adresi alanını otomatik olarak doğrular ve transport.ready vaadini local-network veya loopback-network izin isteminin arkasına kilitler.
/**
 * 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. Permissions-Policy aracılığıyla çapraz kaynak iframe'lere erişim yetkisi verme

Varsayılan olarak local-network ve loopback-network, 'self' için varsayılan bir izin verilenler listesine sahiptir ve kaynaklar arası <iframe> yerleştirmelerin yerel veya loopback istekleri göndermesini engeller. Yerleşik bir çapraz kaynak <iframe> (ör. donanım kurulumu widget'ı veya yerel bir aracıyla iletişim kuran TOA çerçevesi) öğesinin yerel veya loopback istekleri başlatmasına izin vermek için allow özelliğinde ve/veya HTTP Permissions-Policy başlığında ayrıntılı izinleri açıkça yetkilendirin:

<!-- 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 üstbilgisini iframe'in yanıtında değil, üst düzey (yerleştirme) dokümanın yanıtında gönderin:

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

Yedek Stratejiler

Baseline hedefiniz yerel ağ erişimi izinlerini ve targetAddressSpace seçeneklerini desteklemiyorsa kademeli yedekle özellik algılamayı uygulayın:

  1. İzin Sorgusu Yedeklemesi: navigator.permissions.query({ name: permissionName }) öğesini try / catch içine sarmalayın. 'local-network' veya 'loopback-network' tanınmayan bir PermissionName olduğu için tarayıcı TypeError hatası verirse durumu 'prompt' olarak değerlendirin. Böylece istek, kullanıcı açıkça tıkladığında çalışmaya devam eder (bağlantı girişimi, LNA'nın desteklendiği tarayıcı istemini tetikler).
  2. Eski birleşik izin adı { name: 'local-network-access' } için sorgu YAPMAYIN. Chrome'un eski sürümlerinde bu sorgu, oluşturucu işleminin kilitlenmesine neden olur ve try / catch bunu engelleyemez. Chrome'un daha yeni sürümleri, 'local-network-access' politikasını yalnızca ayrıntılı izinler için eski bir takma ad olarak değerlendirir.
  3. Reddedilen İstekleri Sorunsuz Yönetme: fetch(), WebSocket hata etkinlikleri ve transport.ready'yi her zaman TypeError/bağlantı hatalarını yakalayan ve net çözüm adımları (yerel cihazın açık, aynı ağa bağlı ve tarayıcı izinlerinde izin verilmiş olduğunu kontrol etme) sunan hata işleyicilerle sarmalayın.
/**
 * 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';
  }
}