npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@motrix/nat

v0.1.0

Published

Dependency-free UPnP IGD, NAT-PMP, PCP, and STUN for Node.js

Downloads

0

Readme

@motrix/nat

English | 简体中文

@motrix/nat 是一个零运行时依赖、注重安全边界的 Node.js NAT 发现与端口 映射工具包。它支持 UPnP IGD v1/v2、NAT-PMP、PCP 和 STUN,既提供统一的 生命周期管理器,也开放协议客户端、严格的编解码器和底层传输接口。

下列场景适合使用本包:

  • 发现局域网网关及其公网 IPv4 地址;
  • 创建、续租和删除 TCP 或 UDP 端口映射;
  • 优先使用 PCP,并按 NAT-PMP、UPnP 的顺序自动回退;
  • 通过 STUN 查询外部网络观察到的公网端点;
  • 在网络发生变化时重新发现网关和恢复映射;
  • 以有界、校验失败即拒绝的方式解析不可信路由器响应。

为什么选择 @motrix/nat

  • 一套包覆盖三层抽象。 可以直接使用 NatManager,也可以单独调用协议 客户端,或只使用编解码器与传输接口。
  • 默认收紧安全边界。 UPnP 控制端点必须是私有或链路本地 IPv4 字面地址; DNS 主机名、环回地址、重定向、畸形数据包和危险 XML 结构都会被拒绝。
  • 适合常驻应用。 生命周期管理器负责协议发现与回退、映射续租、退避重试、网络 变化恢复,以及退出时的尽力清理。
  • 便于集成和测试。 HTTP、UDP、设置、生命周期钩子、时钟和网络快照均通过 小型接口注入。
  • 零运行时依赖。 发布包仅使用 Node.js 内置模块和 Node.js 提供的全局 fetch

标准与协议模型

NAT 穿越并不是一个单独的协议。端口映射协议用于请求网关创建入站转发 状态;STUN 则用于获知经过 NAT 转换后,远端所看到的传输地址。

本文采用 RFC 2663 中的 NAT 基础术语。其中,私有 IPv4 地址是指 RFC 1918 预留的三个地址块: 10.0.0.0/8172.16.0.0/12192.168.0.0/16

本文使用的关键术语如下:

  • 内部地址与端口:本机在私有网络中监听的传输端点。
  • 外部地址与映射端口:NAT 暴露给外部网络的传输端点。
  • 端口映射:从外部协议和端口到内部协议和端口的显式转发规则。
  • 映射有效期或 TTL:租约持续时间,单位为秒。客户端应在到期前续租;在 NAT-PMP 和 PCP 中,有效期为零表示删除映射。
  • 服务器反射地址(server-reflexive address):数据经过中间 NAT 转换后, STUN 服务器实际观察到的客户端 IP 地址与端口。

已实现的标准

| 协议 | 标准 | 标准定义的用途 | 本包当前实现 | | --- | --- | --- | --- | | UPnP IGD | OCF UPnP IGD 2.0 设备控制协议 | 通过 SSDP、HTTP 设备描述文档和 SOAP 发现并控制互联网网关设备 | 发现 IGD v1/v2;支持 AddPortMappingDeletePortMappingGetExternalIPAddress;在 239.255.255.250:1900 上进行 SSDP 组播 | | NAT-PMP | RFC 6886 | 向默认网关查询外部 IPv4 地址并申请 TCP/UDP 映射 | 通过 UDP 5351 发送版本 0 请求;支持查询外部地址以及创建、续租、删除映射;由于协议没有事务 ID,请求按顺序执行 | | PCP | RFC 6887 | 控制 NAT 与防火墙中的映射,适用于家庭网关、运营商级 NAT(CGN)和 IPv6 过渡环境 | 通过 UDP 5351 发送版本 2 MAP 请求;支持 TCP/UDP 映射;通过 nonce 关联创建、续租和删除操作 | | STUN | RFC 8489 | 获取服务器反射传输地址,并作为更完整 NAT 穿越方案的基础组件 | 基于 UDP 的 Binding 请求与响应;使用显式 host:port 服务器地址;解析 MAPPED-ADDRESSXOR-MAPPED-ADDRESS;RFC 默认端口为 3478,但本包要求调用方显式填写 |

NAT 行为术语

