跳到主要內容

Pinggy Tunnel

通過 Pinggy 使用 SSH 實現零安裝的本地主機隧道。

技能元數據

來源可選 — 使用 hermes skills install official/devops/pinggy-tunnel 安裝
路徑optional-skills/devops/pinggy-tunnel
版本0.1.0
作者Teknium (teknium1), Hermes Agent
許可證MIT
平臺linux, macos, windows
標籤Pinggy, Tunnel, Networking, SSH, Webhook, Localhost
相關技能cloudflared-quick-tunnel, webhook-subscriptions

參考:完整 SKILL.md

信息

以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理看到的指令。

Pinggy Tunnel 技能

使用 Pinggy SSH 反向隧道將本地服務(開發服務器、Webhook 接收器、MCP 端點、演示)暴露給公共互聯網。無需安裝守護進程 — 用戶的標準 SSH 客戶端連接到 a.pinggy.io:443,Pinggy 會返回一個公共 HTTP/HTTPS URL。

免費層:60 分鐘隧道,隨機子域名,無需註冊。專業層($3/月)為可選功能,需使用令牌。

何時使用

  • 用戶要求“暴露此本地服務”、“分享我的開發服務器”、“使此 URL 公開”、“隧道端口 N”、“獲取 Webhook 的公共 URL”
  • 需要在本地任務期間接收 Webhook 回調(Stripe、GitHub、Discord、AgentMail)
  • 與遠程方共享一次性 HTTP 演示(MCP 服務器、Ollama/vLLM 端點、儀表板)
  • 主機擁有 SSH 但沒有 cloudflared / ngrok 二進制文件,且安裝它們過於繁瑣

如果主機已配置 cloudflared,優先使用 cloudflared-quick-tunnel 技能 — Cloudflare 快速隧道不會在 60 分鐘後過期。

前提條件

  • PATH 中存在 ssh (ssh -V)。Linux、macOS 和 Windows 10+ 默認包含。無需其他安裝。
  • 在隧道啟動前,本地服務正在監聽 127.0.0.1:<port>。Pinggy 將返回 URL,但在本地源站上線之前,這些 URL 將返回 502 錯誤。

可選:

  • 用於付費專業功能的 PINGGY_TOKEN 環境變量(持久子域名、自定義域名、多個隧道、無 60 分鐘限制)。免費層無需憑據。

快速參考

# Plain HTTP/HTTPS tunnel for port 8000 (free tier)
ssh -p 443 -o StrictHostKeyChecking=no -o ServerAliveInterval=30 \
-R0:localhost:8000 free@a.pinggy.io

# TCP tunnel (databases, raw SSH, etc.)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:5432 tcp@a.pinggy.io

# TLS tunnel (Pinggy can't decrypt — bring your own certs at origin)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:443 tls@a.pinggy.io

# Basic auth gate (b:user:pass)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"b:admin:secret+free@a.pinggy.io"

# Bearer token gate (k:token)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"k:mysecrettoken+free@a.pinggy.io"

# IP whitelist (w:CIDR)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"w:203.0.113.0/24+free@a.pinggy.io"

# Enable CORS + force HTTPS redirect
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"co+x:https+free@a.pinggy.io"

# Pro tier (persistent URL, no 60-min cap)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 "$PINGGY_TOKEN+a.pinggy.io"

流程 — 啟動隧道並獲取 URL

模型應使用 terminal 工具。隧道必須在共享期間保持活躍,因此將其作為後臺進程運行,並從 stdout 解析公共 URL。

1. 確認本地源站已上線

curl -sI http://127.0.0.1:8000/ | head -1
# expect HTTP/1.x 200 (or any non-connection-refused response)

如果尚未有任何服務在監聽,請先啟動它(例如 python3 -m http.server 8000 --bind 127.0.0.1)。Pinggy 會樂意返回一個指向空內容的 URL — 在源站上線之前,用戶將看到 502 錯誤。

2. 以後臺進程方式啟動隧道

使用 terminal(background=True) 並將輸出捕獲到日誌文件(Pinggy 在 stdout 上打印 URL,然後保持連接打開):

LOG=/tmp/pinggy-8000.log
nohup ssh -p 443 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=3 \
-R0:localhost:8000 free@a.pinggy.io \
> "$LOG" 2>&1 &
echo $! > /tmp/pinggy-8000.pid

StrictHostKeyChecking=no + UserKnownHostsFile=/dev/null 跳過首次運行的主機密鑰提示。ServerAliveInterval=30 防止 SSH 會話因 NAT 空閒而被斷開。

3. 從日誌中解析 URL

sleep 4
grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/pinggy-8000.log | head -1

預期輸出如下所示:

You are not authenticated.
Your tunnel will expire in 60 minutes.
http://yqycl-98-162-69-48.a.free.pinggy.link
https://yqycl-98-162-69-48.a.free.pinggy.link

https://...pinggy.link URL 交給用戶。

4. 驗證

