同事端 - 用过邀请链接的设备再次打开链接时显示自己的授权状态:有效时可申请延长, 访问结束(到期或通道关闭)后 2 小时内也可以;超过 2 小时或被撤销则提示重新获取邀请 - 「申请延长」只有一个按钮,提交后复用审批状态页,批准后显示新的到期时间 - 用过链接的设备不再自动重新放通,统一走申请延长 网关 - 授权结束后保留 2 小时的记录(通道关闭后仍保留,随状态文件持久化);撤销的不能延长, 删除通道 / 解散组时一并清除(unregister 带 deleted 标记) - 新增 extend_lease:有效授权从原到期时间顺延,已结束的从现在起算,不受通道最长开启时长截断, 并批准该设备的延长申请;指标里带上全部可延长设备与申请(包括已关闭的通道) - 授权时长支持分钟数("30m"、"90m",5 分钟到 30 天) Agent / App / 网页面板 - POST .../leases/extend:通道已关闭时先自动开启(组则全组开启),并让通道至少开到授权结束 - 授权管理新增「申请延长」「最近结束 · 2 小时内可延长」,已授权设备可直接「延长」; 卡片、菜单栏和系统通知提示延长申请,菜单栏可直接选时长批准 - 邀请的可访问时长、通道最长开启时长增加 30 分钟与自定义;网页面板弹窗层级高于侧边抽屉 - 邀请分享文案提示「授权结束后 2 小时内可重新打开此链接申请延长」
PortRelay
Share internal services with teammates temporarily and safely: send an invite link, and access expires on its own.
简体中文 · English
Your machine can reach an internal network: a database, a jump host, an internal dashboard. Your teammates can't. The usual fixes are VPN accounts, firewall tickets, or opening the port to everyone and forgetting to close it again.
PortRelay turns that into three steps:
- The agent exposes an internal target on a port of the edge gateway.
- You send a teammate an invite link (or QR code).
- They open it and enter their name. Their device can then connect for a limited time. Access ends when it expires, when the tunnel sits idle, or when you revoke it, and live connections are cut immediately.
Tip
No particular network setup is required. The machine running the agent only needs to reach both the target network and the edge gateway. How the agent reaches the gateway is up to you: the same LAN, a corporate VPN, Tailscale, ZeroTier, WireGuard, or a gateway with a public address. The agent only dials out and opens no inbound ports.
Note
The user interface (macOS app, web panel and invite pages) is currently in Simplified Chinese.
Contents
Features
Access control
- Three access modes: invite-only (the default and safest), an IP / CIDR allowlist (which can be combined with invites), and open to all.
- Each invite link has its own expiry, device limit, access duration once granted (30 min, 1 h, 2 h, 8 h, until end of day, until the tunnel closes, or a custom length) and an optional approval step.
- When approval is required, the app shows a system notification and you approve or deny with one click. The requester's page refreshes on its own.
- The access clock starts at the first connection, so a teammate can accept an invite now and use it later.
- When access expires or is revoked, existing connections are dropped too. Unauthorized connections are reset (TCP RST) and logged.
- Extensions. Within two hours after access ends (even if the tunnel has been closed), a teammate reopens the original link and clicks 申请延长 (Request extension). You approve it by picking a duration in the app, the menu bar or the web panel, and a closed tunnel reopens on its own. You can also extend a running or just-ended grant yourself.
- Tunnel groups: the add-tunnel form takes a list of targets, one host per row with comma-separated ports (a pasted list of IPs is split into rows), creates one tunnel per address on a contiguous block of gateway ports and puts them in one group. A group shares one invite link and one set of grants: a teammate is authorized once for the whole group, and one revoke disconnects them everywhere.
Tunnel lifecycle
- Idle auto-close. The idle timer starts when a tunnel has no connections, no active grants, no pending requests and no invite links with uses left. By default the tunnel closes after 30 minutes of idle time.
- An optional maximum open time closes the tunnel at a fixed point, whether or not it is in use.
- While any address in a group is in use, the whole group counts as in use; members are switched on and off together.
- PortRelay records the first-connection time, the actual connected time and why each tunnel was closed.
- Editing a tunnel keeps existing connections. Moving a tunnel binds the new port before releasing the old one.
Observability
- A 10-minute traffic chart for each tunnel, plus live rates, connection counts and session/total bytes.
- Online devices are grouped by IP and can be disconnected one by one. The connection history is grouped the same way.
- Per-tunnel bandwidth limit in MB/s.
Clients
- A native macOS menu-bar app (SwiftUI). Closing the window leaves only the menu-bar icon; quitting from the menu also stops the background agent.
- An embedded web panel with the same features on Linux and Windows.
- Tunnel search, including pinyin initials (for example
sjkfinds “数据库”), and a status filter.
Screenshots
A tunnel card: access address, grants, connected time and the last 10 minutes of traffic (sped up)
Invites. Generating an invite gives you a ready-to-send message and a QR code. The teammate opens the link on a phone or laptop and enters their name. If approval is required, they wait on a status page that updates when you decide.
![]() ① Open the invite link |
![]() ② Wait for approval (optional) |
![]() ③ Access granted |
Tunnel groups. A group card sums up the state and traffic of the whole group and lists every access address. Each address can be switched, invited or moved out on its own.
Access management. Pending requests, authorized devices (with online state and remaining time) and active invite links are all managed in one place.
Web panel and more screenshots
Without the macOS app, the agent serves a web panel at http://127.0.0.1:33080:
![]() Edit tunnel: access mode, idle close, max open time |
![]() Online devices, grouped by IP |
How it works
- The edge gateway (
portrelay-server) runs where visitors can reach it and manages a port pool (default33000-34000). Port33000is the control port. The agent tunnel and the invite web pages share it: HTTP requests go to the invite pages, and anything else is treated as an agent session. - The agent (
portrelay-agent, bundled in the macOS app) runs on a machine that can reach the target network. It dials the gateway's control port, authenticates with a token, and multiplexes all tunnels over that single TCP connection using yamux. - When a visitor connects to
gateway:33660, the gateway checks the allowlist and access grants.- If the visitor is allowed, the gateway opens a stream through the tunnel, the agent dials
10.0.1.16:3306, and bytes are relayed both ways. - Otherwise the connection is reset and logged.
- If the visitor is allowed, the gateway opens a stream through the tunnel, the agent dials
- Grants, invites and approval requests live in a state file on the gateway and survive restarts. Tunnel settings, traffic history and the connection log are kept on the agent's machine.
Quick start
1. Deploy the edge gateway (Linux)
The gateway must be reachable by visitors and by the agent. Open TCP 33000-34000 in your firewall.
Option A: binary + systemd (recommended)
VERSION=1.0.0 # see the Releases page
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
# The installer generates a random token; you will enter it in the agent
sudo grep PORTRELAY_TOKEN /etc/portrelay-server.env
The installer does four things:
- installs the binary into
/usr/local/bin; - writes a systemd unit, which runs as a dynamic system user and keeps state in
/var/lib/portrelay; - enables the service at boot;
- keeps your existing configuration when you re-run it to upgrade.
Option B: Docker
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
Use
--network host. Allowlists rely on the visitor's real source IP, and port publishing may replace it with a bridge address. See also the docker-compose example.
Option C: build from source
git clone https://github.com/DeepFal/PortRelay.git && cd PortRelay
make server # dist/portrelay-server-linux-{amd64,arm64}
2. Run the agent
macOS app. Download PortRelay-*-macos-universal.zip from Releases, unzip it and move it to Applications.
Note
The app is not notarized. On first launch, right-click it in Finder and choose Open, or run
xattr -dr com.apple.quarantine /Applications/PortRelay.app. You can also build it yourself withmake app.
Open ⚙️ Settings and fill in:
| Setting | Value |
|---|---|
| Server control address (服务器控制地址) | The address the agent uses to reach the gateway, e.g. gw.example.com:33000, 192.168.1.10:33000 or 100.x.y.z:33000 |
| Token (安全令牌) | PORTRELAY_TOKEN from the gateway |
| Public access address (对外访问地址) | The address teammates use to reach the gateway. Leave it empty to use the same host. |
Example: the agent reaches the gateway over Tailscale at
100.64.0.10, while teammates in the office use the LAN address192.168.1.10. Set the control address to100.64.0.10:33000and the public access address to192.168.1.10. Invite links and share messages will use the LAN address.
Linux / Windows. Download the archive for your platform, run the agent, and use the web panel that opens in your browser.
./portrelay-agent # panel at http://127.0.0.1:33080
./portrelay-agent -no-browser # don't open a browser
3. Create a tunnel and invite someone
- Click 添加通道 (Add tunnel) and enter a name, a target host and port such as
10.0.1.16and3306. You can probe the target from your machine first; the gateway port is assigned automatically. - Keep the default access mode, 仅授权链接 (invite-only). Click 邀请 (Invite), then choose how long access lasts, how many devices may use the link, and whether approval is required.
- Send the generated message or QR code. Once the teammate has entered their name, they can connect their database client or SSH to
192.168.1.10:33660.
To open several hosts at once (a set of servers, say), use the same 添加通道 (Add tunnel) form with one host per row (comma-separated ports; you can paste a list of IPs). The form previews the port mapping and puts the addresses in a new tunnel group; idle close, bandwidth and access settings apply to every address. Then click 邀请整组 (Invite group). To add addresses later, click 添加地址 (Add address) on the group card; an existing tunnel can join a group from its edit form. A group's connections and history can be viewed by device or by address.
Configuration
Gateway: portrelay-server
| Flag | Default | Description |
|---|---|---|
-token / PORTRELAY_TOKEN |
required | Shared secret for agent authentication; generate it with openssl rand -hex 24 |
-listen |
:33000 |
Control port, shared by the agent tunnel and the invite pages |
-port-min / -port-max |
33000 / 34000 |
Tunnel port pool |
-state |
/var/lib/portrelay/leases.json |
Where grants, invites and requests are persisted; leave it empty to keep them in memory only |
-public-host |
empty | Address shown to visitors on the invite page; defaults to the host the visitor used to open the link |
-trust-proxy |
false |
Trust X-Forwarded-For / X-Real-IP. Enable it only behind a trusted reverse proxy. |
-version |
Print the version |
Agent: portrelay-agent
| Flag | Default | Description |
|---|---|---|
-port |
33080 |
Local panel / API port; listens on 127.0.0.1 only |
-no-browser |
false |
Don't open the panel on start |
-version |
Print the version |
Agent settings live in ~/.portrelay/config.json. The file has mode 0600 and is written atomically. If it is corrupt, it is backed up and replaced with defaults.
Security model
- Agent authentication. The first message on a new connection must be a token check, compared in constant time. Only authenticated sessions can register tunnels, and a new session replaces the old one.
- Deny by default. New tunnels admit only devices that hold a grant. A grant applies to one tunnel and one source IP, and always has an expiry.
- Expiry is enforced live. The gateway re-checks open connections every second and drops any whose grant has ended.
- The local API is loopback-only. The panel binds to
127.0.0.1and validatesHostandOriginto block DNS rebinding and cross-site requests.
Warning
- The agent ↔ gateway link is not encrypted yet. The token and all relayed data travel in plain text. Run that hop over a trusted network or an encrypted overlay (WireGuard, Tailscale, ZeroTier, an IPsec VPN, …), or wrap it in your own TLS tunnel.
- Grants are tied to the source IP. Visitors behind the same NAT address share a grant.
- Traffic between visitors and the gateway uses the target service's own protocol. Whether it is encrypted depends on that service (for example, whether MySQL has TLS enabled).
FAQ
Do I need Tailscale?
No. PortRelay only needs the agent's machine to reach the target network and to open a TCP connection to the gateway's control port. Tailscale is just one way to provide that path; a LAN, a VPN or a public address work equally well.
How is this different from frp or ssh -R?
Those tools are more general port forwarders. PortRelay focuses on temporary, per-person access:
- deny by default;
- per-device grants through invite links, with optional approval;
- clocks that start at the first connection;
- automatic expiry and idle close;
- a GUI and a connection log.
If you only need one long-lived forward, frp or SSH is simpler.
Can one gateway serve several agents?
Not at the same time: a new authenticated session replaces the previous one. For several agents, run several gateway instances with different port pools and tokens.
What happens when the gateway restarts?
Live connections drop. Grants, invite links and approval requests are kept in the state file and stay valid. The agent reconnects and re-registers its tunnels automatically.
Visitors show up with the wrong IP, or the allowlist doesn't work
Check for NAT, port publishing or a reverse proxy in front of the gateway. With Docker, use --network host. Enable -trust-proxy only behind a trusted proxy; otherwise X-Forwarded-For can be spoofed.
macOS says the developer cannot be verified
Release builds are ad-hoc signed, not notarized. Right-click the app and choose Open, or run xattr -dr com.apple.quarantine /Applications/PortRelay.app.
Development
Building requires Go 1.23+. The macOS app also needs the Xcode command-line tools and runs on macOS 13 or later.
make test # gofmt check + go vet + go test -race
make agent # build the agent for this platform into bin/
make app # build PortRelay.app (UNIVERSAL=1 ./build-app.sh for a universal binary)
make release # package gateway + agent for all platforms into dist/
make snapshots # render app screens offscreen into tools/ui-snapshots/out/
cmd/portrelay-server gateway entry point
cmd/portrelay-agent agent entry point (embeds the web panel in web/)
server/ gateway: agent sessions, listeners, allowlists and grants, invites and invite pages
client/ agent: config, tunnels, local API, idle close, traffic stats
core/ protocol messages, ACL, traffic metering and rate limiting
mac-native/ SwiftUI menu-bar app
deploy/ systemd and Docker Compose examples, install script
tools/ui-snapshots/ offscreen screenshot renderer and README GIF builder (mkgif)
All screenshots and animations in this README come from mock data:
tools/ui-snapshots/render.sh readmerenders the screenshots;FRAMES=40 tools/ui-snapshots/render.sh gifrenders the animation frames;go run ./tools/ui-snapshots/mkgifencodes the frames as a GIF.
Issues and pull requests are welcome. Please run make test before submitting.
License
MIT © DeepFal







