NPS Tunnel Manager
Overview
通过 NPS 服务端 /api/v1 完成服务的发布、查询、修改、删除。全部封装在 scripts/nps_api.py,含鉴权、客户端自动选择、平台域名预检、访问地址组装。
这些是「可选功能」,不是必须按顺序走的流程。用户要干什么就挑哪个,不要一上来把全套跑一遍:
| # | 可选功能 | 入口命令 |
|---|---|---|
| 1 | 装 / 配 NPC(GUI 或服务版、开机自启) | npc-check npc-install npc-autostart |
| 2 | 新建隧道 / 新建域名反代(把服务推出去) | publish-tunnel publish-host |
| 3 | 查询 / 修改 / 删除已有规则 | query modify delete |
| 4 | 环境自检(排障、首次接入) | doctor key-check expiry |
另一条正交的分法——只读 vs 写入("推送性质"= 写入类,会让内网服务对外可达或改变线上状态):
| 类别 | 命令 | 规矩 |
|---|---|---|
| 写入类(改变线上状态) | publish-tunnel publish-host client-add modify delete npc-autostart --apply |
能预演的先 --dry-run(publish-* 已支持);删除/改动先复述目标再执行;执行后复查并回报结果 |
| 只读类 | query client-* tunnel-* host-* 的 list/get、platform-*、doctor key-check expiry verify |
直接跑,不用确认 |
写入类的共同收尾:复查(query / client-list / 删除后确认已消失)——建好了不等于生效了。
唯一前提是凭据:任何功能都要先有 npsu_ Token(见「凭据」一节)。没有就向用户要,不要编。
功能 4:环境自检(可选,排障或首次接入时跑)
python "<skill目录>/scripts/nps_api.py" doctor
可选,不是必跑。 一条命令把「能不能干活」探清楚,返回 steps[] + client_choice + next_action,照 next_action 走。只在首次接入、或发布失败要排障时跑。
顺序按「阻断性 + 依赖关系」排,不是按本地/远程排:
| 步骤 | 检查项 | 阻断 | 失败时怎么办 |
|---|---|---|---|
| 1 | 云平台凭据 | 是 | 向用户索要(见下节),不要猜 |
| 2 | 网络连通 + 时钟 | 是 | 查地址/端口/安全组/代理;时差 >15 秒先校时 |
| 3 | 鉴权 | 是 | 失效 → 重新生成后 configure;若同时时差大,先校时再试 |
| 4 | 客户端盘点 | 否 | 拉出列表,必须问用户用哪个(见下节) |
| 5 | 有效期 | 否 | 已过期就提醒续期,不要继续发布 |
| 6 | 本机 npc | 否 | npc 只装在「跑客户端的那台机器」上;这台机器不一定需要,客户端已在线就完全不用管 |
ready只看阻断项(1/2/3);ready_to_publish额外要求「已选定客户端且至少有一个在线」。npc 永远是提示项,不进ready。- 缺 npc 不会把
ready判成 false —— 只想管理远端规则的用户不该被逼着装它。 - 单项排查:
npc-check/key-check/expiry。
客户端:先拉列表,再问用户用哪个
doctor 的 client_choice 已经带出结论,不要自己替用户决定:
mode |
含义 | 你要做的事 |
|---|---|---|
none |
名下还没有客户端 | 问用户:① 本机 npc 接入(给命令)② 云端新建 |
single |
只有 1 个 | 直接用(auto_use 已给),不必再问 |
multiple |
有多个 | 问「这个服务在哪个客户端的局域网里?」,把 options 原样列给用户点,不要猜 |
client_choice.question是给用户看的话术,options是候选,直接拿来用;选项里带#id。- 用户选定的 ID 传给后续
publish-host/publish-tunnel的--client-id。
分支一:用户要新建 → 通常只需「去后台复制一条命令」
python nps_api.py client-add --remark <服务名>
返回 bind:
bind.online=true(已在线)→needs_bind=false,什么都不用装,直接--client-id发布bind.needs_bind=true(还没上线)→ 三步:
1.steps[0].where是后台路径,引导用户复制「快捷启动命令」——里面已带-server=(桥接地址)与-vkey=(客户端密钥),原样用
2.steps[1]把命令贴到跑服务的那台机器上执行,不一定是当前这台
3.steps[2]等 5–10 秒,client-list --brief看该 id 的online变 true,再接发布
关于 npc:它只装在「跑客户端的那台机器」上,本机不一定需要。那台机器上也可能早就有(Scoop / Homebrew / winget / 手动解压 / Docker 都算),有就跳过;真没有再用 npc-install 兜底。既不要把它当必做步骤,也不要强推安装方式。
⚠️ 不要自己拼 -server= / -vkey=。桥接端口(≠ Web 端口)API 不返回,拼出来大概率是错的,npc 会一直重连而不报明确错误。expected_vkey 只用来核对:用户复制的那条命令里 -vkey= 应等于它,不等说明复制成了别的客户端。
分支二:用已有的 → 只有一个就直接用,多个必须问
python nps_api.py client-list --brief # id / 备注 / 在线 / 版本 / 最后在线
python nps_api.py client-bind --id 60 # 换机器重绑:给出「后台复制」引导
- 只有 1 个客户端:直接用它,不用问(
doctor会给client_choice.auto_use) - 有多个:必须问「这个服务在哪个客户端的局域网里?」,把候选列给用户点,别用「只有一个在线」去猜
- 选定后:
python nps_api.py publish-tunnel --client-id 60 --target 127.0.0.1:22 --remark ssh
python nps_api.py publish-host --client-id 60 --platform-prefix app --target 127.0.0.1:8080 --remark 后台
功能 1:安装 / 配置 NPC(可选功能,不是流程)
什么时候用:用户说「帮我装个 npc」「要让这台机器当客户端」「服务在这台机器上」。
第一步:先问版本,别替用户选
| 版本 | 适合谁 | 包名 |
|---|---|---|
| 服务版(cli,默认) | 服务器、无桌面环境、要跑开机自启 | {系统}_{架构}_client.tar.gz |
| 图形版(gui) | 桌面用户,想点点鼠标填参数 | npc-gui-{系统}-{架构}.zip,只有 Windows/macOS/Linux |
python nps_api.py npc-install --edition cli # 自动下载解压到 ~/.nps-cli/bin,装完不启动
python nps_api.py npc-install --edition cli --dry-run # 只看会下哪个包
python nps_api.py npc-install --edition gui # 只返回手动操作步骤,不下载、不启动
- GUI 版:自动下载解压,配置交给用户。脚本负责把
npc-gui.exe下好解压到安装目录、不启动;返回的config_steps是让用户自己做的四步——双击打开 → 在界面填服务端地址与 vkey(对照后台「快捷启动命令」,别自己拼)→ 连接显示在线 → 自启在界面里勾。不要替用户点界面,也不要代填。 - 服务版装完不启动,只校验
-version;装到哪、加不加 PATH 会一并告知。 - 已装就跳过,别重复装(
npc-check会报found=true)。Scoop / Homebrew / winget / Docker 装的都算。
第二步:让用户去后台建客户端、复制快捷启动命令
python nps_api.py client-add --remark <服务名> # 需要替用户在云端建时用;也可以让用户在控制台自己建
引导用户:控制台 → 客户端 → 该客户端 → 「快捷启动命令」→ 复制。这条命令里已经带了 -server= 和 -vkey=,不要自己拼、不要改端口。
第三步:先别执行
拿到命令后不要立刻跑,也不要替用户跑。先问一句:要不要配置成开机自启?
python nps_api.py npc-autostart --command "-server=... -vkey=..." # 默认只出方案,不落盘
python nps_api.py npc-autostart --command "..." --apply # 用户确认后才真正注册
| 系统 | 服务版自启方式 | GUI 版 |
|---|---|---|
| Windows | schtasks /sc onstart /ru SYSTEM(需管理员),另有 onlogon 备选 |
用界面自带「开机启动」,或快捷方式放进 shell:startup |
| Linux | systemd unit + enable --now(需 root) |
一般不用 GUI |
| macOS | ~/Library/LaunchAgents/*.plist + launchctl load |
界面里勾 |
第四步:用户确认后再执行,然后回验
用户跑完命令,等 5–10 秒 client-list --brief 看该客户端 online=true,才算接上。
- 缺 npc 不影响任何发布动作:客户端跑在别的机器上也一样能发布;只有要让这台机器当客户端时才需要。
- GitHub API 限流(403)时设
GITHUB_TOKEN,或改用--from-file离线装。
凭据:没有就向用户要,不要编
key-check 返回 ok=false 且 action_required=provide_credential 时,停下来问用户,并给出三件事:
- 注册地址
https://nps.dell.4dbim.cc:1143/—— 注册后默认开通试用账户 - 需要的是
npsu_开头的用户 Token(控制台「API Token」处生成),不是管理员密钥 - 拿到后执行
configure --api-token npsu_xxx
禁止编造 Token,也禁止让用户把 Token 贴到公开渠道。
有效期查询
python nps_api.py expiry
python nps_api.py expiry --warn-days 15
- 直接读
GET /api/v1/clients的三字段:ExpireTime(客户端)、UserExpireTime(账号)、EffectiveExpireTime(两者最早值),无需额外接口。 status:ok/expiring(剩余天数 <--warn-days,默认 7)/expired/unset(未设置=不限)。- 账号下没有客户端时拿不到账户级有效期,如实告诉用户去控制台看,不要假设。
凭据(必做)
服务端已内置默认值 https://nps.dell.4dbim.cc:1143,不要问用户地址,只问 Key。
python "<skill目录>/scripts/nps_api.py" config-show
未配置时向用户索取:普通用户 npsu_ Token 就够用(发布/查询/修改/删除全程不需要管理员密钥)。
管理员 auth_key / npsa_ 密钥只在需要查看完整平台域名池(含从未使用过的泛域名)时才需要,不是前置条件。
python nps_api.py configure --auth-key <KEY>
python nps_api.py verify
verify 报 unauthorized → Key 错;时差 >15 秒 → 让用户校时。多服务端用 configure --server-name X --url ...、server-list、server-use --name X,并提醒用户选择。
命名与客户端规则
- 备注(remark)= 服务名,≤8 字;首次发布把「服务名 + 最终访问地址」一起给用户确认,认可后再创建。
- 客户端选择规则(刻意保守):
--client-id显式指定 > 名下只有 1 个就直接用 > 其余一律问用户。禁止用「只有一个在线」去推断——客户端决定的是内网落点,选错就是把服务发布到别人的网络里。 - 有多个客户端时,问法固定为:「这个服务在哪个客户端的局域网里?」 把候选(备注 / #id / 在线状态)列出来让对方点,别替他挑。
云端创建客户端:先问,不许自动建
完整规则见上文「客户端:先拉列表,再问用户用哪个」。要点:
- 创建客户端会在云端留真实资源(占用户
MaxClientNum名额、生成长期 vkey),属于写操作,必须用户点头。 - 看到
_pick_client报「名下没有客户端」时不要顺手client-add,先把两个选项摆给用户。 publish-host/publish-tunnel也要先--dry-run,把服务名与最终地址确认过再正式创建。
发布流程
先预演,后发布(--dry-run 只做预检不创建,用于给用户确认):
# 域名反代(默认平台域名模式)——后缀已知时直接传,最省事
python nps_api.py publish-host --dry-run --remark 文件服务 --platform-prefix wenjian --platform-suffix web.dell.4dbim.cc --target 127.0.0.1:8080
# 用户确认服务名与最终域名后正式发布
python nps_api.py publish-host --remark 文件服务 --platform-prefix wenjian --platform-suffix web.dell.4dbim.cc --target 127.0.0.1:8080
# 后缀未知:先反推池子,再按返回的 ID 发布
python nps_api.py platform-domains
# 自定义域名(用户自己的域名)
python nps_api.py publish-host --remark 后台 --host app.example.net --target 127.0.0.1:8080 --scheme all
# TCP 隧道
python nps_api.py publish-tunnel --dry-run --remark ssh --target 127.0.0.1:22
python nps_api.py publish-tunnel --remark ssh --target 127.0.0.1:22
TCP 端口规则:端口一律由服务端自动分配(1024–65535 随机,须通过 allow_ports 白名单与占用检测),CLI 不提供指定端口的选项;修改隧道也不会改动端口。发布后必须提醒用户确认云安全组/防火墙已放行分配到的端口,否则隧道正常但外部连不上。
- 平台域名 ID 无需管理员密钥:
--platform-suffix <后缀>会按服务端同一算法(platform-+sha256("*.后缀")前 16 位)推导 ID;也可用platform-domains反推、或直接给--platform-domain-id。 - 平台前缀由我们传入(
--platform-prefix),服务端自动转小写、校验唯一性;发布前必须预检,publish-host内部也会强制校验,占用即拒绝创建。 - 平台模式默认
--scheme all(HTTP+HTTPS 全开)。该泛域名若是 HTTP-only,服务端只允许--scheme http,失败时提示用户。 - 命令返回
access_url/access_urls/final_host,直接把这几个给用户,不要自己拼。
查询流程
python nps_api.py query # 隧道 + 域名,附访问地址
python nps_api.py query --kind host # 只看域名反代
python nps_api.py query --kind tunnel # 只看隧道
python nps_api.py query --kind host --id 42 # 单条完整字段
python nps_api.py query --search 文件 # 按关键词搜索
返回里 access_url 是主地址,access_urls.http/https 是全部可用地址。
修改流程(不询问,按用户描述直接执行)
python nps_api.py modify --kind host --id 42 --target 127.0.0.1:9090
python nps_api.py modify --kind host --remark 文件服务 --remark-new 文件管理 --platform-prefix wenjian2
python nps_api.py modify --kind tunnel --id 12 --target 127.0.0.1:2222
- 支持
--id或--remark定位;备注重名时会要求用--id。 tunnel类修改会自动预填type/port/target等现有值——服务端缺type会报错、缺port会重新分配端口,务必让脚本走预填。所以modify --kind tunnel不要凭空改类型;确需改类型时显式传--type,端口仍保持原值。host类修改会自动预填现有域名身份与 Header/证书等字段(服务端是全量替换),只传要改的即可。- 换泛域名(后缀与现在不同)时额外传
--platform-suffix <后缀>,优先级为:--platform-suffix推导 >--platform-domain-id> 现有 ID。不传后缀就只换前缀、沿用原泛域名。 - 修改后返回最新
access_url,回给用户。 - 编辑类不做前缀预检(服务端的
/hosts/check无法把自己排除,预检会把当前域名算成被占用)。前缀真的冲突时服务端返回「平台域名与路径规则已被使用」,脚本会提示换前缀并用platform-check先查空闲。
删除流程(必须先确认)
python nps_api.py delete --kind host --id 42
python nps_api.py delete --kind tunnel --remark ssh
执行前用 query 拿到 ID + 备注 + 访问地址,向用户复述确认;删完返回 verified_gone,为 true 才回报"已删除"。
访问地址规则(重要)
access_url 由接口字段组装,规则:
| 情况 | 结果 |
|---|---|
HttpPort/HttpsPort 为空 |
标准端口,不加端口:https://a.b.com/ |
返回 :30111 形式 |
非标准,直接拼:http://a.b.com:30111/ |
scheme=all(默认,双开) |
主地址给 https,同时返回 http 备选 |
| 只有 http | 只返回 http |
| 只有 https | 只返回 https |
端口永远来自接口返回值,不要写死任何端口号。隧道用接口的 AccessURL。
域名鉴权(HTTP Basic Auth)
给域名反代加访问密码,浏览器访问会弹认证框(或需带 Authorization: Basic)。
# 发布时带鉴权(可重复传多账号)
python nps_api.py publish-host --remark 后台 --platform-prefix houtai --platform-suffix web.dell.4dbim.cc \
--target 127.0.0.1:8080 --auth admin:Passw0rd --auth guest:guest123
# 给已有域名加/改鉴权
python nps_api.py host-edit --id 42 --auth admin:NewPass123
# 清除鉴权
python nps_api.py host-edit --id 42 --clear-auth
规则(与源码 ParseBasicAuth 一致):
- 服务端存的是每行 用户名:密码,多个账号换行分隔,--auth 可重复传入。
- 首个冒号切分用户名/密码,密码里可以含冒号;用户名不能含冒号或换行,用户名不可重复,用户名和密码都不能为空。
- 修改时不传 --auth 会保留原鉴权;只有 --clear-auth 才清除。
- 鉴权按 Host(含 location 规则)生效,未通过返回 401。Basic 凭据是 base64 明文,务必走 HTTPS。
- 鉴权只挡访问,不替代 IP 白名单;需要更细粒度控制用隧道/Host 的 IP 黑白名单。
仅允许已登记 IP(registered_only)
客户端、TCP 隧道、域名反代三处都有这个开关,效果是:只有事先登记过的来源 IP 才能连,其他 IP 一律拒绝。
# 发布时开启
python nps_api.py publish-host --remark 后台 --platform-prefix houtai --platform-suffix web.dell.4dbim.cc \
--target 127.0.0.1:8080 --registered-only on
python nps_api.py publish-tunnel --remark ssh --target 127.0.0.1:22 --registered-only on
# 已有规则开关
python nps_api.py host-edit --id 42 --registered-only on
python nps_api.py tunnel-edit --id 12 --registered-only off
- 参数统一为
--registered-only on|off;编辑时不传 = 保留原值(服务端只在提交了该字段时才改)。 - 客户端级还配套两个字段(管理员 Skill):
--register-key(登记口令)、--register-max-hours(登记有效期小时,0=不限)。 - 登记方式:客户端用自身
vkey+ 登记口令调用POST /auth/ipwhiteauth(v1 为/api/v1/ipwhiteauth),把当前公网 IP 登记进白名单,在有效期内可访问;过期需重新登记。 - 管理员可在
/api/v1/global/ip-register(get/revoke/clean/clear)查看和撤销登记记录。 - ⚠️ 顺序很重要:先登记 IP,再开启 registered_only,否则开启瞬间自己也会被挡在外面。家用宽带 IP 会变,建议登记有效期设长一点或配合
register_max_hours自动过期。
注意事项
- 全部命令输出 JSON;失败输出
{"ok":false,"error":...}且退出码 1。 - 服务端返回登录页 HTML = 鉴权失败;返回
forbidden: 凭据无权访问该资源= 该资源不属于当前用户或不存在。 - v1 更新接口只接受 PUT/PATCH(POST 会 404),脚本已处理。
- 客户端不在线(
IsConnect=false)时规则能建成功但不通,需提醒用户先连 npc。 - 启停(start/stop)、用户管理、全局参数不在范围内;用户要求时告知去管理面板。
- 完整 API 字段表见
references/api_reference.md。