Pinggy Tunnel
Туннели к localhost по SSH через Pinggy без установки.
Метаданные навыка
| Источник | Опциональный — установить командой 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
Открывает локальный сервис (dev-сервер, приёмник вебхуков, MCP-эндпоинт, демо) во внешний интернет через обратный SSH-туннель Pinggy. Никаких демонов устанавливать не нужно — штатный SSH-клиент подключается к a.pinggy.io:443, а Pinggy возвращает публичный HTTP/HTTPS URL.
Бесплатный тариф: туннели на 60 минут, случайный поддомен, регистрация не нужна. Тариф Pro ($3/мес) подключается по желанию через токен.
Когда использовать
- Пользователь просит «открыть это наружу», «расшарить dev-сервер», «сделать этот URL публичным», «пробросить порт N», «получить публичный URL для вебхука»
- Нужно принять обратный вызов вебхука во время локальной задачи (Stripe, GitHub, Discord, AgentMail)
- Разовый показ HTTP-демо (MCP-сервер, эндпоинт Ollama/vLLM, дашборд) удалённому собеседнику
- На хосте есть SSH, но нет бинарника
cloudflared/ngrok— и ставить его излишне
Если на хосте уже настроен cloudflared, берите навык cloudflared-quick-tunnel: быстрые туннели Cloudflare не отключаются через 60 минут.
Требования
sshв PATH (ssh -V). Есть по умолчанию в Linux, macOS и Windows 10+. Ничего дополнительно устанавливать не нужно.- Локальный сервис, слушающий
127.0.0.1:<port>, должен быть запущен до старта туннеля. Pinggy вернёт URL, но они будут отдавать 502, пока источник не поднимется.
Опционально:
- Переменная окружения
PINGGY_TOKENдля платных функций Pro (постоянный поддомен, свой домен, несколько туннелей, без лимита в 60 минут). Бесплатному тарифу учётные данные не нужны.
Краткий справочник
# Обычный туннель HTTP/HTTPS для порта 8000 (бесплатный тариф)
ssh -p 443 -o StrictHostKeyChecking=no -o ServerAliveInterval=30 \
-R0:localhost:8000 free@a.pinggy.io
# TCP-туннель (базы данных, голый SSH и т.п.)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:5432 tcp@a.pinggy.io
# TLS-туннель (Pinggy не расшифровывает — сертификаты держите на источнике)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:443 tls@a.pinggy.io
# Защита Basic-аутентификацией (b:user:pass)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"b:admin:secret+free@a.pinggy.io"
# Защита bearer-токеном (k:token)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"k:mysecrettoken+free@a.pinggy.io"
# Белый список IP (w:CIDR)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"w:203.0.113.0/24+free@a.pinggy.io"
# CORS + принудительный редирект на HTTPS
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
"co+x:https+free@a.pinggy.io"
# Тариф Pro (постоянный URL, без лимита в 60 минут)
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 "$PINGGY_TOKEN+a.pinggy.io"
Процедура — запустить туннель и получить URL
Модели СЛЕДУЕТ использовать инструмент terminal. Туннель должен оставаться живым на всё время раздачи доступа, поэтому его запускают фоновым процессом и вытаскивают публичный URL из stdout.
1. Убедитесь, что локальный источник поднят
curl -sI http://127.0.0.1:8000/ | head -1
# ожидаем HTTP/1.x 200 (или любой ответ, кроме connection refused)
Если пока ничего не слушает, сначала запустите сервис (например, python3 -m http.server 8000 --bind 127.0.0.1). Pinggy охотно отдаст URL, указывающий в пустоту, — пользователь будет видеть 502, пока источник не поднимется.
2. Запустите туннель фоновым процессом
Используйте terminal(background=True) и направляйте вывод в лог-файл (Pinggy печатает URL в stdout, затем держит соединение открытым):
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 пропускают запрос на подтверждение host-key при первом подключении. ServerAliveInterval=30 не даёт простаивающему NAT разорвать SSH-сессию.
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
Передайте пользователю URL вида https://...pinggy.link.
4. Проверьте
curl -sI https://<the-url>/ | head -3
# ожидаем 200/302/что угодно, что реально возвращает локальный источник
Получили 502 Bad Gateway — значит SSH-сессия поднята, но локальный источник не слушает. Сначала разберитесь с шагом 1.
5. Остановка
kill "$(cat /tmp/pinggy-8000.pid)"
# или, если pid-файл потерялся:
pkill -f 'ssh -p 443 .* free@a\.pinggy\.io'
Если есть session_id из terminal(background=True), лучше используйте process(action='kill', session_id=...).
Контроль доступа через ключевые слова в имени пользователя
Pinggy записывает управляющие флаги в имя SSH-пользователя, разделяя их знаком +. Весь аргумент user@host нужно брать в кавычки, если в нём есть +:
| Ключевое слово | Действие |
|---|---|
b:user:pass | Защита HTTP Basic-аутентификацией |
k:token | Защита заголовком bearer-токена (Authorization: Bearer <token>) |
w:CIDR | Белый список IP (одиночный IP или CIDR, можно повторять) |
co | Добавить Access-Control-Allow-Origin: * (CORS) |
x:https | Принудительный HTTPS — авторедирект с HTTP на HTTPS |
a:Name:Value | Добавить заголовок запроса |
u:Name:Value | Обновить заголовок запроса |
r:Name | Удалить заголовок запроса |
qr | Вывести QR-код URL в stdout (удобно для раздачи на телефон) |
Флаги комбинируются свободно: "b:admin:secret+co+x:https+free@a.pinggy.io".
Веб-отладчик (опционально)
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(Pro) либо перезапускайте туннель циклом в шелле — учтите, что на бесплатном тарифе URL меняется при каждом перезапуске. - URL на бесплатном тарифе случайный и меняется при перезапуске. Не добавляйте в закладки, не вставляйте в конфиг. Каждый раз парсите из лога заново.
- Одновременных бесплатных туннелей — не больше одного на IP. Второй туннель с той же машины обычно убивает первый. Тариф Pro снимает это ограничение.
+в именах пользователей нужно экранировать кавычками. Голыйssh ... b:admin:secret+free@a.pinggy.ioработает в bash, но ломается в шеллах, где+имеет особый смысл, или при программной сборке строки. Всегда оборачивайте в двойные кавычки.- Не туннелируйте ничего чувствительного без флага контроля доступа. Голый HTTP-туннель доступен всем, у кого есть URL. Для непубличных сервисов используйте
b:,k:илиw:. process(action='log')может пропустить вывод SSH-баннера. Pinggy печатает URL, после чего SSH-сессия переходит в интерактивный режим. Всегда перенаправляйте вывод в лог-файл иgrep-айте файл напрямую — тот же приём, что и вcloudflared-quick-tunnel.- Запрос host-key при первом запуске. Стандартный конфиг OpenSSH просит принять host-key Pinggy. Для автоматических запусков всегда передавайте
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null. - TCP- и TLS-туннели возвращают пару
<subdomain>.a.pinggy.online:<port>, а не https-URL. Парсите другим регэкспом (tcp://и порт). Не считайте, что каждый туннель Pinggy — HTTP. - В режиме Pro токен передаётся как имя пользователя, а не как флаг. Используйте
"$PINGGY_TOKEN+a.pinggy.io"(безfree@). С токеном можно добавить:persistentдля стабильного поддомена — см.pinggy.io/docs/.
Рецепты
Составные сценарии, объединяющие локальный источник с туннелем Pinggy. Каждый рецепт самодостаточен: поднять источник, поднять туннель, распарсить URL, отдать пользователю.
Рецепт 1 — принять обратный вызов вебхука
Подходит, когда внешнему сервису (Stripe, GitHub, Discord, AgentMail и т.п.) нужно сделать POST на публично доступный URL во время локальной задачи.
# 1. Крошечный сервер-приёмник: каждый запрос дописывается в /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. Туннель — защита bearer-токеном, чтобы посторонние не засоряли лог захвата
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. Пока агент работает, наблюдаем за прилетающими запросами
tail -f /tmp/webhook-hits.log
Передайте $URL сервису, которому нужно вас вызвать. Остановка: kill $(cat /tmp/webhook-server.pid) $(cat /tmp/webhook-pinggy.pid).
Рецепт 2 — открыть MCP-сервер по HTTP/SSE
Подходит, когда удалённому MCP-клиенту (Claude Desktop на другой машине, редактор коллеги и т.п.) нужно достучаться до MCP-сервера на локальной машине. Работает только для MCP-серверов с HTTP-транспортом — серверы в режиме stdio туннелировать нельзя.
# 1. Запускаем MCP-сервер в режиме HTTP (пример: FastMCP-сервер на порту 8765)
nohup python3 my_mcp_server.py --transport http --port 8765 \
>/tmp/mcp-server.log 2>&1 &
echo $! >/tmp/mcp-server.pid
# 2. Туннель с bearer-токеном — MCP-трафик не должен быть открыт в интернет
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"
Удалённый клиент подключается к $URL с заголовком Authorization: Bearer $TOKEN. Конфиг для родного MCP-клиента Hermes: {"transport": "http", "url": "<URL>", "headers": {"Authorization": "Bearer <TOKEN>"}}.
Рецепт 3 — открыть локальный LLM-эндпоинт (Ollama / vLLM / llama.cpp)
Поделитесь локальной моделью с удалённым вызывающим (другой агент, телефон, коллега). Ollama слушает на :11434, vLLM и llama.cpp — обычно на :8000.
# Предусловие: сервер модели уже работает на 127.0.0.1:11434 (порт Ollama по умолчанию)
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"
# Проверка
curl -s "$URL/api/tags" -H "Authorization: Bearer $TOKEN" | head
co включает CORS, чтобы к эндпоинту мог обратиться браузер. Для вызовов только из бэкенда co можно убрать. Для OpenAI-совместимого эндпоинта vLLM/llama.cpp вызывающие используют базовый URL $URL/v1 с заголовком Authorization: Bearer $TOKEN — но учтите, что Pinggy ничего не вырезает и не заменяет в теле запроса, так что сам сервер модели видит токен Pinggy; локальный сервер лучше настроить без проверки аутентификации (он слушает на 127.0.0.1) и отдать контроль доступа Pinggy.
Рецепт 4 — раздать dev-сервер с одноразовым паролем
Самый быстрый вариант «дай коллеге потыкать в моё запущенное приложение». Случайный пароль, печатается один раз, туннель живёт до 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 печатается в терминал. Поделитесь URL + паролем. Ctrl-C для остановки.
b:dev:$PASS закрывает URL HTTP Basic-аутентификацией. x:https принуждает к TLS. co добавляет CORS для SPA-фронтендов.
Проверка
# Сквозной тест: поднять тривиальный источник, протуннелить, постучаться, остановить
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"
Ожидается: URL pinggy.link и HTTP/2 200 в ответе на curl head.