Перейти к основному содержимому

Политики трафика

Обзор​

Функция позволяет описать правила и действия обработки трафика для входящих запросов и более гибко настроить взаимодействие с вашим приложением.

к сведению

Доступно только по подписке в тарифе Легенда.

Чтобы понять, зачем нужны политики и как они работают, разберём пример включения базовой авторизации только для префикса /admin:

---
rules:
- name: Authorization for admin panel
expressions:
- req.URL.Path contains "/admin"
actions:
- name: Enable basic auth
type: basic_auth
config:
userList:
admin: password

Тут описано 1 правило(rules), в нём есть выражение(expressions), оно содержит условие в котором мы проверяем, что запрос предназначается для префикса /admin, если условие истинно, наступает действие(actions), в примере мы применяем базовую авторизацию, задаём список из пар username, password. Если пользователь проходит авторизацию, то далее трафик направляется к вашему сервису. Это правило позволит вам открыть доступ к разрабатываемому сайту для всех, например для заказчиков или коллег, но при этом вы можете запретить доступ к приватным частям сайта. Это лишь один пример, нижу вы найдёте полное описание и больше живых примеров.
Чтобы вам было проще написать нужное правило мы добавили отладчик прямо в личном кабинете, там вы можете проверить корректно ли работает правило, а так же приведено несколько примеров:

policy_debug

Правила (rules)​

Правила состоят из действий - actions (обязательно), которые должны вступить в силу и выражений - expressions (не обязательно), фильтрующих трафик, к которому они применяются. Правила оцениваются и выполняются последовательно в порядке их описания, без прерывания их выполнения. Т.е. если запрос подпадает сразу под несколько правил, то все действия также будет применены к данному запросу. Также можно указать имя name. Правила описываются в формате YAML, где они хранятся — см. ниже.

Например существует 2 правила. В первом мы включаем базовую авторизацию для всех запросов к префиксу /admin. Во втором проверяем есть ли заголовок Accept-Language и присутствует ли в нём русский язык ru-RU. Клиент сначала должен будет ввести username и password, а далее сработает запрет по заголовку Accept-Language если в значении заголовка есть ru-RU.

