# Local Network Access

> Chrome 142 在公网页面请求本地网络或回环地址前先征求权限。本页说明地址空间、两项权限、fetch 的 targetAddressSpace 选项，以及权限提示的原文。

Local Network Access 是浏览器在允许公网页面向用户本地网络上的设备（路由器、打印机、开发服务器）或本机发送请求之前征求的权限。页面不需要调用任何 API 去申请它：第一次指向这类地址的 `fetch()`、子资源加载或 iframe 导航就会触发提示，提示被拒则请求失败。脚本能看到的唯一接口是 `fetch()` 与 `Request` 上的 `targetAddressSpace` 选项，用来声明请求的去向，让浏览器据此免除混合内容拦截。

Chrome 142 默认显示该提示；Chrome 138 起可通过把 `chrome://flags/#local-network-access-check` 设为「Enabled (Blocking)」提前启用。`Request.targetAddressSpace` 自 Chrome 142 存在，Firefox、Safari、Android WebView 的任何版本都没有（BCD `api.Request.targetAddressSpace`）。这一设计取代了 Private Network Access：后者靠本地设备必须应答的 CORS 预检来把关同类请求，Chrome 已将其搁置。

## 工作原理

规范把每个 IP 地址归入三个地址空间之一：`loopback`（127.0.0.0/8、::1/128、0.0.0.0/32、::/128、198.18.0.0/15）、`local`（10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、运营商级 NAT 的 100.64.0.0/10、链路本地的 169.254.0.0/16、fc00::/7、fe80::/10、fec0::/10、0.0.0.0/8，以及 IPv6 文档前缀 2001:db8::/32 与 3fff::/20），其余全部为 `public`。IPv4 映射的 IPv6 地址（::ffff:0:0/96）按其内嵌 IPv4 地址归类。本地网络请求指目标比发起文档所在空间更不公开的请求：public 到 local、public 到 loopback、local 到 loopback。

两项受策略控制的特性守住这些请求，默认允许列表都是 `'self'`：`local-network` 管去往 local 地址的请求，`loopback-network` 管去往 loopback 地址的请求。旧的单一名称 `local-network-access` 在 Chromium 中仍作为两者的别名保留，`Permissions-Policy` 响应头和 iframe 的 `allow` 属性里都能用。因此需要该能力的跨源 iframe 必须以 `allow="local-network"`（或 `allow="local-network-access"`）嵌入；未委托时其请求直接失败，不出现提示。只有安全上下文才能持有该权限，`http://` 页面会被直接拦下。

由于多数本地设备仍只提供明文 HTTP，规范规定：浏览器若能在 DNS 解析前判断这是本地网络请求，就免除混合内容检查。判断依据有三种：主机名是私有 IP 字面量（`http://192.168.0.1/`）、主机名以 `.local` 结尾、调用带有 `targetAddressSpace: "local"` 或 `"loopback"`。声明会在建立连接后核验：解析出的地址不在声明的空间内时请求失败，而不是悄悄变成公网请求。豁免只针对连接而不针对响应：一个恰好解析到 192.168.0.1 的公网主机名若未加注解，仍算混合内容。

