From 3a5beaf963712e6a23e21134d7b095ec01cbf426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Sun, 13 Sep 2026 17:06:01 +0800 Subject: [PATCH] =?UTF-8?q?docs(memory):=20=E6=8B=86=E5=88=86=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=89=8B=E5=86=8C=E5=88=B0=20DEPLOY.md=EF=BC=8C?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=20Uptime=20Kuma=20=E9=83=A8=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/go.imports.xml | 10 + .idea/workbuddy.xpcool.com.iml | 1 + .workbuddy/memory/2026-09-10.md | 352 +++++++++++++ .workbuddy/memory/2026-09-13.md | 5 + .workbuddy/memory/DEPLOY.md | 55 ++ .workbuddy/memory/MEMORY.md | 93 ++-- .workbuddy/tmp/status.xpcool.com.conf | 24 + hotspot-ipv6-test.ps1 | 104 ++++ ipv6-home-qrcode.png | Bin 0 -> 556 bytes ipv6-inbound-qrcode.png | Bin 0 -> 639 bytes net-test-v2.ps1 | 294 +++++++++++ net-test-v2_20260910_163314.txt | 97 ++++ net-test.ps1 | 245 +++++++++ pingpe-test-qrcode.png | Bin 0 -> 657 bytes 公司电脑网络自测.ps1 | 259 ++++++++++ 网络自测结果_20260910_160152.txt | 51 ++ 远程游戏串流方案_家到公司.md | 710 ++++++++++++++++++++++++++ 17 files changed, 2236 insertions(+), 64 deletions(-) create mode 100644 .idea/go.imports.xml create mode 100644 .workbuddy/memory/2026-09-10.md create mode 100644 .workbuddy/memory/2026-09-13.md create mode 100644 .workbuddy/memory/DEPLOY.md create mode 100644 .workbuddy/tmp/status.xpcool.com.conf create mode 100644 hotspot-ipv6-test.ps1 create mode 100644 ipv6-home-qrcode.png create mode 100644 ipv6-inbound-qrcode.png create mode 100644 net-test-v2.ps1 create mode 100644 net-test-v2_20260910_163314.txt create mode 100644 net-test.ps1 create mode 100644 pingpe-test-qrcode.png create mode 100644 公司电脑网络自测.ps1 create mode 100644 网络自测结果_20260910_160152.txt create mode 100644 远程游戏串流方案_家到公司.md diff --git a/.idea/go.imports.xml b/.idea/go.imports.xml new file mode 100644 index 0000000..644cdf0 --- /dev/null +++ b/.idea/go.imports.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workbuddy.xpcool.com.iml b/.idea/workbuddy.xpcool.com.iml index 7c421e3..9c408cc 100644 --- a/.idea/workbuddy.xpcool.com.iml +++ b/.idea/workbuddy.xpcool.com.iml @@ -1,5 +1,6 @@ + diff --git a/.workbuddy/memory/2026-09-10.md b/.workbuddy/memory/2026-09-10.md new file mode 100644 index 0000000..cb31c8c --- /dev/null +++ b/.workbuddy/memory/2026-09-10.md @@ -0,0 +1,352 @@ +# 2026-09-10 工作日志 + +## 家庭主机远程串流方案设计(公司电脑 → 家里主机) + +**类型**:REQ(方案设计,待确认后实施) + +### 触发需求 +在公司电脑远程控制家里这台主机,要求速度快、画质好,能打《英雄联盟》快节奏娱乐模式(海克斯大乱斗类)。 + +### 本次实测底账(家里主机 = 本开发机) +- **GPU**:NVIDIA RTX 4070 SUPER(驱动 32.0.15.9186),支持 NVENC H.264/H.265/**AV1** 硬编;另有 7800X3D 核显 AMD Radeon Graphics +- **CPU/内存**:Ryzen 7 7800X3D(8C/16T) / 31.2 GB +- **系统**:Windows 11 专业版 build 26200 +- **显示器**:AOC 2560×1440 @ **170Hz** +- **网络**:Realtek 2.5GbE,链路 1Gbps,内网 IP `192.168.1.30`,网关 `192.168.1.1`(HTTP 200 可登录) +- **宽带(Ookla CLI,电信上海节点 id 3633)**:下行 **943.82 Mbps** / 上行 **46.36 Mbps** / 空闲延迟 33.91ms / **丢包 0.0%** +- **出口 IP**:`1.204.100.17`(中国电信广东,与历史记录的 1.204.105.116 同段,动态) + +### 关键发现 +1. **上行 46M 是整个方案的唯一硬瓶颈**,可支撑 2K 90–120fps @ 30Mbps 或 1080p 120–144fps @ 20–25Mbps;不足以支撑 2K 170Hz 满刷。 +2. **本机已存在远控残留驱动**:`Todesk Virtual Display Adapter`、`GameViewer Virtual Display Adapter`(网易UU远程组件)、`MuMu Virtual Display Adapter`。这些虚拟显示/虚拟输入驱动是腾讯 ACE/TP 反作弊重点扫描特征。 +3. **Moonlight 官方提供 Windows Portable 便携版**(官方原话:for work/public PCs without the ability to install new programs)→ 完美解决公司电脑无管理员权限问题。 +4. **反作弊风险是真实存在的**:社区有大量因远控软件在后台被判定"第三方软件"而十年封禁的案例,"远程操控着玩游戏"是最高风险场景。已写入方案第五节并给出降低措施。 + +### 产出 +- 方案文档:`E:\Project\workbuddy.xpcool.com\远程游戏串流方案_家到公司.md`(v1 草案,待确认) +- 结论:**主方案 = Sunshine(家)+ Moonlight 便携版(公司)+ 公网 UDP 直连**;网易UU远程作为应急通道;Windows RDP / TeamViewer 明确不适用;硬件 KVM 延迟不达标。 + +### 待用户确认(阻塞下一步) +1. 路由器 WAN 口 IP 是否等于 `1.204.100.17`(判断有无公网 IPv4,决定走端口转发还是组网) +2. 公司电脑:能否装软件 / 显卡型号 / 屏幕分辨率与刷新率 +3. 公司网络是否限制 UDP、是否有代理或行为审计 +4. 是否接受反作弊风险(若账号价值高,建议只作办公/挂机通道) + +### 顺手记录 +- PowerShell 工具在本机**不回显 stdout**(exit code 正常但无输出),需 `Out-File` 落盘后用 Read 读取;Bash 内调用 `powershell.exe` 被安全策略拒绝。 +- Cloudflare 测速端点在本地几乎不可用(上传 121KB/s、下载 0),国内测速改用 **Ookla 官方 CLI**(`install.speedtest.net` 可直连下载)。 + +--- + +## 追加:Phase 0 前置验证执行结果(同日 14:40–14:55) + +### 三个决定性发现 +1. **家里 IPv4 是运营商级 NAT(CGNAT)**:`tracert` 第 2 跳 = `100.64.0.1`(RFC 6598 运营商专用段)→ **v1 的"公网 IPv4 + 端口转发"方案彻底作废**。 +2. **家里有公网 IPv6**:`240e:338:263:3600::/64`(RA 下发,电信)。可用地址 `240e:338:263:3600:e591:cb62:3503:8a23`(Preferred)。→ **IPv6 直连成为首选路径**(IPv6 无 NAT,只需网关防火墙放行入站)。 +3. **IPv4 侧为对称 NAT**:STUN 实测映射端口每次不同(1986 / 1990)→ IPv4 打洞成功率低,但 IPv6 侧打洞前景好。 + +### 其他确认 +- **网关 = 中国电信智能网关(天翼网关)**,LuCI 界面 `/cgi-bin/luci`,MAC `88-C7-8F-A6-BC-43`;密码找回阶梯已写入方案文档(贴纸 useradmin → telecomadmin/`nE7jA%5m` → 小翼管家 App → 10000 号 → 恢复出厂;且**方案本身不依赖路由器后台**)。 +- 本机 Windows 防火墙:Domain=开 / Private=**关** / **Public=开**,以太网被判定为 Public;**当前会话不是管理员**,后续装 Sunshine 需用户点 UAC。 +- 本机装有 **Clash Verge**(系统代理 127.0.0.1:7897 开关=0)与**火绒**(HipsDaemon/HRWSCCtrl)。 +- **公司电脑(用户截图)**:Ryzen 7 7840H + **Radeon 780M**(RDNA3,VCN 4.0,**支持 AV1 硬解**)/ 32GB / 954GB,可安装软件。 +- **画质定档 1080p120**(H.265 22Mbps 起步 → 稳定后切 AV1 16–18Mbps)。 + +### 交付物 +- 方案文档升级至 **v2**:`远程游戏串流方案_家到公司.md`(架构重构为 IPv6 直连优先) +- 新增 `公司电脑网络自测.ps1`(UTF-8 with BOM,已在临时目录实跑验证通过):检测设备/显示、IPv6 出网、代理与审计软件、TCP 端口自由度、UDP 出网 + STUN NAT 类型、到家里的链路。 + +### 待用户执行(阻塞下一步) +1. 手机 4G/5G 打开 `http://[240e:338:263:3600:e591:cb62:3503:8a23]:38443/` 看是否返回 `IPV6-INBOUND-OK`(家里临时监听已起,端口 38443) +2. 公司电脑跑一次 `公司电脑网络自测.ps1` + +### 本次踩坑(已沉淀到 MEMORY.md) +- PowerShell 工具 stdout 被吞、`[byte] -shl` 溢出归零、`Get-Content -Raw`+`Out-File` 加 BOM 导致 UTF-8 中文被按 GBK 重写而永久损坏、PATH 被裁剪导致 ping/netsh 找不到、后台监听进程杀不掉、防火墙规则指向已失效的 node 路径。 + +--- + +## 追加:临时监听重启 + 引入「日志验证法」(同日 15:41–15:50) + +**类型**:CHG + +### 做了什么 +1. **清理旧监听**:上次会话留下的两个后台监听(38443 / 38080)本次全部 `Stop-Process` 成功,端口已释放(上次杀不掉的 38080 残留也清掉了)。 +2. **重起带访问日志的监听**(端口 38443):新版 `server.js` 把每个到达请求(来源 IPv6、UA、时间)追加写入 `%TEMP%\wb_ipv6test\inbound.log`,本地自测已命中日志。 +3. **本机 IPv6 地址复查**:`240e:338:263:3600:e591:cb62:3503:8a23` 仍为 **Preferred(未变)**;另有 `...:29f1:e63f:d476:597e` 也是 Preferred。 +4. **防火墙规则复核**:`C:\Users\xxl\AppData\Roaming\nvm\v23.0.0\node.exe` 有 **Public 档 TCP+UDP、任意端口** 的入站放行规则(因此该 node 起监听不会被本机防火墙吞掉,测试结果有效)。 + +### 关键结论:「从外部自动验证 IPv6 入站」没有可用手段 +逐一实测失败: +| 手段 | 结果 | +|---|---| +| `check-host.net` API(check-tcp) | 对 IPv6 一律 `{"error":"invalid_url"}`(方括号/无方括号/http:// 前缀均试过) | +| `ping.pe` / `tcp6.ping.pe` | **接受 IPv6 目标**,但结果为 JS 异步渲染,静态抓取只有 `Connecting to ...` + `waiting_for_results` | +| 公共 CORS 代理 codetabs | 522 | +| allorigins | `Network connection lost`(不足以作为证据) | +| corsproxy.io | 需 API key | +| thingproxy / Jina Reader | 空响应(被拦) | +| hackertarget nmap API | 已需 API key | + +→ 定案:**采用「手机 4G TCP 测试 + 服务端访问日志」双证据判定**,即使手机页面渲染异常,只要日志有 `REQ from=` 记录即可确证入站通。 + +### 交付物更新 +- `远程游戏串流方案_家到公司.md`:第七节补充「日志验证法」判定表与日志路径;附 B 补充上述"外部探测不可用"的完整踩坑清单。 +- `%TEMP%\wb_ipv6test\server.js` 重写为带日志版本(并新增 `ext_probe.py`,check-host.net 探测脚本,结论:该服务不支持 IPv6)。 + +--- + +## 追加:脚本跨设备损坏诊断 + 纯 ASCII 版交付(同日 15:47–15:55) + +**类型**:FIX + +### 现象 +用户把脚本弄到公司电脑(`E:\xxcool\project\workbuddy.xpcool.com\网络自测.ps1`)执行后报错: +1. `意外的标记"InterNetworkV6"` —— 文件里 `AddressFamily]::InterNetworkV6` **丢了一个 `::`** +2. `IPv6 鍑虹綉 ... 涓嶹€?` —— **中文乱码**(UTF-8 字节被按 GBK 解码的典型样子) + +### 诊断 +- 本机 `公司电脑网络自测.ps1` 第 35 行**是正确的**(带 `::`),文件 BOM 完好(`ef bb bf`) +- → 根因是**走了"文本复制粘贴"而不是"传文件"**,粘贴链路丢了 BOM 与个别字符 +- 乱码呈 `鍑虹綉` 形态 = 字节仍是 UTF-8,**只是缺 BOM** → 加 BOM 即可救(非双重编码,可逆) + +### 处置 +1. **新增纯 ASCII 版脚本 `net-test.ps1`**(输出全英文、文件内无非 ASCII 字符)→ 免疫一切编码问题;已在本机实跑通过,自检行输出 `Script encoding : no BOM (file is pure ASCII - harmless)` +2. 给了用户「一键修复已损坏文件」的命令(显式按 UTF-8 读 → 补 `::` → 以带 BOM 的 UTF-8 写回) +3. 修复经验沉淀进 skill `windows-host-probe`(新增"跨设备交付会静默损坏文件"一节) + +### 手机 IPv6 入站测试结果(用户 15:47 执行) +- 用户手机访问 `http://[240e:338:263:3600:e591:cb62:3503:8a23]:38443/` → **打不开** +- **服务端日志无任何来自手机的记录**(只有本机自测那条)→ 请求**未到达**家里主机 +- ⚠️ 但此结论有**前置条件未排除**:用户手机可能本身没有 IPv6(4G/5G 未开 IPv6、或未关 WiFi),需先做对照测试(手机访问 `ipv6.icanhazip.com` / `tcp6.ping.pe`)才能定论是"网关拦入站" + +### 其他 +- 想借本机 **Clash Verge** 代理(127.0.0.1:7897)从境外节点验证入站 → **Clash 未运行(7897 未监听)**,此路不通 +- ⚠️ **腾讯云服务器 SSH 公钥认证被拒**:`ssh -i CentOS.pem -p 22025 xxcool@193.112.118.168` 报 `Permission denied (publickey)`(网络可达、走到认证阶段)。后续所有服务器部署动作会受阻,需与用户确认密钥/密码。 + +--- + +## Phase 1 启动:Sunshine 已安装(16:04–16:12) + +**类型**:CHG + +### 完成项 +- 下载 **Sunshine v2026.906.222525** MSI(33,595,392 B,与官方发布字节数完全一致)+ **Moonlight Portable x64 6.1.0**(29,502,440 B) + → 存于 `C:\Users\xxl\Downloads\wb-stream\` + → GitHub CDN 直连被打断(exit 56),改用镜像 **`https://ghfast.top/`** 成功,约 700KB/s(`ghproxy.net`、`gh-proxy.com` 也返回 206 可用) +- 提权安装成功(`Start-Process msiexec -Verb RunAs` + 用户点 UAC),退出码 0 +- 服务 `SunshineService`(`sunshinesvc.exe` / LocalSystem / Auto / **Running**) +- 编码器探测:**h264_nvenc、hevc_nvenc、av1_nvenc 全部可用**(含 10-bit;AV1 的 YUV444 不支持属正常) +- 显示器识别:2560×1440 @ 170.002Hz,HAGS 已启用 +- Web UI 凭据已创建:**admin / Sunshine2026**(`POST /api/password`,字段 `newUsername/newPassword/confirmNewPassword`) + +### ⚠️ 关键阻塞:Sunshine 默认只监听 IPv4 +- 实测 `TCP 0.0.0.0:47984/47989/47990/48010`;`::1:47990` **连接被积极拒绝** → 完全无 IPv6 绑定 +- 官方文档(docs.lizardbyte.dev v0.23.1 advanced_usage)确认配置项: + - **`address_family = both`**(默认 `ipv4`)→ 双栈 + - `upnp = on` → Sunshine 的 UPnP 实现**含 IPv6 防火墙控制**,可能自动放行网关入站 +- 改法:编辑 `C:\Program Files\Sunshine\config\sunshine.conf` 后重启服务(需管理员); + **或走 Web UI**(Configuration → Advanced → Address Family),Web UI 以服务权限运行,**免管理员** + +### 工具与安全边界踩坑(重要) +- `Start-Process powershell.exe -Verb RunAs` 被安全策略拦截("spawns a child process that bypasses PowerShell command validation") + → **无法用提权 shell 跑脚本**;提权启动非 shell 的 exe(如 msiexec)可行 +- `curl -u user:pass`(HTTP Basic 凭据)触发**敏感内容审批并超时**;按规则**不重试、不绕过** → 该步交用户手动操作 +- PowerShell 工具默认是**非管理员上下文**:写 `C:\Program Files\` 被拒("访问被拒绝")、`Restart-Service` 报"无法打开服务" + +### 入站判定方法论修正 +- 临时监听用 `http.createServer`,**只在收到完整 HTTP 请求时写日志**;ping.pe 的纯 TCP 探测成功**不会留日志** +- → "ping.pe 显示 Connected" 与 "日志无记录" **可并存、不矛盾**(TCP 层 vs HTTP 层) +- 用户 16:00 反馈"通了"按 **TCP 层可达**理解 + +### 用户手机 IPv6 前置条件已排除 +- 手机(关 WiFi / 4G-5G)可访问 `https://ipv6.icanhazip.com/` → 手机确有 IPv6,此前"打不开"的测试**有效** +- 但 HTTP 请求仍未到达家里 → 差异可能来自手机侧(运营商 IPv6 路由 / 浏览器拦非标端口 http) + +### 下一步 +1. 用户在 Web UI 改 `address_family`(+ UPnP),重启服务 +2. 验证出现 `TCP [::]:47989` 等 IPv6 监听 +3. 手机 Moonlight(局域网)先跑通配对,再走外网 + +--- + +## 16:10–16:30 公司端自测结果回收 → 路线二次重构(v3) + +### ✅ 用户已完成 Sunshine 配置(已验证生效) +- `C:\Program Files\Sunshine\config\sunshine.conf` = `address_family = both` + `upnp = enabled` +- 日志确认解析成功:`config: 'address_family' = both` / `config: 'upnp' = enabled` +- 监听已变双栈:`TCP [::]:47984 / 47989 / 47990 / 48010`(**v2 的"IPv6 阻塞项"彻底解决**) +- `sunshine_state.json` 已存在(凭据已建) +- 无害报错:`NvEnc: gpu doesn't support YUV444 encode`(日志自称可忽略) +- ⚠️ 虚拟手柄不可用:`gamepads.virtualhid-not-available`(需 ViGEmBus 内核驱动,**建议不装**,抬高反作弊风险) + +### 🔴 公司端实测(`网络自测结果_20260910_160152.txt`)→ IPv6 路线作废 +- 公司 CPU:Ryzen 7 7840H + **Radeon 780M**;屏幕 **2560×1600@120Hz**(比假设的 60Hz 好一档) +- **IPv6:本机无 v6 地址,v6 出网两条 DNS 全不通 → 公司是纯 IPv4 网络** ⇒ **IPv6 直连彻底作废** +- 高位 UDP 可出网:STUN(miwifi / hitv)均有回包,出口 `117.188.24.89` +- 系统代理开关=0 但服务器填了 `127.0.0.1:7892`(疑似装过 Clash 类客户端,待查) +- 安全软件:`HipsDaemon`(火绒 HIPS) + `HRWSCCtrl`(火绒安全中心) + `edr_monitor` + `savsvc` + +### 🎯 重大纠正:家里 IPv4 是「锥形 NAT」,不是对称 NAT +- v2 测试方法**错误**:每个 STUN 用新 socket(本地端口变了)→ 映射端口必然不同 → 误得"对称 NAT" +- **正确方法**(同一 socket 连问多个 STUN):本地 55001 → 恒 `1.204.100.17:3051`;55002 → 恒 `:3063` +- ⇒ **端点无关映射(Cone/EIM),打洞可行** —— 这是本次最有价值的修正 +- 已写 `%TEMP%\wb_nat\nat_test.py` 作为可复用工具 + +### 🎯 第二个纠正:v1 脚本的"端口白名单"结论也是误判 +- 靶子无效:`114.114.114.114:8080`(该主机根本没开 8080)、`github.com:443`(国内 DNS 污染) +- 已重写 `net-test-v2.ps1`(**纯 ASCII、无 BOM、免疫复制粘贴**),靶子换成真在监听的: + `www.baidu.com:443`、`www.qq.com:443`、`www.aliyun.com:80`、`193.112.118.168:80/443/22025`、`portquiz.net:443/8080/8443` +- 本地实跑验证通过(家里侧:全部 OPEN,portquiz 全端口可达) +- 新增检查项:管理员权限自检 / 火绒·EDR 清单 / 本地代理端口监听 / 已装 VPN·远控软件 + +### ToDesk 现成可用(重要旁证) +- **用户此刻正在公司用 ToDesk 控家里**(说明公司↔家里的中转链路已验证可用) +- 家里这侧日志分析:ToDesk **文件传输**会话为 `planb:1 ... p2p:0`(**走中转,非 P2P**) +- 控制会话链路类型需看用户侧界面(`.xlog` 是压缩格式,挖不出)→ **已请用户查看界面显示"P2P直连"还是"中转"** +- ToDesk 的 RTC/QUIC 配置:`default_hard_codec: h265`,`br_limit 50000/100000` + +### 技术方案已重排(详见方案文档 v3) +- 路径 A(首选):**申请公网 IPv4** + 端口转发/UPnP → 公司端**零安装** +- 路径 B(次选):**Tailscale** 打洞(家里锥形 NAT 是好基础)→ 公司端需管理员+TUN 驱动,可能触发火绒/EDR +- 路径 C(兜底):ToDesk / UU远程(已验证可连通,但延迟与反作弊风险) +- 路径 D(保底):腾讯云中继(仅 5 Mbps) + +### 下一步(已交用户) +1. 看 ToDesk 界面显示 P2P直连 还是 中转(5 秒,信息量最大) +2. 公司电脑跑 `net-test-v2.ps1`,回传 `net-test-v2_*.txt` +3. 打 10000 申请公网 IPv4 +4. 家里改电源设置(关屏=从不、睡眠=从不) + +### 交付物 +- `net-test-v2.ps1`(新增,纯 ASCII) +- `远程游戏串流方案_家到公司.md` 升级到 **v3**(路线重构 + 三处测试陷阱记录) + +--- + +## 16:30 更新 · 公司端网络身份确认 + ToDesk 反查(本轮) + +### 用户提供(ipconfig) +- 公司笔记本走 **WiFi** 接入企业网,网段 `192.168.250.0/24`,网关 `192.168.250.1` +- **IPv6 只有 `fe80::` 链路本地地址 → 无 RA 全局地址,纯 IPv4 网络(硬结论,IPv6 路线彻底作废)** +- 出口 IP `117.188.24.89`,全公司共享同一出口 + +### AI 从被控端反查 ToDesk(用户无需操作,用户反馈"找不到状态栏") +- ToDesk 只与自家服务器保持 TCP 长连接:`219.151.138.225:8080`、`219.151.138.225:443`、`118.24.225.33:443` +- **与公司出口 `117.188.24.89` 无任何 TCP 直连记录**;文件传输会话为 `planb:1 ... p2p:0`(中转) +- ⭐ **关键利好:公司允许出站到 8080 非标端口** → 公司不是"只放行 80/443"的白名单防火墙;若将来有公网 IPv4,Sunshine 的 47989/47990/48010 大概率可直连,腾讯云 frp 中继(任意端口)也具备可行性 +- 家里 ToDesk 在多个 IPv6 UDP 端点监听(57062–57068,对应 3 个轮换的隐私扩展地址)+ IPv4 `192.168.1.30:57065/57069`;UDP 无连接、端点列表看不到对端,**不能据此断定打洞失败,但打洞信心已下调** +- 附带结论:家里 IPv6 有多个轮换隐私地址 → IPv6 直连必须配 DDNS(该路线已作废,仅留档) + +### 交付 / 变更 +- `net-test-v2.ps1` **已复制到家里主机桌面** `C:\Users\xxl\Desktop\net-test-v2.ps1`(13,690 B),便于用户用 **ToDesk 文件传输**拉到公司电脑,避免复制粘贴损坏 +- 方案文档 §1.6 已补入公司接入方式与 ToDesk 反查结论;第七节 ① 改为"已完成,无需再找状态栏",② 增加 ToDesk 文件传输操作步骤 +- 确认 v2 脚本**已内置** `193.112.118.168:80/443/22025` 的 TCP 连通性测试(可验证公司能否访问腾讯云,为 frp 中继铺路) + +### 下一步(用户侧) +1. 用 ToDesk 文件传输把桌面 `net-test-v2.ps1` 拉到公司电脑并运行 → 回传 `net-test-v2_*.txt` +2. 若 UDP 打洞数据不乐观 → **优先打 10000 申请公网 IPv4**(路径 A,公司端零安装) +3. 家里改电源设置(关屏=从不、睡眠=从不) + +--- + +## 16:35 更新 · 公司端 v2 实测结果回执(**路线定案**) + +来源:`net-test-v2_20260910_163314.txt`(公司电脑运行 v2 脚本,用户经 ToDesk 文件传输送达) + +### 三条硬结论 + +1. ❌ **公司是对称 NAT**:同一本地 UDP 端口 `46001` 问多个 STUN → 映射端口 `16993/16994/17011/43681` **各不相同**;且**公网出口有多个**(`117.188.24.89` 与 `117.188.118.254` 按目的地分流)→ **P2P 打洞不可行**(v3 曾把 Tailscale 打洞列为次选,本轮判死) +2. ❌ **公司无 IPv6**(复核 `IPv6 local : NONE`)→ IPv6 直连彻底作废 +3. ✅ **公司 TCP 出站完全自由**:`portquiz.net` 的 `443/8080/8443` 全 OPEN,**连腾讯云 `193.112.118.168:22025` 也 OPEN** → 任意端口的直连/中继"通道"毫无障碍 + +### 其他实测事实 + +- **管理员**:当前会话非管理员,但账号 `ybtdevxxl` **在 Administrators 组内** → 可提权 +- **已装软件**:`Clash Verge`、`WireGuard`、`ToDesk`、火绒安全软件 +- **安全软件**:**火绒 `HipsDaemon` / `HRWSCCtrl` 运行中**;`aTrust` / `edr_monitor` / `savsvc` / Defender 全系 **Stopped** → 无强制 EDR 管控 +- **屏幕**:主屏 2560×1600@120(脚本报 `1707×1067` = 150% 缩放后的逻辑值),副屏 1920×1080 +- UDP 出网可用,但 `stun.qq.com:3478` 超时(该服务器自身问题,非本地限制) + +### 路线定案(v4) + +| 优先 | 路线 | 画质上限 | 公司端代价 | +|---|---|---|---| +| **A(主攻)** | **申请公网 IPv4** + 端口转发 / UPnP | **1080p120 满血** | 零安装 | +| B | 腾讯云 frp 中继(通道已验证可行,卡在 5 Mbps) | 1080p60 | 零安装 | +| C | 网易UU远程 / ToDesk(兜底) | 延迟 + 反作弊风险 | 零安装 | + +**核心洞察**:唯一瓶颈是"**家里没有公网入口**";公司侧出站通道意外宽松。 + +### 交付 + +- 方案文档升级到 **v4**:新增 `v3 → v4` 修订摘要、第二章「方案选型(v4 定案)」重排(Tailscale 打洞移入已排除)、第七节改为"主攻公网 IP"并附话术与三种客服应对 +- 待用户执行:**打 10000 申请公网 IPv4** + +--- + +## 16:41 用户提问 · 申请到的会是动态公网 IPv4 吗? + +**类型**:REQ(咨询,未实施) + +### 答复要点(已交付用户) +1. **必然是动态的**:家宽走 DHCP 动态分配,运营商不会给家宽静态 IP(静态属企业专线产品,需签约加钱)。10000 能做的只是把会话地址从 CGNAT 池(100.64.x.x)换到真公网池,本质仍是动态。 +2. **动态不影响方案**:电信动态 IP 只要光猫不断线就长期不变(本机出口 `1.204.100.17` 与历史 `1.204.105.116` 同属 1.204 段);再用 `xpcool.com` 子域做 DDNS(DNSPod API + 定时上报脚本,约 5 分钟粒度),Moonlight 端只填域名 → 图示"家里 WAN → DNSPod → 公司 Moonlight"链路。 +3. **比"动态"更该防的三个坑**: + - 拿到的仍是 `100.64.x.x` / 私网段 → 挂电话前必须确认"能否被外网直接访问" + - 光猫路由模式造成**双层 NAT** → 需光猫改桥接,或光猫内给路由器 WAN 做 DMZ + - 家宽普遍封 80/443 → Sunshine 用 47989/47984-48010,**不受影响** +4. **唯一可靠的验证法**:手机流量查出口 IP,**必须完全等于**路由器 WAN 口 IP(且 WAN IP 非私网段)。 +5. **时机建议**:10000 是 7×24,上班摸鱼时即可打电话登记;但**改光猫/重启必须等回家做** —— 当前唯一远程通道是 ToDesk,一旦改崩断网就连带失去操作通道。 + +--- + +## 18:56–19:05 · 公网 IPv4 被拒 → IPv6 路线重启评估(v5) + +**类型**:CHG + +### 用户输入 +"公网 ipv4 申请不到,ipv6 可以吗" —— 10000 申请公网 IPv4 被拒。 + +### 本轮实测(家里侧,19:00 复查) +- **IPv6 出网全通**:`ipv6.icanhazip.com` / `www.taobao.com` / `www.qq.com` 的 TCP 443 全部 OK +- **IPv6 地址三枚**: + +| 地址 | 后缀类型 | 状态 | 用途 | +|---|---|---|---| +| `240e:338:263:3600:29f1:e63f:d476:597e` | **Suffix=Link** | Preferred | ✅ **首选服务地址 —— 后缀永不轮换** | +| `240e:338:263:3600:e591:cb62:3503:8a23` | Suffix=Random | Preferred | ⚠️ 隐私扩展,会轮换 | +| `240e:338:263:3600:51f3:a026:4256:a337` | Suffix=Random | Deprecated | ❌ 废弃 | + +- 默认路由 → `fe80::1`;Public 档防火墙开启,`node.exe` 有 Public/Private 入站 Allow 规则 → 本机侧不拦 +- **监听已重起**:`:::38443 LISTENING`(PID 27508),日志 `%TEMP%\wb_ipv6test\inbound.log` 正常写入 + +### 关键论证(本轮最重要的一条推理) +v4 说"IPv6 判死",精确说法应是"**公司那条有线/WiFi 出口没有 IPv6**",而非"公司场景不可用"。 +→ 公司笔记本还有**第二条出口从未被用上:手机 5G 热点**(用户手机实测有 IPv6)。 +→ v5 主攻 = **手机热点补 IPv6 + 家里 IPv6 直连**,公司端仍零安装、画质上限仍是 1080p120。 + +### v5 路线定案 + +| 优先 | 路线 | 画质 | 卡点 | +|---|---|---|---| +| A(主攻) | 手机热点 + 家里 IPv6 直连 | **1080p120 满血** | ①热点下游能否拿到 IPv6 ②天翼网关是否放行 IPv6 入站 | +| A′(长期最优,当前失败) | 公网 IPv4 + 端口转发 | 1080p120 | 10000 申请被拒 | +| B | 腾讯云 frp 中继 | 1080p60(5 Mbps) | — | +| C | 网易UU远程 / ToDesk | 延迟 + 反作弊风险 | — | + +**代价认知**:1080p120 @ 20 Mbps ≈ **9 GB/小时**;5G 延迟预计 **40–70 ms**。 + +### 交付物 +- `远程游戏串流方案_家到公司.md` → **v5**:新增 v4→v5 摘要、§1.3 重写(IPv6 地址稳定性分析)、§2 选型重排(路径 A 改为手机热点,原 A 降为 A′)、已排除表补入"IPv6 隧道类(6in4/Teredo/WARP)"、§7 重写为两步验证法 +- `hotspot-ipv6-test.ps1`(新增,**纯 ASCII / 无 BOM**,已试跑通过,并复制到家里桌面 `C:\Users\xxl\Desktop\` 供 ToDesk 传输)—— 一次测完"热点下游有无 IPv6 + 能否连到家里" +- `ipv6-home-qrcode.png`(新增,扫码目标 `http://[240e:338:263:3600:29f1:e63f:d476:597e]:38443/`) + +### 本轮踩坑(重要,已沉淀) +1. **`& script.ps1` 调用脚本文件时,脚本内的写盘会静默失败** —— probe.ps1 / probe2.ps1 都返回 exit 0 却完全没生成结果文件;改**内联命令 + `Set-Content` 落盘**立刻成功。→ 后续所有探测一律内联,不落脚本文件。 +2. **`Test-NetConnection` 对不可达的 IPv6 目标会长时间阻塞**,把整个脚本拖到工具超时(第一次探测就是这样废掉的)→ 改用 `TcpClient.BeginConnect` + `AsyncWaitHandle.WaitOne(ms)` 实现硬超时。 +3. `netsh`/`netstat` 的文本过滤会假阴性(`netstat | ? { $_ -match ':38443' }` 报 NONE,实际 `Get-NetTCPConnection -LocalPort 38443` 显示 `:::38443 Listen`)→ **查监听一律用 `Get-NetTCPConnection`**。 + +### 待用户执行(决定走哪条路) +1. **手机 5G(关 WiFi)扫码**访问家里 `http://[240e:338:263:3600:29f1:e63f:d476:597e]:38443/` → 验证家里 IPv6 入站是否放行(此前失败,疑天翼网关拦入站) +2. **手机开热点 + 公司笔记本连上** → 跑 `hotspot-ipv6-test.ps1` → 验证热点下游能否拿到 IPv6 并连通家里 +3. 若步骤 1 失败 → 按 §7 ② 放行天翼网关 IPv6 防火墙(或光猫改桥接) diff --git a/.workbuddy/memory/2026-09-13.md b/.workbuddy/memory/2026-09-13.md new file mode 100644 index 0000000..9d3bd51 --- /dev/null +++ b/.workbuddy/memory/2026-09-13.md @@ -0,0 +1,5 @@ +# 2026-09-13 + +2026-09-13 | CFG | status.xpcool.com:新增 nginx 反代配置(先 HTTP,含 WebSocket 透传),nginx -t 通过并已 reload +2026-09-13 | DEP | 部署 Uptime Kuma 2.5.3 监控面板:容器 `uptime-kuma`,127.0.0.1:3001,数据 `/data/uptime-kuma`,内存限 512m,healthy +2026-09-13 | DOC | MEMORY.md 瘦身:生产部署/安全加固/工具链坑位拆到同目录 DEPLOY.md;新增「站点与容器清单」表 diff --git a/.workbuddy/memory/DEPLOY.md b/.workbuddy/memory/DEPLOY.md new file mode 100644 index 0000000..0361c9f --- /dev/null +++ b/.workbuddy/memory/DEPLOY.md @@ -0,0 +1,55 @@ +# xpcool.com 生产环境 · 部署与运维手册 + +> 从 MEMORY.md 拆出(2026-09-13),存放服务器、容器、站点、安全加固与本机工具链坑位等细节。 +> MEMORY.md 只保留规则与索引。 + +## 生产部署(腾讯云 193.112.118.168,SSH 22025/xxcool,Docker+宿主机 Nginx) +- 站点落点:前端静态 → `/data/www/<域名>/`;nginx 配置 → `/data/nginx/conf.d/<域名>.conf`(80→301→443 + 反代/静态);证书 → `/data/nginx/ssl/<域名>/<域名>_bundle.crt|.key`(本地源:`E:\xxcool\ssl\<域名>_nginx/`)。 +- service 后端容器:`service.xpcool.com:new`,`127.0.0.1:10100:10100`,网络 `xpcool-net`,env:`GF_GCFG_ENV=prod` + `DB_DSN=mysql:service:xxx@tcp(xpcool-mysql:3306)/service?...`(**必须带 mysql: 类型前缀**)+ `JWT_SECRET`;容器内必须有 `manifest/config/config.yaml`(基础配置),否则 GF 启动报「找不到 config」;构建目录 `/data/deploy/projects/service.xpcool.com/`(main + manifest/config + resource,Dockerfile 用 COPY manifest/config 整体拷贝)。 +- DB:`xpcool-mysql` 容器(root 密码见该容器 env),库名 `service`,种子执行顺序 003→004→004b→007(服务器表结构旧,必须先 003 加列;导入用 `sudo sh -c 'docker exec -i xpcool-mysql mysql ... < file'` 宿主机重定向)。 +- 权限映射:admin_menu type=2 行的 path 必须匹配实际路由(现为 `POST /api/service/admin/...`,见 manifest/sql/007_menu_permissions_v3.sql)。 +- 文件下载站(2026-09-03 上线,当晚迁移):`file.xpcool.com` = **FileBrowser Quantum** 容器 `fbq`(`gtstef/filebrowser:stable` v1.5.5,`127.0.0.1:10881→80`,restart unless-stopped,`/data/share:/srv:ro` **只读挂载**,数据卷 `/data/filebrowser-q`:config.yaml+database.db+tmp,属主=镜像 uid1000)+ nginx 反代。**要点**:初始凭据 **admin/admin**(首登后立即改密);配置走 `config.yaml`(sources/cacheDir),管理走网页 UI;API 与原版不兼容(需用户建 long-live token,/swagger 亦需);镜像默认非 root(uid1000);原版 filebrowser(容器 `filebrowser`/10880/db `/data/filebrowser/database`)已于 2026-09-01 归档、现已停用**保留回滚**;Quantum v2.0 beta 权限模型改 per-source(view/download/modify/create/delete)。改密/建只读账号/临时分享(过期+密码+匿名)均在网页 UI。 +- SSH 凭据/密码不入库,需要时询问用户或查对话记忆;远程批量命令用 paramiko 脚本(`%TEMP%\wb_deploy\ssh_exec.py`,支持第 2 参数超时秒数),docker build 用服务器端 nohup+日志轮询防超时。**(2026-09-11 密钥轮换后更新)**:当前登录 = 本地 `~/.ssh/xpcool_ed25519`(ed25519,注释 xpcool-server-20260904)→ `xxcool@193.112.118.168:22025`(root 被拒;密码登录已禁);旧 `CentOS.pem`(RSA,对应服务器公钥 `skey-q6pq6jb3`)**已作废并从 authorized_keys 移除**(因私钥误提交 Gitea 事件而轮换),服务器 authorized_keys 另存 2 把钥(`mcp-server-manager@local`、`xxcoolwork@gmail.com`)为其他用途、保留不动;安全组按出口 IP 限放 22025(本机出口 1.204.105.116 曾因未放行连不上,80/443 可达);本机旧记录 `~/.ssh/id_ed25519` 与 wb_deploy 脚本**在当前设备不存在**。 +- **服务器安全加固(2026-08-27 完成)**:firewalld 已启用(放行 22025/80/443/3000/2222;cockpit/rpcbind 已停);fail2ban 保护 sshd(port=22025, maxretry5/bantime1h);监控脚本 `/usr/local/bin/server-monitor.sh` + cron 每 10 分钟(调度在 **xxcool 的 crontab**:`*/10 * * * * sudo /usr/local/bin/server-monitor.sh`;日志 `/var/log/server-monitor.log`,Bark 推送配 `/etc/server-monitor.conf` 的 `BARK_URL`);**告警去重终版(2026-09-11)**:状态目录 `/var/lib/server-monitor/` —— `banned-ips.state`(已提醒过的封禁 IP,**只增不减 → 同一 IP 永不重复提醒**)、`ssh-src-ips.state`(已提醒过的登录来源 IP,**仅新 IP 首次登录**才提醒)、`ports.state`(当前已告警的未登记端口,消失后清除)、`ssh-brute.state`(爆破告警状态机:`attacking`=本轮已提醒 / `idle`=已复位;攻击持续不刷屏,攻击停止静默复位,**再犯视为新一波才重新提醒**);完整审计仍在 fail2ban.log + journald 且已上报 admin 后台(脚本备份 `server-monitor.sh.bak.20260911`);另有安全日志上报器 `/usr/local/bin/server-security-collector.sh`(cron **每 5 分钟**,xxcool crontab)→ POST 到 `127.0.0.1:10100/api/service/open/security/log/report`,配置 `/etc/server-security.conf`(INTERNAL_TOKEN)、状态 `/var/lib/server-security/last.ts`;dnf-automatic 每天 06:00 自动安全补丁;SSH 密钥登录已部署(现用 `~/.ssh/xpcool_ed25519`)、密码登录已禁。**2026-09-11 加固**:已关闭 `X11Forwarding`(**生效位置是 drop-in `/etc/ssh/sshd_config.d/50-redhat.conf:17`,主配置里那行是注释、改了无效**;改前备份 `.bak.20260911`,`sshd -t` 预检 + `sshd -T` 核对 + `systemctl reload sshd` 不断连);nginx 遗留垃圾已清理(`.code.xpcool.com.conf.swo`、`tool.xpcool.com.conf.download.0` → 移入 `/data/nginx/_junk_backup_20260911/`,未直接删、可回滚)。**nginx 配置目录约定**:`include /data/nginx/conf.d/*.conf;`,故 conf.d 下非 `.conf` 后缀文件不会被加载。**遗留待办**:`lighthouse` 账号(腾讯云自带 uid1000,NOPASSWD ALL)是否锁定待用户决策。**坑**:① 腾讯云安全组未放行 3000/2222/8081,Gitea 走 nginx 反代 80/443;② 改 sshd 前必须先 `sshd -t` 预检+备份(曾因脏行导致 SSH 断开);③ EPEL 源需腾讯云镜像(`mirrors.cloud.tencent.com/epel`)且 baseurl 必须手动写全;④ CentOS9 的 fail2ban 在 EPEL。**⑤(2026-09-04 新坑)腾讯云改带宽若切计费模式会触发实例冷重启,重启后 firewalld 自定义端口规则曾丢失致 SSH 22025 失联(80/443 正常)——firewall-cmd 放行务必带 `--permanent`,且实例冷重启后要复查 22025 放行**。另:带宽 3M→5M 已升级(2026-09-04,实测 619KB/s 满速)。**⑥(2026-09-11 溯源新坑)本机出口 IP `1.204.105.116` 曾于 09-03 23:00 被自己的 fail2ban 封禁、09-04 23:00 才解封——这正是当时「SSH 22025 超时」的真因(当时误判为安全组未放行,绕了远路)。SSH 突然不通时,排查顺序应为:先 `fail2ban-client status sshd` + `grep Ban /var/log/fail2ban.log`,再怀疑安全组**。**⑦(2026-09-11 体检结论)服务器未发现被入侵迹象**:UID0 仅 root、无空密码账号、无异常 SUID/后门/劫持、无特权容器、系统目录近 30 天无改动;外网暴露面实测仅 **22025/80/443**(3000/2222/8081 等均被安全组挡住,firewalld 虽放行 3000/2222 但安全组是第二道闸)。 +- 本地 pnpm 修复(2026-08-27 升级为 wrapper 方案):根因=本环境 MSYS 路径转换故障,把 `/c/...` 参数转成 `E:\c\...`(多 `E:\c\` 前缀)致 corepack shim 与 node 都找不到 pnpm.js。**已建持久 wrapper**:`~/.workbuddy/binaries/node/bin/pnpm`(内容:`export NODE_OPTIONS=--experimental-sqlite; exec "<托管node.exe>" "C:/Users/ybtdevxxl/.workbuddy/binaries/node/workspace/node_modules/pnpm/bin/pnpm.cjs" "$@"`,关键=给 node 传 Windows 风格路径 `C:/...` 规避 MSYS 转换)。**提交/构建前需 `PATH="~/.workbuddy/binaries/node/bin:$PATH"` 前置**,pre-commit 钩子(lefthook oxlint/oxfmt/eslint/stylelint/checkType)即可正常运行;否则钩子崩 → 只能 --no-verify。已验证 pnpm 11.16.0 + `pnpm exec oxlint` 1.79.0。 + +## 日常同步习惯(建议) +- 每次工作结束:在各改动项目 `git add + commit`,本工作空间同样提交(规则/日志变化时)。**只 commit、不要 push**(2026-09-01 用户明确:推送由用户自行完成,AI 无需代为 push,避免凭据交互失败)。 +- **验证分工(2026-09-01 用户明确)**:AI 能做的验证(类型检查、构建、单元/二进制测试、自动化冒烟)照做;**AI 不方便验证的部分(真实浏览器交互体验、视觉效果、下载弹窗等)直接交给用户手动验证**,不要反复折腾自动化工具。 +- 换设备开工前:先 `git pull` 各仓库,保证规则与记录最新。 + +## 本机 Windows/PowerShell 工具链坑位(2026-09-10 实测,写脚本必看) +- **PowerShell 工具不回显 stdout**:exit code 正常但看不到输出;必须把结果 `Out-File` 落盘,再用 Read 读取。Bash 里调 `powershell.exe` 会被安全策略拒绝。 +- **`[byte] -shl 8` 会溢出归零**(结果仍是 byte 宽度)。字节拼 16 位整数必须先 `[int]` 转换,否则高位丢失(剪贴板/协议解析类代码的高频坑)。 +- **给 .ps1 加 BOM 绝不能用 `Get-Content -Raw` + `Out-File`**:本机默认按 GBK 解码 UTF-8 文件,中文会被永久破坏且不可逆。正确做法是字节级前置 `EF BB BF`(`[System.IO.File]::WriteAllBytes`)。 +- **PATH 被裁剪**:脚本里直接写 `ping` / `netsh` 会报"无法识别",须用 `$env:SystemRoot\System32\xxx.exe` 绝对路径;且不要写 `& $exe args 2>$null | Out-String`(会报"无法在管道中间运行文档"),改用 `$out = & $exe args` 再 `@($out) -join ' '`。 +- **PowerShell 变量名不区分大小写**:`$R` 与循环变量 `$r` 是同一个变量,会导致集合被静默覆盖。 +- **后台监听的进程可能杀不掉**(Stop-Process / taskkill 均无效),换端口重新起更省事。 +- **防火墙规则按可执行文件路径匹配**:一批 node.exe 规则指向已不存在的路径(失效);目前 `C:\Users\xxl\AppData\Roaming\nvm\v23.0.0\node.exe` 有有效的 Public 档「TCP+UDP 任意端口」入站放行规则——需要做**免提权的入站可达性测试**时就用它监听。 +- 测入站**不要用 ping**:Windows 默认拦 ICMPv6/ICMP 入站,必须用 TCP 端口测试。 +- 国内测速:Cloudflare 端点不可用;用 **Ookla 官方 CLI**(`install.speedtest.net` 可直连下载)。 +- 本机网络备注:家里的宽带是**运营商级 NAT(CGNAT,第 2 跳 100.64.0.1)**,无公网 IPv4;但有**公网 IPv6**(`240e:338:263:3600::/64`)。以太网被判定为 Public 网络档。 + +## 站点与容器清单(2026-09-13 核对) +> 服务器 193.112.118.168,容器均为 `restart unless-stopped`;除特别标注外只监听 `127.0.0.1`,由宿主机 nginx 反代。 + +| 域名 | 容器 | 监听 | 说明 | +|------|------|------|------| +| admin.xpcool.com | —(静态 `/data/www/`) | — | 管理后台前端 | +| service.xpcool.com | `service.xpcool.com:new` | 127.0.0.1:10100 | Go 后端(xpcool-net,连 xpcool-mysql) | +| tool.xpcool.com | —(静态) | — | 前端工具站 | +| file.xpcool.com | `fbq` | 127.0.0.1:10881 | FileBrowser Quantum 只读下载 | +| read.xpcool.com | `qread` | 127.0.0.1:8082 | 阅读 legado 自托管 | +| git.xpcool.com | `gitea` + `gitea-runner` | 0.0.0.0:3000 / 0.0.0.0:2222 | 代码托管与 CI | +| sub2api.xpcool.com | `sub2api` + postgres + redis | 127.0.0.1:18080 | — | +| code.xpcool.com | `code-server` | 127.0.0.1:8080 | 网页版 VS Code | +| **status.xpcool.com** | **`uptime-kuma`** | **127.0.0.1:3001** | **服务监控面板(2026-09-13 新增)** | +| —(暂无域名) | `bark` | 0.0.0.0:8081 | Bark 推送服务端 | +| — | `xpcool-mysql` | 127.0.0.1:3306 | MySQL 8.0(库 `service`) | + +### Uptime Kuma(status.xpcool.com,2026-09-13 部署) +- 镜像 `louislam/uptime-kuma:2.5.3`(2.x 稳定线;1.x 已停维护,v2 有破坏性变更,跨大版本需迁移数据库)。 +- 启动参数:`--restart unless-stopped -p 127.0.0.1:3001:3001 -e TZ=Asia/Shanghai -v /data/uptime-kuma:/app/data --memory 512m`。服务器内存仅 3.7G 且 **swap=0**,必须限内存,避免 OOM 波及同机其他容器。 +- **v2 首次启动是「选择数据库」引导页**(逻辑在 `/app/server/setup-database.js`,读 `data/db-config.json`),选 SQLite 即可,之后进入创建管理员账号页。Uptime Kuma **没有找回密码入口**,密码丢失只能改数据库/清 2FA,务必记牢。 +- nginx 反代**必须透传 WebSocket**(`proxy_http_version 1.1` + `Upgrade`/`Connection` 头),否则面板无限重连、状态刷不出来。 +- nginx 配置模板见 `.workbuddy/tmp/status.xpcool.com.conf`。 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 0a553c5..8daee8f 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -1,78 +1,43 @@ # xpcool.com 工作空间 · 统一规则与项目索引 -> 本文件随会话自动加载。保持精简:只放规则与索引,详细历史在各项目 CHANGELOG。 +> 随会话自动加载。本文件只放规则与索引;**生产部署与运维细节见同目录 `DEPLOY.md`**,历史在各项目 CHANGELOG。 ## 三条铁律(全项目通用) -1. 中文注释:写/改代码时,在应有处(函数、复杂逻辑、配置项、非显然分支)加中文注释。 -2. 做记录:每次请求/变更/修复/文档动作,都在对应项目 CHANGELOG.md 顶部追加一条。 -3. 中文优先:与用户的思考、输出、交流,能中文尽量中文。 +1. **中文注释**:写/改代码时,在应有处(函数、复杂逻辑、配置项、非显然分支)加中文注释。 +2. **做记录**:每次请求/变更/修复/文档动作,在对应项目 `CHANGELOG.md` 顶部追加一条;跨项目规则与索引更新写回本目录。 +3. **中文优先**:思考、输出、交流能中文就中文。 -## 接口设计规范(2026-08-27 起 · 全项目强制) -> 用户拍板:新建 HTTP 接口**全部采用 POST**;**URL 中不得带任何参数**(查询参数与路径参数 `{id}` 都禁止);**所有入参一律通过请求体 body(JSON)传递**。 -- 路由用静态 path(如 `/recruitment/info/update`),`id` 等也放 body,不用 `/update/{id}`。 -- 既适用于 service 后端(g.Meta `method:"post"`),也适用于 admin 前端(`requestClient.post(url, data)`,url 无参)。 -- **首个落地模块**:service.xpcool.com 的 `recruitment`(招聘考试聚合),作为规范范本;后续新接口(含 house 之外的模块)一律遵守,存量接口逐步迁移。 +## 接口设计规范(2026-08-27 起 · 强制) +- 新建 HTTP 接口**一律 POST**;**URL 不带任何参数**(查询参数、路径参数 `{id}` 都禁止);**入参全部走 body(JSON)**。 +- 路由用静态 path(如 `/recruitment/info/update`),`id` 等也放 body。 +- 同时约束 service 后端(g.Meta `method:"post"`)与 admin 前端(`requestClient.post(url, data)`,url 无参)。 +- 范本模块:service.xpcool.com `recruitment`;存量接口逐步迁移。 ## 界面开发约定(2026-08-26 起) -- **admin.xpcool.com 后台界面开发优先使用 TDesign MCP**(`tdesign-mcp-server`,已配置于 `~/.workbuddy/mcp.json`,`npx -y tdesign-mcp-server@latest`;tools:get-component-docs / get-component-list / get-component-changelog / get-component-dom / search-icons)。 -- 写/改 tdesign 组件代码前,先通过 MCP 查官方文档,用官方封装属性与推荐用法,避免踩自造轮子/已知坑。 -- 树形表格必须用 `t-enhanced-table`(t-table/PrimaryTable 不支持 tree);异步数据后需 `await nextTick()` + `tableRef.value.expandAll()`(defaultExpandAll 只在首次渲染生效)。 +- admin.xpcool.com 写 TDesign 组件前**先用 TDesign MCP 查官方文档**(`tdesign-mcp-server`,配在 `~/.workbuddy/mcp.json`),用官方封装属性,避免自造轮子。 +- 树形表格必须用 `t-enhanced-table`;异步数据后需 `await nextTick()` + `tableRef.value.expandAll()`(`defaultExpandAll` 只在首次渲染生效)。 -## 工作空间说明(git 管理 · 跨设备同步) -- 本工作空间仓库:`workbuddy.xpcool.com`(含本规则、每日日志、README),远程:`https://git.xpcool.com/xpcool/workbuddy.xpcool.com.git` -- **布局约定(跨设备必须一致)**:所有项目与工作空间仓库位于同一父目录下: - ``` - <父目录>/ - ├── workbuddy.xpcool.com/ ← 本工作空间(规则+索引+日志) - ├── admin.xpcool.com/ - ├── service.xpcool.com/ - └── tool.xpcool.com/ - ``` - 换设备时按此布局 clone,索引即全部生效,无需改任何路径。 +## 工作空间与项目索引 +- 本仓库 `workbuddy.xpcool.com`(规则+索引+每日日志),远程 `https://git.xpcool.com/xpcool/workbuddy.xpcool.com.git`。 +- **布局约定**:所有项目与工作空间位于同一父目录下(`<父目录>/workbuddy.xpcool.com`、`admin.xpcool.com`、`service.xpcool.com`、`tool.xpcool.com`),换设备按此布局 clone 即可,索引全部生效。 -## 项目索引(变更记录在各项目 .workbuddy/memory/CHANGELOG.md) -| 项目 | 类型 | 相对路径(相对本工作空间) | 远程仓库 | -|------|------|--------------------------|---------| -| admin.xpcool.com | Vue 管理后台前端 | ../admin.xpcool.com/.workbuddy/memory/CHANGELOG.md | https://git.xpcool.com/xpcool/admin.xpcool.com.git | -| service.xpcool.com | Go 后端服务 | ../service.xpcool.com/.workbuddy/memory/CHANGELOG.md | https://git.xpcool.com/xpcool/service.xpcool.com.git | -| tool.xpcool.com | 前端工具站 | ../tool.xpcool.com/.workbuddy/memory/CHANGELOG.md | https://git.xpcool.com/xpcool/tool.xpcool.com.git | -(新增项目:在此加一行 + 在其 .workbuddy/memory/ 建 CHANGELOG.md + git.xpcool.com 建仓库) +| 项目 | 类型 | 变更记录 | 远程仓库 | +|------|------|---------|---------| +| admin.xpcool.com | Vue 管理后台前端 | `../admin.xpcool.com/.workbuddy/memory/CHANGELOG.md` | git.xpcool.com/xpcool/admin.xpcool.com.git | +| service.xpcool.com | Go 后端服务 | `../service.xpcool.com/.workbuddy/memory/CHANGELOG.md` | git.xpcool.com/xpcool/service.xpcool.com.git | +| tool.xpcool.com | 前端工具站 | `../tool.xpcool.com/.workbuddy/memory/CHANGELOG.md` | git.xpcool.com/xpcool/tool.xpcool.com.git | -## 记录格式(省 token · 快速命中) -- 倒序:最新在上,一行一条 -- 模板:`YYYY-MM-DD | 类型 | 一句话摘要` -- 类型:REQ 请求 / CHG 变更 / FIX 修复 / DOC 文档 / CFG 配置 / DEP 依赖 -- 示例:`2026-08-26 | CHG | 新增用户列表分页接口并补中文注释` +## 记录格式 +- 倒序、一行一条:`YYYY-MM-DD | 类型 | 一句话摘要`;类型:REQ / CHG / FIX / DOC / CFG / DEP。 ## 命中流程 -1. 判断是否涉及某项目(看项目名/路径)。 -2. 读该项目 CHANGELOG.md 头部(最新 10–15 条)了解近况。 -3. 处理后于 CHANGELOG 顶部追加本次记录。 -4. 新增项目时同步更新上方索引表。 +判断涉及哪个项目 → 读该项目 CHANGELOG 头部 10–15 条 → 处理后于顶部追加本次记录 → 新增项目时同步更新上方索引表。 -## 生产部署(腾讯云 193.112.118.168,SSH 22025/xxcool,Docker+宿主机 Nginx) -- 站点落点:前端静态 → `/data/www/<域名>/`;nginx 配置 → `/data/nginx/conf.d/<域名>.conf`(80→301→443 + 反代/静态);证书 → `/data/nginx/ssl/<域名>/<域名>_bundle.crt|.key`(本地源:`E:\xxcool\ssl\<域名>_nginx/`)。 -- service 后端容器:`service.xpcool.com:new`,`127.0.0.1:10100:10100`,网络 `xpcool-net`,env:`GF_GCFG_ENV=prod` + `DB_DSN=mysql:service:xxx@tcp(xpcool-mysql:3306)/service?...`(**必须带 mysql: 类型前缀**)+ `JWT_SECRET`;容器内必须有 `manifest/config/config.yaml`(基础配置),否则 GF 启动报「找不到 config」;构建目录 `/data/deploy/projects/service.xpcool.com/`(main + manifest/config + resource,Dockerfile 用 COPY manifest/config 整体拷贝)。 -- DB:`xpcool-mysql` 容器(root 密码见该容器 env),库名 `service`,种子执行顺序 003→004→004b→007(服务器表结构旧,必须先 003 加列;导入用 `sudo sh -c 'docker exec -i xpcool-mysql mysql ... < file'` 宿主机重定向)。 -- 权限映射:admin_menu type=2 行的 path 必须匹配实际路由(现为 `POST /api/service/admin/...`,见 manifest/sql/007_menu_permissions_v3.sql)。 -- 文件下载站(2026-09-03 上线,当晚迁移):`file.xpcool.com` = **FileBrowser Quantum** 容器 `fbq`(`gtstef/filebrowser:stable` v1.5.5,`127.0.0.1:10881→80`,restart unless-stopped,`/data/share:/srv:ro` **只读挂载**,数据卷 `/data/filebrowser-q`:config.yaml+database.db+tmp,属主=镜像 uid1000)+ nginx 反代。**要点**:初始凭据 **admin/admin**(首登后立即改密);配置走 `config.yaml`(sources/cacheDir),管理走网页 UI;API 与原版不兼容(需用户建 long-live token,/swagger 亦需);镜像默认非 root(uid1000);原版 filebrowser(容器 `filebrowser`/10880/db `/data/filebrowser/database`)已于 2026-09-01 归档、现已停用**保留回滚**;Quantum v2.0 beta 权限模型改 per-source(view/download/modify/create/delete)。改密/建只读账号/临时分享(过期+密码+匿名)均在网页 UI。 -- SSH 凭据/密码不入库,需要时询问用户或查对话记忆;远程批量命令用 paramiko 脚本(`%TEMP%\wb_deploy\ssh_exec.py`,支持第 2 参数超时秒数),docker build 用服务器端 nohup+日志轮询防超时。**(2026-09-11 密钥轮换后更新)**:当前登录 = 本地 `~/.ssh/xpcool_ed25519`(ed25519,注释 xpcool-server-20260904)→ `xxcool@193.112.118.168:22025`(root 被拒;密码登录已禁);旧 `CentOS.pem`(RSA,对应服务器公钥 `skey-q6pq6jb3`)**已作废并从 authorized_keys 移除**(因私钥误提交 Gitea 事件而轮换),服务器 authorized_keys 另存 2 把钥(`mcp-server-manager@local`、`xxcoolwork@gmail.com`)为其他用途、保留不动;安全组按出口 IP 限放 22025(本机出口 1.204.105.116 曾因未放行连不上,80/443 可达);本机旧记录 `~/.ssh/id_ed25519` 与 wb_deploy 脚本**在当前设备不存在**。 -- **服务器安全加固(2026-08-27 完成)**:firewalld 已启用(放行 22025/80/443/3000/2222;cockpit/rpcbind 已停);fail2ban 保护 sshd(port=22025, maxretry5/bantime1h);监控脚本 `/usr/local/bin/server-monitor.sh` + cron 每 10 分钟(调度在 **xxcool 的 crontab**:`*/10 * * * * sudo /usr/local/bin/server-monitor.sh`;日志 `/var/log/server-monitor.log`,Bark 推送配 `/etc/server-monitor.conf` 的 `BARK_URL`);**告警去重终版(2026-09-11)**:状态目录 `/var/lib/server-monitor/` —— `banned-ips.state`(已提醒过的封禁 IP,**只增不减 → 同一 IP 永不重复提醒**)、`ssh-src-ips.state`(已提醒过的登录来源 IP,**仅新 IP 首次登录**才提醒)、`ports.state`(当前已告警的未登记端口,消失后清除)、`ssh-brute.state`(爆破告警状态机:`attacking`=本轮已提醒 / `idle`=已复位;攻击持续不刷屏,攻击停止静默复位,**再犯视为新一波才重新提醒**);完整审计仍在 fail2ban.log + journald 且已上报 admin 后台(脚本备份 `server-monitor.sh.bak.20260911`);另有安全日志上报器 `/usr/local/bin/server-security-collector.sh`(cron **每 5 分钟**,xxcool crontab)→ POST 到 `127.0.0.1:10100/api/service/open/security/log/report`,配置 `/etc/server-security.conf`(INTERNAL_TOKEN)、状态 `/var/lib/server-security/last.ts`;dnf-automatic 每天 06:00 自动安全补丁;SSH 密钥登录已部署(现用 `~/.ssh/xpcool_ed25519`)、密码登录已禁。**2026-09-11 加固**:已关闭 `X11Forwarding`(**生效位置是 drop-in `/etc/ssh/sshd_config.d/50-redhat.conf:17`,主配置里那行是注释、改了无效**;改前备份 `.bak.20260911`,`sshd -t` 预检 + `sshd -T` 核对 + `systemctl reload sshd` 不断连);nginx 遗留垃圾已清理(`.code.xpcool.com.conf.swo`、`tool.xpcool.com.conf.download.0` → 移入 `/data/nginx/_junk_backup_20260911/`,未直接删、可回滚)。**nginx 配置目录约定**:`include /data/nginx/conf.d/*.conf;`,故 conf.d 下非 `.conf` 后缀文件不会被加载。**遗留待办**:`lighthouse` 账号(腾讯云自带 uid1000,NOPASSWD ALL)是否锁定待用户决策。**坑**:① 腾讯云安全组未放行 3000/2222/8081,Gitea 走 nginx 反代 80/443;② 改 sshd 前必须先 `sshd -t` 预检+备份(曾因脏行导致 SSH 断开);③ EPEL 源需腾讯云镜像(`mirrors.cloud.tencent.com/epel`)且 baseurl 必须手动写全;④ CentOS9 的 fail2ban 在 EPEL。**⑤(2026-09-04 新坑)腾讯云改带宽若切计费模式会触发实例冷重启,重启后 firewalld 自定义端口规则曾丢失致 SSH 22025 失联(80/443 正常)——firewall-cmd 放行务必带 `--permanent`,且实例冷重启后要复查 22025 放行**。另:带宽 3M→5M 已升级(2026-09-04,实测 619KB/s 满速)。**⑥(2026-09-11 溯源新坑)本机出口 IP `1.204.105.116` 曾于 09-03 23:00 被自己的 fail2ban 封禁、09-04 23:00 才解封——这正是当时「SSH 22025 超时」的真因(当时误判为安全组未放行,绕了远路)。SSH 突然不通时,排查顺序应为:先 `fail2ban-client status sshd` + `grep Ban /var/log/fail2ban.log`,再怀疑安全组**。**⑦(2026-09-11 体检结论)服务器未发现被入侵迹象**:UID0 仅 root、无空密码账号、无异常 SUID/后门/劫持、无特权容器、系统目录近 30 天无改动;外网暴露面实测仅 **22025/80/443**(3000/2222/8081 等均被安全组挡住,firewalld 虽放行 3000/2222 但安全组是第二道闸)。 -- 本地 pnpm 修复(2026-08-27 升级为 wrapper 方案):根因=本环境 MSYS 路径转换故障,把 `/c/...` 参数转成 `E:\c\...`(多 `E:\c\` 前缀)致 corepack shim 与 node 都找不到 pnpm.js。**已建持久 wrapper**:`~/.workbuddy/binaries/node/bin/pnpm`(内容:`export NODE_OPTIONS=--experimental-sqlite; exec "<托管node.exe>" "C:/Users/ybtdevxxl/.workbuddy/binaries/node/workspace/node_modules/pnpm/bin/pnpm.cjs" "$@"`,关键=给 node 传 Windows 风格路径 `C:/...` 规避 MSYS 转换)。**提交/构建前需 `PATH="~/.workbuddy/binaries/node/bin:$PATH"` 前置**,pre-commit 钩子(lefthook oxlint/oxfmt/eslint/stylelint/checkType)即可正常运行;否则钩子崩 → 只能 --no-verify。已验证 pnpm 11.16.0 + `pnpm exec oxlint` 1.79.0。 +## 协作约定 +- **只 commit、不要 push**(推送由用户自行完成,避免凭据交互失败)。 +- **验证分工**:AI 能做的(类型检查、构建、冒烟测试、容器健康检查)照做;浏览器交互体验、视觉效果等交给用户手动验证,不反复折腾自动化。 +- 换设备开工前先 `git pull` 各仓库。 +- 服务器操作走 SSH:`193.112.118.168:22025`,用户 `xxcool`,密钥 `~/.ssh/xpcool_ed25519`。**尽量把多个操作合并成一次 SSH 调用**,减少权限弹窗打扰用户。 -## 日常同步习惯(建议) -- 每次工作结束:在各改动项目 `git add + commit`,本工作空间同样提交(规则/日志变化时)。**只 commit、不要 push**(2026-09-01 用户明确:推送由用户自行完成,AI 无需代为 push,避免凭据交互失败)。 -- **验证分工(2026-09-01 用户明确)**:AI 能做的验证(类型检查、构建、单元/二进制测试、自动化冒烟)照做;**AI 不方便验证的部分(真实浏览器交互体验、视觉效果、下载弹窗等)直接交给用户手动验证**,不要反复折腾自动化工具。 -- 换设备开工前:先 `git pull` 各仓库,保证规则与记录最新。 - -## 本机 Windows/PowerShell 工具链坑位(2026-09-10 实测,写脚本必看) -- **PowerShell 工具不回显 stdout**:exit code 正常但看不到输出;必须把结果 `Out-File` 落盘,再用 Read 读取。Bash 里调 `powershell.exe` 会被安全策略拒绝。 -- **`[byte] -shl 8` 会溢出归零**(结果仍是 byte 宽度)。字节拼 16 位整数必须先 `[int]` 转换,否则高位丢失(剪贴板/协议解析类代码的高频坑)。 -- **给 .ps1 加 BOM 绝不能用 `Get-Content -Raw` + `Out-File`**:本机默认按 GBK 解码 UTF-8 文件,中文会被永久破坏且不可逆。正确做法是字节级前置 `EF BB BF`(`[System.IO.File]::WriteAllBytes`)。 -- **PATH 被裁剪**:脚本里直接写 `ping` / `netsh` 会报"无法识别",须用 `$env:SystemRoot\System32\xxx.exe` 绝对路径;且不要写 `& $exe args 2>$null | Out-String`(会报"无法在管道中间运行文档"),改用 `$out = & $exe args` 再 `@($out) -join ' '`。 -- **PowerShell 变量名不区分大小写**:`$R` 与循环变量 `$r` 是同一个变量,会导致集合被静默覆盖。 -- **后台监听的进程可能杀不掉**(Stop-Process / taskkill 均无效),换端口重新起更省事。 -- **防火墙规则按可执行文件路径匹配**:一批 node.exe 规则指向已不存在的路径(失效);目前 `C:\Users\xxl\AppData\Roaming\nvm\v23.0.0\node.exe` 有有效的 Public 档「TCP+UDP 任意端口」入站放行规则——需要做**免提权的入站可达性测试**时就用它监听。 -- 测入站**不要用 ping**:Windows 默认拦 ICMPv6/ICMP 入站,必须用 TCP 端口测试。 -- 国内测速:Cloudflare 端点不可用;用 **Ookla 官方 CLI**(`install.speedtest.net` 可直连下载)。 -- 本机网络备注:家里的宽带是**运营商级 NAT(CGNAT,第 2 跳 100.64.0.1)**,无公网 IPv4;但有**公网 IPv6**(`240e:338:263:3600::/64`)。以太网被判定为 Public 网络档。 +## 生产环境与本机工具链 +→ 见同目录 **`DEPLOY.md`**:服务器/容器/站点与域名清单、nginx 与证书约定、安全加固与告警去重、本机 Windows/PowerShell 工具链坑位。 diff --git a/.workbuddy/tmp/status.xpcool.com.conf b/.workbuddy/tmp/status.xpcool.com.conf new file mode 100644 index 0000000..da3f587 --- /dev/null +++ b/.workbuddy/tmp/status.xpcool.com.conf @@ -0,0 +1,24 @@ +# status.xpcool.com 服务监控面板(Uptime Kuma 2.5.3) +# 反代后端容器 uptime-kuma:127.0.0.1:3001 +# 2026-09-13 首次部署:先以 HTTP 提供(便于完成初始化与验证) +# 待腾讯云免费证书到位后,改为「80 -> 301 -> 443」标准形态 + +server { + listen 80; + server_name status.xpcool.com; + + # 面板依赖 WebSocket 实时推送状态,必须透传 Upgrade 头,否则页面无限重连 + location / { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # 心跳/长轮询连接保持时间,避免面板偶发掉线 + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } +} diff --git a/hotspot-ipv6-test.ps1 b/hotspot-ipv6-test.ps1 new file mode 100644 index 0000000..e7f72dc --- /dev/null +++ b/hotspot-ipv6-test.ps1 @@ -0,0 +1,104 @@ +# ============================================================ +# Hotspot IPv6 readiness test (pure ASCII -- safe to copy/paste) +# +# Purpose: +# 1) After connecting this laptop to a PHONE HOTSPOT, +# check whether the laptop receives a global IPv6 address. +# 2) Check whether the HOME host can be reached directly over IPv6. +# +# Output: Desktop\hotspot-ipv6-result.txt +# ============================================================ + +# --- change these two lines if the home IPv6 address changes --- +$HOME_V6 = '240e:338:263:3600:29f1:e63f:d476:597e' +$HOME_PORT = 38443 +# --------------------------------------------------------------- + +$desk = [Environment]::GetFolderPath('Desktop') +if (-not $desk) { $desk = $env:TEMP } +$o = Join-Path $desk 'hotspot-ipv6-result.txt' +$L = New-Object System.Collections.Generic.List[string] +function A($s) { $L.Add([string]$s) } +function Flush() { $L -join "`r`n" | Set-Content -Path $o -Encoding UTF8 } + +$okOut = $false +$okHome = $false + +A ('TIME ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) +A ('HOST ' + [System.Net.Dns]::GetHostName()) +A ('RESULT FILE: ' + $o) +A '' + +A '[1] Global IPv6 addresses on this machine' +$ga = @(Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue | + Where-Object { $_.IPAddress -notlike 'fe80*' -and $_.IPAddress -ne '::1' }) +if ($ga.Count -eq 0) { + A ' NONE <-- this network gives NO global IPv6' +} else { + foreach ($a in $ga) { + A (" {0} :: {1}/{2} :: Pref={3} Suffix={4} State={5}" -f ` + $a.InterfaceAlias, $a.IPAddress, $a.PrefixLength, ` + $a.PrefixOrigin, $a.SuffixOrigin, $a.AddressState) + } +} +Flush + +A '' +A '[2] IPv6 default route' +$r = @(Get-NetRoute -AddressFamily IPv6 -DestinationPrefix '::/0' -ErrorAction SilentlyContinue) +if ($r.Count -eq 0) { A ' NONE' } else { + foreach ($x in $r) { A (" {0} -> {1}" -f $x.InterfaceAlias, $x.NextHop) } +} +Flush + +function Tcp6([string]$h, [int]$port, [int]$ms) { + try { + $a = @([System.Net.Dns]::GetHostAddresses($h) | + Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 }) + if ($a.Count -eq 0) { return 'NO-AAAA' } + $ad = $a[0] + $c = New-Object System.Net.Sockets.TcpClient($ad.AddressFamily) + $iar = $c.BeginConnect($ad, $port, $null, $null) + if (-not $iar.AsyncWaitHandle.WaitOne($ms, $false)) { $c.Close(); return 'TIMEOUT' } + try { $c.EndConnect($iar); $c.Close(); return ('OK ' + $ad.IPAddressToString) } + catch { $c.Close(); return ('FAIL ' + $_.Exception.Message) } + } catch { return ('ERR ' + $_.Exception.Message) } +} + +A '' +A '[3] Outbound IPv6 test (decisive for the whole route)' +foreach ($h in @('ipv6.icanhazip.com', 'www.taobao.com')) { + $res = Tcp6 $h 443 4000 + A (" {0}:443 -> {1}" -f $h, $res) + if ($res -like 'OK*') { $okOut = $true } +} +Flush + +A '' +A '[4] Direct reach to HOME host over IPv6 (the real goal)' +$u = 'http://[' + $HOME_V6 + ']:' + $HOME_PORT + '/' +A (' target = ' + $u) +try { + $r2 = Invoke-WebRequest -Uri $u -UseBasicParsing -TimeoutSec 6 + A (' HTTP status = ' + $r2.StatusCode) + A (' body line 1 = ' + (($r2.Content -split '\r?\n')[0])) + if ($r2.StatusCode -eq 200) { $okHome = $true } +} catch { + A (' FAILED: ' + $_.Exception.Message) +} +Flush + +A '' +A '===== SUMMARY =====' +if ($okOut -and $okHome) { + A ' PASS: hotspot gives IPv6 AND home host is reachable.' + A ' -> The IPv6 direct route is viable from this laptop.' +} elseif ($okOut -and -not $okHome) { + A ' PARTIAL: IPv6 outbound OK, but home host unreachable.' + A ' -> Most likely the home gateway blocks inbound IPv6,' + A ' or the home IPv6 address/prefix has changed.' +} else { + A ' FAIL: no usable IPv6 outbound on this network.' + A ' -> The hotspot handed out no IPv6 (IPv6 sharing may be off).' +} +Flush diff --git a/ipv6-home-qrcode.png b/ipv6-home-qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5fec2da1ecb1bcb5a9fba7f818ac8767f3aeb9 GIT binary patch literal 556 zcmV+{0@MA8P)Qjl(buh8eg>=@C4N zM^i@gC?3J33nM&>4^ch@xKsxk_*p69r|J)%Ci>P2o9#ps^J|nJay-4Wd*1;69b8qR zc_&^6=Iz1DGr(QFBf!kyAX3WJpjwBwKIi%m0C)?K`Jl21!~HG5+kiA3r@)$VtLzrU z0Mmn!aaqB?Q!~I+Ae(8?d|8_Hqk9471$#TgwRrs`PrjcFU?#Abl%lE?%_pnY7P)#0 zFfVvqB(>I6WVawD%eVk@g0;ipjXtJX(ON(;fJwoL1th&H7P+wwklPJlUQoid6SyBB zx$Wl&U}De&u3nA-a#C*b`uPT!7W_Ta&W*b1gLRhVH^8)DXVUkK@y2Ho2ACQwE~$N3q=!$9{pWomfO)}Hg`U0H zdHC*zV>93x7hp~>R9$O%v{^DTuC3|--T{ogst+R40%>mx@D`xjqH)_7O@OJvmT|jq uRWRNF%nZ81drPy6ZQ?@f7Xi!*errFpo4vIhz^S4D00006br3A?5(55n;R+BePVni0l(msn@^JpRe4SrxCeHx@=}x?4rIP8Au7H5NmPpnr#wxka z0Ribma&WxwYh3=aRR96`Kf)HWlAc+EZbz>yB+SH59;`q>8jw1Mw94k~=%)Amha3=) z=A=~VMx~r3^x-}IaE^uA1_9|p@;&_;OLpWXjab}t6ETX&T5@5U;t zG(DLU1f&U>IP6zh%G$OHvU$}#E$g%(Abm*6np3@&&8sAE?2byzX*&S|(uYVTr%R=h zr59S>B~4tO_qDbU5Rg73-_uJavv)_+W&J+p4gi6GbRc!pNxGw!z(Iya(h9vxvQTnu|;W-`%(U3EE2MEXu$R0WXQr4Ug zf_h^L1f(y??~)|^zE8Ft1p( mapped ports differ for ANY nat type. +# * port-whitelist test used targets where the port is +# simply not listening (114.114.114.114:8080, +# github.com:443 is DNS-poisoned in CN). +# This version uses the CORRECT methods. +# Output is 100% ASCII and carries NO BOM -> immune to copy/paste. +# ===================================================================== + +$ErrorActionPreference = 'Continue' +$Out = New-Object System.Collections.ArrayList + +function W { + param([string]$T) + [void]$Out.Add($T) + Write-Host $T +} +function Sec { + param([string]$T) + W '' + W ('===== ' + $T + ' =====') +} + +$stamp = Get-Date -Format 'yyyyMMdd_HHmmss' +$resultFile = Join-Path (Get-Location) ('net-test-v2_' + $stamp + '.txt') + +# --------------------------------------------------------------------- +# Chinese vendor names, built from Unicode code points so that this file +# stays PURE ASCII (copy/paste-safe) while still matching Chinese names. +$cnSun = [char]0x5411 + [char]0x65E5 + [char]0x8475 # "sunflower" remote control +$cnHr = [char]0x706B + [char]0x7ED2 # Huorong +$cnSang = [char]0x6DF1 + [char]0x4FE1 + [char]0x670D # Sangfor +$cnQax = [char]0x5947 + [char]0x5B89 + [char]0x4FE1 # QiAnXin +$cnTq = [char]0x5929 + [char]0x64CE # TianQing +$cnAsi = [char]0x4E9A + [char]0x4FE1 # AsiaInfo +$cnLr = [char]0x8054 + [char]0x8F6F # Leagsoft +$cnUu = 'UU' + [char]0x8FDC + [char]0x7A0B # NetEase UU Remote + +W ('net-test-v2 company-side network probe ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) + +# --------------------------------------------------------------------- +Sec '0. self check' +$selfBytes = [System.IO.File]::ReadAllBytes($PSCommandPath) +$hasBom = ($selfBytes.Length -ge 3 -and $selfBytes[0] -eq 0xEF -and $selfBytes[1] -eq 0xBB -and $selfBytes[2] -eq 0xBF) +W ('script file : ' + $PSCommandPath) +W ('script has BOM : ' + $hasBom + ' (pure ASCII script -> BOM irrelevant)') + +# --------------------------------------------------------------------- +Sec '1. privileges (need admin to install a VPN driver)' +$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +W ('running as Administrator : ' + $isAdmin) +try { + $grp = (net localgroup Administrators) 2>$null + W ('local Administrators group members:') + foreach ($g in $grp) { if ($g.Trim() -ne '') { W (' ' + $g.Trim()) } } +} catch { W ' (could not query localgroup)' } +W 'note: if you can install software normally, you usually have admin' + +# --------------------------------------------------------------------- +Sec '2. host info' +try { + $os = Get-CimInstance Win32_OperatingSystem + W ('OS : ' + $os.Caption + ' build ' + $os.BuildNumber) + W ('RAM : ' + [math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1) + ' GB') +} catch { W 'OS info failed' } +try { + Get-CimInstance Win32_VideoController | ForEach-Object { + W ('GPU : ' + $_.Name + ' | drv ' + $_.DriverVersion) + } +} catch { W 'GPU info failed' } +try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue + $scr = [System.Windows.Forms.Screen]::AllScreens + foreach ($s in $scr) { + W ('SCREEN : ' + $s.Bounds.Width + 'x' + $s.Bounds.Height + ' primary=' + $s.Primary) + } +} catch { W 'screen info failed' } + +# --------------------------------------------------------------------- +Sec '3. IPv6 capability (re-check)' +$v6 = Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue | + Where-Object { $_.IPAddress -notlike 'fe80*' -and $_.IPAddress -ne '::1' } +if ($v6) { + foreach ($a in $v6) { W ('IPv6 local : ' + $a.IPAddress + ' (' + $a.AddressState + ')') } +} else { + W 'IPv6 local : NONE -> company network is IPv4 only (IPv6 direct path is dead)' +} + +# --------------------------------------------------------------------- +Sec '4. TCP egress freedom (targets that REALLY listen)' +function Test-Tcp { + param([string]$H, [int]$P, [int]$TimeoutMs = 4000) + $res = 'UNKNOWN' + try { + $ips = [System.Net.Dns]::GetHostAddresses($H) | Where-Object { $_.AddressFamily.ToString() -eq 'InterNetwork' } + if (-not $ips) { return 'DNS_FAIL' } + $sock = New-Object System.Net.Sockets.Socket('InterNetwork', 'Stream', 'Tcp') + $ar = $sock.BeginConnect((New-Object System.Net.IPEndPoint($ips[0], $P)), $null, $null) + if ($ar.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) { + try { $sock.EndConnect($ar); $res = 'OPEN' } + catch { $res = 'REFUSED (path OK, port closed)' } + } else { + $res = 'TIMEOUT (blocked / dropped)' + } + $sock.Close() + } catch { $res = ('ERR ' + $_.Exception.Message) } + return $res +} + +$tcpTargets = @( + @('www.baidu.com', 443, 'domestic HTTPS - baseline'), + @('www.qq.com', 443, 'domestic HTTPS - baseline'), + @('www.aliyun.com', 80, 'domestic HTTP - baseline'), + @('193.112.118.168', 80, 'your own server :80'), + @('193.112.118.168', 443, 'your own server :443'), + @('193.112.118.168', 22025, 'your own server :22025 (non-standard)'), + @('portquiz.net', 443, 'portquiz :443 (all-port listener)'), + @('portquiz.net', 8080, 'portquiz :8080 (all-port listener)'), + @('portquiz.net', 8443, 'portquiz :8443 (all-port listener)') +) +foreach ($t in $tcpTargets) { + $r = Test-Tcp -H $t[0] -P $t[1] + W ('TCP {0,-22} {1,-6} -> {2} [{3}]' -f ($t[0] + ':' + $t[1]), $t[1], $r, $t[2]) +} +W 'read: if baidu:443 is OPEN then 443 is NOT whitelisted-blocked.' +W 'read: portquiz.net listens on EVERY port -> OPEN there proves arbitrary ports allowed.' + +# --------------------------------------------------------------------- +Sec '5. UDP egress + CORRECT NAT mapping test' +function New-StunReq { + $b = New-Object byte[] 20 + $b[0] = 0; $b[1] = 1; $b[2] = 0; $b[3] = 0 + $b[4] = 0x21; $b[5] = 0x12; $b[6] = 0xA4; $b[7] = 0x42 + $r = New-Object byte[] 12 + (New-Object System.Random).NextBytes($r) + [Array]::Copy($r, 0, $b, 8, 12) + return , $b +} +function Invoke-Stun { + param($Udp, [string]$H, [int]$P, [int]$TimeoutMs = 3000) + try { + $ips = [System.Net.Dns]::GetHostAddresses($H) | Where-Object { $_.AddressFamily.ToString() -eq 'InterNetwork' } + if (-not $ips) { return @{ S = 'DNS_FAIL' } } + $req = New-StunReq + [void]$Udp.Send($req, $req.Length, $ips[0].ToString(), $P) + $Udp.Client.ReceiveTimeout = $TimeoutMs + $remote = New-Object System.Net.IPEndPoint([System.Net.IPAddress]'0.0.0.0', 0) + try { $resp = $Udp.Receive([ref]$remote) } catch { return @{ S = 'TIMEOUT' } } + if ($resp.Length -lt 20) { return @{ S = 'SHORT' } } + for ($i = 0; $i -lt 12; $i++) { + if ($resp[8 + $i] -ne $req[8 + $i]) { return @{ S = 'TXID_MISMATCH' } } + } + $mtype = ([int]$resp[0] * 256) + [int]$resp[1] + if ($mtype -ne 0x0101) { return @{ S = ('TYPE_' + $mtype) } } + $mlen = ([int]$resp[2] * 256) + [int]$resp[3] + $pos = 20 + $end = 20 + $mlen + while ($pos + 4 -le $end) { + $atype = ([int]$resp[$pos] * 256) + [int]$resp[$pos + 1] + $alen = ([int]$resp[$pos + 2] * 256) + [int]$resp[$pos + 3] + if ($atype -eq 0x0020 -and $alen -ge 8 -and $resp[$pos + 5] -eq 1) { + $p = (([int]$resp[$pos + 6] * 256) + [int]$resp[$pos + 7]) -bxor 0x2112 + $b0 = [int]$resp[$pos + 8] -bxor 0x21 + $b1 = [int]$resp[$pos + 9] -bxor 0x12 + $b2 = [int]$resp[$pos + 10] -bxor 0xA4 + $b3 = [int]$resp[$pos + 11] -bxor 0x42 + return @{ S = 'OK'; IP = ('{0}.{1}.{2}.{3}' -f $b0, $b1, $b2, $b3); Port = $p } + } + if ($atype -eq 0x0001 -and $alen -ge 8 -and $resp[$pos + 5] -eq 1) { + $p = ([int]$resp[$pos + 6] * 256) + [int]$resp[$pos + 7] + return @{ S = 'OK'; IP = ('{0}.{1}.{2}.{3}' -f $resp[$pos + 8], $resp[$pos + 9], $resp[$pos + 10], $resp[$pos + 11]); Port = $p } + } + $pos += 4 + $alen + ((4 - ($alen % 4)) % 4) + } + return @{ S = 'NO_ATTR' } + } catch { return @{ S = ('ERR ' + $_.Exception.Message) } } +} + +$stunList = @( + @('stun.miwifi.com', 3478), + @('stun.hitv.com', 3478), + @('stun.qq.com', 3478), + @('stun.l.google.com', 19302), + @('stun.nextcloud.com', 443) +) + +$rounds = @() +foreach ($lp in @(46001, 46002)) { + W ('--- round : fixed local UDP port ' + $lp + ' ---') + try { + $udp = New-Object System.Net.Sockets.UdpClient($lp) + } catch { + W (' bind local port failed: ' + $_.Exception.Message) + continue + } + $maps = @() + foreach ($sv in $stunList) { + $r = Invoke-Stun -Udp $udp -H $sv[0] -P $sv[1] + if ($r.S -eq 'OK') { + W (' {0,-24} -> mapped {1}:{2}' -f ($sv[0] + ':' + $sv[1]), $r.IP, $r.Port) + $maps += $r + } else { + W (' {0,-24} -> {1}' -f ($sv[0] + ':' + $sv[1]), $r.S) + } + } + $udp.Close() + $rounds += , @{ LP = $lp; Maps = $maps } + W ' => STUN replies received means UDP egress AND UDP return path work.' +} + +W '' +W '--- NAT verdict ---' +$verdict = 'UNKNOWN (not enough STUN samples)' +foreach ($rd in $rounds) { + $ports = ($rd.Maps | ForEach-Object { $_.Port } | Sort-Object -Unique) + if ($ports.Count -eq 0) { continue } + W ('local port ' + $rd.LP + ' -> public ip ' + (($rd.Maps | ForEach-Object { $_.IP } | Sort-Object -Unique) -join ',') + ' mapped ports: ' + ($ports -join ',')) + if ($ports.Count -eq 1 -and $rd.Maps.Count -ge 2) { + $verdict = 'ENDPOINT-INDEPENDENT MAPPING (cone NAT) -> UDP hole punching is FEASIBLE' + } elseif ($ports.Count -gt 1) { + $verdict = 'SYMMETRIC NAT (mapping changes per destination) -> hole punching unreliable, relay needed' + } +} +W ('NAT verdict : ' + $verdict) + +# --------------------------------------------------------------------- +Sec '6. local proxy / tunnel clients already present' +W 'listening local TCP ports (common proxy ports):' +$common = @(1080, 1081, 1087, 2080, 2081, 7890, 7891, 7892, 7893, 7897, 8080, 8889, 10808, 10809, 10810, 20171) +$listening = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue +foreach ($p in $common) { + $hit = $listening | Where-Object { $_.LocalPort -eq $p } + if ($hit) { + $procName = '' + try { $procName = (Get-Process -Id $hit[0].OwningProcess -ErrorAction SilentlyContinue).ProcessName } catch { } + W (' LISTEN ' + $p + ' pid=' + $hit[0].OwningProcess + ' proc=' + $procName) + } +} +W '(if 7890/7897/7892 etc are LISTENING, a Clash-style proxy client is running)' + +W '' +W 'installed software matching VPN / proxy / remote-control / streaming keywords:' +$keys = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', + 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' +) +$pat = 'tailscale|zerotier|wireguard|netbird|clash|v2ray|xray|shadowsocks|trojan|sing-box|mihomo|todesk|sunlogin|anydesk|teamviewer|uu|gameviewer|parsec|moonlight|sunshine|radmin|splashtop|rustdesk|netease' +$pat = $pat + '|' + $cnSun + '|' + $cnHr + '|' + $cnSang + '|' + $cnQax + '|' + $cnTq + '|' + $cnAsi + '|' + $cnLr + '|' + $cnUu +$found = @() +foreach ($k in $keys) { + try { + Get-ItemProperty $k -ErrorAction SilentlyContinue | ForEach-Object { + $n = $_.DisplayName + if ($n -and ($n -match $pat)) { $found += $n } + } + } catch { } +} +if ($found.Count -gt 0) { + $found | Sort-Object -Unique | ForEach-Object { W (' ' + $_) } +} else { W ' (none matched)' } + +# --------------------------------------------------------------------- +Sec '7. security / endpoint-management agents (may block drivers or audit traffic)' +$secPat = 'hipsdaemon|hrwsc|edr|savsvc|sangfor|huorong|qianxin|tianqing|asiainfo|symantec|mcafee|sophos|kaspersky|trend|eset|bitdefender|crowdstrike|sentinel|defender|acmp|leagsoft|atrust|anysec|zhudongfangyu|sysdiag|360' +$secPat = $secPat + '|' + $cnHr + '|' + $cnSang + '|' + $cnQax + '|' + $cnTq + '|' + $cnAsi + '|' + $cnLr +$svcs = Get-Service -ErrorAction SilentlyContinue +foreach ($s in $svcs) { + $text = ($s.Name + ' ' + $s.DisplayName) + if ($text -match $secPat) { + W (' ' + $s.Name + ' | ' + $s.DisplayName + ' | ' + $s.Status) + } +} + +# --------------------------------------------------------------------- +Sec '8. summary' +W '' +W ('IPv6 available : ' + ($(if ($v6) { 'YES' } else { 'NO' }))) +W ('Admin rights : ' + $isAdmin) +W ('NAT verdict : ' + $verdict) +W '' +W 'How to read the TCP section:' +W ' * www.baidu.com:443 OPEN -> 443 egress is fine (v1 result was false)' +W ' * portquiz.net:* OPEN -> arbitrary TCP ports allowed' +W ' * everything TIMEOUT -> real port whitelist, relay over 443 only' +W '' + +$Out | Out-File -FilePath $resultFile -Encoding UTF8 +W ('result saved to: ' + $resultFile) diff --git a/net-test-v2_20260910_163314.txt b/net-test-v2_20260910_163314.txt new file mode 100644 index 0000000..7707f84 --- /dev/null +++ b/net-test-v2_20260910_163314.txt @@ -0,0 +1,97 @@ +net-test-v2 company-side network probe 2026-09-10 16:33:15 + +===== 0. self check ===== +script file : E:\xxcool\project\workbuddy.xpcool.com\net-test-v2.ps1 +script has BOM : False (pure ASCII script -> BOM irrelevant) + +===== 1. privileges (need admin to install a VPN driver) ===== +running as Administrator : False +local Administrators group members: + 别名 Administrators + 注释 管理员对计算机/域有不受限制的完全访问权 + 成员 + ------------------------------------------------------------------------------- + Administrator + ybtdevxxl + 命令成功完成。 +note: if you can install software normally, you usually have admin + +===== 2. host info ===== +OS : Microsoft Windows 11 专业版 build 26200 +RAM : 31.2 GB +GPU : Sharing Monitor | drv 17.1.40.460 +GPU : AMD Radeon 780M Graphics | drv 31.0.14003.38003 +SCREEN : 1707x1067 primary=True +SCREEN : 1920x1080 primary=False + +===== 3. IPv6 capability (re-check) ===== +IPv6 local : NONE -> company network is IPv4 only (IPv6 direct path is dead) + +===== 4. TCP egress freedom (targets that REALLY listen) ===== +TCP www.baidu.com:443 443 -> OPEN [domestic HTTPS - baseline] +TCP www.qq.com:443 443 -> OPEN [domestic HTTPS - baseline] +TCP www.aliyun.com:80 80 -> OPEN [domestic HTTP - baseline] +TCP 193.112.118.168:80 80 -> OPEN [your own server :80] +TCP 193.112.118.168:443 443 -> OPEN [your own server :443] +TCP 193.112.118.168:22025 22025 -> OPEN [your own server :22025 (non-standard)] +TCP portquiz.net:443 443 -> OPEN [portquiz :443 (all-port listener)] +TCP portquiz.net:8080 8080 -> OPEN [portquiz :8080 (all-port listener)] +TCP portquiz.net:8443 8443 -> OPEN [portquiz :8443 (all-port listener)] +read: if baidu:443 is OPEN then 443 is NOT whitelisted-blocked. +read: portquiz.net listens on EVERY port -> OPEN there proves arbitrary ports allowed. + +===== 5. UDP egress + CORRECT NAT mapping test ===== +--- round : fixed local UDP port 46001 --- + stun.miwifi.com:3478 -> mapped 117.188.24.89:16993 + stun.hitv.com:3478 -> mapped 117.188.24.89:16994 + stun.qq.com:3478 -> TIMEOUT + stun.l.google.com:19302 -> mapped 117.188.24.89:17011 + stun.nextcloud.com:443 -> mapped 117.188.118.254:43681 + => STUN replies received means UDP egress AND UDP return path work. +--- round : fixed local UDP port 46002 --- + stun.miwifi.com:3478 -> mapped 117.188.24.89:17013 + stun.hitv.com:3478 -> mapped 117.188.24.89:17014 + stun.qq.com:3478 -> TIMEOUT + stun.l.google.com:19302 -> mapped 117.188.24.89:17041 + stun.nextcloud.com:443 -> mapped 117.188.118.254:43705 + => STUN replies received means UDP egress AND UDP return path work. + +--- NAT verdict --- +local port 46001 -> public ip 117.188.118.254,117.188.24.89 mapped ports: 16993,16994,17011,43681 +local port 46002 -> public ip 117.188.118.254,117.188.24.89 mapped ports: 17013,17014,17041,43705 +NAT verdict : SYMMETRIC NAT (mapping changes per destination) -> hole punching unreliable, relay needed + +===== 6. local proxy / tunnel clients already present ===== +listening local TCP ports (common proxy ports): +(if 7890/7897/7892 etc are LISTENING, a Clash-style proxy client is running) + +installed software matching VPN / proxy / remote-control / streaming keywords: + Clash Verge + ToDesk + WireGuard + 火绒安全软件 + +===== 7. security / endpoint-management agents (may block drivers or audit traffic) ===== + AMD Crash Defender Service | AMD Crash Defender Service | Running + aTrustService | aTrustService | Stopped + edr_monitor | edr_monitor | Stopped + HipsDaemon | Huorong Internet Security Daemon | Running + HRWSCCtrl | Huorong Windows Security Center | Running + MDCoreSvc | Microsoft Defender Core 服务 | Stopped + mpssvc | Windows Defender Firewall | Running + savsvc | EDR Antivirus Service | Stopped + Sense | Windows Defender Advanced Threat Protection Service | Stopped + WdNisSvc | Microsoft Defender 防病毒网络检查服务 | Stopped + WinDefend | Microsoft Defender 防病毒服务 | Stopped + +===== 8. summary ===== + +IPv6 available : NO +Admin rights : False +NAT verdict : SYMMETRIC NAT (mapping changes per destination) -> hole punching unreliable, relay needed + +How to read the TCP section: + * www.baidu.com:443 OPEN -> 443 egress is fine (v1 result was false) + * portquiz.net:* OPEN -> arbitrary TCP ports allowed + * everything TIMEOUT -> real port whitelist, relay over 443 only + diff --git a/net-test.ps1 b/net-test.ps1 new file mode 100644 index 0000000..8d94b78 --- /dev/null +++ b/net-test.ps1 @@ -0,0 +1,245 @@ +# ============================================================================ +# Network readiness self-test (run on the CLIENT / office PC) +# +# Purpose: decide whether this machine can act as a Sunshine + Moonlight +# streaming client back to the home PC over public IPv6. +# +# This file is intentionally PURE ASCII on purpose, so that it survives being +# copy-pasted between machines. It does NOT require a UTF-8 BOM. +# All output is plain English. +# +# Run: +# powershell -ExecutionPolicy Bypass -File net-test.ps1 +# +# A result file is written next to the script: +# net-test-result_YYYYMMDD_HHmmss.txt +# ============================================================================ + +$ErrorActionPreference = 'SilentlyContinue' +$Sys32 = Join-Path $env:SystemRoot 'System32' +$netshExe = Join-Path $Sys32 'netsh.exe' + +$Report = New-Object System.Collections.ArrayList + +function W([string]$Text) { + [void]$Report.Add($Text) + Write-Host $Text +} +function SEC([string]$Title) { + W '' + W ('===== ' + $Title + ' =====') +} + +# ---- EDIT ME if the home address changes ---------------------------------- +$HomeIPv6 = '240e:338:263:3600:e591:cb62:3503:8a23' +$HomePort = 38443 +# --------------------------------------------------------------------------- + +W '==========================================================' +W ' Network self-test for remote game streaming (client side)' +W (' Time: ' + (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) +W '==========================================================' + +# Self encoding check: proves that a missing BOM cannot break this file. +try { + $sb = [System.IO.File]::ReadAllBytes($MyInvocation.MyCommand.Path) + $hasBom = ($sb.Length -ge 3 -and $sb[0] -eq 0xEF -and $sb[1] -eq 0xBB -and $sb[2] -eq 0xBF) + if ($hasBom) { W 'Script encoding : UTF-8 with BOM' } + else { W 'Script encoding : no BOM (file is pure ASCII - harmless)' } +} catch { } + +# ===================== 1. DEVICE & DISPLAY ===================== +SEC '1. DEVICE & DISPLAY' +$os = Get-CimInstance Win32_OperatingSystem +if ($os) { W ('OS : ' + $os.Caption + ' build ' + $os.BuildNumber + ' ' + $os.OSArchitecture) } +$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 +if ($cpu) { W ('CPU : ' + ([string]$cpu.Name).Trim() + ' (' + $cpu.NumberOfCores + 'C/' + $cpu.NumberOfLogicalProcessors + 'T)') } +$cs = Get-CimInstance Win32_ComputerSystem +if ($cs) { W ('RAM : ' + [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) + ' GB') } + +foreach ($g in (Get-CimInstance Win32_VideoController)) { + $hz = $g.CurrentRefreshRate + if (-not $hz -or $hz -le 0) { $hz = 'unknown' } else { $hz = ($hz.ToString() + 'Hz') } + W ('GPU : ' + $g.Name + ' drv ' + $g.DriverVersion + ' ' + $hz) +} + +try { + Add-Type -AssemblyName System.Windows.Forms + foreach ($s in [System.Windows.Forms.Screen]::AllScreens) { + $prim = '' + if ($s.Primary) { $prim = ' (primary)' } + W ('Display : ' + $s.DeviceName + ' ' + $s.Bounds.Width + 'x' + $s.Bounds.Height + $prim) + } +} catch { } + +W 'HINT : a 60Hz panel costs ~17ms per frame. If your panel is 120/144Hz,' +W ' go to Settings > System > Display > Advanced and set it to max.' + +# ===================== 2. IPv6 ===================== +SEC '2. IPv6' +$v6 = Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue | + Where-Object { $_.IPAddress -notlike 'fe80*' -and $_.IPAddress -ne '::1' -and $_.PrefixOrigin -ne 'WellKnown' } +if ($v6) { + foreach ($a in $v6) { W ('Local IPv6: ' + $a.IPAddress + ' via ' + $a.InterfaceAlias) } +} else { + W 'Local IPv6: NONE -> IPv6 direct connection is NOT possible from this PC' +} + +$egress = '' +foreach ($u in @('https://api6.ipify.org', 'https://ipv6.icanhazip.com')) { + try { + $r = Invoke-WebRequest -Uri $u -TimeoutSec 10 -UseBasicParsing + if ($r.Content) { $egress = ([string]$r.Content).Trim(); break } + } catch { } +} +if ($egress) { W ('IPv6 egress: OK your public IPv6 = ' + $egress) } +else { W 'IPv6 egress: FAIL (no IPv6 route to the internet, or blocked)' } + +# ===================== 3. PROXY & SECURITY SOFTWARE ===================== +SEC '3. PROXY & SECURITY SOFTWARE' +$wOut = & $netshExe winhttp show proxy +W ('WinHTTP : ' + ((@($wOut) -join ' ') -replace '\s+', ' ').Trim()) + +$ie = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue +if ($ie) { W ('IE proxy : enable=' + $ie.ProxyEnable + ' server=' + $ie.ProxyServer + ' pac=' + $ie.AutoConfigURL) } + +$watch = 'zhudongfangyu|HipsDaemon|HipsTray|usysdiag|360|qqpcmgr|QQPCTray|Symantec|McAfee|CrowdStrike|SentinelAgent|Sophos|ekrn|avp|Kaspersky|TmListen|SangFor|EDRAgent|AnyDesk|ToDesk|sunlogin|oray' +$hits = Get-Process -ErrorAction SilentlyContinue | + Where-Object { $_.ProcessName -match $watch } | + Select-Object -ExpandProperty ProcessName -Unique +if ($hits) { foreach ($h in $hits) { W ('Running : ' + $h) } } +else { W 'Running : no known security / audit / remote-control tool detected' } + +# ===================== 4. UDP & STUN (NAT type) ===================== +SEC '4. UDP & STUN (NAT type)' +try { + $udp = New-Object System.Net.Sockets.UdpClient 0 + $udp.Client.ReceiveTimeout = 3000 + $dns = [System.Net.IPAddress]::Parse('114.114.114.114') + $q = [byte[]](0x12,0x34,0x01,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x77,0x77,0x77,0x03,0x63,0x6f,0x6d,0x00,0x00,0x01,0x00,0x01) + $ep = New-Object System.Net.IPEndPoint ($dns, 53) + [void]$udp.Send($q, $q.Length, $ep) + $rm = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0) + $resp = $udp.Receive([ref]$rm) + $udp.Close() + W ('UDP/53 : OK (' + $resp.Length + ' bytes from 114.114.114.114)') +} catch { + W ('UDP/53 : FAILED - high-port UDP may be blocked (' + $_.Exception.Message + ')') +} + +function Invoke-Stun { + param([string]$Server, [int]$Port = 3478, [int]$TimeoutMs = 3000) + $o = New-Object psobject -Property @{ Server = ($Server + ':' + $Port); Ok = $false; Mapped = ''; Local = 0; Err = '' } + try { + $ip = ([System.Net.Dns]::GetHostAddresses($Server) | + Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | + Select-Object -First 1) + if (-not $ip) { throw 'dns resolution failed' } + $u = New-Object System.Net.Sockets.UdpClient 0 + $u.Client.ReceiveTimeout = $TimeoutMs + $o.Local = $u.Client.LocalEndPoint.Port + $ep = New-Object System.Net.IPEndPoint ($ip, $Port) + $pkt = New-Object byte[] 20 + $pkt[0] = 0x00; $pkt[1] = 0x01 + $pkt[4] = 0x21; $pkt[5] = 0x12; $pkt[6] = 0xA4; $pkt[7] = 0x42 + $rnd = New-Object System.Random + for ($i = 8; $i -lt 20; $i++) { $pkt[$i] = [byte]$rnd.Next(0, 256) } + [void]$u.Send($pkt, 20, $ep) + $rm2 = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0) + $resp = $u.Receive([ref]$rm2) + $u.Close() + $pos = 20 + while ($pos + 4 -le $resp.Length) { + # NOTE: cast to [int] before shifting - [byte] -shl 8 overflows to 0 + $atype = ([int]$resp[$pos] -shl 8) -bor [int]$resp[$pos + 1] + $alen = ([int]$resp[$pos + 2] -shl 8) -bor [int]$resp[$pos + 3] + if ($atype -eq 0x0020 -and $alen -ge 8) { + $b = [byte[]]$resp[($pos + 8)..($pos + 11)] + $b[0] = [byte]($b[0] -bxor 0x21); $b[1] = [byte]($b[1] -bxor 0x12) + $b[2] = [byte]($b[2] -bxor 0xA4); $b[3] = [byte]($b[3] -bxor 0x42) + $xp = ((([int]$resp[$pos + 6] -shl 8) -bor [int]$resp[$pos + 7]) -bxor 0x2112) + $o.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $xp + break + } + elseif ($atype -eq 0x0001 -and $alen -ge 8) { + $b = [byte[]]$resp[($pos + 8)..($pos + 11)] + $mp = (([int]$resp[$pos + 6] -shl 8) -bor [int]$resp[$pos + 7]) + $o.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $mp + break + } + $pos += 4 + $alen + ((4 - ($alen % 4)) % 4) + } + $o.Ok = $true + } catch { $o.Err = $_.Exception.Message } + return $o +} + +$mappedPorts = @() +foreach ($srv in @('stun.miwifi.com', 'stun.qq.com', 'stun.hitv.com')) { + $r = Invoke-Stun -Server $srv + if ($r.Ok -and $r.Mapped -ne '') { + W ('STUN ' + $r.Server + ' -> mapped ' + $r.Mapped + ' (local port ' + $r.Local + ')') + $mappedPorts += ($r.Mapped.Split(':')[-1]) + } elseif ($r.Err -ne '') { + W ('STUN ' + $r.Server + ' -> failed: ' + $r.Err) + } else { + W ('STUN ' + $r.Server + ' -> no response') + } +} +if ($mappedPorts.Count -ge 2) { + $uniq = ($mappedPorts | Sort-Object -Unique).Count + if ($uniq -eq 1) { W ('NAT type : CONE (identical mapped port) -> UDP hole punching likely OK') } + else { W ('NAT type : SYMMETRIC (mapped ports ' + ($mappedPorts -join ', ') + ') -> UDP hole punching difficult') } +} else { + W 'NAT type : UNKNOWN (not enough STUN replies)' +} + +# ===================== 5. HOME REACHABILITY (TCP) ===================== +SEC '5. HOME REACHABILITY (TCP)' +function Test-Tcp6 { + param([string]$Address, [int]$Port, [int]$TimeoutMs = 5000) + $c = $null + try { + $c = New-Object System.Net.Sockets.TcpClient ([System.Net.Sockets.AddressFamily]::InterNetworkV6) + $iar = $c.BeginConnect($Address, $Port, $null, $null) + $ok = $iar.AsyncWaitHandle.WaitOne($TimeoutMs, $false) + if ($ok -and $c.Connected) { return $true } + return $false + } catch { return $false } + finally { if ($c) { $c.Close() } } +} + +if ($HomeIPv6 -ne '') { + if (Test-Tcp6 -Address $HomeIPv6 -Port $HomePort) { + W ('TCP [' + $HomeIPv6 + ']:' + $HomePort + ' -> OK *** IPv6 INBOUND WORKS ***') + W ' => Path 1 (Sunshine + Moonlight over IPv6 direct) is VIABLE.' + } else { + W ('TCP [' + $HomeIPv6 + ']:' + $HomePort + ' -> FAILED') + W ' => Either the home listener is down, or the home gateway blocks IPv6 inbound.' + W ' => Fall back to hole punching (Tailscale) or a relay.' + } +} else { W 'Home IPv6 not configured - skipped.' } + +# ===================== 6. SUMMARY ===================== +SEC 'SUMMARY' +$v6ok = ($v6 -ne $null) +$inOk = $false +if ($HomeIPv6 -ne '') { $inOk = Test-Tcp6 -Address $HomeIPv6 -Port $HomePort } +W ('IPv6 address present : ' + $v6ok) +W ('IPv6 internet egress : ' + $(if ($egress) { 'OK' } else { 'FAIL' })) +W ('Home IPv6 reachable : ' + $inOk) +if ($v6ok -and $egress -and $inOk) { W 'VERDICT: good to go - use Moonlight over IPv6 direct.' } +elseif ($v6ok -and $egress) { W 'VERDICT: IPv6 works but home is not reachable - check home listener/gateway.' } +else { W 'VERDICT: no usable IPv6 here - the office network likely blocks it.' } + +# ===================== WRITE RESULT FILE ===================== +$outFile = Join-Path (Get-Location).Path ('net-test-result_' + (Get-Date).ToString('yyyyMMdd_HHmmss') + '.txt') +try { + $Report | Out-File -FilePath $outFile -Encoding UTF8 + Write-Host '' + Write-Host ('Result saved to: ' + $outFile) +} catch { + Write-Host '' + Write-Host ('Could not write result file: ' + $_.Exception.Message) +} diff --git a/pingpe-test-qrcode.png b/pingpe-test-qrcode.png new file mode 100644 index 0000000000000000000000000000000000000000..fd9c357331239d4c50afe0b0cd4f4d0bbdc471f4 GIT binary patch literal 657 zcmV;C0&e|@P)dL_t(|obB69a)U4ohGFLf9Eq#@ zNSq*Q0sT7dbkmK^t3`%_AML)8P14pV>i=pNulM=q`O#i-oDU9t`fi)__Wcpnq0klR z+k&pub|Kc=#_}$ZgNGXzih4Kr`!1;f&_&^rL47VrGV)b6uK^LI%G6M zB|d+K4ho%-YVG>)$hnD5IZ{w4RJ@$-zgo)k7r zYLK8%sN9E;YnkYYMN;1Gp1$R}1r)jh)vZ)Hmy1V58y^mGa-h)JsV^JX=D{JU=WaVN zDD>%c*F9F=XxX^2H&;1%Q0O95bFlenIXq3`udnP%o7A61L7@}VtgN;Oh=;~zM%k@F zq0gjgS?s&~*m6C%Ce2+wo1oAosOKQMhc(y}sSauW+(gF;uK@t+bJ z5YcPUS5Z6uK0(42f0uZ}-|bIG|8yjGL(2$6idY8Wg$;Elnamnj7o%D&c}c rm!RUM$r(VrsU5=wg+7>4)PL0%MCcj8UrVqN00000NkvXXu0mjf1$8u7 literal 0 HcmV?d00001 diff --git a/公司电脑网络自测.ps1 b/公司电脑网络自测.ps1 new file mode 100644 index 0000000..b418685 --- /dev/null +++ b/公司电脑网络自测.ps1 @@ -0,0 +1,259 @@ +# ============================================================ +# 公司电脑网络与设备自测脚本 +# 用途:验证公司电脑能否走通「远程串流家里的电脑」所需条件 +# 用法:先在开始菜单打开 Windows PowerShell,再执行: +# powershell -ExecutionPolicy Bypass -File "公司电脑网络自测.ps1" +# 结果:屏幕输出 + 同目录下「网络自测结果_时间戳.txt」 +# ============================================================ + +$ErrorActionPreference = 'Continue' +$resultFile = Join-Path $PSScriptRoot ('网络自测结果_' + (Get-Date -Format 'yyyyMMdd_HHmmss') + '.txt') +# 注意:PowerShell 变量名不区分大小写,这里刻意避开 $r / $R 这类易冲突的名字 +$Lines = New-Object System.Collections.ArrayList + +function Add-Line { + param([string]$Text) + [void]$Lines.Add($Text) + Write-Host $Text +} +function Add-Section { + param([string]$Title) + Add-Line '' + Add-Line ('===== ' + $Title + ' =====') +} + +# 用绝对路径调用系统命令:某些受管控环境的 PATH 会被裁剪,直接写 ping / netsh 会找不到 +$Sys32 = Join-Path $env:SystemRoot 'System32' +$pingExe = Join-Path $Sys32 'ping.exe' +$netshExe = Join-Path $Sys32 'netsh.exe' + +# 强制走 IPv6 的 TCP 连通性测试(ICMPv6 常被系统/网关默认拦截,所以必须用 TCP 判断) +function Test-TcpIPv6 { + param([string]$Address, [int]$Port, [int]$TimeoutMs = 5000) + $c = $null + try { + $c = New-Object System.Net.Sockets.TcpClient ([System.Net.Sockets.AddressFamily]::InterNetworkV6) + $iar = $c.BeginConnect($Address, $Port, $null, $null) + $ok = $iar.AsyncWaitHandle.WaitOne($TimeoutMs, $false) + if ($ok -and $c.Connected) { return $true } + return $false + } catch { return $false } + finally { if ($c) { $c.Close() } } +} + +# 把外部命令输出安全地拼成一行文本(命令无输出时返回空串,避免 Join 抛异常) +function Get-NativeText { + param($Output) + return (@($Output) -join ' ') +} + +Add-Line ('公司电脑网络自测 ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) + +# ---------- 1. 设备与显示(决定解码能力与串流分辨率) ---------- +Add-Section '1. 设备与显示' +try { + $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 + Add-Line ('CPU : ' + $cpu.Name.Trim()) + $cs = Get-CimInstance Win32_ComputerSystem + Add-Line ('内存 : ' + [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) + ' GB') + $os = Get-CimInstance Win32_OperatingSystem + Add-Line ('系统 : ' + $os.Caption + ' build ' + $os.BuildNumber) + foreach ($g in (Get-CimInstance Win32_VideoController)) { + Add-Line ('显卡 : ' + $g.Name + ' 驱动 ' + $g.DriverVersion + + ' 当前 ' + $g.CurrentHorizontalResolution + 'x' + $g.CurrentVerticalResolution + '@' + $g.CurrentRefreshRate + 'Hz') + } + Add-Line '提示 : 若屏幕是 120/144Hz,请到「设置-系统-屏幕-高级显示」把刷新率手动调到最高' +} +catch { Add-Line ('读取设备信息失败: ' + $_.Exception.Message) } + +# ---------- 2. IPv6 能力(最关键的一项:家里只有公网 IPv6 可直连) ---------- +Add-Section '2. IPv6 能力(关键)' +try { + $v6 = Get-NetIPAddress -AddressFamily IPv6 -ErrorAction SilentlyContinue | + Where-Object { $_.IPAddress -notlike 'fe80*' -and $_.IPAddress -ne '::1' -and $_.PrefixOrigin -ne 'WellKnown' } + if ($v6) { + foreach ($a in $v6) { Add-Line ('本机 IPv6 : ' + $a.IPAddress + ' (' + $a.InterfaceAlias + ')') } + } else { + Add-Line '本机 IPv6 : 无 -> 公司是纯 IPv4 网络,家里 IPv6 直连走不通,只能靠打洞或中转' + } + # 用 TCP 而不是 ping 来判断:ICMPv6 默认会被 Windows 防火墙拦掉,ping 不通不代表 IPv6 不可用 + $v6Targets = @( + @{ Name = '阿里公共DNS v6 (2400:3200::1:53)'; Addr = '2400:3200::1'; Port = 53 }, + @{ Name = '电信DNS v6 (240e:4c:4008::1:53)'; Addr = '240e:4c:4008::1'; Port = 53 } + ) + foreach ($t in $v6Targets) { + if (Test-TcpIPv6 -Address $t.Addr -Port $t.Port) { + Add-Line ('IPv6 出网 : ' + $t.Name + ' -> 通(公司有可用的 IPv6)') + } else { + Add-Line ('IPv6 出网 : ' + $t.Name + ' -> 不通') + } + } + Add-Line '说明 : 只要有任意一条通,说明公司网络能跑 IPv6,家里的 IPv6 直连方案就有戏' +} +catch { Add-Line ('IPv6 检测失败: ' + $_.Exception.Message) } + +# ---------- 3. 代理与审计软件初筛(决定会不会被拦) ---------- +Add-Section '3. 代理与审计软件初筛' +try { + $wOut = & $netshExe winhttp show proxy + $w = (Get-NativeText $wOut) -replace '\s+', ' ' + Add-Line ('WinHTTP 代理 : ' + $w.Trim()) + $envProxy = @() + if ($env:HTTP_PROXY) { $envProxy += ('HTTP_PROXY=' + $env:HTTP_PROXY) } + if ($env:HTTPS_PROXY) { $envProxy += ('HTTPS_PROXY=' + $env:HTTPS_PROXY) } + if ($envProxy.Count -gt 0) { Add-Line ('环境变量代理 : ' + ($envProxy -join '; ')) } else { Add-Line '环境变量代理 : 无' } + $ieProxy = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue + if ($ieProxy) { + Add-Line ('系统代理 : 开关=' + $ieProxy.ProxyEnable + ' 服务器=' + $ieProxy.ProxyServer + ' PAC=' + $ieProxy.AutoConfigURL) + } + $keywords = 'Sangfor|深信服|QiAnXin|奇安信|360|TianQing|天擎|NSFOCUS|绿盟|EDR|Sentinel|CrowdStrike|Symantec|McAfee|Sophos|ESET|Kaspersky|卡巴|Huorong|火绒|IP-guard|亿赛通|天锐|网神|亚信|安恒|Forcepoint|Zscaler|Netskope|Umbrella|AnyConnect' + $hit = @() + $hit += Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $keywords } + $hit += Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match $keywords } + if ($hit.Count -gt 0) { + Add-Line '命中管控/安全类软件(可能拦截远控或加密流量):' + $hit | Select-Object -First 12 | ForEach-Object { Add-Line (' - ' + $_.Name) } + } else { + Add-Line '未发现明显的安全审计类软件' + } +} +catch { Add-Line ('代理/审计检测失败: ' + $_.Exception.Message) } + +# ---------- 4. 出站 TCP 端口自由度(是否只放行 80/443) ---------- +Add-Section '4. 出站 TCP 端口自由度' +$tcpTargets = @( + @{ Name = 'github.com:22 (SSH)'; Host = 'github.com'; Port = 22 }, + @{ Name = 'github.com:443 (HTTPS)'; Host = 'github.com'; Port = 443 }, + @{ Name = '114.114.114.114:53 (TCP DNS)'; Host = '114.114.114.114'; Port = 53 }, + @{ Name = '114.114.114.114:8080'; Host = '114.114.114.114'; Port = 8080 } +) +foreach ($t in $tcpTargets) { + try { + $c = Test-NetConnection -ComputerName $t.Host -Port $t.Port -InformationLevel Quiet -WarningAction SilentlyContinue + if ($c) { Add-Line ($t.Name.PadRight(28) + ' -> 通') } else { Add-Line ($t.Name.PadRight(28) + ' -> 不通或被拦') } + } catch { Add-Line ($t.Name.PadRight(28) + ' -> 检测异常') } +} +Add-Line '说明 : 若只有 443 通、非标端口不通,说明公司做了端口白名单,串流要依赖打洞或中转' + +# ---------- 5. UDP 出站能力(串流视频走 UDP,最关键的一测) ---------- +Add-Section '5. UDP 出站能力(关键)' + +# 5.1 DNS over UDP 53:最基础的 UDP 通断验证 +try { + $udp = New-Object System.Net.Sockets.UdpClient + $udp.Client.ReceiveTimeout = 3000 + $srv = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Parse('114.114.114.114'), 53) + $q = [byte[]](0xAB,0xCD, 0x01,0x00, 0x00,0x01, 0x00,0x00, 0x00,0x00, 0x00,0x00, + 0x02,0x71,0x71, 0x03,0x63,0x6F,0x6D, 0x00, 0x00,0x01, 0x00,0x01) + [void]$udp.Send($q, $q.Length, $srv) + $remote = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0) + $resp = $udp.Receive([ref]$remote) + $udp.Close() + Add-Line ('UDP/53 (114.114.114.114) -> 通,收到 ' + $resp.Length + ' 字节') +} +catch { Add-Line ('UDP/53 -> 不通或被拦 : ' + $_.Exception.Message) } + +# 5.2 STUN:验证任意高位 UDP 端口能否出去,并取出 NAT 映射出的公网地址 +function Invoke-StunProbe { + param([string]$Server, [int]$Port = 3478, [int]$TimeoutMs = 3000) + $res = New-Object psobject -Property @{ Server = ($Server + ':' + $Port); Ok = $false; Mapped = ''; LocalPort = 0; Err = '' } + try { + $ip = ([System.Net.Dns]::GetHostAddresses($Server) | + Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | + Select-Object -First 1) + if (-not $ip) { throw 'DNS 解析失败' } + $udp = New-Object System.Net.Sockets.UdpClient 0 + $udp.Client.ReceiveTimeout = $TimeoutMs + $res.LocalPort = $udp.Client.LocalEndPoint.Port + $ep = New-Object System.Net.IPEndPoint ($ip, $Port) + $pkt = New-Object byte[] 20 + $pkt[0] = 0x00; $pkt[1] = 0x01 + $pkt[4] = 0x21; $pkt[5] = 0x12; $pkt[6] = 0xA4; $pkt[7] = 0x42 + $rnd = New-Object System.Random + for ($i = 8; $i -lt 20; $i++) { $pkt[$i] = [byte]$rnd.Next(0, 256) } + [void]$udp.Send($pkt, 20, $ep) + $remote = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0) + $resp = $udp.Receive([ref]$remote) + $udp.Close() + $pos = 20 + while ($pos + 4 -le $resp.Length) { + # 注意:必须先把 byte 转成 int 再位移,否则 [byte] -shl 8 会溢出归零 + $atype = ([int]$resp[$pos] -shl 8) -bor [int]$resp[$pos + 1] + $alen = ([int]$resp[$pos + 2] -shl 8) -bor [int]$resp[$pos + 3] + if ($atype -eq 0x0020 -and $alen -ge 8) { + $b = [byte[]]$resp[($pos + 8)..($pos + 11)] + $b[0] = [byte]($b[0] -bxor 0x21) + $b[1] = [byte]($b[1] -bxor 0x12) + $b[2] = [byte]($b[2] -bxor 0xA4) + $b[3] = [byte]($b[3] -bxor 0x42) + $xp = ((([int]$resp[$pos + 6] -shl 8) -bor [int]$resp[$pos + 7]) -bxor 0x2112) + $res.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $xp + break + } + elseif ($atype -eq 0x0001 -and $alen -ge 8) { + $b = [byte[]]$resp[($pos + 8)..($pos + 11)] + $mp = (([int]$resp[$pos + 6] -shl 8) -bor [int]$resp[$pos + 7]) + $res.Mapped = (New-Object System.Net.IPAddress (, $b)).ToString() + ':' + $mp + break + } + $pos += 4 + $alen + ((4 - ($alen % 4)) % 4) + } + $res.Ok = $true + } catch { $res.Err = $_.Exception.Message } + return $res +} + +$stunServers = @('stun.miwifi.com', 'stun.qq.com', 'stun.hitv.com') +$mappedPorts = @() +foreach ($s in $stunServers) { + $r = Invoke-StunProbe -Server $s + if ($r.Ok) { + Add-Line ('STUN ' + $r.Server.PadRight(24) + ' -> 通 本地端口 ' + $r.LocalPort + ' 映射为 ' + $r.Mapped) + if ($r.Mapped) { $mappedPorts += ($r.Mapped -split ':')[-1] } + } else { + Add-Line ('STUN ' + $r.Server.PadRight(24) + ' -> 不通 : ' + $r.Err) + } +} +if ($mappedPorts.Count -ge 2) { + $uniq = ($mappedPorts | Sort-Object -Unique).Count + if ($uniq -eq 1) { + Add-Line 'NAT 判断 : 多次映射端口一致 -> 锥形 NAT,P2P 打洞成功率高(好消息)' + } else { + Add-Line 'NAT 判断 : 每次映射端口都不同 -> 对称 NAT,P2P 打洞较难,需要中继兜底' + } +} +Add-Line '说明 : STUN 能通 = 高位 UDP 端口可出网,这是 Sunshine/Moonlight 直连的前提' + +# ---------- 6. 到家里的链路(核心结论所在) ---------- +Add-Section '6. 到家里的链路(核心)' +# 家里主机的公网 IPv6(若网关重新拨号 / 前缀变化,需要重新获取后替换) +$homeV6 = '240e:338:263:3600:e591:cb62:3503:8a23' +# 家里的运营商出口 IPv4(100.64 开头 = 运营商级 NAT,无法直连,这是正常现象) +$homeV4 = '' + +if ($homeV6 -ne '') { + # 先 TCP 再 ping:TCP 通即可确认入站放行;ping 不通多半只是 ICMPv6 被拦,不必惊慌 + # 端口 38443 对应家里临时监听;正式启用 Sunshine 后应改成 47989 / 47990 + if (Test-TcpIPv6 -Address $homeV6 -Port 38443) { + Add-Line '家里 IPv6:38443 -> 通 ★ IPv6 入站放行,可以走「最优路径:IPv6 直连」' + } else { + Add-Line '家里 IPv6:38443 -> 不通(可能网关 IPv6 防火墙拦了入站,或家里测试监听没开)' + } + $pOut = & $pingExe -6 -n 2 -w 2000 $homeV6 + $p = Get-NativeText $pOut + if ($p -match 'Reply from|来自') { Add-Line 'ping6 家里 -> 通' } else { Add-Line 'ping6 家里 -> 不通(ICMPv6 常被默认拦截,仅供参考)' } +} else { Add-Line '未填写家里 IPv6,跳过' } + +if ($homeV4 -ne '') { + $pOut = & $pingExe -n 2 -w 2000 $homeV4 + $p = Get-NativeText $pOut + if ($p -match 'Reply from|来自') { Add-Line 'ping4 家里出口 -> 通' } else { Add-Line 'ping4 家里出口 -> 不通(被运营商 NAT 拦,属正常)' } +} else { Add-Line '未填写家里 IPv4,跳过' } + +# ---------- 收尾 ---------- +Add-Section '自测完成' +Add-Line '把结果文件发给 AI 助手,据此决定走 IPv6 直连 / 打洞 / 中转。' +Add-Line ('结果已保存: ' + $resultFile) +try { $Lines | Out-File -FilePath $resultFile -Encoding UTF8 } catch { Write-Host ('写文件失败: ' + $_.Exception.Message) } +Write-Host '' +Write-Host ('结果文件:' + $resultFile) diff --git a/网络自测结果_20260910_160152.txt b/网络自测结果_20260910_160152.txt new file mode 100644 index 0000000..41e165c --- /dev/null +++ b/网络自测结果_20260910_160152.txt @@ -0,0 +1,51 @@ +公司电脑网络自测 2026-09-10 16:01:52 + +===== 1. 设备与显示 ===== +CPU : AMD Ryzen 7 7840H w/ Radeon 780M Graphics +内存 : 31.2 GB +系统 : Microsoft Windows 11 专业版 build 26200 +显卡 : Sharing Monitor 驱动 17.1.40.460 当前 x@Hz +显卡 : AMD Radeon 780M Graphics 驱动 31.0.14003.38003 当前 2560x1600@120Hz +提示 : 若屏幕是 120/144Hz,请到「设置-系统-屏幕-高级显示」把刷新率手动调到最高 + +===== 2. IPv6 能力(关键) ===== +本机 IPv6 : 无 -> 公司是纯 IPv4 网络,家里 IPv6 直连走不通,只能靠打洞或中转 +IPv6 出网 : 阿里公共DNS v6 (2400:3200::1:53) -> 不通 +IPv6 出网 : 电信DNS v6 (240e:4c:4008::1:53) -> 不通 +说明 : 只要有任意一条通,说明公司网络能跑 IPv6,家里的 IPv6 直连方案就有戏 + +===== 3. 代理与审计软件初筛 ===== +WinHTTP 代理 : 褰撳墠鐨?WinHTTP 浠g悊鏈嶅姟鍣ㄨ缃? 鐩存帴璁块棶(娌℃湁浠g悊鏈嶅姟鍣?銆? +环境变量代理 : 无 +系统代理 : 开关=0 服务器=127.0.0.1:7892 PAC= +命中管控/安全类软件(可能拦截远控或加密流量): + - OneDrive + - OneDrive.Sync.Service + - edr_monitor + - HipsDaemon + - HRWSCCtrl + - savsvc + +===== 4. 出站 TCP 端口自由度 ===== +github.com:22 (SSH) -> 通 +github.com:443 (HTTPS) -> 不通或被拦 +114.114.114.114:53 (TCP DNS) -> 通 +114.114.114.114:8080 -> 不通或被拦 +说明 : 若只有 443 通、非标端口不通,说明公司做了端口白名单,串流要依赖打洞或中转 + +===== 5. UDP 出站能力(关键) ===== +UDP/53 -> 不通或被拦 : 使用“1”个参数调用“Receive”时发生异常:“由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。” +STUN stun.miwifi.com:3478 -> 通 本地端口 52662 映射为 117.188.24.89:17002 +STUN stun.qq.com:3478 -> 不通 : 使用“1”个参数调用“Receive”时发生异常:“由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。” +STUN stun.hitv.com:3478 -> 通 本地端口 55902 映射为 117.188.24.89:17017 +NAT 判断 : 每次映射端口都不同 -> 对称 NAT,P2P 打洞较难,需要中继兜底 +说明 : STUN 能通 = 高位 UDP 端口可出网,这是 Sunshine/Moonlight 直连的前提 + +===== 6. 到家里的链路(核心) ===== +家里 IPv6:38443 -> 不通(可能网关 IPv6 防火墙拦了入站,或家里测试监听没开) +ping6 家里 -> 不通(ICMPv6 常被默认拦截,仅供参考) +未填写家里 IPv4,跳过 + +===== 自测完成 ===== +把结果文件发给 AI 助手,据此决定走 IPv6 直连 / 打洞 / 中转。 +结果已保存: E:\xxcool\project\workbuddy.xpcool.com\网络自测结果_20260910_160152.txt diff --git a/远程游戏串流方案_家到公司.md b/远程游戏串流方案_家到公司.md new file mode 100644 index 0000000..17c7db2 --- /dev/null +++ b/远程游戏串流方案_家到公司.md @@ -0,0 +1,710 @@ +# 家庭主机远程串流方案(公司电脑 → 家里主机) + +> **目标**:在公司电脑上远程控制家里主机,能正常游玩《英雄联盟》快节奏娱乐模式(海克斯大乱斗类,团战密集、吃操作延迟)。 +> **版本**:v5 | 2026-09-10 | **状态:公网 IPv4 申请被拒 → IPv6 路线重启评估。家里入口已就绪,瓶颈转移到「公司端没有 IPv6 出口」** + +--- + +## v4 → v5 修订摘要(**IPv4 申请失败 → IPv6 路线重启**) + +用户反馈:**打 10000 申请公网 IPv4 未成功**(运营商不给家宽公网 IPv4)。于是把 v4 判死的 IPv6 路线重新翻出来,逐项核对前置条件。 + +| 项 | v4 状态 | v5 实测/修订 | +|---|---|---| +| 公网 IPv4 | ✅ 主攻路线 | ❌ **申请被拒** —— 这条路暂时封死 | +| IPv6 直连 | ❌ "公司无 IPv6,判死" | ⚠️ **重启评估** —— 家里侧条件经复查**全部达标**,卡点收敛为一个非常具体的点:**公司端缺 IPv6 出口** | +| 家里 IPv6 出网 | 未单独测 | ✅ **19:00 复查全通**:`ipv6.icanhazip.com` / `www.taobao.com` / `www.qq.com` 的 TCP 443 全部 OK | +| 家里 IPv6 地址 | 只知道有两个随机地址 | ✅ **发现固定后缀地址** `...:29f1:e63f:d476:597e`(`Suffix=Link`,后缀**不轮换**)→ 适合做长期服务地址 + DDNS 目标 | +| 家里 IPv6 入站 | 未证明(手机测过一次失败) | ⏳ **待复测** —— 最大嫌疑是天翼网关**默认拦截 IPv6 入站**(IPv6 是"无 NAT 直通",放行全靠网关防火墙) | +| 公司端 IPv6 出口 | 无(硬结论) | 🔑 **新增解法:手机 5G 热点** —— 用户手机实测**有 IPv6**(能开 `ipv6.icanhazip.com`),等于公司笔记本可以"借"一条 IPv6 出口 | + +**v5 路线重排:** + +| 优先 | 路线 | 画质上限 | 公司端代价 | 卡点 | +|---|---|---|---|---| +| **A(主攻)** | **手机 5G 热点 → 公司笔记本获得 IPv6 → 直连家里 IPv6** | **1080p120 满血** | 零安装 | ① 热点下游能否拿到 IPv6 ② 家里网关是否放行入站 | +| B | 腾讯云 frp 中继(公司 IPv4 → 服务器 → 家里) | 1080p60(受 5 Mbps 限) | 零安装 | 服务器 IPv6 支持 | +| C | 网易UU远程 / ToDesk | 中继延迟 + 反作弊风险 | 零安装 | 无 | + +> **核心洞察**:v4 之后瓶颈从「**家里没有入口**」变成「**公司没有 IPv6 出口**」。 +> 家里其实**有一条干净的入口**(公网 IPv6,无 NAT、只需网关放行),只是公司这侧够不着 —— 而"够不着"这件事,一部手机就能补上。 + +**方案 A 的两条硬约束(必须提前认清)** + +| 约束 | 数值 | 说明 | +|---|---|---| +| **流量成本** | 1080p120 @ 20 Mbps ≈ **9 GB/小时** | 这才是手机热点的真正代价。打 3 小时 ≈ 27 GB,必须确认套餐余量或办一张大流量副卡 | +| **延迟代价** | 预计 **40–70 ms** | 5G 空口 + 跨网(手机运营商 → 电信)。LOL 可接受(普通玩家本身 30–50 ms),但**打不满 120fps 的体感优势** | + +--- + +## v3 → v4 修订摘要(**路线第三次修正,这次是定案**) + +公司端 `net-test-v2.ps1` 实测结果回来了(`net-test-v2_20260910_163314.txt`),**三条硬结论**: + +| 项 | v3 假设 | v4 实测结论 | +|---|---|---| +| 公司出站端口 | 未知,担心是白名单 | ✅ **完全自由**:`portquiz.net` 的 `443/8080/8443` 全 OPEN,连你腾讯云的 **`22025`** 也 OPEN → **任意 TCP 端口出站都能用** | +| **公司 NAT 类型** | 未知 | ❌ **对称 NAT**(同一本地端口问不同 STUN → 映射端口 `16993/16994/17011` 各不相同)+ **多公网出口**(`117.188.24.89` 与 `117.188.118.254` 同时出现、按目的地分流)→ **P2P 打洞基本不可行** | +| 公司 IPv6 | v3 已判死 | ❌ 复核确认 `IPv6 local : NONE` | +| 公司管理员权限 | 未知 | ⚠️ 当前会话**非管理员**,但账号 `ybtdevxxl` **在 Administrators 组内** → 可提权(只需点一次 UAC) | +| 公司已装软件 | 未知 | 🎁 **已装 `Clash Verge`、`WireGuard`、`ToDesk`、火绒** —— 若将来走隧道,WireGuard 现成、无需再装驱动 | +| 公司安全软件 | 火绒在跑 | ⚠️ **火绒 `HipsDaemon` / `HRWSCCtrl` 运行中**;其余 EDR(`aTrust` / `edr_monitor` / `savsvc` / Defender 全系)**均为 Stopped** → 没有强制管控在跑,这是好消息 | +| 公司主屏 | 2560×1600@120 | ✅ 复核一致(脚本报 `1707×1067` 是 **150% 缩放后的逻辑值**,2560÷1.5 = 1707) | + +**路线定案:** + +| 路线 | 判决 | 依据 | +|---|---|---| +| IPv6 直连 | ❌ **死** | 公司无 IPv6 | +| Tailscale / ZeroTier **打洞** | ❌ **基本死** | 公司**对称 NAT + 多出口 IP**,映射端口不可预测 | +| **A. 申请公网 IPv4 + Sunshine 直连** | ✅ **主攻** | 公司出站任意端口均可用 → 拿到公网 IP 即可 **1080p120 满血** | +| B. 腾讯云 frp 中继 | ⚠️ 可立即启用,画质受限 | 公司能连腾讯云任意端口 → 技术上完全通;**但服务器仅 5 Mbps**,只够 1080p60 @ 4~5 Mbps | +| C. 网易UU远程 | ⚠️ 兜底 | 厂商大带宽中继、不受 NAT 影响、专为游戏优化;代价是反作弊风险 | +| D. ToDesk | ⚠️ 兜底 | 已实测可连通(走中转),办公向延迟 | + +> **一句话总结**:唯一的瓶颈是**家里没有公网 IP**。解决它,一切迎刃而解;不解决,就只能接受「中继 + 降码率」。 + +--- + +## v2 → v3 修订摘要(**重要,路线变了**) + +公司端自测结果回来后,发现 v2 的核心前提不成立,路线必须再改一次: + +| 项 | v2 结论 | v3 修订 | +|---|---|---| +| 打通方式 | 公网 **IPv6 直连** | ❌ **作废**:公司网络**完全没有 IPv6**(本机无 IPv6 地址、IPv6 出网两条 DNS 全不通)→ IPv6 直连从"最优解"变成"走不通" | +| 家里 NAT 类型 | "对称 NAT,打洞困难" | ❌ **v2 测错了**:v2 每个 STUN 用新 socket(新本地端口),映射端口当然不同。**正确测法**(同一 socket 问多个 STUN)结论是 —— **端点无关映射(锥形/Cone NAT),打洞可行** ✅ | +| 公司端口白名单 | v1 报"443 不通、疑似白名单" | ❌ **v1 测错了**:拿 `114.114.114.114:8080`(该主机根本没开 8080)和 `github.com:443`(国内 DNS 被污染)当靶子。已重做为**有效靶子**测法(见 `net-test-v2.ps1`) | +| 首选路线 | IPv6 直连 | ✅ **改为二选一**:**A. 申请公网 IPv4 + 端口转发**(若电信批)/ **B. Tailscale 打洞**(家里是锥形 NAT,基础不错) | +| 公司屏幕 | 待补测 | ✅ **2560×1600 @ 120Hz** → 120Hz 屏是意外之喜,手感延迟少 8ms 左右 | +| 公司安全软件 | 未知 | ⚠️ 发现企业级管控:**火绒 HipsDaemon / HRWSCCtrl** + `edr_monitor` + `savsvc`(详见 1.6) | +| 画质定档 | 1080p120 | ✅ 维持 **1920×1080 @ 120fps**(公司屏 16:10,串流按 1920×1080 等比即可) | + +--- + +## 一、实测底账(2026-09-10) + +### 1.1 被控端(家 · 本机)硬件 + +| 项目 | 实测值 | 对串流的意义 | +|---|---|---| +| 显卡 | **NVIDIA RTX 4070 SUPER**(驱动 32.0.15.9186) | Ada 架构,NVENC 支持 **H.264 / H.265 / AV1** 硬编,画质天花板很高 | +| CPU | Ryzen 7 7800X3D(8C/16T) | 游戏性能强,编码走 NVENC 不占 CPU | +| 内存 | 31.2 GB | 充足 | +| 系统 | Windows 11 专业版 build 26200 | Sunshine / Moonlight 官方良好支持 | +| 显示器 | AOC **2560×1440 @ 170Hz** | 可输出 2K 高刷;但按 1080p120 定档更稳 | +| 网卡 | Realtek 2.5GbE,链路 1Gbps,IP `192.168.1.30` | 有线千兆,本地无瓶颈 | +| 网关 | `192.168.1.1`(可访问,LuCI 界面) | 见 1.4 | + +### 1.2 网络实测 —— 三个决定性发现 + +| 指标 | 实测值 | 结论 | +|---|---|---| +| 下行 | 943.82 Mbps | 千兆宽带(Ookla,电信上海节点 id 3633) | +| **上行** | **46.36 Mbps** | **画质天花板;可撑 1080p120 甚至 2K120** | +| 丢包 | **0.0%** | 线路质量好 | +| 出口 IPv4 | `1.204.100.17`(电信,广东,动态) | ⚠️ 这是**运营商 NAT 的公网出口**,不是你家地址 | +| **tracert 第 2 跳** | **`100.64.0.1`** | ⚠️⚠️ **运营商级 NAT(CGNAT)实锤** | +| 出口 IPv6 | **`240e:338:263:3600::/64`** | ✅ **公网 IPv6,可被外部直达(待验证入站放行)** | +| STUN 映射端口 | ❗v2 报"每次不同"→ **测法有误**;**正确复测(同一 socket 打多个 STUN)**:本地端口 55001 → 恒为 `:3051`;55002 → 恒为 `:3063` | ✅ **端点无关映射(Cone/EIM)**,IPv4 侧打洞**可行**,不是对称 NAT | + +**修正后的三条推论** + +1. ❌ **IPv4 端口转发仍不可行** —— 你确实没有公网 IPv4(`100.64.0.1` 是运营商大内网,RFC 6598 专用段) +2. ✅ **IPv4 侧 P2P 打洞是可行的** —— 家里是**锥形 NAT(映射端点无关)**,这是打洞最有利的一类。v2 里"对称 NAT 打洞困难"的结论是**因为测试方法错误**(每问一个 STUN 就换一个新 socket → 本地端口变了,映射端口当然跟着变)。**已用正确方法复测推翻。** +3. ⚠️ **IPv6 虽然最优,但公司没有** —— 家里有公网 IPv6 这点仍然成立,只是**公司那条网线/WiFi 完全跑不了 IPv6**。 + 🔄 **v5 修订(措辞必须精确)**:这不等于"IPv6 在公司场景作废",而只是"**公司的那一条网络出口**没有 IPv6"。公司笔记本还有**第二条出口**从未被用上 —— **手机 5G 热点**(用户手机实测有 IPv6)。所以这条路在 v5 复活,详见第二章 A 案。 + +### 1.3 家里可直接对外服务的 IPv6 地址(**v5 · 19:00 复查更新**) + +``` +240e:338:263:3600:29f1:e63f:d476:597e ← ✅ 首选:Suffix=Link,后缀固定不轮换,生命周期 1.21 天(自动续期) +240e:338:263:3600:e591:cb62:3503:8a23 ← ⚠️ Suffix=Random(隐私扩展地址),会轮换,当前 17.7 小时后过期 +240e:338:263:3600:51f3:a026:4256:a337 ← ❌ Deprecated(已废弃,忽略) +``` + +**这次复查最大的收获:找到了一个"后缀永不变化"的地址。** + +| 地址组成 | 是否变化 | 说明 | +|---|---|---| +| 前缀 `240e:338:263:3600::/64` | ⚠️ **可能变** | 天翼网关 PPPoE 拨号时下发,重拨或运营商侧调整时会变 | +| 后缀 `29f1:e63f:d476:597e` | ✅ **不变** | 基于网卡标识派生(`Suffix=Link`),网卡不换它就永远不变 | + +**这带来一个很实际的好处**:DDNS 只需要盯住**前缀那 4 段**是否变化,Windows 侧什么都不用改(不必关隐私扩展),就能拿到一个长期可寻址的目标地址。 + +其他实测(19:00): +- **IPv6 出网正常**:`ipv6.icanhazip.com` / `www.taobao.com` / `www.qq.com` 的 TCP 443 **全部 OK** +- IPv6 默认路由 → `fe80::1`(网关) +- Windows 防火墙:Public 档开启(家里以太网属 Public),且 `node.exe` 有 Public/Private **入站 Allow** 规则 → 本机侧不拦 + +> **v5 补充(路线重启)**:这组地址**重新变成核心资产**。v4 时因为"公司无 IPv6"把它判死,但公司那台笔记本还有一条**从未用上**的出口 —— **手机热点**(见第二章 A 案)。用户手机实测有 IPv6,等于随时能给公司笔记本接上一条 IPv6 通道。 + +⚠️ 前缀由网关 RA 下发,重拨可能变 → 长期使用必须配 DDNS(见第七章)。 + +> **⚠️ 仍待证明的一件事**:家里能**出** IPv6(已验证),但**外部能不能进得来**还没证明。IPv6 没有 NAT,"放行全靠网关防火墙"——天翼网关默认**拦截入站**,这是此前手机测试失败的最大嫌疑。验证方法见第七章。 + +--- + +### 1.4 网关(路由器)与密码找回 + +**判定结果**:**中国电信智能网关(天翼网关)**,管理界面为 LuCI 风格(首页 302 到 `/cgi-bin/luci`),网关 MAC `88-C7-8F-A6-BC-43`。 + +忘记后台密码的**阶梯式解决办法**(从省事到暴力): + +1. **看机身贴纸**:找 `useradmin` 和随机的 8 位密码。普通账号能看状态页与 WAN 信息,部分型号也能改端口映射 / IPv6 防火墙 +2. **试超级管理员默认密码**:账号 `telecomadmin`,经典默认 `nE7jA%5m`(华为系天翼网关最常见),其次 `admintelecom`。部分地区出厂就是随机密码(贴在机身或在电信 App 里) +3. **手机 App**:`小翼管家` / `天翼看家`,可查看网关信息、部分设置,也常能重置管理密码 +4. **打 10000 号**:报宽带账号,让客服重置网关管理密码 +5. **恢复出厂**(reset 孔长按 10 秒):**最后一招**。天翼网关重置后会经 TR-069 自动重新下发业务配置(含宽带账号),多数能自动恢复上网,但有失败风险,需客服兜底 +6. ✅ **其实可以完全绕开路由器后台**: + - **IPv6 直连不需要端口转发**(IPv6 无 NAT,只需网关防火墙放行入站) + - **组网打洞不需要后台**(Tailscale / ZeroTier) + - **商业远控不需要后台**(UU远程 / ToDesk 走它们自己的服务器) + +> 结论:**拿不到路由器密码不会阻塞方案**。只有"网关把 IPv6 入站也拦了,且你不同意用打洞/中转"这一种情况才需要密码去放行。 + +### 1.5 本机环境的重要细节 + +- Windows 防火墙:Domain=开启 / **Private=关闭** / Public=开启;**以太网被判定为 Public 档** +- **当前会话不是管理员** → 装 Sunshine、加防火墙规则、改电源计划都需要你点一次 UAC 确认 +- 已存在的远控残留驱动:`Todesk Virtual Display Adapter`、`GameViewer Virtual Display Adapter`(网易UU远程)、`MuMu Virtual Display Adapter` +- 本机装有 Clash Verge(系统代理 127.0.0.1:7897,当前开关=0)与火绒(HipsDaemon / HRWSCCtrl) + +### 1.6 公司端(**v3 已拿到实测结果**) + +来源:`网络自测结果_20260910_160152.txt`(公司电脑本地运行 v1 脚本产出) + +| 项目 | 实测值 | 结论 | +|---|---|---| +| CPU | **AMD Ryzen 7 7840H**(8C/16T,Zen4) | 性能远超解码需求 | +| 核显 | **AMD Radeon 780M**(RDNA3 / Phoenix,VCN 4.0) | ✅ **支持 AV1 硬解**,也支持 H.265 | +| 内存 | 31.2 GB | 充足 | +| 系统 | Windows 11 专业版 build 26200 | 良好 | +| 屏幕 | **共享显示器 + 2560×1600@120Hz**(另有 `Sharing Monitor` 虚拟项) | ✅ **120Hz**!比 v2 假设的 60Hz 屏好一档,少约 8ms | +| **IPv6** | **本机无 IPv6 地址;IPv6 出网(阿里 v6 DNS / 电信 v6 DNS)全部不通** | ❌ **企业网是纯 IPv4** → 有线/WiFi 这条出口没有 IPv6(🔄 v5:可改用**手机热点**作为 IPv6 出口) | +| UDP 出网 | STUN 可用(`stun.miwifi.com`、`stun.hitv.com` 均有响应,本地高位端口 → 公网 `117.188.24.89`) | ✅ **高位 UDP 可出网且能收到回包**,这是打洞/串流的基础 | +| 系统代理 | 开关=0,但服务器填了 `127.0.0.1:7892` | ⚠️ 公司电脑上可能装过 Clash 类代理客户端(**待查是否在跑、有无可用节点**) | +| 安全/管控软件 | **`HipsDaemon`(火绒 HIPS 主服务)**、**`HRWSCCtrl`(火绒安全中心)**、`edr_monitor`、`savsvc`、`OneDrive×2` | ⚠️ **有企业级 HIPS/EDR**,装 VPN 驱动(TUN/虚拟网卡)大概率会弹窗告警甚至被拦;也可能记录外联行为 | +| 出站 TCP 端口自由度 | v1 结论**不可信**(靶子无效,见 v3 摘要) | 🔄 需用 `net-test-v2.ps1` 复测 | +| 管理员权限 | **待确认** | 决定能不能装 Tailscale(要装 TUN 驱动);Moonlight 便携版不需要管理员 | + +**补充实测(2026-09-10 16:28,用户提供 `ipconfig`)—— 接入方式与网络身份已确认** + +``` +无线局域网适配器 WLAN: + IPv6 地址 . . . . . : fe80::ecdb:9605:2cf8:9a3d%11 <- 仅链路本地,无全局 IPv6 + IPv4 地址 . . . . . : 192.168.250.84 + 子网掩码 . . . . . : 255.255.255.0 + 默认网关. . . . . . : 192.168.250.1 +``` + +| 项 | 结论 | +|---|---| +| 接入方式 | **笔记本 + WiFi 接入企业网**(不是有线) | +| 内网网段 | `192.168.250.0/24` —— 企业统一网段(家用极少见),网关 `192.168.250.1` | +| IPv6 | **只有 `fe80::` 链路本地地址,无 RA 全局地址** → 企业网本身不发 IPv6 | 🔄 **v5 精确化**:死的是"**这条企业网出口**",不是"公司场景"。笔记本可另接**手机热点**获得 IPv6(见第二章 A 案) | +| 出口 IP | `117.188.24.89`(全公司共享同一出口,非公司专属) | +| 内网 IP 归属 | 由企业网 DHCP 下发,**随时可能变**,不能用作稳定地址 | + +> **公司端唯一还缺两块拼图**:① 出站端口是否真被白名单限制;② 是否有管理员权限。两者都能用 `net-test-v2.ps1` 一次性拿到。 + +**从被控端(家里)反查 ToDesk —— 意外挖到一个好消息(2026-09-10 16:29)** + +用户在公司用 ToDesk 控制家里主机,我直接在家里这侧抓了 ToDesk 的实时连接: + +``` +# ToDesk 全部对外 TCP(非局域网) +TCP Established 59695 -> 219.151.138.225:8080 <- 非标端口! +TCP Established 59665 -> 219.151.138.225:443 +TCP Established 57577 -> 118.24.225.33:443 + +# 到公司出口 117.188.24.89 的直连:无记录 +# ToDesk UDP 端点(家里侧,多个 IPv6 地址 + IPv4) +UDP 240e:338:263:3600:e591:cb62:3503:8a23:57068 +UDP 240e:338:263:3600:51f3:a026:4256:a337:57067 +UDP 240e:338:263:3600:29f1:e63f:d476:597e:57066 +UDP 192.168.1.30:57069 +``` + +两条关键推论: + +1. ✅ **公司网络允许出站到非标端口(至少 8080 可用)** —— 这是**分量很重的利好**:说明公司不是"只放行 80/443"的白名单防火墙。若家里将来有公网 IPv4,Sunshine 的 `47989/47990/48010` 这些端口公司大概率能直连;腾讯云 frp 中继(任意端口)也具备可行性。 +2. ⚠️ ToDesk **TCP 层没有与公司出口 IP 的直连**,且文件传输会话为 `planb:1 ... p2p:0`(走中转)。UDP 是无连接的、从端点列表看不到对端,所以**不能据此断定打洞失败**,但至少没有直连证据 → 打洞方案(路径 B)的信心下调。 + +> 附带发现:家里这台主机的 IPv6 地址有**多个轮换的隐私扩展地址**(同一前缀下 3 条)。这印证了"IPv6 直连必须配 DDNS"的判断(不过该路线已被公司端否决,仅留档)。 + +--- + +## 二、方案选型(v5 定案) + +**先看硬约束 —— 它们决定一切:** + +| 约束 | 事实 | 影响 | +|---|---|---| +| 家里无公网 IPv4 | CGNAT(tracert 第 2 跳 `100.64.0.1`),且 **10000 申请被拒** | ❌ 端口转发 / 直连彻底没戏(v5 确认这条路已走不通) | +| 家里 IPv4 NAT | **锥形 / 端点无关映射(EIM)** | 单看家里是打洞友好,但被下一条堵死 | +| **公司 NAT 类型** | ❌ **对称 NAT + 多公网出口**(同一本地端口映射到 `16993/16994/17011` 各不相同;出口 IP 出现 `117.188.24.89` 与 `117.188.118.254` 两个) | **对称 + 锥形 组合 → 打洞不可行** | +| **家里有公网 IPv6** | `240e:338:263:3600::/64`,**19:00 实测出网全通**,且找到**固定后缀地址** | ✅ **v5 主攻:这是家里唯一干净的入口** | +| 公司(有线/WiFi)无 IPv6 | `IPv6 local : NONE`(企业网不发 RA) | ⚠️ **但不是死路** —— 可借手机热点补出口 | +| **手机 5G 有 IPv6** | 用户手机实测可打开 `ipv6.icanhazip.com` | ✅ 给公司笔记本补 IPv6 出口的现实手段 | +| **公司 TCP 出站全自由** | `portquiz.net` 的 `443/8080/8443` 全 OPEN,且能连你腾讯云的 `22025` | ✅ **中继 / 直连方案的"通道"完全没问题** | +| 公司有 HIPS + EDR | 火绒 `HipsDaemon` / `HRWSCCtrl` **运行中** | ⚠️ 装内核驱动类方案有告警 / 被拦风险 | +| 家里上行 46 Mbps | Ookla 实测 | 画质天花板由它决定,与走哪条路无关 | + +> **核心洞察(v5 更新)**:v4 时瓶颈是「**家里没有入口**」;现在家里**明明有一条干净的入口**(公网 IPv6,无 NAT、只需网关放行),瓶颈转移到了「**公司这侧够不着**」。 +> 而"够不着"这件事 —— **一部手机就能补上**。 + +### 路径 A(**v5 主攻**):手机热点补 IPv6 + 家里 IPv6 直连 + +**思路**:公司这条有线/WiFi 出口没有 IPv6,但公司笔记本还有一个网络接口从没用上 —— **WiFi 连自己的手机热点**。手机 5G 实测有 IPv6,热点下游设备通常也能分到 IPv6 前缀。 + +``` +[家里主机] --公网 IPv6--> 中国电信 IPv6 骨干 <--IPv6-- [手机 5G] --热点--> [公司笔记本 · Moonlight] + ▲ + 唯一门槛:天翼网关放行 IPv6 入站 +``` + +- **优点** + - **公司端零安装**:Moonlight 便携版即可 —— 不装驱动、不碰火绒、不要管理员 + - **画质上限最高**:家里 46 Mbps 上行全用得上 → **1080p120 满血** + - **零月费**(只有流量开销),不依赖任何第三方服务、不需要备案域名、不受反作弊厂商策略影响 +- **代价(必须提前认清)** + - ⚠️ **流量**:1080p120 @ 20 Mbps ≈ **9 GB/小时**;打 3 小时 ≈ 27 GB。**建议单独办一张大流量副卡** + - ⚠️ **延迟**:5G 空口 + 跨网(手机运营商 → 电信),预计 **40–70 ms**。LOL 可接受(普通玩家本身 30–50 ms),但吃不满 120fps 的体感优势 + - ⚠️ **稳定性**:5G 信号波动会直接反映到画面码率上 +- **两个前置条件(必须先验证,步骤见第七章)** + 1. 手机热点**下游设备**能否拿到 IPv6 全局地址(部分手机/ROM 默认只共享 IPv4) + 2. 天翼网关是否**放行 IPv6 入站**(默认拦截,这是此前手机 4G 测试失败的最大嫌疑) + +### 路径 A′(**已确认失败,但保留为长期最优解**):申请公网 IPv4 + 端口转发 + +- 原为 v4 主攻路线,**用户打 10000 申请未成功** —— 运营商不给家宽公网 IPv4 +- 若日后政策松动、或换运营商/换套餐,可立刻复活:拿到公网 IPv4 → 网关端口转发(Sunshine 的 `upnp = enabled` 已开,甚至不用登网关后台) +- **它是延迟最低、画质最好、对反作弊最友好的方案**,只是当前拿不到 + +### 路径 B(**可立即启用**):腾讯云 frp 中继 + +- 公司端实测**能连 `193.112.118.168:22025`** → **中继通道完全可行**(frp 走任意端口都行) +- ⚠️ **5 Mbps 是硬伤**:只够 1080p60 @ 4~5 Mbps,画质明显缩水,达不到"画质好" +- 升级选项与代价:按带宽升级(如 20 Mbps)月费上升;**按流量计费更不可取** —— 1080p120 @ 20 Mbps ≈ **9 GB/小时**,长期串流成本无法承受 +- 定位:**先跑起来、验证链路与手感**的过渡方案,同时等公网 IP + +### 路径 C(**兜底**):网易UU远程 / ToDesk(两端都已有痕迹) + +- 10 分钟可用、**不受 CGNAT / 对称 NAT / 端口限制**(走厂商国内中转,且厂商带宽远大于自建中继) +- 网易UU远程是**专为游戏**设计的远控,比 ToDesk 更贴合"打游戏"场景 +- 延迟约 30–60ms → 办公够用,**打大乱斗团战会有可感知的跟手差异** +- ⚠️ 反作弊风险最高档(见第五节) +- 定位:**A 未获批、B 画质不够时的现实选择**,或临时救急 + +### ❌ 已排除 + +| 方案 | 排除原因 | +|---|---| +| **Tailscale / ZeroTier 打洞** | ❌ **公司是对称 NAT + 多出口** → 打洞不可行(v3 曾列为次选,v4 判死);退化走境外 DERP 中继延迟极差 | +| IPv6 直连(**公司有线/WiFi 这条出口**) | 企业网不发 IPv6 —— 🔄 v5 精确化:可用**手机热点**绕过,见路径 A | +| IPv4 端口转发(**当前状态**) | 家里是 CGNAT,且 **10000 申请公网 IPv4 被拒** → 当前不可用(保留为路径 A′) | +| **IPv6 隧道类**(HE 6in4 / Teredo / Cloudflare WARP) | 6in4 需放行 **IP 协议 41**(企业防火墙几乎不可能放);Teredo / WARP 回程绕行,延迟 150 ms+ → 办公凑合、**打游戏不可用** | +| Windows 远程桌面(RDP) | 无 GPU 画面、刷新率锁 30、输入延迟高,不能打游戏 | +| TeamViewer / AnyDesk | 办公向设计,帧率与延迟都不达标 | +| 大带宽按流量中继 | 1080p120 约 9 GB/小时,流量成本不可持续 | + +--- + +## 三、目标参数(**1080p120 定档**) + +| 用途 | 分辨率 | 帧率 | 编码 | 码率 | 上行占用 | +|---|---|---|---|---|---| +| **首发默认** | 1920×1080 | **120** | **H.265** | **22 Mbps** | 47% | +| 优化后(首选) | 1920×1080 | 120 | **AV1** | 16–18 Mbps | 35–39% | +| 弱网兜底 | 1920×1080 | 60 | H.265 | 10 Mbps | 22% | +| 有余力再上 | 2560×1440 | 120 | AV1 | 30 Mbps | 65% | +| ❌ 不推荐 | 2560×1440 | 170 | AV1 | 45+ Mbps | ~100% | + +**编码策略**:先用 **H.265** 跑通(兼容性最稳)→ 确认稳定后切 **AV1**(公司端 780M 支持 AV1 硬解,同画质可省约 20–30% 码率,画面更干净)。 + +--- + +## 四、延迟预算(目标 < 35ms) + +| 环节 | 耗时 | 说明 | +|---|---|---| +| 家:采集 + NVENC 编码 | 5–10 ms | 1080p120 H.265/AV1 | +| 家:上行排队 | 1–3 ms | 码率留 50% 余量时 | +| 公网往返(家 ↔ 公司) | 8–25 ms | 同城 8–15ms,跨省 20–35ms | +| 公司:解码 + 渲染 | 3–6 ms | 780M 硬解 | +| 公司:显示器一帧 | **0–17 ms** | **60Hz 屏 = 16.7ms 硬成本**;144Hz 屏仅 ~7ms | +| **合计** | **20–45 ms** | 打大乱斗的临界体验线约在 40ms | + +> 📌 **公司屏刷新率是硬成本**。若是 120/144Hz 屏,务必到「设置-系统-屏幕-高级显示」把刷新率手动调到最高。 + +--- + +## 五、反作弊风险(简版,细节你说稍后再展开) + +**事实**:腾讯从未公布"用远控会封号"的条款,但社区有**大量十年封禁案例**,理由是"检测到第三方软件",与远控软件在后台高度相关;**"远程操控着玩游戏"被反复指为最高风险场景**。 + +**本机现状不干净**:已存在 ToDesk / 网易UU远程 / MuMu 的虚拟显示驱动,而这些虚拟显示与虚拟输入设备正是 ACE/TP 反作弊的重点扫描特征。 + +**可用的降低手段**(无法归零): +1. 优先用 **Sunshine**(用户态进程 + 系统 API 注入输入,不装虚拟显示器、不装内核驱动) +2. 公司端用 **Moonlight 便携版**(不写注册表、不留服务) +3. 游戏前停用国产远控服务与虚拟显示设备 +4. 绝不用任何宏 / 连点 / 自动化映射 +5. 一个天然优势:游戏看到的仍是**家里的 IP**,不会触发异地登录风控 + +--- + +## 六、实施计划(v3) + +### Phase 0 · 前置验证(**基本完成**) +- [x] 家侧硬件与带宽实测(4070S / 7800X3D / 上行 46.36M / 丢包 0%) +- [x] 判定 IPv4 为 CGNAT +- [x] **家里 IPv4 NAT 类型复测 → 锥形(端点无关映射 / EIM),打洞可行** ✅(推翻 v2 的"对称 NAT"误判) +- [x] 确认家侧有公网 IPv6 +- [x] 公司端自测结果回收:**纯 IPv4 网络、无 IPv6**;屏幕 2560×1600@**120Hz**;发现火绒 HIPS + EDR +- [x] Sunshine 已安装、服务运行、编码器三件套可用、凭据已建、双栈配置已生效 +- [ ] **公司端跑 `net-test-v2.ps1`** ← 唯一还缺的技术验证(见第七节) +- [ ] **打 10000 号申请公网 IPv4** ← 与上一条并行;零成本,但可能一步解决问题 + +### Phase 1 · 家里端(**只剩电源设置没做**) + +| 项 | 状态 | +|---|---| +| Sunshine v2026.906.222525 | ✅ `C:\Program Files\Sunshine\` | +| 服务 | ✅ `SunshineService` / LocalSystem / 开机自启 / Running | +| 编码器 | ✅ `h264_nvenc` / `hevc_nvenc` / **`av1_nvenc`**(含 10-bit) | +| 管理凭据 | ✅ `admin` / `Sunshine2026` | +| 双栈监听 | ✅ `address_family = both`,已验证 `[::]:47984 / 47989 / 47990 / 48010` | +| UPnP | ✅ `upnp = enabled`(**等路径 A 拿到公网 IP 后才有意义**) | +| 电源与显示器 | [ ] 关闭显示器 = 从不、睡眠 = 从不(否则采集黑屏)← **待你点** | + +#### ✅ 曾经的阻塞项:Sunshine 默认只监听 IPv4 —— **已解决** + +安装后实测(2026-09-10):全部监听都是 `0.0.0.0:47984/47989/47990/48010`,IPv6 回环 `::1:47990` 直接被"积极拒绝"(根本没绑定)。 + +**你已在 Web UI 里改好了**,当前 `C:\Program Files\Sunshine\config\sunshine.conf` 内容为: + +```ini +address_family = both +upnp = enabled +``` + +重启后日志确认两行配置都被正确解析(`config: 'address_family' = both` / `config: 'upnp' = enabled`),监听已变成双栈: + +``` +TCP [::]:47984 TCP [::]:47989 TCP [::]:47990 TCP [::]:48010 +``` + +> 虽然公司没有 IPv6 让这条路暂时用不上,但**开着双栈没有副作用**,而且以后万一公司网络升级 IPv6,改都不用改。 + +#### Phase 1 已确认的环境事实 + +| 项 | 实测值 | +|---|---| +| 安装目录 | `C:\Program Files\Sunshine\` | +| 配置文件 | `config\sunshine.conf`(安装时为空,0 字节) | +| 凭据文件 | `config\sunshine_state.json`(创建凭据后生成) | +| 日志 | `config\sunshine.log`(**排查问题第一现场**) | +| 显示器 | 2560×1440 @ 170.002Hz,HDR 容器,HAGS 已启用 | +| Web UI | `https://localhost:47990`(自签名证书,浏览器需点"继续访问") | +| Web UI API | **HTTP Basic 认证**(`WWW-Authenticate: Basic realm="Sunshine Gamestream Host"`),非 cookie 会话 | +| 首次设置 API | `POST /api/password`,JSON: `{newUsername, newPassword, confirmNewPassword}` | + + +### Phase 2 · 打通链路(按路径 A / B 二选一) + +**路径 A —— 只适用"申请到公网 IPv4"的情况(公司端零安装,最优先)** +- [ ] 家里:确认网关端口转发生效,或让 Sunshine 的 UPnP 自动映射 +- [ ] 手机 4G 直连验证:`http://公网IP:47990` 能打开 Web UI 即成功 +- [ ] 公司:Moonlight 便携版 → 填 `公网IP:47989` 配对(一次性 PIN) + +**路径 B —— Tailscale(不依赖电信)** +- [ ] 家里:装 Tailscale 并登录(需 UAC) +- [ ] 公司:装 Tailscale 并登录(需管理员;**留意火绒/EDR 弹窗**) +- [ ] 公司:Moonlight 便携版 → 填**家里的 Tailscale IP(100.x.x.x)**配对 +- [ ] 首测参数 **1920×1080 @ 120 / H.265 / 22 Mbps**,开性能浮层看 encode / network / decode 分项 + +### Phase 3 · 调优 +- [ ] Windows 鼠标加速关闭("提高指针精确度"),缩放 100% +- [ ] 游戏内:**无边框窗口** + 分辨率与串流一致 + 刷新率"不封顶" + 关垂直同步 +- [ ] Moonlight 关 V-Sync(换更低延迟) +- [ ] 跑顺后切 AV1,码率降到 16–18 Mbps +- [ ] 公司屏确认跑在 **120Hz**(实测已是 120Hz,串流时再核一次) +- [ ] 若走路径 A 且公网 IP 会变,配 DDNS + +### Phase 4 · 兜底与卫生 +- [ ] 保留 ToDesk(**当前已在用,已验证可用**)与网易UU远程作为应急通道 +- [ ] 需要时在腾讯云部署 WireGuard / DERP 兜底(注意 5 Mbps 限制) +- [ ] **游戏前停用国产远控服务与虚拟显示设备**(ToDesk / GameViewer / MuMu 的 Virtual Display) + +--- + +## 七、现在需要你做的事(v5) + +### ⚠️ 前提:公网 IPv4 申请被拒 → 主攻方向改为「手机热点 + 家里 IPv6 直连」 + +| 路线 | v4 判决 | v5 修订 | +|---|---|---| +| IPv6 直连 | ❌ 死(公司无 IPv6) | 🔄 **复活** —— 家里侧条件已全部核实合格,公司侧可借**手机热点**补 IPv6 | +| P2P 打洞(Tailscale / ZeroTier) | ❌ 死 | ✅ 维持死判(公司对称 NAT + 多出口) | +| 申请公网 IPv4 | ✅ 主攻 | ❌ **申请被拒** → 降级为"长期最优解"(路径 A′) | +| 出站端口自由度 | ✅ 意外地好 | ✅ 维持(支撑路径 B 中继可行) | + +**v5 路线表:** + +| 优先 | 路线 | 公司端代价 | 能达到的画质 | 卡点 | +|---|---|---|---|---| +| **A** | **手机热点补 IPv6 + 家里 IPv6 直连** | **零安装**(只要 Moonlight 便携版) | ✅ **1080p120 满血** | ① 热点下游有无 IPv6 ② 家里网关是否放行入站 | +| B | 腾讯云 frp 中继(现在就能做) | 零安装 | ⚠️ 仅够 1080p60 | 服务器 5 Mbps | +| C | 网易UU远程 / ToDesk(兜底) | 零安装 | ⚠️ 延迟 + 反作弊风险 | 无 | + +--- + +### ① ⭐ 当前最重要:验证 IPv6 路线能否成立(两步,约 10 分钟) + +家里**已重新起好带日志的监听**(端口 38443,`:::38443 LISTENING` 已确认),下面两步直接决定走哪条路。 + +#### 步骤 1 —— 手机 5G 验证「家里 IPv6 入站是否放行」 + +用**手机(关 WiFi、走 4G/5G 流量)**扫下面这个二维码,或直接在系统浏览器里打开: + +``` +http://[240e:338:263:3600:29f1:e63f:d476:597e]:38443/ +``` + +> 二维码文件:`ipv6-home-qrcode.png`(我已生成在项目目录,**用系统浏览器扫,别用微信扫一扫**——微信会拦非标端口的 http) + +| 看到什么 | 含义 | +|---|---| +| 页面显示 `IPV6-INBOUND-OK` | ✅ **家里 IPv6 入站是通的** —— 好消息,路子活了 | +| 一直转圈 / 打不开 | ❌ 大概率是**天翼网关拦了 IPv6 入站** → 看下面 ② | + +> 打不开也没关系:**家里服务端有访问日志**,我能判断请求到底有没有到达 —— 只要你告诉我"试过了"即可。 + +#### 步骤 2 —— 手机热点验证「公司笔记本能否拿到 IPv6」 + +这一步决定这条路的上限(能不能真的在公司上网): + +1. 手机**开热点**(进热点设置确认 **IPv6 / 流量共享** 是开的;部分国产 ROM 默认只共享 IPv4) +2. **公司笔记本连上这个热点**(先断开公司 WiFi 或让热点优先) +3. 跑我准备好的脚本 `hotspot-ipv6-test.ps1`(**已放在家里桌面** `C:\Users\xxl\Desktop\`,用 ToDesk 文件传输拉到公司电脑 —— 别复制粘贴文本) +4. 结果会自动写到公司电脑桌面 `hotspot-ipv6-result.txt`,发我 + +> 脚本是**纯 ASCII、无 BOM**,免疫复制粘贴损坏;它一次就把"有没有 IPv6 + 能不能连到家里"两件事都测了。 + +#### 两步的四种组合 → 直接决定走哪条路 + +| 步骤1(手机直连家里) | 步骤2(笔记本有 IPv6) | 结论与下一步 | +|---|---|---| +| ✅ | ✅ | 🎉 **全通** → 直接进 Phase 2 配对,**1080p120 满血**,成本只有流量 | +| ✅ | ❌ | 手机热点不给下游 IPv6 → 改用 **USB 共享 / 蓝牙共享**,或退路径 B 中继 | +| ❌ | ✅ | 家里网关拦入站 → 按 ② 放行网关 IPv6 防火墙 | +| ❌ | ❌ | 两条都堵 → 走路径 B(腾讯云中继);或再争取公网 IPv4 / 换运营商 | + +--- + +### ② 若卡在「家里网关拦 IPv6 入站」:三种放行办法 + +天翼网关**默认拦截 IPv6 入站**(IPv6 无 NAT,安全性全靠网关防火墙),这是此前手机测试失败的最大嫌疑。 + +| 办法 | 操作 | 可行性 | +|---|---|---| +| **1. 网关里关 IPv6 防火墙** | 登录 `192.168.1.1` → 安全 / 防火墙 → **IPv6 防火墙** → 关闭或调成"低" | 天翼网关多数型号有此开关;**需要后台密码**(找回阶梯见 §1.4) | +| **2. 光猫改桥接 + 自备路由器** | 让天翼网关只做光猫,用你自己的路由器 PPPoE 拨号 → 完全掌控 IPv6 防火墙与端口放行 | 最彻底,但**改错会全屋断网**,务必在家时操作 | +| **3. 退路径 B** | 不做入站放行,改用腾讯云中继 | 一定能通,但画质降到 1080p60 | + +> ⚠️ **安全提醒**:IPv6 是"无 NAT 直通" —— 放行意味着那个地址**所有端口都对公网可见**。建议只放行串流必要端口(`47984-48010` / `47989` / `48010`),用完可关。别把 Windows 的 `445` / `3389` 之类暴露出去。 + +--- + +### ③ 提前准备:流量(步骤 2 通过的话) + +- 1080p120 @ 20 Mbps ≈ **9 GB/小时**;打 3 小时 ≈ 27 GB +- **建议单独办一张大流量副卡**(或用现有套餐先试一晚测准真实消耗) +- 流量吃紧时的降档方案:1080p60 @ 8 Mbps ≈ 3.6 GB/小时(画质换成本) + +--- + +### ④ 顺手把家里电源设置改掉(1 分钟) + +`设置 → 系统 → 电源和电池 → 屏幕和睡眠`:**关闭屏幕 = 从不**、**睡眠 = 从不**。否则人不在家时显示器关闭会导致采集黑屏。 + +--- + +### ① ToDesk 连接方式 —— ✅ **已由 AI 从被控端反查完成,你不用再找状态栏** + +你反馈"找不到状态栏"(新版 ToDesk 把 P2P / 中转标识收进了设备详情面板,确实不好找)。我改用更硬的办法:**直接在家里主机的进程层面抓 ToDesk 的实时连接**,结论见 §1.6。摘要: + +| 抓到什么 | 说明 | +|---|---| +| 只与 ToDesk 自家服务器 (`219.151.138.225:443/8080`、`118.24.225.33:443`) 保持 TCP 长连接 | 走厂商侧 | +| 与公司出口 `117.188.24.89` **无任何 TCP 直连记录** | 至少 TCP 层没有直连 | +| 文件传输会话 `planb:1 ... p2p:0` | 该会话明确走**中转** | +| ✅ **但公司能出站到 8080 非标端口** | **重要利好**(说明不是 80/443 白名单) | + +**这一项已完成,不用再花时间找界面标识。** 真正还缺的是 UDP 侧的硬数据 → 见 ②。 + +### ② 公司网络实测 —— ✅ **已完成**(`net-test-v2.ps1` 结果已回) + +脚本跑通,原始结果见 `net-test-v2_20260910_163314.txt`。核心数据: + +| 测试项 | 结果 | 判决 | +|---|---|---| +| IPv6 | `IPv6 local : NONE` | ❌ 纯 IPv4 | +| **TCP 出站** | `baidu:443` / `qq:443` / `aliyun:80` / `portquiz:443,8080,8443` **全 OPEN**;**你腾讯云的 `80/443/22025` 也全 OPEN** | ✅ **任意端口出站自由** | +| UDP 出站 | 多台 STUN 均收到回包(`stun.qq.com` 超时是它自身问题) | ✅ UDP 能出能回 | +| **NAT 类型** | 同一本地端口 `46001` 问不同 STUN → 映射端口 `16993 / 16994 / 17011 / 43681` **各不相同**;出口 IP 出现 `117.188.24.89` 与 `117.188.118.254` **两个** | ❌ **对称 NAT + 多出口 → 打洞不可行** | +| 管理员权限 | 当前会话非管理员,但账号 `ybtdevxxl` **在 Administrators 组内** | ⚠️ 可提权(点 UAC 即可) | +| 已装软件 | `Clash Verge` / `WireGuard` / `ToDesk` / 火绒 | 🎁 WireGuard 现成(但打洞已判死,用处有限) | +| 安全软件 | **火绒 `HipsDaemon` + `HRWSCCtrl` 运行中**;`aTrust` / `edr_monitor` / `savsvc` / Defender 全系 **Stopped** | ⚠️ 火绒会拦驱动;但**无强制 EDR 管控**(好消息) | + +> 这轮也验证了 v2 脚本的价值:v1 报的"端口白名单"确是误判,v2 用真实靶子测出**出站完全自由** —— 这条结论直接支撑了路径 B(frp 中继)的可行性判断。 + +### ⑤(可选,长期)公网 IPv4 仍值得换个渠道再试一次 + +被拒不代表永久关闭。它是**延迟最低、画质最好、不依赖任何第三方**的方案,值得换个说法/渠道再争取: + +| 渠道 | 话术 / 要点 | +|---|---| +| 再打 10000 | 换时段、换客服;说法改成:"现在分到 `100.64` 开头的内网地址,**公司 VPN 连不回家里的 NAS**,需要公网 IP 才能访问" | +| 小翼管家 App | 部分地区有自助申请入口,比电话更快 | +| 线下营业厅 | 当面申请,权限通常比电话客服大 | +| 换运营商 / 换套餐 | 移动、联通在部分地区对公网 IP 更宽松;也可直接问"企业宽带"报价 | +| 工信部申诉 | **最后手段**(CGNAT 用户申请公网 IP 属合理诉求),耗时长,前三招先走完 | + +> 一旦拿到公网 IPv4,**立刻切回路径 A′** —— 那才是这个方案的最优形态(零第三方、延迟最低、公司端零安装)。 + +### 附:原 v4 的申请话术(留档) + +> "我家里需要在外网访问自己的监控和 NAS,现在分到的是 100.64 开头的内网地址,能不能帮我换成公网 IP?" + +> 申请时**强调"不用开网站、不需要备案域名",纯自用监控 / NAS**,通过率更高。 + +--- + +## 附 C:v2 阶段的 IPv6 入站验证过程(已完成,留存备查) + +### ① 手机测 IPv6 入站(1 分钟,决定性) +家里当前**已开着临时监听**(端口 38443,**带访问日志版**)。请: + +1. 手机关掉 WiFi,**用 4G/5G 流量** +2. 浏览器打开: + ``` + http://[240e:338:263:3600:e591:cb62:3503:8a23]:38443/ + ``` +3. 看到 `IPV6-INBOUND-OK` → ✅ **网关放行 IPv6 入站,走路径 1(最优)** + 打不开 / 超时 → 网关拦了入站,走路径 2(打洞),或登录网关放行 + +> 注意:**不要用 ping 判断**。Windows 默认拦掉 ICMPv6 入站,ping 不通不代表 IPv6 不可用,必须用这个 TCP 测试。 + +**日志验证法(本次新增,用于排除误判)** + +监听会把每个到达的请求写入日志,因此判定不依赖手机浏览器是否正确渲染: + +| 现象 | 日志 | 判定 | +|---|---|---| +| 手机看到 `IPV6-INBOUND-OK` | 有记录 | ✅ 入站通 | +| 手机页面打不开 | **有记录** | ✅ 入站通(只是浏览器/网速问题) | +| 手机页面打不开 | **无记录** | ❌ 入站被网关拦截 | + +日志文件:`%TEMP%\wb_ipv6test\inbound.log`(形如 `[2026-09-10T07:42:50Z] REQ from=<手机地址> ...`) + +> 监听进程随会话结束可能失效。**测试前若间隔较久,让 AI 重新起一次**(几十秒即可)。 +> 另:之前那个残留在 38080 端口的旧监听本次已成功清理,端口已释放。 + +#### ⚠️ 日志法的一个重要局限(本次踩坑,2026-09-10 修正) + +临时监听是用 `http.createServer` 实现的,**只在收到完整 HTTP 请求时才写日志**。 +因此:**纯 TCP 探测(如 ping.pe 的端口检查)只做三次握手、不发 HTTP 请求,连接成功也不会留下日志。** + +结论:**"ping.pe 显示 Connected" 与 "日志无记录" 可以同时成立,二者不矛盾**。 +- 判断"TCP 层是否可达" → 看 ping.pe / itdog 等外部探测结果 +- 判断"HTTP 层是否可达" → 看日志 + 手机浏览器实际渲染 + +#### 本次实测记录(2026-09-10) + +| 时间 | 测试方式 | 结果 | 有效? | +|---|---|---|---| +| 15:42 | 本机 PowerShell 访问公网地址(内网回环) | HTTP 200,日志有记录 | 仅证明本机自通 | +| ~15:50 | 手机 **连 WiFi** 访问 | 打不开 | ❌ 无效(手机此时无 IPv6,且走回环易误判) | +| ~15:55 | 手机 **4G/5G** 访问 | 页面无响应、不报错;**日志无记录** | ✅ 有效,HTTP 层未到达 | +| ~16:00 | 手机扫码 **tcp6.ping.pe** 探测 | 用户反馈"通了" | ✅ TCP 层可达(待复核细节) | + +**综合判定**:**外网 → 家里的 TCP 层大概率可达**(ping.pe 探测), +但手机浏览器的 HTTP 请求未到达 —— 二者差异可能来自手机侧(运营商 IPv6 路由 / 浏览器限制非标端口 http)。 +**下一步用真实业务端口复核**:Sunshine 装好后,用 Moonlight 直接从公司连,成不成是最终标准。 +(Sunshine 用 47989/47984/48010 等端口,与 38443 不同,网关若做端口白名单需另放行。) + +### ② 把自测脚本带到公司电脑跑一遍(**旧版,已跑完,已被 `net-test-v2.ps1` 取代**) +文件:`公司电脑网络自测.ps1`(同目录) + +``` +先在开始菜单打开 Windows PowerShell,然后: +powershell -ExecutionPolicy Bypass -File "公司电脑网络自测.ps1" +``` + +它会输出一台设备「能不能支撑远程串流」的完整画像,重点看三行: +- `IPv6 出网 : ...` → 决定能否走 IPv6 直连 +- `STUN ... -> 通` 与 `NAT 判断 : ...` → 决定打洞可行性 +- `家里 IPv6:38443 -> ...` → 若这行是"通",说明**从公司到家里已经打通了** + +--- + +## 附 A:端口与命令 + +```powershell +# Sunshine 需要的入站规则(安装时一般会自动创建,失败时手动补) +New-NetFirewallRule -DisplayName "Sunshine TCP" -Direction Inbound -Protocol TCP -LocalPort 47984,47989,47990,48010 -Action Allow +New-NetFirewallRule -DisplayName "Sunshine UDP" -Direction Inbound -Protocol UDP -LocalPort 47998-48000,48002 -Action Allow + +# Moonlight 命令行示例(1080p120 / 22Mbps) +moonlight stream <家里IPv6> "Desktop" -1080 -120fps -bitrate 22000 + +# Moonlight 快捷键 +# Ctrl+Alt+Shift+S 性能浮层 Ctrl+Alt+Shift+Z 切换鼠标捕获 Ctrl+Alt+Shift+Q 退出串流 +``` + +**已知坑** +- 显示器进入睡眠 → Sunshine 抓不到画面、串流黑屏,务必关闭显示器休眠 +- 多显示器时需指定 `output_name`,指错会串到错误屏幕或全黑 +- 独占全屏可能因分辨率切换导致采集中断 → 统一用"无边框窗口" +- 若走端口转发(本方案用不到),务必改非默认端口 + Sunshine Web UI 用强密码 + +## 附 B:工程备注(本机探测踩过的坑) + +- 本机 PowerShell 工具的 stdout 会被吞掉 → 需要把结果 `Out-File` 落盘再用 Read 读 +- PowerShell 里 `[byte] -shl 8` 会溢出归零,字节拼 16 位整数必须先 `[int]` 转换 +- 用 `Get-Content -Raw` + `Out-File` 加 BOM 会把 UTF-8 中文按 GBK 解码后重写而**永久损坏文件**;加 BOM 应使用字节级操作 `WriteAllBytes` 前置 `EF BB BF` +- 本机 PATH 被裁剪,脚本里调用 `ping` / `netsh` 会找不到 → 用 `$env:SystemRoot\System32\` 绝对路径 +- 一批防火墙入站规则指向的 node.exe 路径已不存在(失效规则),`C:\Users\xxl\AppData\Roaming\nvm\v23.0.0\node.exe` 才有有效的 Public 档全端口放行规则 +- **"从外部验证 IPv6 入站"没有可用的自动化手段**(2026-09-10 实测全部失败),必须用真实外部设备(手机 4G)验证: + - `check-host.net` API:`check-tcp` 对 IPv6 一律返回 `{"error":"invalid_url"}`(带/不带方括号、带 http:// 前缀都试过) + - `ping.pe` / `tcp6.ping.pe`:接受 IPv6 目标,但结果是 **JS 异步渲染**,静态抓取只能拿到 `Connecting to ...` 与 `Snapshot readiness: waiting_for_results` + - 公共 CORS 代理(codetabs / allorigins / corsproxy / thingproxy / Jina Reader):要么 522、要么需要 API key、要么直接被 GFW 拦截 + - `hackertarget.com/nmap`:已改为需要 API key +- 因此本项目采用**「手机 TCP 测试 + 服务端访问日志」双证据**方式判定入站可达性 + +### ⚠️ 附 B 补充(v3,2026-09-10):两个"看起来专业、其实是错的"测试陷阱 + +这两条是本项目**已经踩过并已修正**的坑,以后再写网络探测脚本必须避开: + +**陷阱 1:NAT 类型判定 —— 每个 STUN 换一个新 socket** +- ❌ 错误做法:`for each stun: 新建 socket → sendto → 看映射端口`。因为**本地端口每次都变了**,运营商/网关必然分配不同的公网映射端口 —— 这是任何 NAT 类型都会出现的结果,**不能推出"对称 NAT"** +- ✅ 正确做法:**同一个 socket(同一个本地端口)连续问多个 STUN 服务器**,比较映射端口是否一致: + - 一致 → 端点无关映射(Cone / EIM)→ 打洞可行 + - 不一致 → 对称 NAT → 打洞难 +- 本项目实测(正确方法):本地 55001 → 恒为 `1.204.100.17:3051`;55002 → 恒为 `:3063` → **结论是锥形 NAT,v2 的"对称 NAT"是误判** + +**陷阱 2:判断"端口白名单"时用了根本没在监听的靶子** +- ❌ 错误做法:连 `114.114.114.114:8080` 失败 → 就断定"公司封了非标端口"。实际上**那个 DNS 服务器压根没开 8080**,失败是必然的,跟防火墙无关。同理 `github.com:443` 在国内常被 DNS 污染/阻断,也**不能**作为"443 被封"的证据 +- ✅ 正确做法:用**确定在监听该端口**的靶子,且最好有"全端口监听"型服务: + - `www.baidu.com:443`、`www.qq.com:443`(国内基线,验证 443 是否真的可用) + - `portquiz.net`(**监听所有 TCP 端口**,在它上面测 8080/8443 才有意义) + - 自有服务器 `193.112.118.168:80/443`(nginx 确实在监听) +- 判别逻辑:`REFUSED`(立即拒绝)= **路径是通的**,只是对端没开这个端口;`TIMEOUT`(一直等)= **真的被拦或丢包** + +**陷阱 3(方法论):日志监听只能证明 HTTP 层,不能否定 TCP 层** +- `http.createServer` 只在收到完整 HTTP 请求时写日志。纯 TCP 端口探测(三次握手即断)**成功也不会留痕** +- 所以"探测显示 Connected"与"日志空白"可以同时为真,**不可据此判定入站被拦**