---
rules:
- expressions:
- req.URL.Path contains "/admin"
actions:
- type: basic_auth
config:
userList:
admin: admin
- expressions:
- any(req.Headers["Accept-Language"], {# matches "ru-RU"})
actions:
- type: deny

Выражения (expressions)​

примечание

Сайт с описанием синтаксиса expr-lang

Выражения основаны на синтаксисе expr-lang. Это богатый язык с множеством функций, операциями сравнения, вхождения, работы с строками и многим другим. Перечисление выражений в списке воспринимается как логическое И (and).

Пример блокировки POST запросов от ботов:

---
rules:
- expressions:
- lower(req.UserAgent) matches "(chatgpt-user|gptbot)"
- req.Method in ["POST"]
actions:
- type: deny

Переменные с данными из запроса​

Чтобы работать с входящим запросом есть несколько переменные для получения информации или метаданных:

НазваниеОписание
req.ProtoСодержит версию протокола HTTP, которая была использована при отправке запроса (например HTTP/1.1 или HTTP/2.0)
req.HostСодержит хост URL, включая порт, если он указан (алиас для req.URL.Host)
req.MethodСодержит метод запроса GET / POST и другие
req.URLПолный URL запроса (имеет свои методы, которые описаны в таблице ниже)
req.UserAgentСодержимое заголовка User-Agent
req.RemoteAddrIP адрес клиента с которого пришёл запрос
req.ContentLengthВозвращает длину тела запроса в байтах, если она известна, берётся из заголовка Content-Length.
req.HeadersСписок всех заголовков

Переменные внутри req.URL позволяют получить отдельные части URL:

НазваниеОписание
req.URL.SchemeСодержит схему URL, которая определяет протокол (например, http, https, ftp и т.д.).
req.URL.OpaqueСодержит непрозрачные (закодированные) данные URL, которые не могут быть напрямую разобраны на части, такие как схема, хост и путь.
req.URL.UserСодержит информацию о пользователе, содержащую имя пользователя и, возможно, пароль в URL.
req.URL.HostСодержит хост URL, включая порт, если он указан (например, example.com:8080).
req.URL.PathСодержит путь URL, который указывает на конкретный ресурс (например, /articles/2024/).
req.URL.RawPathСодержит закодированную версию пути (если она отличается), может быть полезен для случаев, когда путь содержит специальные символы.
req.URL.RawQueryСодержит закодированные параметры запроса без символа ? (например, name=John&age=30).
req.URL.FragmentСодержит фрагмент URL, который указывает на определенную часть документа, без символа # (например, section2).
req.URL.RawFragmentСодержит закодированную версию фрагмента, если он содержит специальные символы (например, пробелы или символы Unicode).

Метаданные запроса:

НазваниеОписание
meta.TimeТочное время в которое пришёл запрос, +- текущее время
meta.RequestIDИндивидуальный идентификатор запроса

Встроенные функции​

Помимо достаточно богатого синтаксиса expr-lang, доступны также и встроенные функции для быстрой работы с данными.

НазваниеОписаниеПример
IpInRangeПроверяет входит ли IP в подсетьIpInRange(req.RemoteAddr, "127.0.0.0/8")
IsIPV6Проверяет является ли строка IPv6 адресомIsIPV6(req.RemoteAddr)
IsIPV4Проверяет является ли строка IPv6 адресомIsIPV4(req.RemoteAddr)
IsIPПроверяет является ли строка IP адресомIsIP(req.RemoteAddr)
RandIntГенерация случайного целого числаRandInt(0, 10)
RandDoubleГенерация случайного дробного числа в диапазоне от 0 до 1RandDouble()

Примеры expressions​

Проверяем, что запрос пришёл по https:

- expressions:
- req.URL.Scheme == "https"

Запрос содержит префикс /admin:

- expressions:
- req.URL.Path contains "/admin"

Запрос не из подсети 10.0.0.0/8:

- expressions:
- not IpInRange(req.RemoteAddr, "10.0.0.0/8")

Действия (actions)​

Выполнение действий зависит от того истинно ли условие в выражениях (expressions), если выражение отсутствует, то действие выполняется всегда. Действие должно содержать тип type и конфигурацию config (не всегда обязательно). Также можно указать имя name.

Типы действий (type)​

Доступно несколько типов действий, одни модифицируют запрос, другие отвечают за самостоятельную обработку. Можно указывать несколько типов, они будут обработаны по порядку.

add_headers​

Добавить заголовки.

Можно добавлять заголовки как при передаче их к вышестоящему приложению context: request, так и при ответе клиенту context: response. Ключевое слово headers определяет карту ключ=значение, где ключ - название заголовка, а значение - содержимое заголовка.

---
rules:
- actions:
- type: add_headers
config:
context: request
headers:
foo: bar
qwe: rty
- actions:
- type: add_headers
config:
context: response
headers:
foo: bar
qwe: rty
remove_headers​

Удалить заголовки.

Можно удалять заголовки как при передаче их к вышестоящему приложению context: request, так и при ответе клиенту context: response. Ключевое слово headers определяет список заголовков.

---
rules:
- actions:
- type: remove_headers
config:
context: request
headers:
- foo
- qwe
- actions:
- type: remove_headers
config:
context: response
headers: ["foo", "qwe"]
basic_auth​

Включить базовую авторизацию.

Ключевое слово userList определяет карту ключ=значение, где ключ - username, а значение - password.

---
rules:
- actions:
- type: basic_auth
config:
userList:
admin: admin
foma: kiniaev
ащьф: лштшфум
url_rewrite​

Перезаписать URI.

Ключевое слово from определяет path с которого нужно перезаписать запрос к вышестоящему серверу, а to определяет path который будет передан к к вышестоящему серверу. Также работают регулярные выражения.

---
rules:
- actions:
- type: url_rewrite
config:
from: /foo
to: /bar
redirect​

Перенаправление URL.

Ключевое слово to определяет Location на который перенаправлять, также можно переопределить statusCode (по умолчанию 307). Ключевое слово headers определяет карту ключ=значение, где ключ - название заголовка, а значение - содержимое заголовка.

---
rules:
- actions:
- type: redirect
config:
to: /foobar
statusCode: 304
headers:
foo: bar
deny​

Запретить обращение к вышестоящему приложению.

Можно переопределить statusCode (по умолчанию 403).

---
rules:
- actions:
- type: deny
config:
statusCode: 451
restrict_ips​

Ограничить обращения по подсетям.

Ключевое слово allow определяет список подсетей с которых разрешены обращения. Ключевое слово deny определяет список подсетей с которых запрещены обращения.

---
rules:
- actions:
- type: restrict_ips
config:
allow:
- 127.0.0.1/32
deny:
- 10.0.0.10/32
rate_limit​

Ограничить обращения по количеству запросов.

Ключевое слово rate определяет промежуток времени, а capacity количество запросов которые можно совершить. При превышении этого порога, клиент начнёт получать 429 Too Many Requests.

---
rules:
- actions:
- type: rate_limit
config:
rate: "60s"
capacity: 2
custom_response​

Пользовательский ответ.

Обязательный параметр statusCode определяет HTTP статус. Можно также передать тело, содержимое задаётся в параметре content. И заголовки, headers определяет карту ключ=значение, где ключ - название заголовка, а значение - содержимое заголовка.

---
rules:
- actions:
- type: custom_response
config:
statusCode: 200
content: |
User-agent: *
Disallow: /
headers:
content-type: "text/plain"
reverse_proxy​

Реверс-прокси в другой сервис.

С помощью этого действия можно направлять отдельные запросы в сторонний сервис – это может быть полезно, к примеру, при разработке микро-сервисов.

Обязательный параметр url указывает альтернативную точку для реверс-прокси, необязательный параметр onError определяет поведение, при котором запрос не удалось передать в сторонний сервис:

  • halt (по умолчанию) – обработка запроса при ошибке будет прервана;
  • continue – запрос будет обрабатываться дальше согласно нижестоящим правилам.

Стандартное поведение:

---
rules:
- actions:
- type: reverse_proxy
config:
url: "http://127.0.0.1:8000"

С параметром onError в значении continue:

---
rules:
- actions:
- type: reverse_proxy
config:
url: "http://127.0.0.1:8000"
onError: continue
- type: custom_response
config:
statusCode: 500
content: Unavailable

В данном случае, если ресурс http://127.0.0.1:8000 недоступен, в ответ на запрос будет возвращен HTTP 500 и текст Unavailable.

compress_response​

Сжать ответ.

Сжимает ответ, если вышестоящее приложение не сделало этого само. Необязательный параметр algorithms задаёт список разрешённых алгоритмов: gzip, deflate, br, zstd (по умолчанию все четыре). Если указать другое значение, политика не пройдёт проверку.

---
rules:
- actions:
- type: compress_response
config:
algorithms: [br, gzip]
- type: reverse_proxy
config:
url: "https://example.com"

Само действие ничего не отвечает клиенту. Оно сжимает ответ, который сформируют следующие действия и правила (reverse_proxy, custom_response и другие), поэтому ставить его нужно до действия, которое формирует ответ. Если сработало несколько compress_response, применяется первое.

Алгоритм выбирается по заголовку Accept-Encoding клиента с учётом весов q и *. При равном весе выигрывает алгоритм, указанный в заголовке раньше. Если клиент не просит ни один из разрешённых алгоритмов или запрещает их через q=0, ответ уходит без сжатия.

При сжатии в ответ добавляются заголовки Content-Encoding и Vary: Accept-Encoding, а Content-Length и Accept-Ranges удаляются. Строгий ETag становится слабым (W/"..."). Ответ сжимается на лету, без буферизации, поэтому SSE и потоковые ответы продолжают работать: события не накапливаются.

Ответ не сжимается, если:

  • у него уже есть заголовок Content-Encoding;
  • это частичный ответ (Content-Range);
  • запрос пришёл с методом HEAD;
  • статус ответа 1xx, 204 или 304;
  • соединение переключается на WebSocket или другой протокол через Upgrade;
  • содержимое уже сжато по своей природе: image/* (кроме SVG), video/*, audio/*, font/woff*, архивы (zip, gzip, 7z, rar, bzip2, xz, lzma, zstd, brotli).

Проверить сжатие можно GET-запросом (curl -I отправляет HEAD, а такой ответ не сжимается):

curl -s -o /dev/null -D - -H 'Accept-Encoding: br' https://<домен>/

В заголовках ответа должны быть content-encoding: br и vary: Accept-Encoding. Без заголовка Accept-Encoding ответ приходит несжатым.

Где хранятся правила​

По умолчанию правила читаются из файла .tuna.yml в текущем каталоге. Путь к файлу можно переопределить флагом --policy-file или переменной окружения TUNA_POLICY_FILE.

Загрузка по ссылке​

Начиная с версии 0.36.0 правила для tuna http можно загрузить по ссылке флагом --policy-url или переменной окружения TUNA_POLICY_URL. Так одну политику удобно держать в репозитории или хранилище и использовать на разных машинах:

tuna http 8080 --policy-url=https://gitlab.com/example/policies/-/raw/main/policy.yml
  • Ссылка — только https. http допускается лишь для localhost, 127.0.0.1 и ::1.
  • Редиректы допускаются (до 10), каждый проверяется по тому же правилу.
  • Формат — YAML или JSON со структурой как в .tuna.yml. Из ответа берётся только rules, остальные поля игнорируются.
  • Ответ должен быть 200, не пустым и не больше 1 МБ. Таймаут — 15 секунд.
  • Политика загружается один раз при запуске. Чтобы применить изменения, перезапустите туннель.
  • С --policy-url локальный .tuna.yml для политики не читается. Вместе с --policy-file или TUNA_POLICY_FILE флаг использовать нельзя.
warning

Политика по ссылке может всё то же, что и локальная, в том числе проксировать запросы в вашу локальную сеть через reverse_proxy. Загружайте её только из источника, которому доверяете.

Ошибки политики​

Начиная с версии 0.36.0 туннель не запускается, если политику не удалось загрузить:

  • файл, указанный в --policy-file или TUNA_POLICY_FILE, не найден;
  • файл, в том числе .tuna.yml по умолчанию, не разбирается;
  • ответ по ссылке не загрузился или не разбирается.

Если .tuna.yml в текущем каталоге просто нет, туннель запускается без политики.

Туннель с политикой запускается только с подпиской в тарифе Легенда, без неё tuna завершится с ошибкой Traffic policy is available only in Team subscription. Подписка проверяется при запуске: если она закончится, пока туннель работает, туннель и политика продолжат работать до перезапуска. В более ранних версиях tuna в обоих случаях выводила предупреждение и запускала туннель.

Примеры​

Блокировка запросов по заголовкам​

Блокируем запросы, если нет заголовка X-Api-Version, и отвечаем HTTP 400, если ниже 3:

---
rules:
- name: Блокируем API v0
expressions:
- len(req.Headers["X-Api-Version"]) == 0
actions:
- name: Запретить
type: deny
- name: 400 на API v1 и v2
expressions:
- req.Headers["X-Api-Version"][0] in ["1", "2"]
actions:
- name: Ответить 400 JSON
type: custom_response
config:
statusCode: 400
headers:
content-type: "application/json"
content: |
{"msg":"Вы используете старую версию API, минимальная версия 3"}

Реверс-прокси в сторонние сервисы по префиксу и заголовкам​

Перенаправляем запросы к /v1 в сторонний сервис на порту 8000, а запросы с User-Agent содержащий mobile к сервису на порту 3000. При этом команда запуска tuna http 8080 по умолчанию проксирует все запросы на 8080.

---
rules:
- expressions:
- hasPrefix(req.URL.Path, "/v1")
- lower(req.UserAgent) contains "mobile"
actions:
- type: reverse_proxy
config:
url: "http://127.0.0.1:3000"
- expressions:
- hasPrefix(req.URL.Path, "/v1")
actions:
- type: reverse_proxy
config:
url: "http://127.0.0.1:8000"

Пользовательский ответ в выходные дни​

---
rules:
- expressions:
- now().Weekday().String() in ["Saturday", "Sunday"]
actions:
- type: custom_response
config:
statusCode: 200
content: Take a rest!

Базовая аутентификация​

---
rules:
- expressions:
- req.URL.Path contains "/admin"
actions:
- type: basic_auth
config:
userList:
admin: password

Несколько действий без выражений​

Ограничение запросов с 127.0.0.1 и 10.0.0.10, а для всех остальных ограничить количество запросов в секунду до 2х.

---
rules:
- actions:
- type: restrict_ips
config:
allow:
- 127.0.0.1/32
deny:
- 10.0.0.10/32
- actions:
- type: rate_limit
config:
rate: "1s"
capacity: 2

Перезапись (rewrite) с регулярным выражением​

---
rules:
- expressions:
- hasPrefix(req.URL.Path, "/blog")
actions:
- type: url_rewrite
config:
from: "/blog/([0-9]+)/([a-zA-Z]+)/"
to: /index.php?p=$1&title=$2"