Chrome 对 `fetch()`、子资源加载（`<img>`、`<script>`、CSS）和 iframe 导航执行检查；Service Worker 与 Shared Worker 只有在同源文档已经获得授权后才能发起这类请求（Worker 无法弹出提示，[crbug.com/404887282](https://crbug.com/404887282) 跟踪修复）。在 Chrome 138 的可选测试阶段，WebSocket、WebTransport 和 WebRTC 连接尚未纳入权限管控（[crbug.com/421156866](https://crbug.com/421156866)、[crbug.com/421216834](https://crbug.com/421216834)、[crbug.com/421223919](https://crbug.com/421223919)）。Chrome 的第一阶段只管控从公网地址发起的请求；`https://router.local` 这样的本地源访问另一台本地设备或访问回环地址时不会弹出提示，Chrome 博客把这列为计划中的扩展。

:::observed
在启用了该检查的 Chrome 中，`https://` 页面发起 `fetch("http://192.168.1.1/status")` 时权限气泡显示 `<origin> wants to access other devices on your local network`；请求 `http://127.0.0.1:8080/` 时则显示 `<origin> wants to access other apps and services on this device`（英文界面）。这两项权限在站点设置中的条目分别是 `Access other devices on your local network` 和 `Access other apps and services on this device`。因缺少权限而被拒的请求在 DevTools Network 面板中以 `net::ERR_LOCAL_NETWORK_PERMISSION_MISSING`（-36）失败；被地址空间检查拦截的请求以 `net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS`（-385）失败。来源：[`permissions_strings.grdp`](https://chromium.googlesource.com/chromium/src/+/main/components/permissions_strings.grdp)（chromium.googlesource.com）与 [`net_error_list.h`](https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h)（chromium.googlesource.com）。
:::

## 示例

两个示例都在 `https://` 页面中运行。提示与请求本身绑定，所以每个示例都把被拒绝的 `fetch()` 当作预期结果，给用户一条手动路径而不是重试。

### 从已安装的 PWA 读取路由器状态页

`'targetAddressSpace' in Request.prototype` 是浏览器 fetch 栈是否理解地址空间的唯一运行时信号；它不存在时（Firefox、Safari、Chrome 142 之前），该请求在 HTTPS 页面里就是普通混合内容，浏览器在任何网络活动之前就会拦下。下面的回退逻辑在新标签页打开设备自己的页面，那里不涉及跨源请求。

```js
const ROUTER = 'http://192.168.1.1/status.json';

function browserGatesLocalNetwork() {
  return typeof Request !== 'undefined' && 'targetAddressSpace' in Request.prototype;
}

async function readRouterStatus() {
  if (!window.isSecureContext || !browserGatesLocalNetwork()) {
    return { ok: false, reason: 'unsupported' };
  }
  try {
    const res = await fetch(ROUTER); // IP 字面量：免除混合内容检查，提示只出现一次
    if (!res.ok) return { ok: false, reason: `http-${res.status}` };
    return { ok: true, status: await res.json() };
  } catch {
    return { ok: false, reason: 'blocked-or-offline' }; // 提示被拒、CORS 失败或设备离线
  }
}

document.querySelector('#check-router').addEventListener('click', async () => {
  const result = await readRouterStatus();
  if (!result.ok) {
    window.open('http://192.168.1.1/', '_blank', 'noopener');
    return;
  }
  renderStatus(result.status);
});
```

被拒绝的 Promise 不会说明原因：提示被拒、设备上的 CORS 失败、路由器断电，都表现为 `fetch()` 抛出的 `TypeError`。记录原因没问题，但脚本无法据此分支。

### 为解析到本地地址的主机名声明地址空间

`devbox.example.com` 这样解析到 10.0.0.5 的公网主机名本身并不豁免混合内容。给调用加上 `targetAddressSpace: "local"` 后，浏览器允许建立 `http://` 连接，再核验解析出的地址确实在 local 空间。不支持该选项的浏览器会忽略这个未知的 `RequestInit` 成员，同一调用于是退回到被当作混合内容拦截；因此代码在选项缺失时改走 `https://` 备选地址。

```js
async function callDevBox(path) {
  const supportsOption = 'targetAddressSpace' in Request.prototype;
  const url = supportsOption
    ? `http://devbox.example.com${path}`
    : `https://devbox.example.com${path}`; // 设备上需要有效证书

  const init = supportsOption ? { targetAddressSpace: 'local' } : {};
  const res = await fetch(url, init);
  return res.json();
}

callDevBox('/api/health').catch((err) => {
  document.querySelector('#devbox-note').textContent =
    `无法连接开发机（${err.name}）。请确认你在办公网络内。`;
});
```

如果 `devbox.example.com` 日后迁到公网 IP，带注解的请求会以 `ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS` 失败，而不是把请求泄露到互联网上，这正是声明的意义。

## 另请参阅

- [WebRTC：浏览器中的点对点音视频与数据](/zh/reference/capabilities/webrtc/)，尚未纳入该权限的连接类型之一
- [WebTransport：HTTP/3 的流与数据报](/zh/reference/capabilities/webtransport/)
- [Web Bluetooth API：连接蓝牙低功耗设备](/zh/reference/capabilities/web-bluetooth/)，完全不经过 IP 的另一条近场设备路径
- [Local Network Access: Permissions](https://wicg.github.io/local-network-access/#permission-prompt)（wicg.github.io）
- [New permission prompt for Local Network Access](https://developer.chrome.com/blog/local-network-access)（developer.chrome.com）
- [Request: targetAddressSpace property](https://developer.mozilla.org/en-US/docs/Web/API/Request/targetAddressSpace)（developer.mozilla.org）
- [Local Network Access explainer](https://github.com/WICG/local-network-access/blob/main/explainer.md)（github.com）