curl -sI https://<the-url>/ | head -3
# expect 200/302/whatever the local origin actually returns

如果收到 502 Bad Gateway,說明 SSH 會話已建立,但本地源站未在監聽 — 請先修復步驟 1。

5. 清理

kill "$(cat /tmp/pinggy-8000.pid)"
# or, if the pid file got lost:
pkill -f 'ssh -p 443 .* free@a\.pinggy\.io'

如果你從 terminal(background=True) 獲得了 session_id,優先使用 process(action='kill', session_id=...)

通過用戶名關鍵字進行訪問控制

Pinggy 將通過 + 分隔的控制標誌堆疊到 SSH 用戶名中。當包含 + 時,始終引用整個 user@host 參數:

關鍵字效果
b:user:passHTTP 基本認證網關
k:tokenBearer 令牌頭網關 (Authorization: Bearer <token>)
w:CIDRIP 白名單(單個 IP 或 CIDR,可重複)
co添加 Access-Control-Allow-Origin: * (CORS)
x:https強制 HTTPS — 自動將 HTTP 重定向到 HTTPS
a:Name:Value添加請求頭
u:Name:Value更新請求頭
r:Name移除請求頭
qr將 URL 的二維碼打印到 stdout(便於移動設備共享)

自由組合:"b:admin:secret+co+x:https+free@a.pinggy.io"

Web 調試器(可選)

Pinggy 可以將入站流量鏡像到 localhost:4300 以供檢查。向 SSH 命令添加本地轉發:

ssh -p 443 -L4300:localhost:4300 -R0:localhost:8000 free@a.pinggy.io

然後在瀏覽器中打開 http://localhost:4300 以查看實時的請求/響應對。

常見陷阱

  • 免費層有 60 分鐘的硬性上限。 SSH 會話將在 60 分鐘時終止;URL 將失效。對於更長時間的共享,請使用 PINGGY_TOKEN(專業版)或通過 shell 循環自動重啟(請注意,免費層每次重啟時 URL 都會更改)。
  • 免費層的 URL 是隨機的,且在重啟時會更改。 不要將其加入書籤,也不要將其粘貼到配置文件中。每次都要從日誌中重新解析。
  • 每個源 IP 的併發免費隧道限制為一個。 從同一臺機器啟動第二個隧道通常會殺死第一個隧道。專業版解除了此限制。
  • 用戶名中的 + 必須加引號。 裸寫的 ssh ... b:admin:secret+free@a.pinggy.io 在 bash 中有效,但在將 + 視為特殊字符的 shell 中或以編程方式組裝時會出錯。始終用雙引號包裹。
  • 不要在沒有訪問控制標誌的情況下隧道傳輸任何敏感內容。 裸 HTTP 隧道可被任何擁有 URL 的人訪問。對於非公共服務,請使用 b:k:w:
  • process(action='log') 可能會遺漏 SSH banner 輸出。 Pinggy 打印 URL,然後 SSH 會話進入交互模式。始終重定向到日誌文件並直接 grep 該文件——與 cloudflared-quick-tunnel 相同的模式。
  • 首次運行時的主機密鑰提示。 默認 OpenSSH 配置要求用戶接受 Pinggy 的主機密鑰。對於無人值守的運行,始終傳遞 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
  • TCP 和 TLS 隧道返回 <subdomain>.a.pinggy.online:<port> 對,而不是 https URL。 使用不同的正則表達式解析(tcp:// 和端口)。不要假設每個 Pinggy 隧道都是 HTTP。
  • 專業模式需要將令牌作為用戶名,而不是作為標誌。 使用 "$PINGGY_TOKEN+a.pinggy.io"(無 free@)。使用令牌時,還可以添加 :persistent 以獲得穩定的子域名——參見 pinggy.io/docs/

配方

將本地源與 Pinggy 隧道相結合的複合模式。每個配方都是自包含的——啟動源,啟動隧道,解析 URL,將其交還給用戶。

配方 1 — 接收 webhook 回調

當外部服務(Stripe、GitHub、Discord、AgentMail 等)需要在本地任務期間 POST 到公開可達的 URL 時使用此方法。

# 1. Tiny capturing server: every request gets appended to /tmp/webhook-hits.log
cat >/tmp/webhook-server.py <<'PY'
import http.server, json, datetime, pathlib
LOG = pathlib.Path("/tmp/webhook-hits.log")
class H(http.server.BaseHTTPRequestHandler):
def _capture(self):
n = int(self.headers.get("content-length") or 0)
body = self.rfile.read(n).decode("utf-8", "replace") if n else ""
rec = {"t": datetime.datetime.utcnow().isoformat(), "path": self.path,
"method": self.command, "headers": dict(self.headers), "body": body}
with LOG.open("a") as f: f.write(json.dumps(rec) + "\n")
self.send_response(200); self.send_header("content-type","application/json")
self.end_headers(); self.wfile.write(b'{"ok":true}\n')
def do_GET(self): self._capture()
def do_POST(self): self._capture()
def log_message(self,*a,**k): pass
http.server.HTTPServer(("127.0.0.1", 18080), H).serve_forever()
PY
nohup python3 /tmp/webhook-server.py >/tmp/webhook-server.log 2>&1 &
echo $! >/tmp/webhook-server.pid

# 2. Tunnel — bearer-token-gate so randos can't pollute the capture log
nohup ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 \
-R0:localhost:18080 "k:$(openssl rand -hex 12)+free@a.pinggy.io" \
>/tmp/webhook-pinggy.log 2>&1 &
echo $! >/tmp/webhook-pinggy.pid
sleep 5
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/webhook-pinggy.log | head -1)
echo "Webhook URL: $URL"