RFC 4787 分别从映射行为和过滤行为 描述 UDP NAT。该规范不再把较早的完全锥形、受限锥形、端口受限锥形和对称型 标签当作完整的 NAT 分类,因为这些标签不足以准确描述现实中的全部行为。

| 行为 | 映射行为定义 | 过滤行为定义 | | --- | --- | --- | | 端点无关 | 对同一个内部端点复用相同的外部映射,不受远端端点影响 | 映射建立后,允许来自任意外部端点的数据包 | | 地址相关 | 仅在远端 IP 地址相同时复用映射,远端端口可以不同 | 仅允许内部端点曾向其发送数据的远端 IP 地址回包 | | 地址和端口相关 | 仅在远端 IP 地址与端口都相同时复用映射 | 仅允许内部端点曾向其发送数据的同一远端 IP 地址和端口回包 |

映射行为和过滤行为是两个独立维度。一次 STUN Binding 请求只能得到一个服务器 反射端点,不能据此判断上述任一维度。

上述协议表同时说明了标准本身的用途和本包目前实现的子集,并不表示已经覆盖规范中的 所有可选操作、传输方式、认证方式、扩展或地址族。尤其需要注意:STUN 本身不是 完整的 NAT 穿越方案;本版本不实现 ICE 或 TURN。

运行要求与安装

  • Node.js 22 或更高版本
  • ESM
pnpm add @motrix/nat

也可以使用 npm 或 Yarn:

npm install @motrix/nat
yarn add @motrix/nat

快速开始:创建 UPnP 映射

如果只需要 UPnP,这是最直接的调用方式。请把示例中的 internalIp 替换为 实际接收入站连接的网卡私有 IPv4 地址。

import {
  nodeHttpClient,
  nodeUdpSocketFactory,
  UpnpClient,
} from '@motrix/nat'

const upnp = new UpnpClient({
  udpFactory: nodeUdpSocketFactory,
  http: nodeHttpClient,
})

const discovered = await upnp.discover({ timeoutMs: 3_000 })
if (!discovered.ok) {
  throw new Error(
    `UPnP discovery failed: ${discovered.error} (${discovered.detail ?? 'no detail'})`
  )
}

const gateway = discovered.value
const mapping = {
  internalIp: '192.168.1.25',
  internalPort: 51413,
  externalPort: 51413,
  protocol: 'TCP' as const,
  ttl: 3_600,
  description: 'my-app',
}

const mapped = await upnp.mapPort(gateway, mapping)
if (!mapped.ok) {
  throw new Error(
    `UPnP mapping failed: ${mapped.error} (${mapped.detail ?? 'no detail'})`
  )
}

try {
  const externalIp = await upnp.getExternalIp(gateway)
  if (externalIp.ok) {
    console.log(`Listening on ${externalIp.value}:${mapping.externalPort}`)
  }
} finally {
  await upnp.unmapPort(gateway, {
    externalPort: mapping.externalPort,
    protocol: mapping.protocol,
  })
}

如何选择 API 层级

| 目标 | 推荐 API | | --- | --- | | 统一处理发现、协议回退、续租、重试和退出清理 | NatManager | | 通过 UPnP IGD 发现网关或管理映射 | UpnpClient | | 创建 PCP 或 NAT-PMP 映射 | PmpPcpClient | | 查询 STUN 服务器观察到的公网端点 | StunClient | | 通过应用自有的 HTTPS 服务检查映射端口 | PortChecker | | 监听稳定的本地网络接口变化 | NetworkMonitor | | 构造或检查协议数据包 | codecs 命名空间或具名编解码器导出 | | 与浏览器或 Electron 渲染进程共享 NAT 状态 | @motrix/nat/types |

返回值与错误模型

协议和传输操作统一返回可辨识联合类型 ParseResult<T>

import type { ParseResult } from '@motrix/nat'

function valueOrThrow<T>(result: ParseResult<T>): T {
  if (!result.ok) {
    const suffix = result.detail ? `: ${result.detail}` : ''
    throw new Error(`${result.error}${suffix}`)
  }
  return result.value
}

因此,预期内的网络错误、校验失败和协议错误无需通过异常控制流程处理。构造参数 误用或命令式更新中的无效值仍可能抛出异常;例如,传入无效 IPv4 地址时, PmpPcpClient.setGatewayIp() 会抛出 RangeError

NatErrorCode 定义了稳定的包级错误码:

