Pinggy Tunnel
Туннели localhost без установки через SSH и Pinggy.
Метаданные навыка
| Источник | Опционально — установка: vibeos skills install official/devops/pinggy-tunnel |
| Путь | optional-skills/devops/pinggy-tunnel |
| Версия | 0.1.0 |
| Автор | Teknium (teknium1), VibeOS |
| Лицензия | MIT |
| Платформы | linux, macos, windows |
| Теги | Pinggy, Tunnel, Networking, SSH, Webhook, Localhost |
| Связанные навыки | cloudflared-quick-tunnel, webhook-subscriptions |
Справочник: полный SKILL.md
Ниже приведено полное описание навыка, которое VibeOS загружает при его активации. Агент видит эти инструкции, когда навык активен.
Навык 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-туннель (базы данных, raw 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
# Шлюз с базовой аутентификацией (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 пропускает запрос подтверждения ключа хоста при первом запуске. 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
Передайте пользователю 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) или авто-перезапуск с циклом в shell (учтите, что 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.- Запрос ключа хоста при первом запуске. Стандартная конфигурация OpenSSH просит пользователя принять ключ хоста 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-клиента VibeOS: {"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.