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

## Обзор

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

к сведению

Доступно только по [подписке](https://tuna.am/#pricing) в тарифе **Легенда**.

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

```yaml
---
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`. Если пользователь проходит авторизацию, то далее трафик направляется к вашему сервису. Это правило позволит вам открыть доступ к разрабатываемому сайту для всех, например для заказчиков или коллег, но при этом вы можете запретить доступ к приватным частям сайта. Это лишь один пример, нижу вы найдёте полное описание и больше живых примеров.<br />Чтобы вам было проще написать нужное правило мы добавили [отладчик](https://my.tuna.am/policy_debug) прямо в личном кабинете, там вы можете проверить корректно ли работает правило, а так же приведено несколько примеров:

[![policy_debug](/docs/img/policy_debug.png)](https://my.tuna.am/policy_debug)

## Правила (rules)

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

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

```yaml
---
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](https://expr-lang.org/)

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

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

```yaml
---
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.RemoteAddr    | IP адрес клиента с которого пришёл запрос                                                                           |
| 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](https://expr-lang.org/), доступны также и встроенные функции для быстрой работы с данными.

| Название   | Описание                                                  | Пример                                   |
| ---------- | --------------------------------------------------------- | ---------------------------------------- |
| 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 до 1 | RandDouble()                             |

#### Примеры expressions

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

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

```

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

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

```

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

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

```

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

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

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

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

##### add_headers

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

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

```yaml
---
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` определяет список заголовков.

```yaml
---
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`.

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

```

##### url_rewrite

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

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

```yaml
---
rules:
  - actions:
      - type: url_rewrite
        config:
          from: /foo
          to: /bar

```

##### redirect

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

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

```yaml
---
rules:
  - actions:
      - type: redirect
        config:
          to: /foobar
          statusCode: 304
          headers:
            foo: bar

```

##### deny

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

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

```yaml
---
rules:
  - actions:
      - type: deny
        config:
          statusCode: 451

```

##### restrict_ips

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

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

```yaml
---
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`.

```yaml
---
rules:
  - actions:
      - type: rate_limit
        config:
          rate: "60s"
          capacity: 2

```

##### custom_response

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

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

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

```

##### reverse_proxy

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

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

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

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

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

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

```

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

```yaml
---
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` (по умолчанию все четыре). Если указать другое значение, политика не пройдёт проверку.

```yaml
---
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`, а такой ответ не сжимается):

```bash
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`. Так одну политику удобно держать в репозитории или хранилище и использовать на разных машинах:

```shell
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:

```yaml
---
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**.

```yaml
---
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"

```

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

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

```

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

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

```

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

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

```yaml
---
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) с регулярным выражением

```yaml
---
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"

```