| 错误码 | 含义 | | --- | --- | | NAT_DISCOVERY_FAILED | 未发现受支持的网关 | | NAT_MAPPING_FAILED | 无法创建端口映射 | | NAT_MAPPING_CONFLICT | 请求的映射与现有条目冲突 | | NAT_PROTOCOL_REJECTED | 输入或路由器响应不符合协议约束 | | NAT_PARSE_ERROR | 无法安全解析响应 | | NAT_SECURITY_VIOLATION | 安全边界或响应来源校验失败 | | NAT_TIMEOUT | 操作超时或被取消 | | NAT_NETWORK_CHANGED | 网络变化或退出流程使操作失效 | | NAT_GATEWAY_UNREACHABLE | 与网关通信失败 | | STUN_DETECTION_FAILED | 所有已配置 STUN 服务器均未返回可用结果 |

当生命周期管理器选用 NAT-PMP 时,还会发出 NAT_SECURITY_WARNING。这是因为 NAT-PMP 响应没有认证机制;该事件是安全提醒,不表示映射失败。

高层生命周期管理

NatManager 负责协调协议客户端和应用生命周期。主要公共方法如下:

| 方法 | 行为 | | --- | --- | | start() / enable() | 订阅生命周期钩子、启动网络监控并发现网关 | | mapConfiguredPorts() | 映射已配置的 TCP 监听端口和 UDP DHT 端口 | | remapAll() / forceRemap() | 续租或重新创建当前映射 | | getStatus() | 返回状态、网关、映射、重试次数和最近错误的快照 | | runDiagnostic() | 执行当前版本提供的最小 STUN 诊断 | | exportBundle() | 导出已遮蔽本地 IP 的精简诊断信息 | | stop() / disable() | 取消任务、删除映射、停止监控并关闭 UDP 资源 |

生命周期管理器通过适配器与应用集成。下面是一套完整的基础配置:

import {
  NatManager,
  NatState,
  NetworkMonitor,
  ipv4ToBuffer,
  nodeHttpClient,
  nodeUdpSocketFactory,
  PmpPcpClient,
  PortChecker,
  StunClient,
  UpnpClient,
  type NatEvent,
} from '@motrix/nat'

function toIpv4MappedAddress(ip: string): Buffer {
  return Buffer.concat([
    Buffer.alloc(10),
    Buffer.from([0xff, 0xff]),
    ipv4ToBuffer(ip),
  ])
}

const networkMonitor = new NetworkMonitor()
const network = networkMonitor.snapshot()
if (!network.internalIp || !network.gatewayIp) {
  throw new Error('No usable private IPv4 interface was detected')
}

const manager = new NatManager({
  settingsProvider: {
    getEngine: () => ({ listenPort: 51413, dhtListenPort: 51413 }),
    getNat: () => ({
      enabled: true,
      preferredProtocol: 'auto',
      mappingTtl: 3_600,
      natTypeDetectionEnabled: false,
      stunServers: [],
      portReachabilityCheckEnabled: false,
      portCheckerEndpoints: [],
    }),
  },
  hooks: {
    // 在实际应用中,应替换为对应用生命周期事件的订阅。
    onReady: () => () => {},
    onConfigChanged: () => () => {},
  },
  onEvent: (event: NatEvent) => console.log('NAT event', event),
  upnpClient: new UpnpClient({
    udpFactory: nodeUdpSocketFactory,
    http: nodeHttpClient,
  }),
  pmpPcpClient: new PmpPcpClient({
    udpFactory: nodeUdpSocketFactory,
    gatewayIp: network.gatewayIp,
    clientIp: toIpv4MappedAddress(network.internalIp),
  }),
  stunClient: new StunClient({ udpFactory: nodeUdpSocketFactory }),
  portChecker: new PortChecker(),
  networkMonitor,
})

await manager.start()
if (manager.getStatus().state === NatState.Ready) {
  await manager.mapConfiguredPorts()
}

console.log(manager.getStatus())

// 应在应用退出时调用。
await manager.stop()

在事件驱动的应用中,可以通过事件总线实现 NatManagerHooks.onReady()onConfigChanged()。应用进入就绪状态后,生命周期管理器会创建映射;相关配置 变化后,生命周期管理器会重新映射端口。

管理器事件

onEvent 回调接收 NatEvent 联合类型:

  • state-changed
  • error
  • gateway-changed
  • mapping-updated
  • diagnostic-completed

NatState 的状态依次可能为 idlediscoveringreadymappingactivefailedstoppingstopped

