Files
PortRelay/README.md
T
DeepFal a9e6ca7080 docs: 授权延长在 README 里单独成节,主界面与动图带上延长申请
- 「申请延长」单独成节:同事端 ④⑤⑥ 与 App 的连接与授权截图放在一起,
  写明卡片提示、点「批准延长」选时长、最近结束的设备也可直接延长
- 演示数据:MySQL 通道带一条延长申请和一台最近结束的设备,主界面、动图、
  授权管理与网页面板展示同一组数据,卡片上能看到「申请延长 1」
2026-10-14 15:00:08 +08:00

333 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<p align="center">
<img src="mac-native/AppIcon.iconset/icon_256x256.png" width="96" alt="PortRelay">
</p>
<h1 align="center">PortRelay</h1>
<p align="center">
<b>把内网服务临时、可控地开放给同事:发一条邀请链接,到期自动收回。</b>
</p>
<p align="center">
<a href="https://github.com/DeepFal/PortRelay/actions/workflows/ci.yml"><img src="https://github.com/DeepFal/PortRelay/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="https://github.com/DeepFal/PortRelay/releases"><img src="https://img.shields.io/github/v/release/DeepFal/PortRelay?sort=semver" alt="Release"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a>
<img src="https://img.shields.io/github/go-mod/go-version/DeepFal/PortRelay" alt="Go">
<img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey" alt="Platform">
</p>
<p align="center">
简体中文 · <a href="README_EN.md">English</a>
</p>
<p align="center">
<img src="docs/images/architecture.svg" alt="PortRelay 架构" width="100%">
</p>
你的电脑能连上某个内网网段:数据库、跳板机、内部面板,但同事连不上。常见的做法是开 VPN 账号、改防火墙,或者干脆把端口对所有人放开,事后还经常忘记关。
PortRelay 把这件事变成三步:
1. **Agent** 把内网目标挂到 **边缘网关** 的一个端口上。
2. 你把**邀请链接**(或二维码)发给同事。
3. 同事打开链接、填写姓名,他的设备就获得一段时间的访问权。到期、闲置或被你撤销后,连接立即断开。
> [!TIP]
> **不依赖任何特定组网方案。** 只要 Agent 所在的机器**同时能连到目标网段和边缘网关**就能工作。Agent 到网关的线路随你选:同一局域网、公司 VPN、Tailscale、ZeroTier、WireGuard,或者网关本身有公网地址。Agent 只需主动连出,本机不用开放任何端口。
## 目录
- [功能](#功能)
- [界面一览](#界面一览)
- [工作原理](#工作原理)
- [快速开始](#快速开始)
- [配置参考](#配置参考)
- [安全模型](#安全模型)
- [常见问题](#常见问题)
- [开发](#开发)
## 功能
**访问控制**
- 三种访问方式:**仅授权链接**(默认,最安全)、IP / 网段白名单(可与授权链接叠加),以及全部放通。
- 邀请链接可以设置链接有效期、可用设备数、授权后可访问时长(30 分钟、1 小时、2 小时、8 小时、当天有效、通道开启期间,或自定义),以及是否需要审批。
- 需要审批时,App 会弹出系统通知,一键批准或拒绝;访问者的页面会自动刷新结果。
- **计时从首次连接开始**:同事拿到授权后不用急着连,开始用的时候才开始计时。
- 授权到期或被撤销后,**已建立的连接也会立刻断开**;未授权的连接直接被重置(TCP RST)并留下拦截记录。
- **申请延长**:同事重新打开原来的邀请链接,点「申请延长」,你在 App、菜单栏或网页面板里选好延长多久即可批准。授权还没到期时,从原到期时间往后顺延;已经结束的从批准时算起,结束后 2 小时内都能申请,通道关了也没关系,批准后会自动重新开启。被撤销的授权不能延长。你也可以主动给已授权或刚结束的设备延长。
- **通道组**:添加通道时目标是一个列表,每行一台主机、多个端口用逗号隔开(也可以直接粘贴一串 IP),每个地址一个通道,自动分配一段连续端口,并放进同一个通道组。组内地址共用一条邀请链接和同一份授权:同事授权一次即可访问整组,撤销一次全组断开。
**通道生命周期**
- **闲置自动关闭**:没有连接、没有有效授权、没有待处理申请,也没有还能使用的邀请链接时开始计时,默认 30 分钟后关闭。
- 可选的**最长开启时长**,到点无论是否在用都关闭。批准延长时,如果新的授权到期时间更晚,通道会开到授权结束。
- 通道组的闲置按整组计算:任一地址在用,整组都不会因闲置关闭,到时也是整组一起关。每个地址仍然可以单独开关。
- 记录首次连接时间、实际连接时长和每次关闭的原因。
- 编辑通道不会打断现有连接;更换端口时先绑定新端口再释放旧端口。
**可观测**
- 每个通道近 10 分钟的流量曲线,以及实时速率、连接数、本次与历史累计流量。
- 在线设备按 IP 合并展示,可逐条断开;历史接入台账同样按 IP 合并。
- 按通道限速(MB/s)。
**客户端**
- 原生 macOS 菜单栏 App(SwiftUI):关闭窗口只保留菜单栏图标,从菜单栏退出时连同后台 Agent 一起退出。
- 内嵌网页面板,Linux、Windows 上也能用同一套功能。
- 通道搜索(支持拼音首字母,比如输入 `sjk` 就能找到“数据库”),以及按运行状态筛选。
## 界面一览
<p align="center">
<img src="docs/images/live-traffic.gif" alt="通道卡片与实时流量" width="100%">
<br><sub>通道卡片:访问地址、待处理的访问与延长申请、实际连接时长,以及近 10 分钟流量(动图为加速演示)</sub>
</p>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/app-main-dark.png">
<img src="docs/images/app-main-light.png" alt="PortRelay macOS App 主界面">
</picture>
**邀请与授权**:生成链接后得到一段可直接发送的文案和二维码。同事在手机或电脑上打开链接填写姓名,需要审批时进入等待页,批准后页面自动更新。
<p align="center">
<img src="docs/images/app-invite.png" alt="邀请分享卡片" width="100%">
</p>
<table>
<tr>
<td align="center"><img src="docs/images/auth-form.png" alt="填写姓名" width="100%"><br><sub>① 打开邀请链接</sub></td>
<td align="center"><img src="docs/images/auth-pending.png" alt="等待审批" width="100%"><br><sub>② 等待管理员批准(可选)</sub></td>
<td align="center"><img src="docs/images/auth-ok.png" alt="授权成功" width="100%"><br><sub>③ 授权成功</sub></td>
</tr>
</table>
**申请延长**:授权快到期或刚结束时,同事再打开同一个链接,就能看到自己的授权状态并申请延长,不用你重新发邀请。
<table>
<tr>
<td align="center"><img src="docs/images/auth-renew.png" alt="申请延长" width="100%"><br><sub>④ 再次打开链接,申请延长</sub></td>
<td align="center"><img src="docs/images/auth-renew-pending.png" alt="等待批准延长" width="100%"><br><sub>⑤ 等待管理员选择时长</sub></td>
<td align="center"><img src="docs/images/auth-renew-ok.png" alt="延长已批准" width="100%"><br><sub>⑥ 延长已批准</sub></td>
</tr>
</table>
你这边,通道卡片上会出现「申请延长」。打开「连接与授权」,延长申请排在最前面,点「批准延长」选好时长即可,同事的页面随即显示新的到期时间。这里还列出待审批的访问申请、已授权设备(在线状态与剩余时间)、最近结束的授权和仍然有效的邀请链接,刚结束的设备也可以直接延长。
<p align="center">
<img src="docs/images/app-access.png" alt="连接与授权:延长申请、待审批、已授权设备、最近结束" width="80%">
</p>
**通道组**:组卡片汇总整组的状态与流量,下面逐行列出每个访问地址,可以单独开关、单独邀请或移出组。
<p align="center">
<img src="docs/images/app-group.png" alt="通道组" width="100%">
</p>
<details>
<summary><b>网页面板与更多截图</b></summary>
<br>
不使用 macOS App 时,Agent 自带网页面板,地址为 `http://127.0.0.1:33080`:
<img src="docs/images/web-panel.png" alt="网页面板">
<table>
<tr>
<td align="center"><img src="docs/images/app-edit.png" alt="编辑通道"><br><sub>编辑通道:访问方式、闲置关闭、最长时长</sub></td>
<td align="center"><img src="docs/images/app-clients.png" alt="在线设备"><br><sub>在线设备:按 IP 合并,可逐条断开</sub></td>
</tr>
</table>
</details>
## 工作原理
<p align="center">
<img src="docs/images/invite-flow.svg" alt="邀请流程" width="100%">
</p>
- **边缘网关**(`portrelay-server`)运行在访问者能连到的机器上,管理一个端口池(默认 `33000-34000`)。`33000` 是控制端口:Agent 的隧道和访问者打开的授权网页共用这一个端口,网关根据请求开头自动分流(HTTP 请求进入授权网页,其余视为 Agent 会话)。
- **Agent**(`portrelay-agent`,macOS App 内置)运行在能访问目标网段的机器上。它主动连接网关的控制端口,先用 token 认证,再通过 [yamux](https://github.com/hashicorp/yamux) 在这条 TCP 连接上多路复用所有通道。
- 访问者连接 `网关:33660` 时,网关先检查白名单和授权租约。通过后在隧道里开一个流,由 Agent 连向 `10.0.1.16:3306`,双向转发。不通过则直接重置连接并记录。
- 授权租约、邀请和审批请求保存在网关的状态文件里,重启后仍然有效。通道配置、流量历史和接入台账保存在 Agent 本机。
## 快速开始
### 1. 部署边缘网关(Linux)
网关需要满足两点:**访问者能连到它**,**Agent 也能连到它**。防火墙放通 TCP `33000-34000`。
**方式 A:二进制 + systemd(推荐)**
```bash
VERSION=1.1.0 # 见 Releases 页面;ARM64 机器把 amd64 换成 arm64
curl -LO https://github.com/DeepFal/PortRelay/releases/download/v$VERSION/portrelay-server-$VERSION-linux-amd64.tar.gz
tar xzf portrelay-server-$VERSION-linux-amd64.tar.gz && cd portrelay-server-$VERSION-linux-amd64
sudo ./deploy/install-server.sh ./portrelay-server
# 安装脚本会生成随机 token,记下来填到 Agent 里
sudo grep PORTRELAY_TOKEN /etc/portrelay-server.env
```
安装脚本会把程序装到 `/usr/local/bin`,写入 [systemd 单元](deploy/portrelay-server.service)(以临时系统用户运行,状态保存在 `/var/lib/portrelay`),并设置开机自启。再次运行同一个脚本即可升级,已有配置会保留。
**方式 B:Docker**
```bash
TOKEN=$(openssl rand -hex 24); echo "token: $TOKEN"
docker run -d --name portrelay --network host --restart unless-stopped \
-e PORTRELAY_TOKEN=$TOKEN -v portrelay:/var/lib/portrelay \
ghcr.io/deepfal/portrelay-server:latest
```
> [!IMPORTANT]
> 请使用 `--network host`。白名单依赖访问者的真实 IP,端口映射可能会把来源地址换成网桥地址。另见 [docker-compose 示例](deploy/docker-compose.yml)。
**方式 C:从源码构建**
```bash
git clone https://github.com/DeepFal/PortRelay.git && cd PortRelay
make server # 产物:dist/portrelay-server-linux-{amd64,arm64}
```
### 2. 运行 Agent
**macOS App**:从 [Releases](https://github.com/DeepFal/PortRelay/releases) 下载 `PortRelay-*.dmg`(Apple Silicon 与 Intel 通用),打开后把 PortRelay 拖进“应用程序”。
> [!NOTE]
> App 未经 Apple 公证。首次打开时,在 Finder 中右键选择“打开”,或者执行 `xattr -dr com.apple.quarantine /Applications/PortRelay.app`。也可以 `make app` 自行构建。
打开后点右上角的 ⚙️ 设置:
| 设置项 | 填什么 |
| --- | --- |
| 服务器控制地址 | Agent 连接网关用的地址,例如 `gw.example.com:33000`、`192.168.1.10:33000` 或 `100.x.y.z:33000` |
| 安全令牌 | 网关上的 `PORTRELAY_TOKEN` |
| 对外访问地址 | **同事**访问网关用的地址,留空则与上面相同 |
> 举例:Agent 通过 Tailscale 地址 `100.64.0.10` 连网关,同事在办公室局域网里用 `192.168.1.10` 访问。这时“服务器控制地址”填 `100.64.0.10:33000`,“对外访问地址”填 `192.168.1.10`,邀请链接和分享文案都会使用后者。
**Linux / Windows**:下载对应平台的 `portrelay-agent-*` 压缩包,直接运行 Agent,在浏览器打开的面板里完成同样的设置。
```bash
./portrelay-agent # 面板:http://127.0.0.1:33080
./portrelay-agent -no-browser # 不自动打开浏览器
```
### 3. 创建通道并邀请同事
1. 点 **添加通道**,填写名称和目标主机、端口(如 `10.0.1.16`、`3306`)。可以先测试一下本机能否连通;网关上的访问端口自动分配。
2. 访问方式保持默认的 **仅授权链接**。保存后会直接弹出邀请设置,选择授权后可访问时长、可用设备数,以及是否需要审批。以后要再发邀请,点通道卡片上的 **邀请**。
3. 把生成的文案或二维码发给同事。同事打开链接、填写姓名后,就可以用数据库客户端或 SSH 连接 `192.168.1.10:33660`。
需要一次开放多台主机时(比如一组服务器),同样点 **添加通道**,每行填一台主机(端口用逗号隔开,可以直接粘贴一串 IP),表单会预览端口映射并自动放进新建的通道组,闲置关闭、限速、访问控制等设置对每个地址都生效。创建后弹出的邀请对整组有效。之后要加地址,点组卡片上的 **添加地址**;已有通道可以在编辑时选择加入某个组。组的「连接与授权」可以按设备或按地址查看在线连接和接入记录。
## 配置参考
**网关 `portrelay-server`**
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `-token` / `PORTRELAY_TOKEN` | 必填 | Agent 认证用的共享密钥,建议用 `openssl rand -hex 24` 生成 |
| `-listen` | `:33000` | 控制端口(Agent 隧道与授权网页共用) |
| `-port-min` / `-port-max` | `33000` / `34000` | 通道端口池 |
| `-state` | `/var/lib/portrelay/leases.json` | 授权租约、邀请与审批的持久化文件,留空则只保存在内存 |
| `-public-host` | 空 | 授权页上展示给访问者的地址,默认取访问者打开链接时使用的地址 |
| `-trust-proxy` | `false` | 信任 `X-Forwarded-For` / `X-Real-IP`,**仅在网关前有可信反向代理时开启** |
| `-version` | | 打印版本 |
**Agent `portrelay-agent`**
| 参数 | 默认值 | 说明 |
| --- | --- | --- |
| `-port` | `33080` | 本地面板 / API 端口(只监听 `127.0.0.1`) |
| `-no-browser` | `false` | 启动时不自动打开面板 |
| `-version` | | 打印版本 |
Agent 的配置保存在 `~/.portrelay/config.json`(权限 0600,原子写入;文件损坏时会先备份再使用默认值)。
## 安全模型
- **Agent 认证**:连接建立后的第一条消息必须是 token 认证(常量时间比较),认证通过的会话才能注册通道。新会话会替换旧会话。
- **默认拒绝**:新通道默认只允许持有授权的设备访问。授权按“通道 + 来源 IP”生效,并且有明确的到期时间。
- **到期即断**:网关每秒检查一次存量连接,授权失效的连接立即断开。
- **本地 API 只对本机开放**:面板只监听回环地址,并校验 `Host` 与 `Origin`,防止 DNS 重绑定和跨站请求。
> [!WARNING]
> - **Agent 与网关之间的链路目前没有加密**,token 与转发的数据都以明文传输。请让这段链路跑在可信网络或加密通道上(WireGuard、Tailscale、ZeroTier、IPSec VPN 等),或者在两端之间自行加一层 TLS 隧道。
> - 授权以**来源 IP** 为准。如果多人通过同一个 NAT 出口访问网关,他们会共享同一份授权。
> - 访问者到网关的流量就是目标服务本身的协议,加密与否取决于目标服务(例如 MySQL 是否启用 TLS)。
## 常见问题
<details>
<summary><b>必须用 Tailscale 吗?</b></summary>
不需要。PortRelay 只要求 Agent 所在机器能访问目标网段,并且能通过 TCP 连到网关的控制端口。Tailscale 只是让 Agent 连上网关的方式之一,局域网直连、VPN 或公网地址都可以。
</details>
<details>
<summary><b>和 frp、<code>ssh -R</code> 有什么区别?</b></summary>
它们都能做端口转发,而且更通用。PortRelay 专注于**临时、按人授权**的场景:默认拒绝、通过邀请链接按设备授权、可选审批、首次连接起计时、到期或闲置自动收回,并配有图形界面和接入台账。如果只需要一条长期开着的转发,frp 或 SSH 更简单。
</details>
<details>
<summary><b>一个网关能接多个 Agent 吗?</b></summary>
目前同一时间只服务一个 Agent,新的认证会话会替换旧会话。需要多个 Agent 时,可以部署多个网关实例(使用不同的端口池和 token)。
</details>
<details>
<summary><b>网关重启会怎样?</b></summary>
正在进行的连接会断开。授权租约、邀请链接和审批请求保存在状态文件里,重启后依然有效。Agent 会自动重连并重新注册通道。
</details>
<details>
<summary><b>访问者看到的 IP 不对 / 白名单不生效?</b></summary>
检查网关前面是否有 NAT、端口映射或反向代理。Docker 部署请使用 `--network host`。只有在可信反向代理之后才开启 `-trust-proxy`,否则 `X-Forwarded-For` 可以被伪造。
</details>
<details>
<summary><b>App 提示“无法验证开发者”?</b></summary>
发布的 App 只做了 ad-hoc 签名,没有经过 Apple 公证。请右键选择“打开”,或者执行 `xattr -dr com.apple.quarantine /Applications/PortRelay.app`。
</details>
## 开发
需要 Go 1.23+;构建 macOS App 还需要 Xcode 命令行工具,运行环境为 macOS 13 及以上。
```bash
make test # gofmt 检查 + go vet + go test -race
make agent # 编译当前平台的 Agent 到 bin/
make app # 构建 PortRelay.app(UNIVERSAL=1 ./build-app.sh 构建通用二进制)
make release # 打包全部平台的网关与 Agent 到 dist/
make snapshots # 离屏渲染 App 各界面截图到 tools/ui-snapshots/out/
make readme-images # 重新生成 README 的全部截图和动图到 docs/images/
```
```text
cmd/portrelay-server 边缘网关入口
cmd/portrelay-agent Agent 入口(内嵌网页面板 web/)
server/ 网关:Agent 会话、端口监听、白名单与授权租约、邀请与授权网页
client/ Agent:配置、隧道、本地 API、闲置关闭、流量统计
core/ 协议消息、ACL、流量计量与限速
mac-native/ SwiftUI 菜单栏 App
deploy/ systemd、Docker Compose 部署示例与安装脚本
tools/ui-snapshots/ README 截图与动图:App 离屏渲染、网页截图、GIF 合成
```
README 里的截图和动图都使用模拟数据,改了界面后运行 `make readme-images` 即可全部重新生成(需要 Google Chrome)。App 界面离屏渲染,动图由逐帧画面合成,授权页和网页面板用无头 Chrome 截图。
欢迎提交 Issue 和 Pull Request。提交前请先运行 `make test`。
## 许可证
[MIT](LICENSE) © DeepFal