# 3. While the agent works, watch hits land
tail -f /tmp/webhook-hits.log

$URL 交給需要調用您的服務。 teardown:kill $(cat /tmp/webhook-server.pid) $(cat /tmp/webhook-pinggy.pid)

配方 2 — 通過 HTTP/SSE 暴露 MCP 服務器

當遠程 MCP 客戶端(另一臺機器上的 Claude Desktop、隊友的編輯器等)需要訪問在本地機器上運行的 MCP 服務器時使用。僅適用於使用 HTTP 傳輸的 MCP 服務器——stdio 模式服務器無法通過隧道傳輸。

# 1. Start the MCP server in HTTP mode (example: a FastMCP server on port 8765)
nohup python3 my_mcp_server.py --transport http --port 8765 \
>/tmp/mcp-server.log 2>&1 &
echo $! >/tmp/mcp-server.pid

# 2. Tunnel with a bearer token — MCP traffic should not be open to the internet
TOKEN=$(openssl rand -hex 16)
nohup ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 \
-R0:localhost:8765 "k:$TOKEN+free@a.pinggy.io" \
>/tmp/mcp-pinggy.log 2>&1 &
echo $! >/tmp/mcp-pinggy.pid
sleep 5
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/mcp-pinggy.log | head -1)
echo "MCP URL: $URL"
echo "Bearer token: $TOKEN"

遠程客戶端使用 Authorization: Bearer $TOKEN 連接到 $URL。Hermes 自己的原生 MCP 客戶端配置:{"transport": "http", "url": "<URL>", "headers": {"Authorization": "Bearer <TOKEN>"}}

配方 3 — 暴露本地 LLM 端點(Ollama / vLLM / llama.cpp)

與遠程調用者(另一個代理、手機、隊友)共享本地模型。Ollama 監聽 :11434,vLLM 和 llama.cpp 通常監聽 :8000

# Pre-req: the model server is already running on 127.0.0.1:11434 (Ollama default)
TOKEN=$(openssl rand -hex 16)
nohup ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 \
-R0:localhost:11434 "k:$TOKEN+co+free@a.pinggy.io" \
>/tmp/llm-pinggy.log 2>&1 &
echo $! >/tmp/llm-pinggy.pid
sleep 5
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/llm-pinggy.log | head -1)
echo "Endpoint: $URL"
echo "Token: $TOKEN"

# Verify
curl -s "$URL/api/tags" -H "Authorization: Bearer $TOKEN" | head

co 啟用 CORS,以便瀏覽器調用者可以訪問端點。對於僅後端調用者,請去掉 co。對於兼容 OpenAI 的 vLLM/llama.cpp 端點,調用者使用基礎 URL $URL/v1Authorization: Bearer $TOKEN——但請注意 Pinggy 不會剝離/替換正文中的任何內容,因此模型服務器本身會看到 Pinggy 的令牌;本地服務器應配置為忽略身份驗證(它已經在 127.0.0.1 上),讓 Pinggy 進行 gating。

配方 4 — 使用一次性密碼共享開發服務器

最快的“讓隊友試探我正在運行的應用”模式。隨機密碼,打印一次,當您按 Ctrl-C 時失效。

PASS=$(openssl rand -base64 12 | tr -d '+/=' | head -c 12)
echo "Dev server password: $PASS"
ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ServerAliveInterval=30 \
-R0:localhost:3000 "b:dev:$PASS+co+x:https+free@a.pinggy.io"
# URL prints to the terminal. Share URL + password. Ctrl-C to tear down.

b:dev:$PASS 使用 HTTP Basic auth 保護 URL。x:https 強制使用 TLS。co 為 SPA 前端添加 CORS。

驗證

# End-to-end: spin up a trivial origin, tunnel it, hit it, tear down
python3 -m http.server 18000 --bind 127.0.0.1 >/tmp/origin.log 2>&1 &
ORIGIN_PID=$!

nohup ssh -p 443 \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-R0:localhost:18000 free@a.pinggy.io >/tmp/pinggy-verify.log 2>&1 &
SSH_PID=$!

sleep 5
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/pinggy-verify.log | head -1)
echo "URL: $URL"
curl -sI "$URL/" | head -1

kill "$SSH_PID" "$ORIGIN_PID"

預期:curl head 上出現 pinggy.link URL 和 HTTP/2 200