协议客户端

UPnP IGD

UpnpClient 提供以下方法:

  • discover(options?)
  • mapPort(gateway, params, signal?)
  • unmapPort(gateway, params, signal?)
  • getExternalIp(gateway, signal?)

发现过程会为每个搜索目标发送一条 M-SEARCH 请求。默认覆盖 IGD v1 和 v2,超时时间为 3 秒,最多处理 10 条响应。若映射操作属于更大的可取消生命周期, 可以传入 AbortSignal

PCP 与 NAT-PMP

建议为每个本地网关创建一个 PmpPcpClient 实例,并在退出时关闭:

import {
  ipv4ToBuffer,
  nodeUdpSocketFactory,
  PmpPcpClient,
} from '@motrix/nat'

const clientIp = Buffer.concat([
  Buffer.alloc(10),
  Buffer.from([0xff, 0xff]),
  ipv4ToBuffer('192.168.1.25'),
])

const client = new PmpPcpClient({
  udpFactory: nodeUdpSocketFactory,
  gatewayIp: '192.168.1.1',
  clientIp,
})

try {
  const created = await client.pcpMap({
    protocol: 'TCP',
    internalPort: 51413,
    externalPort: 51413,
    ttl: 3_600,
    timeoutMs: 1_000,
  })

  if (created.ok) {
    // 删除 PCP 映射时必须带回创建映射时使用的 nonce。
    await client.pcpMap({
      protocol: 'TCP',
      internalPort: created.value.internalPort,
      externalPort: created.value.externalPort,
      ttl: 0,
      nonce: created.value.nonce,
    })
  }
} finally {
  await client.close()
}

同一个客户端还提供:

  • natPmpGetExternalIp({ timeoutMs, signal }?)
  • natPmpMap({ protocol, internalPort, externalPort, ttl, timeoutMs, signal })
  • pcpMap({ protocol, internalPort, externalPort, ttl, timeoutMs, signal, nonce })
  • setGatewayIp(ip)
  • close()

NAT-PMP 没有用于关联请求与响应的事务 ID,因此请求会串行执行。PCP 使用 nonce 关联请求;每个客户端最多同时处理四个 PCP 请求。

STUN 公网端点发现

import { nodeUdpSocketFactory, StunClient } from '@motrix/nat'

const stun = new StunClient({ udpFactory: nodeUdpSocketFactory })
const observed = await stun.detectNatType({
  servers: ['stun.example.net:3478'],
  timeoutMs: 2_000,
})

if (observed.ok) {
  console.log(observed.value.mappedIp, observed.value.mappedPort)
}

虽然当前方法名为 detectNatType(),本版本实际返回的是第一台有效 STUN 服务器 观察到的公网 IP 与端口。无论是区分旧式的锥形/对称型标签,还是判断 RFC 4787 定义的映射行为和过滤行为,都需要进行多服务器行为测试,目前尚未实现。

辅助 API

网络变化监控

NetworkMonitor 会周期性获取网络快照。只有新快照连续稳定达到指定轮数后,才会 触发变化事件:

const monitor = new NetworkMonitor({
  intervalMs: 5_000,
  stableRounds: 2,
  snapshotFn: readPlatformNetworkSnapshot,
})

const unsubscribe = monitor.onChange((snapshot) => {
  console.log('Network changed', snapshot)
})

monitor.start()
// 稍后执行:
unsubscribe()
monitor.stop()

若需要依据精确路由做生产决策,请注入能读取操作系统路由表的 snapshotFn。 默认实现只会选择第一个非回环 IPv4 网卡,并把同一 /24 网段的 .1 地址估算为网关;它不会读取系统路由表。

端口可达性检查

PortChecker 会向应用配置的 HTTPS 端点发起请求,并附加 ipport 查询 参数。包内不预置端点;只有调用方显式执行检查时才会访问外部服务。

const result = await new PortChecker().checkPortReachable({
  endpoints: ['https://status.example.net/check-port'],
  externalIp: '203.0.113.10',
  port: 51413,
  timeoutMs: 3_000,
})

服务响应应包含 openclosedreachableunreachable 等含义明确的 单词。如果使用结构化 API,可以通过 PortCheckerOptions 注入自定义 fetcher

日志器注入

默认日志器为空操作实现。应用可以在启动时注入一个兼容 Pino 的日志器:

