- 「申请延长」单独成节:同事端 ④⑤⑥ 与 App 的连接与授权截图放在一起, 写明卡片提示、点「批准延长」选时长、最近结束的设备也可直接延长 - 演示数据:MySQL 通道带一条延长申请和一台最近结束的设备,主界面、动图、 授权管理与网页面板展示同一组数据,卡片上能看到「申请延长 1」
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, while the tunnel stays open, 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. A teammate reopens the original invite link and clicks 申请延长 (Request extension); you approve by picking a duration in the app, the menu bar or the web panel. A grant that is still running is extended from its current expiry. One that has ended starts again from the moment you approve; that works for up to two hours after it ended, even if the tunnel has been closed, and the tunnel reopens on approval. Revoked grants cannot be extended. 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. If you approve an extension that runs past that point, the tunnel stays open until the grant ends.
- Idle time is counted per group: while any address is in use, none of them is closed for being idle, and when the group does go idle it closes as a whole. Each address can still be switched on or off on its own.
- 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, pending access and extension requests, 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 |
Extensions. When access is about to run out or has just ended, the teammate opens the same link again, sees where their access stands and asks for more time. You don't need to send a new invite.
![]() ④ Reopen the link and request an extension |
![]() ⑤ Wait while you choose a duration |
![]() ⑥ Extension approved |
On your side, the tunnel card shows 申请延长 (Extension requested). Open 连接与授权 (Connections & access): extension requests are listed first, and you approve one by clicking 批准延长 (Approve) and picking a duration. The teammate's page then shows the new expiry. The same view lists pending access requests, authorized devices (with online state and remaining time), recently ended grants and active invite links, and a device whose access just ended can be extended from there too.
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.
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.1.0 # see the Releases page; use arm64 instead of amd64 on ARM hosts
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
# 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-*.dmg (universal: Apple Silicon and Intel) from Releases, open it and drag PortRelay 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 portrelay-agent-* 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). After you save, the invite form opens: choose how long access lasts, how many devices may use the link, and whether approval is required. To send another invite later, click 邀请 (Invite) on the tunnel card.
- 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. The invite form that opens after creation covers the whole 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/
make readme-images # regenerate every README screenshot and animation in docs/images/
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/ README images: offscreen app rendering, web screenshots, GIF encoding
All screenshots and animations in this README use mock data. After a UI change, run make readme-images to regenerate them (Google Chrome required). App screens are rendered offscreen, the animation is encoded from rendered frames, and the invite pages and web panel are captured with headless Chrome.
Issues and pull requests are welcome. Please run make test before submitting.
License
MIT © DeepFal










