apppublic/aidocs/across_nat.md
2025-10-05 11:23:33 +08:00

8.5 KiB
Raw Blame History

AcrossNat 技术文档

# AcrossNat - 穿越 NAT 的端口映射与公网 IP 获取工具

`AcrossNat` 是一个用于在 NAT网络地址转换环境中获取公网 IP 地址、自动探测并配置端口映射的 Python 类。它支持多种协议和技术,包括 **NAT-PMP****UPnP IGD**,并在无法使用这些协议时回退到公共 API 查询公网 IP。

---

## 概述

该类主要用于 P2P 应用、远程访问服务等需要穿透家庭路由器进行端口暴露的场景。通过优先使用本地 NAT 协议(如 UPnP 或 NAT-PMP避免手动配置端口转发提升部署自动化能力。

---

## 依赖库

- `natpmp`: 实现 NAT-PMP 协议通信
- `upnpclient`: 发现和操作 UPnP 兼容的网关设备
- `requests`: 用于 HTTP 请求(获取公网 IP
- `.background` (内部模块): 可能用于后台任务处理(未在代码中直接使用)

> 注意:需确保上述第三方库已安装:
>
> ```bash
> pip install natpmp upnpclient requests
> ```

---

## 类定义

```python
class AcrossNat(object)

封装了 NAT 穿透相关的功能,包括:

  • 自动探测 NAT-PMP / UPnP 支持状态
  • 获取外部公网 IP
  • 动态映射内网端口到外网端口
  • 查询/删除端口映射

初始化方法

__init__(self)

初始化 AcrossNat 实例,并尝试自动检测 NAT-PMP 和 UPnP 是否可用。

属性说明:

属性 类型 描述
external_ip str or None 缓存的公网 IP 地址
upnp UPnP Service Object or None UPnP WANConnectionDevice 服务对象
pmp_supported bool 是否支持 NAT-PMP 协议
upnp_supported bool 是否支持 UPnP 协议

行为:

  1. 调用 init_pmp() 尝试获取公网 IP测试 NAT-PMP 支持)
  2. 调用 init_upnp() 探测 UPnP 网关并绑定服务

核心方法

init_upnp(self)

尝试发现局域网中的 UPnP 网关设备,并绑定第一个匹配的 WAN 连接服务。

异常处理:

  • 若发现失败或无可用设备,则设置 self.upnp_supported = False
  • 打印异常信息(含 traceback

实现细节:

  • 使用 upnpclient.discover() 发现设备
  • 匹配服务名称包含 'WAN''Conn' 的服务(如 WANIPConnection1

init_pmp(self)

尝试通过 NAT-PMP 协议获取公网 IP 地址以判断是否支持。

异常处理:

  • 若设备不支持 NAT-PMP返回 NATPMPUnsupportedError),则设 self.pmp_supported = False

get_external_ip(self) -> str or None

获取当前设备的公网 IPv4 地址,按优先级尝试以下方式:

  1. NAT-PMP(最快,本地协议)
  2. UPnP IGD GetExternalIPAddress
  3. 备用方案HTTP 请求公共 IP 服务
    • https://api.ipify.org
    • https://ipapi.co/ip/

返回值:

  • 成功时返回字符串格式的 IP 地址(如 "8.8.8.8"
  • 所有方式均失败时返回 None

示例:

nat = AcrossNat()
ip = nat.get_external_ip()
print(ip)  # 输出: 123.45.67.89

upnp_check_external_port(eport, protocol='TCP') -> bool

检查指定的外网端口是否已被映射。

参数:

参数 类型 默认值 说明
eport int —— 外部端口号
protocol str 'TCP' 协议类型,可选 'TCP''UDP'

返回值:

  • True:端口已被映射
  • False:未被映射或调用失败

注意:

此方法仅适用于 UPnP 支持环境。


upnp_map_port(inner_port, protocol='TCP', from_port=40003, ip=None, desc='test') -> int or None

使用 UPnP 映射一个内网端口到外网端口。

参数:

参数 类型 默认值 说明
inner_port int —— 内部主机监听端口
protocol str 'TCP' 协议类型(大小写不敏感)
from_port int 40003 起始外网端口
ip str or None 当前主机 IP 内部客户端 IP 地址(若为空由网关自动识别)
desc str 'test' 端口映射描述

行为逻辑:

from_port 开始递增查找空闲外网端口(最大至 52332直到成功添加映射。

返回值:

  • 成功返回分配的外网端口号int
  • 失败:返回 None

示例:

ext_port = nat.upnp_map_port(8080, protocol='tcp', desc='Web Server')
if ext_port:
    print(f"Port mapped: {ext_port} -> 8080")

is_port_mapped(external_port, protocol='TCP') -> bool

查询某个外网端口是否已被映射。

参数:

参数 类型 默认值 说明
external_port int —— 外部端口号
protocol str 'TCP' 协议类型

返回值:

  • True:已映射
  • False:未映射
  • 若 UPnP 不支持,抛出异常:Exception('not implemented')

port_unmap(external_port, protocol='TCP')

删除指定的端口映射条目。

参数:

参数 类型 默认值 说明
external_port int —— 要解绑的外网端口
protocol str 'TCP' 协议类型

行为:

仅当 UPnP 支持时调用 delete_port_mapping 删除映射。

⚠️ 当前 upnpclient 库可能不提供 delete_port_mapping 方法,请确认版本兼容性。

否则抛出异常:Exception('not implemented')


pmp_map_port(inner_port, protocol='TCP', from_port=40003) -> int or None

使用 NAT-PMP 协议映射端口。

参数:

参数 类型 默认值 说明
inner_port int —— 内部端口
protocol str 'TCP' 协议类型
from_port int 40003 起始外网端口

实现:

  • TCP调用 pmp.map_tcp_port(...)
  • UDP调用 pmp.map_udp_port(...)
  • 永久映射(lifetime=999999999

返回值:

  • 成功:返回公网端口号
  • 失败:抛出异常(由 natpmp 抛出)

map_port(inner_port, protocol='tcp', from_port=40003, lan_ip=None, desc=None) -> int or None

统一入口自动选择最佳方式NAT-PMP > UPnP来映射端口。

参数:

参数 类型 默认值 说明
inner_port int —— 内部端口
protocol str 'tcp' 协议(不区分大小写)
from_port int 40003 起始外网端口
lan_ip str None 内部客户端 IP
desc str None 映射描述

优先级策略:

  1. pmp_supported == True → 使用 pmp_map_port
  2. 否则使用 upnp_map_port

返回值:

  • 成功:返回外网端口号
  • 失败:返回 None

使用示例

from acrosnat import AcrossNat

# 创建实例
nat = AcrossNat()

# 获取公网 IP
ip = nat.get_external_ip()
print("Public IP:", ip)

# 映射本地 8000 端口
mapped_port = nat.map_port(8000, protocol='tcp', desc='MyApp')
if mapped_port:
    print(f"Successfully mapped external port {mapped_port} to 8000")
else:
    print("Failed to map port")

# 查询某端口是否已映射
if nat.is_port_mapped(mapped_port, 'tcp'):
    print("Port still mapped.")

错误处理与健壮性设计

  • 所有关键操作都包裹在 try-except 中,防止因单个功能失效导致程序崩溃
  • 对不可用的功能自动降级(如关闭 pmp_supported 标志)
  • 提供多级 fallback
    • NAT-PMP → UPnP → Public APIIP 获取)
    • UPnP 端口扫描避免冲突

已知限制与注意事项

  1. 依赖网关支持

    • 需要路由器开启 UPnP 或 NAT-PMP 功能
    • 部分运营商级 NATCGNAT环境下仍无法映射
  2. 安全性提示

    • 自动端口映射存在安全风险,建议配合防火墙使用
    • NewLeaseDuration=0 表示永久有效,重启后可能保留
  3. upnpclient.delete_port_mapping

    • 原生 upnpclient 可能未实现此方法,需自行扩展或打补丁
  4. 并发问题

    • 多线程同时调用 map_port 可能造成端口竞争,建议加锁控制

未来改进方向

  • 添加日志系统替代 print()
  • 支持异步非阻塞操作(结合 Background 模块)
  • 增加对 IPv6 的支持
  • 实现定期刷新和自动清理机制
  • 提供更详细的错误码反馈

许可证

请根据项目实际情况填写许可证信息(如 MIT、Apache-2.0 等)。


---

> ✅ 提示:将此文档保存为 `README.md` 或集成进 Sphinx 文档系统以便团队协作维护。