import pino from 'pino'
import { setNatLogger } from '@motrix/nat'

setNatLogger(pino())

NatLogger 接口只要求实现 childdebuginfowarnerror。 Pino 是应用侧的可选依赖,不会随 @motrix/nat 安装。不带参数调用 setNatLogger() 可以恢复默认的空操作日志器。

限流与并发工具

  • TokenBucket 提供同步获取令牌和计算等待时间的能力。
  • TransitionMutex 用于串行执行异步状态转换,并可报告当前持有者,便于诊断。
  • GenerationGuard 用于在生命周期变化后使旧异步任务失效。

编解码器与传输接口

所有编解码器既可以具名导入,也可以通过 codecs 命名空间使用:

import { codecs } from '@motrix/nat'

const request = codecs.buildMSearch(codecs.SSDP_IGD_V2_ST, 2)
const response = codecs.parseMSearchResponse(datagram)

主要公共编解码器如下:

| 领域 | 主要导出 | | --- | --- | | SSDP | buildMSearchparseMSearchResponsevalidateLocationUrl | | 设备描述 | parseDeviceDescription | | SOAP | buildSoapEnvelopeparseSoapResponsexmlEscape | | NAT-PMP | buildExternalIpRequestbuildMappingRequestparseNatPmpResponse | | PCP | buildPcpMapRequestparsePcpMapResponsepeekPcpNonce | | STUN | buildBindingRequestparseBindingResponse | | XML | tokenizeXmlparseXmlfindChildfindDescendants | | IP 工具 | IPv4 解析、地址分类和 Buffer 转换函数 |

自定义运行环境和测试代码可以实现 HttpClientUdpSocketUdpSocketFactory。包内提供 Node.js 适配器:NodeHttpClientnodeHttpClientNodeUdpSocketnodeUdpSocketFactory

包入口

@motrix/nat

Node.js 主入口导出生命周期管理器、协议客户端、编解码器、传输接口、领域类型、 错误类型、日志器和并发工具。该入口会加载 Node.js 内置模块,应仅用于 Node.js 进程、Electron 主进程、服务器、工作线程或同类可信运行环境。

@motrix/nat/types

这个与传输无关的入口导出用于传递 NAT 状态的运行时枚举和 TypeScript 类型:

import { NatState, type NatStatus } from '@motrix/nat/types'

这个子路径不导入 Node.js 内置模块,可以安全用于浏览器和 Electron 渲染进程的 打包产物。

安全模型

本包始终把路由器和网络响应视为不可信输入。

  • UPnP SSRF 防护:控制端点必须使用私有或链路本地 IPv4 字面地址;环回地址、 公网 IP、DNS 主机名、用户信息、重定向、URL 片段和查询字符串都会被拒绝。
  • 有界 HTTP:响应上限为 128 KiB;请求带有超时,且绝不跟随重定向。
  • 有界 XML:分词器拒绝 DTD、ENTITY、CDATA、注释、控制字符、过深嵌套、 过长文本和超大输入。
  • 严格来源校验:NAT-PMP 与 PCP 响应必须来自已配置的网关;PCP nonce 和 STUN 事务 ID 必须匹配。
  • 严格二进制解析:数据包长度、版本、操作码、结果码、地址族、属性数量、 端口和有效期都会经过校验。
  • 外部服务由应用明确配置:STUN 服务器和端口检查端点均由调用方提供; PortChecker 只接受 HTTPS 端点。

上述安全约束由单元测试、基于性质的测试、Docker 恶意输入测试夹具和有时间上限的 编解码器模糊测试共同覆盖。

当前范围与限制

  • 网关发现与传输当前以 IPv4 为主。
  • NetworkMonitor 默认提供的是网关估算值;需要精确的默认网关时,应注入读取 系统路由表的实现。
  • StunClient.detectNatType() 当前只返回映射后的公网端点,不提供完整的 RFC 3489 风格 NAT 行为分类。
  • 0.1.0 中的 NatManager.runDiagnostic() 仍是最小实现。
  • 每个 UpnpClient 实例同一时间只允许执行一次发现操作。
  • NAT-PMP 本身没有认证机制;生命周期管理器选用它时会发出安全提醒。

开发

pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm fuzz
pnpm test:integration # 需要 Docker

模糊测试运行器的 --duration 是所有选中编解码器共享的总实际运行时间,而不是 每个编解码器单独占用的时长。

License

MIT