PortRelay

PortRelay

把内网服务临时、可控地开放给同事:发一条邀请链接,到期自动收回。

CI Release MIT Go Platform

简体中文 · English

PortRelay 架构

你的电脑能连上某个内网网段:数据库、跳板机、内部面板,但同事连不上。常见的做法是开 VPN 账号、改防火墙,或者干脆把端口对所有人放开,事后还经常忘记关。 PortRelay 把这件事变成三步: 1. **Agent** 把内网目标挂到 **边缘网关** 的一个端口上。 2. 你把**邀请链接**(或二维码)发给同事。 3. 同事打开链接、填写姓名,他的设备就获得一段时间的访问权。到期、闲置或被你撤销后,连接立即断开。 > [!TIP] > **不依赖任何特定组网方案。** 只要 Agent 所在的机器**同时能连到目标网段和边缘网关**就能工作。Agent 到网关的线路随你选:同一局域网、公司 VPN、Tailscale、ZeroTier、WireGuard,或者网关本身有公网地址。Agent 只需主动连出,本机不用开放任何端口。 ## 目录 - [功能](#功能) - [界面一览](#界面一览) - [工作原理](#工作原理) - [快速开始](#快速开始) - [配置参考](#配置参考) - [安全模型](#安全模型) - [常见问题](#常见问题) - [开发](#开发) ## 功能 **访问控制** - 三种访问方式:**仅授权链接**(默认,最安全)、IP / 网段白名单(可与授权链接叠加),以及全部放通。 - 邀请链接可以设置链接有效期、可用设备数、授权后可访问时长(1 小时、2 小时、8 小时、当天有效,或直到通道关闭),以及是否需要审批。 - 需要审批时,App 会弹出系统通知,一键批准或拒绝;访问者的页面会自动刷新结果。 - **计时从首次连接开始**:同事拿到授权后不用急着连,开始用的时候才开始计时。 - 授权到期或被撤销后,**已建立的连接也会立刻断开**;未授权的连接直接被重置(TCP RST)并留下拦截记录。 **通道生命周期** - **闲置自动关闭**:没有连接、没有有效授权、没有待处理申请,也没有还能使用的邀请链接时开始计时,默认 30 分钟后关闭。 - 可选的**最长开启时长**,到点无论是否在用都关闭。 - 记录首次连接时间、实际连接时长和每次关闭的原因。 - 编辑通道不会打断现有连接;更换端口时先绑定新端口再释放旧端口。 **可观测** - 每个通道近 10 分钟的流量曲线,以及实时速率、连接数、本次与历史累计流量。 - 在线设备按 IP 合并展示,可逐条断开;历史接入台账同样按 IP 合并。 - 按通道限速(MB/s)。 **客户端** - 原生 macOS 菜单栏 App(SwiftUI):关闭窗口只保留菜单栏图标,从菜单栏退出时连同后台 Agent 一起退出。 - 内嵌网页面板,Linux、Windows 上也能用同一套功能。 - 通道搜索(支持拼音首字母,比如输入 `sjk` 就能找到“数据库”),以及按运行状态筛选。 ## 界面一览

通道卡片与实时流量
通道卡片:访问地址、授权状态、实际连接时长,以及近 10 分钟流量(动图为加速演示)

PortRelay macOS App 主界面 **邀请与授权**:生成链接后得到一段可直接发送的文案和二维码。同事在手机或电脑上打开链接填写姓名,需要审批时进入等待页,批准后页面自动更新。

邀请分享卡片

填写姓名
① 打开邀请链接
等待审批
② 等待管理员批准(可选)
授权成功
③ 授权成功
**连接与授权管理**:待审批的申请、已授权设备(在线状态与剩余时间),以及仍然有效的邀请链接,都在同一处管理。

授权管理

网页面板与更多截图
不使用 macOS App 时,Agent 自带网页面板,地址为 `http://127.0.0.1:33080`: 网页面板
编辑通道
编辑通道:访问方式、闲置关闭、最长时长
在线设备
在线设备:按 IP 合并,可逐条断开
## 工作原理

邀请流程

- **边缘网关**(`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.0.0 # 见 Releases 页面 curl -LO https://github.com/DeepFal/PortRelay/releases/download/v$VERSION/portrelay-$VERSION-linux-amd64.tar.gz tar xzf portrelay-$VERSION-linux-amd64.tar.gz && cd portrelay-$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-*-macos-universal.zip`,解压后拖进“应用程序”。 > [!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**:下载对应平台的压缩包,直接运行 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`。 ## 配置参考 **网关 `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)。 ## 常见问题
必须用 Tailscale 吗? 不需要。PortRelay 只要求 Agent 所在机器能访问目标网段,并且能通过 TCP 连到网关的控制端口。Tailscale 只是让 Agent 连上网关的方式之一,局域网直连、VPN 或公网地址都可以。
和 frp、ssh -R 有什么区别? 它们都能做端口转发,而且更通用。PortRelay 专注于**临时、按人授权**的场景:默认拒绝、通过邀请链接按设备授权、可选审批、首次连接起计时、到期或闲置自动收回,并配有图形界面和接入台账。如果只需要一条长期开着的转发,frp 或 SSH 更简单。
一个网关能接多个 Agent 吗? 目前同一时间只服务一个 Agent,新的认证会话会替换旧会话。需要多个 Agent 时,可以部署多个网关实例(使用不同的端口池和 token)。
网关重启会怎样? 正在进行的连接会断开。授权租约、邀请链接和审批请求保存在状态文件里,重启后依然有效。Agent 会自动重连并重新注册通道。
访问者看到的 IP 不对 / 白名单不生效? 检查网关前面是否有 NAT、端口映射或反向代理。Docker 部署请使用 `--network host`。只有在可信反向代理之后才开启 `-trust-proxy`,否则 `X-Forwarded-For` 可以被伪造。
App 提示“无法验证开发者”? 发布的 App 只做了 ad-hoc 签名,没有经过 Apple 公证。请右键选择“打开”,或者执行 `xattr -dr com.apple.quarantine /Applications/PortRelay.app`。
## 开发 需要 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/ ``` ```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 动图生成(mkgif) ``` README 里的截图和动图都用模拟数据生成:`tools/ui-snapshots/render.sh readme` 渲染截图,`FRAMES=40 tools/ui-snapshots/render.sh gif` 渲染逐帧画面,再用 `go run ./tools/ui-snapshots/mkgif` 合成 GIF。 欢迎提交 Issue 和 Pull Request。提交前请先运行 `make test`。 ## 许可证 [MIT](LICENSE) © DeepFal