# SCLiteLink **Repository Path**: SC-SPM/SCLiteLink ## Basic Information - **Project Name**: SCLiteLink - **Description**: 专为 SurvivalCraft (SCNET) 联机版设计的高性能、高可用网络连结枢纽。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-30 - **Last Updated**: 2026-10-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SCLiteLink > **主仓库已迁至 CNB**: > > 开发、Issue、PR 请统一在 CNB 进行。Gitee()是由流水线自动镜像的**只读副本**,不接受提交,在那里提的 Issue / PR 不会被处理。 SC 联机的**打洞转发服务器**:让玩家无需公网 IP、无需配置路由器即可开房间联机,并允许第三方贡献转发节点。 - 协议规范(唯一真源):[`docs/PROTOCOL.md`](docs/PROTOCOL.md) - 打洞方案设计:[`docs/联机打洞方案设计报告.md`](docs/联机打洞方案设计报告.md) - NAT 矩阵联调手册:[`docs/联机NAT矩阵联调runbook.md`](docs/联机NAT矩阵联调runbook.md) - 安全审计:[`docs/安全审计报告.md`](docs/安全审计报告.md) - C# 客户端实现位于游戏仓库 `sc2.2network/Survivalcraft/NetWork/ScLiteLink/`(随游戏编译,无独立库)。 --- ## 目录 - [一、介绍](#一介绍) - [二、在系统里的位置](#二在系统里的位置) - [三、连接路径按序降级](#三连接路径按序降级) - [四、快速开始](#四快速开始) - [五、配置文件详解(config.json)](#五配置文件详解configjson) - [六、客户端配置详解(ScLiteLinkConfig.json)](#六客户端配置详解scLiteLinkconfigjson) - [七、编译与命令行](#七编译与命令行) - [八、部署与运维](#八部署与运维) - [九、工作原理](#九工作原理) - [十、安全](#十安全) - [十一、目录结构](#十一目录结构) - [十二、测试](#十二测试) --- ## 一、介绍 SCLiteLink 解决的核心问题:**房主通常在 NAT / 防火墙之后,没有公网可达的入站地址**,访客无法直接连进来。 它提供两条互补的能力: 1. **UDP 打洞(P2P)** —— 房主与访客各自经 STUN 发现反射(公网)地址,再经 ScKey 的 `/room/punch/exchange` 互相交换,双向打洞后直连,不占中继带宽。 2. **中继转发(Relay)** —— 唯一 **100% 可达** 的兜底路径:两端都主动出站到 SCLiteLink 节点,由节点做 UDP 双向转发。对称型 NAT、CGNAT、多重 NAT 等打洞不可达的场景全部退回中继。 > 打洞的「地址协调」发生在 ScKey 后端;SCLiteLink 节点负责把**中继流量**可靠地转发,并在打洞不可达时作为兜底。两者由 ScKey 统一调度(节点注册、健康检查、可用节点下发、路径选择)。 ## 二、在系统里的位置 | 项目 | 职责 | |---|---| | `sc2.2network` | 游戏。房主进程注册房间、访客侧按路径选路(含 STUN/UPnP/v6 探测与打洞) | | `ScKey` | 后端。房间管理、票据签发、节点注册与健康检查、打洞端点交换、下发可用节点列表 | | `sckey-ui` | 前端。联机房间大厅、我的转发节点、平台审核 | | **SCLiteLink** | **本项目(Go 单二进制)**。UDP 中继转发 + 节点注册/心跳 + 反射端点发现 | ## 三、连接路径按序降级 访客侧选路(详见 `ScLiteLinkRoomClient.SelectCandidate`): ``` IPv6 直连 → 内网直连 → UDP 打洞(P2P) → Relay 中继(兜底) (无NAT) (同局域网) (STUN+交换+双向打洞) (两端均主动出站,100% 可达) ``` - 房主 NAT 可被打洞(`hostNatType` ∈ 未知 / 无NAT / 锥形)且本端启用打洞时,才会出现「打洞」候选; 对称型 / CGNAT / 多重 NAT / 阻断型房主 NAT 不打洞,直接退回中继。 - 每条路径命中失败时**不**自动运行时降级(避免玩家卡在加载页几十秒),而是在「加入房间」时一次性按本地能力判定,判不了的(如 IPv6 入站被上级路由禁止)统一落到中继。 --- ## 四、快速开始 ### 1. 生成节点令牌 在 `sckey-ui` → 会话中心 → 访问令牌,创建一个 `purpose = server` 的令牌(形如 `sck_...`)。 ### 2. 写配置 ```bash cp config.example.json config.json ``` 必填三项: | 字段 | 说明 | |---|---| | `node.id` | 节点唯一 ID,小写字母数字与 `-_.`,最长 64 | | `node.token` | 上一步生成的 `sck_` 令牌 | | `coordinator.hmacKey` | 与 ScKey 共享的票据 HMAC 密钥(≥16 位),**不要提交到仓库** | ### 3. 校验并启动 ```bash sclitelink -config config.json -check # 只校验配置后退出 sclitelink -config config.json # 前台运行 ``` ### 4. 放行防火墙 只需放开**一个 UDP 主端口**入站(默认 `41000`)以及**端口池**范围(默认 `41001–42000`)。 所有 HTTP 均为**出站**方向,无需任何入站 TCP。 ```bash # iptables 示例 iptables -A INPUT -p udp --dport 41000 -j ACCEPT iptables -A INPUT -p udp --dport 41001:42000 -j ACCEPT ``` --- ## 五、配置文件详解(config.json) 配置文件为 JSON(刻意不用 YAML:零外部依赖、单文件即可部署,降低第三方贡献者门槛)。 以 `config.example.json` 为模板,**不要提交含真实 `token` / `hmacKey` 的 `config.json`**。 完整字段表(默认值见 [5.8](#58-默认值)): ### 5.1 `node` —— 节点身份 | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `id` | string | ✅ | — | 节点唯一 ID,小写字母数字与 `-_.`,最长 64 | | `name` | string | ❌ | 空 | 展示名(前端节点列表可见) | | `region` | string | ❌ | 空 | 区域(如「华南」「华北」),用于就近调度 | | `isp` | string | ❌ | 空 | 运营商(如「电信」「联通」) | | `token` | string | ✅ | — | ScKey 签发的 `sck_` 服务器令牌 | | `capacity` | int | ❌ | 0 | 自报带宽上限(Mbps),用于调度 | | `version` | string | ❌ | 由 `-ldflags` 注入 | 软件版本,上报 ScKey;为空时取编译期版本 | | `publicEndpoint` | string | ❌ | 空 | 本节点对外可直连的地址,形如 `1.2.3.4:41000`。**必须为 IP 字面量**,不支持域名。留空则尝试 STUN 自动探测 | | `stunServers` | string[] | ❌ | 内置公共列表 | 用于自动探测公网地址的 STUN 服务器。显式配置 `[]` 可关闭探测 | > **⚠️ 关于 `publicEndpoint` —— 节点位于 CDN 或 NAT 之后时必须处理** > > 注册时若未上报地址,ScKey 会**按 HTTP 请求的来源 IP 推断**节点地址。当节点在 > CDN(如 EdgeOne)或 NAT 网关之后时,后端看到的是 CDN 回源节点的 IP 而非节点真实 > 公网 IP,于是客户端会拿到一个错误地址,**连不上中继**。 > > 地址解析优先级:`publicEndpoint`(手工) → STUN 探测 → 后端按来源 IP 推断。 > > - **节点直接暴露公网**(有独立公网 IP、无 CDN):留空即可,无需额外配置。 > - **节点在 NAT 之后但有端口映射**:填 `publicEndpoint` 最可靠。 > - **节点在 CDN 之后**:必须显式填 `publicEndpoint`。STUN 探测出的 IP 虽然是节点 > 真实出口 IP,但 CDN 回源流量可能落在别的链路上,仍建议手工指定。 > - STUN 探测只取回公网 IP,端口固定用本节点监听端口(STUN 返回的是 NAT 映射的 > 临时源端口,不可用于对外服务)。 ### 5.2 `listen` —— 监听 | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `addr` | string | ❌ | `:41000` | 主监听地址(UDP)。为 `:41000` 表示监听所有网卡 | | `ipv6` | bool | ❌ | `false` | 是否同时监听 IPv6(双栈) | ### 5.3 `portPool` —— 每访客独立端口范围 每个访客分配一个独立 UDP socket(详见[第九章](#九工作原理)),因此需要一个端口区间。 | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `min` | int | ❌ | `41001` | 端口池下界(含) | | `max` | int | ❌ | `42000` | 端口池上界(含)。约束:`min < max` 且 `max - min ≤ 60000` | ### 5.4 `coordinator` —— 与 ScKey 通信 | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `baseUrl` | string | ❌ | `https://api.sckey.net` | ScKey 后端地址(自动去除结尾 `/`) | | `hmacKey` | string | ✅ | — | 票据 HMAC 共享密钥(≥16 位),与 ScKey 一致 | | `timeoutSec` | int | ❌ | `10` | 单次 HTTP 请求超时(秒) | | `heartbeatSec` | int | ❌ | `30` | 心跳间隔(秒) | ### 5.5 `limits` —— 限速与配额 所有阈值缺省时自动取安全默认值(见 [5.8](#58-默认值)),避免「阈值为 0 导致全拒」这类隐蔽问题。 | 字段 | 类型 | 默认值 | 说明 | |---|---|---|---| | `unauthorizedPps` | int | `20` | 未授权源 IP 的包速率上限(pps) | | `probeEchoPps` | int | `5` | 健康探测(`PROBE_ECHO`)单源速率上限(pps),响应体不大于请求体以防放大 | | `sessionPps` | int | `600` | 单会话包速率(pps) | | `sessionBps` | int | `8_000_000` | 单会话字节速率(≈8 Mbps) | | `totalMbps` | int | `100` | 节点总带宽上限(Mbps) | | `maxSessions` | int | `200` | 最大并发会话数 | | `maxGuestsPerRoom` | int | `16` | 单房间最大访客数 | | `roomQuotaBytes` | int64 | `2_147_483_648` | 单房间流量配额(≈2 GiB),超出后**降速而非断开** | | `throttleBps` | int | `2_000_000` | 超配额后的降速目标(≈2 Mbps) | > ScKey 也可在心跳响应中下发运行时 `NodeConf`(`maxSessions` / `sessionBps` / `totalMbps` / `trustLevel`),覆盖本地配置,用于临时调度。 ### 5.6 `ppv2` —— Proxy Protocol v2 | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `enabled` | bool | ❌ | `true`(强制) | 向房主转发时是否附 PPv2 头携带访客真实 IP | > 一期强制开启:房主侧依赖 PPv2 拿到访客真实 IP,否则经同一中继的玩家会被误判为同一 IP。 ### 5.7 `log` —— 日志 | 字段 | 类型 | 必填 | 默认值 | 说明 | |---|---|---|---|---| | `level` | string | ❌ | `info` | 日志级别:`debug` / `info` / `warn` / `error` | | `file` | string | ❌ | 空 | 日志文件路径;为空则只输出 stdout | ### 5.8 默认值 | 配置项 | 默认值 | |---|---| | `listen.addr` | `:41000` | | `portPool.min` / `portPool.max` | `41001` / `42000` | | `coordinator.baseUrl` | `https://api.sckey.net` | | `coordinator.timeoutSec` / `heartbeatSec` | `10` / `30` | | `limits.unauthorizedPps` / `probeEchoPps` | `20` / `5` | | `limits.sessionPps` / `sessionBps` | `600` / `8_000_000` | | `limits.totalMbps` / `maxSessions` / `maxGuestsPerRoom` | `100` / `200` / `16` | | `limits.roomQuotaBytes` / `throttleBps` | `2_147_483_648` / `2_000_000` | | `log.level` | `info` | | `ppv2.enabled` | `true`(强制) | | `node.publicEndpoint` | 空(依次尝试 STUN 探测 → 后端按来源 IP 推断) | | `node.stunServers` | `stun.miwifi.com:3478`, `stun.qq.com:3478`, `stun.cloudflare.com:3478`, `stun.l.google.com:19302` | ### 5.9 校验规则(`-check`) - `node.id` 非空且 ≤ 64 字符; - `node.token` 非空且以 `sck_` 开头; - `coordinator.hmacKey` 非空且 ≥ 16 字符; - `portPool.min < portPool.max` 且 `max - min ≤ 60000`; - `node.publicEndpoint` 若填写,必须是 `IP:端口` 形式(IP 字面量,不接受域名,端口 1–65535)。 --- ## 六、客户端配置详解(ScLiteLinkConfig.json) 游戏侧(房主 / 访客进程)在运行时把 `ScLiteLinkConfig.json` 写在可执行文件同目录(`AppContext.BaseDirectory`),缺失时自动写入默认模板,所有字段均有默认值,文件损坏不影响启动。 | 字段 | 类型 | 默认值 | 说明 | |---|---|---|---| | `enableHolePunch` | bool | `true` | 是否尝试 UDP 打洞(P2P)替代中继 | | `enableUpnp` | bool | `true` | 是否对上级网关做 UPnP IGD 端口映射,提升入站可达性 | | `enableForwarding` | bool | `true` | **本实例是否作为转发节点**。`false` = **单打洞服务器**:只参与 NAT 打洞协调,**不承载中继流量**。房主在心跳中将该值上报 ScKey,ScKey 据此决定是否把它分配去做中继(`tb_sc_relay_node.forwarding_enabled`) | | `preferV6Inbound` | bool | `true` | 是否优先尝试 IPv6 直连(含入站可达性探测) | | `stunServers` | string[] | `stun.l.google.com:19302`, `stun1.l.google.com:19302`, `stun.cloudflare.com:3478` | STUN 服务器列表,用于发现反射地址与 NAT 类型判定 | | `natProbeTimeoutMs` | int | `3000` | 单次 STUN 探测超时 | | `upnpTimeoutMs` | int | `2000` | UPnP 网关发现 / 映射超时 | | `v6InboundProbeTimeoutMs` | int | `1000` | IPv6 入站探测超时 | | `punchAttempts` | int | `30` | 打洞突发包发送轮数 | | `punchIntervalMs` | int | `100` | 打洞突发包发送间隔 | > **单打洞服务器**模式(关注点):把某节点的 `enableForwarding` 设为 `false`,ScKey 在 `requireForwarding=true` 的节点筛选中会把它排除,因此它不会被分配中继流量,只作为打洞协调的「反射 / 探测」参与者存在。 --- ## 七、编译与命令行 ### 7.1 编译 ```bash go build -o sclitelink ./cmd/sclitelink ``` 零外部依赖,单二进制。注入版本号: ```bash go build -ldflags "-X main.version=1.2.0" -o sclitelink ./cmd/sclitelink ``` ### 7.2 交叉编译(给 Linux 服务器) ```bash GOOS=linux GOARCH=amd64 go build -o sclitelink ./cmd/sclitelink # ARM64 GOOS=linux GOARCH=arm64 go build -o sclitelink ./cmd/sclitelink ``` ### 7.3 命令行参数 | 参数 | 说明 | 默认 | |---|---|---| | `-config ` | 配置文件路径 | `config.json` | | `-check` | 只校验配置后退出(不启动服务) | — | | `-version` | 输出版本后退出 | — | --- ## 八、部署与运维 ### 8.1 systemd `deploy/sclitelink.service` 已提供单元文件。典型用法: ```bash cp deploy/sclitelink.service /etc/systemd/system/ systemctl daemon-reload systemctl enable --now sclitelink journalctl -u sclitelink -f ``` 服务以 `SIGINT` / `SIGTERM` 优雅关停(先 `Shutdown()` 再退出)。 ### 8.2 防火墙 见 [第四章 4.4](#4-放行防火墙):放行 UDP 主端口 + 端口池,无需入站 TCP。 ### 8.3 节点注册与心跳 - 启动即向 ScKey 注册(`Register`);失败不阻断启动,后台每 60 秒重试。 - 注册成功后按 `heartbeatSec` 周期心跳,上报 `sessions / peers / portPoolUsed / bytesUp/Down / loadPercent / uptimeSec`。 - ScKey 心跳响应可下发: - `command`:`""` 正常 / `"drain"` 排空(不再接新会话)/ `"disable"` 停用; - `config`(`NodeConf`):临时覆盖 `maxSessions` / `sessionBps` / `totalMbps` / `trustLevel`。 ### 8.4 反射端点发现 房主可经中继发送 `HELLO`,中继回 `HELLO_ACK` 携带中继侧观测到的房主源地址,用于房主发现自身反射(公网)端点(与 STUN 互补)。 --- ## 九、工作原理 ### 9.1 为什么每个访客占用一个端口 游戏用 `IPEndPoint`(IP + 端口)区分 peer。如果所有访客都从**中继主端口**进入房主侧,房主会把他们识别为**同一个人**。因此中继为**每个访客分配独立的 UDP socket**: ``` Guest ──主端口(41000)──▶ Relay ──访客端口──▶ Host (附 PPv2 头,携带访客真实 IP) Host ──访客端口──────▶ Relay ──主端口──────▶ Guest ``` 房主侧看到的入站 IP 全是中继 IP,故转发时附 **Proxy Protocol v2** 头。房主需在 `ServerSetting.json` 开启: ```json "ProxyProtocolV2Enabled": true, "ProxyProtocolV2TrustedProxies": "<中继IP或CIDR>" ``` > ⚠️ 房主侧的「每 IP 连接数限制」与封禁逻辑必须改走 `ProxyProtocolV2PacketLayer.TryGetRealClient()` 拿到的真实 IP,否则经同一中继的玩家会被误判为同一 IP。 ### 9.2 打洞与中继的分工 - **打洞(P2P)**:由游戏侧 STUN 探测 + ScKey `/room/punch/exchange` 交换反射地址完成,SCLiteLink 不直接参与数据面。 - **中继(兜底)**:当打洞不可达(对称 / CGNAT / 多重 NAT / IPv6 入站被禁等),访客与房主都主动出站到 SCLiteLink 节点,由节点做 UDP 双向转发。 --- ## 十、安全 中继是公网服务,必须防被当作 DDoS 放大器。已实现的防线: 1. **magic 白名单**:非 `SL` 开头的未授权包直接丢弃; 2. **HMAC 票据**:每个 `BIND` / `JOIN` 必须携带 ScKey 签发的票据,本地校验零往返,含 nonce 防重放; 3. **未授权源限流**:默认 20 pps; 4. **健康检查不放大**:`PROBE_ECHO` 响应体不大于请求体,单源 5 pps; 5. **单会话双令牌桶**:600 pps / 8 Mbps; 6. **超配额降速而非断开**:单房间超出 2 GiB 后降到 2 Mbps。 详见 [`docs/安全审计报告.md`](docs/安全审计报告.md)。 --- ## 十一、目录结构 ``` cmd/sclitelink/ 可执行程序入口(flag 解析、注册重试、优雅关停) internal/config/ 配置解析(JSON,零依赖,含默认值与校验) internal/protocol/ 协议编解码与 PPv2 封装 internal/ticket/ HMAC 票据校验(含 nonce 防重放) internal/limits/ 令牌桶与计数器 internal/coord/ 与 ScKey 的 HTTP 通信(注册/心跳/会话上报) internal/relay/ 转发引擎(会话模型 + 主循环) internal/logx/ 极简分级日志 docs/ 协议规范与设计报告 deploy/ systemd 单元文件 ``` --- ## 十二、测试 ```bash go test ./... ``` `internal/relay/e2e_test.go` 覆盖完整转发闭环:房主绑定、访客加入、双向数据转发、PPv2 头正确性、错误票据拒绝、反射端点探测。