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.

ESC