OpenClaw как личный помощник в Telegram: ВМ за NAT, GRE+IPsec и рублёвый API
Захотелось личного AI-агента, который живёт в Telegram, помнит контекст, умеет ходить в интернет и запускать скрипты. Взял OpenClaw - open-source агент-шлюз с поддержкой Telegram, skills и cron, - и поставил на домашнюю ВМ. По дороге пришлось решить две неочевидные задачи: вывести ВМ в интернет через свой сервер за границей и найти, где платить за модели рублями. Плюс один раз наступил на грабли с расходом токенов, о которых стоит рассказать отдельно. Ниже - что и как.
Схема
ВМ jarwise: Ubuntu 24.04, 4 vCPU, 4 ГБ RAM, стоит за домашним NAT, снаружи доступен только SSH через проброс порта. Отдельный VPS в Европе (хаб) - на нём уже был один GRE+IPsec туннель до другой площадки.
Задача формулируется так: весь исходящий трафик ВМ пустить через хаб, но при этом не потерять входящий SSH через проброс.
Туннель через NAT
Существующий туннель на хабе сделан в transport mode - через NAT так не выйдет: NAT переписывает адреса, а ESP (proto 50) большинство роутеров не пробрасывают. Рабочая схема - GRE внутри IPsec в tunnel mode с NAT-T (UDP 500/4500), где инициатор - сторона за NAT.
Конфиг хаба, strongSwan:
conn gre-openclaw
type=tunnel
keyexchange=ikev2
authby=secret
left=<hub-ip>
leftsubnet=<hub-ip>/32
leftprotoport=gre
right=%any
rightid=@openclaw
rightsubnet=<vm-lan-ip>/32
rightprotoport=gre
ike=aes256-sha256-modp2048!
esp=aes256gcm16!
dpdaction=clear
auto=add
Плюс интерфейс gre2 (10.10.11.1/30, remote = приватный IP ВМ, MTU 1360), MASQUERADE для 10.10.11.0/30 и MSS clamp. Отдельным правилом дропаем GRE от пира вне IPsec - чтобы туннель физически не мог подняться в открытом виде.
Сторона ВМ настраивается зеркально: left=<vm-lan-ip> leftid=@openclaw right=<hub-ip> auto=start dpdaction=restart closeaction=restart, интерфейс gre2 - 10.10.11.2/30.
Маршрутизация - самая интересная часть. Нужно свести три требования вместе:
- Хаб всегда достижим через локальный шлюз. Внешняя точка IPsec не должна ходить сама через себя:
ip route add <hub-ip>/32 via <lan-gw>. - Ответы на входящий SSH уходят обратно в локальный шлюз, а не в туннель. Здесь помогает policy routing:
ip rule add from <vm-lan-ip> table 100, а в table 100 - default через<lan-gw>и link-route на LAN. Новые исходящие соединения сорсятся с адреса туннеля, под это правило не попадают и уходят в default основной таблицы. - Default в интернет - через туннель:
ip route add default via 10.10.11.1 dev gre2 metric 50. DHCP-default с metric 100 остаётся про запас, но GRE-интерфейс не падает при обрыве IPsec - поэтому трафик в обход не утечёт, просто пропадёт интернет. Это осознанный компромисс: лучше остаться без сети, чем случайно выпустить трафик мимо туннеля.
Всё это собрано в один идемпотентный скрипт, который дёргается через systemd unit gre2.service до старта strongSwan. При установке default route я ставил себе страховку: фоновый sleep 180 && ip route del ..., который откатывал маршрут, если за три минуты не убеждался, что SSH жив. Проверка после reboot прошла чисто: SA поднялась за минуту, curl ifconfig.me отдаёт адрес хаба, DF-пакет на 1332 байта проходит.
Установка OpenClaw
Ставил rootless - через install-cli.sh под отдельным пользователем openclaw (свой Node 24 в ~/.openclaw, системный не трогал). Дальше loginctl enable-linger, gateway как systemd user unit, bind только на loopback. Telegram-бот заводится через @BotFather, а доступ к нему я закрыл наглухо: dmPolicy: allowlist плюс allowFrom с моим user ID - писать боту могу только я.
Где платить за модель
Вот тут началось самое весёлое, и не по технической причине.
Первая попытка - Gemini напрямую. Ключ работает, но на первом же запросе прилетает 429 RESOURCE_EXHAUSTED: free tier для новых моделей даёт нулевую квоту, а billing из России не включается (OR_BACR2_44 при любых плясках с браузером). Anthropic - та же история с оплатой.
Значит, агрегаторы. OpenClaw умеет подключать любой OpenAI-совместимый endpoint как custom provider:
models: {
providers: {
polza: {
baseUrl: "https://polza.ai/api/v1",
apiKey: "${POLZA_API_KEY}",
api: "openai-completions",
models: [{ id: "google/gemini-3.8-flash", name: "Gemini 3.8 Flash", contextWindow: 1000000, maxTokens: 65536 }]
}
}
}
Ключ лежит в ~/.openclaw/.env, в конфиг не попадает. Сравнил цены на gemini-3.8-flash за 1M токенов ввода/вывода (по курсу ЦБ 86,19):
| Провайдер | Ввод | Вывод |
|---|---|---|
| Google напрямую | 65 ₽ | 323 ₽ |
| OpenRouter Flex | 32 ₽ | 162 ₽ |
| Polza.ai | 45 ₽ | 226 ₽ |
| AITunnel | 15 ₽ | 750 ₽ |
| GPTunnel | 259 ₽ | 1293 ₽ |
| ProxyAPI | 455 ₽ | 2275 ₽ |
Google и OpenRouter из России не оплатить, так что они выпадают. Из рублёвых реально конкурируют Polza и AITunnel: AITunnel дешевле на вводе, но вывод втрое дороже - а у 3.8 Flash thinking-токены идут как вывод. Точка равновесия - примерно 5,8% вывода от объёма ввода; у агента с инструментами обычно больше, так что остался на Polza. Любопытная деталь: цены Polza - ровно 0,7 от гугловских, похоже на Flex-тир с наценкой; под нагрузкой у Google Flex отвечает медленнее.
Как агент сжёг токены за ночь
А теперь обещанные грабли - на мой взгляд, самая полезная часть всей истории.
Утром обнаружил, что баланс заметно просел, хотя боту ночью никто не писал. Полез в лог gateway: с полуночи до 8 утра - 18 вызовов модели, ровно два в час. Это heartbeat, фоновая проверка «есть ли что-то, требующее внимания», по умолчанию раз в 30 минут. Сама по себе она дешёвая. Но гоняется она в главной сессии, а главная сессия после вечера, когда мы с ботом писали скрипты, весила 154k токенов. Каждые полчаса модели уходили все эти 154k - чтобы она ответила NO_REPLY. За ночь набежало около 2,8M входных токенов впустую.
Что сделал:
agents.defaults.heartbeat.every: "0m"- у меня нет monitor scratch, heartbeat не делал ничего полезного. Все реальные проверки живут в cron, где у каждой задачи свой интервал и своя сессия./compactв Telegram - главная сессия ужалась со 154k до 15k. Без этого каждое обычное сообщение тоже тащило бы все 154k.- Если heartbeat всё-таки нужен - его спасают
isolatedSession: true+lightContext: true+activeHours. В документации прямо сказано: ~100k токенов на вызов превращаются в 2-5k.
Отдельно проверил cron-задачу, которая опрашивает Zabbix каждые 3 минуты, - она оказалась чистым Python-скриптом без обращения к модели, её трогать не надо. Вообще, смотреть, кто тратит, проще всего по метке [model-fetch] в логе gateway: там есть провайдер, модель и время ответа, а runtime.llm отдаёт usage с costUsd.
Мелочи, которые сэкономили время
openclaw doctor --fixвыключил 32 skill’а без бинарников (apple-notes, sonos, 1password и подобное) - они только раздували промпт.- Chrome для browser tool ставил
.debс dl.google.com, а не snap:browser.executablePath=/usr/bin/google-chrome, headless сам определился по отсутствию DISPLAY. Кириллица на скриншотах требует пакетаfonts-noto. - Секреты - только через
${VAR}из.env. Иopenclaw config patch --dry-runперед каждым изменением конфига, чтобы не удивляться результату.
Что ещё предстоит
Семантическая память (embeddings) у меня пока на FTS-only: провайдер по умолчанию openai, а ключа к нему нет. Polza отдаёт /v1/embeddings, так что переключить - следующий шаг.
И ещё: с 1 января 2027 Google удваивает цену на 3.8 Flash, а агрегаторы наверняка подтянутся следом - так что таблицу с ценами выше к тому моменту стоит пересчитать заново.