Документация Нативные протокольные моки (gRPC, WebSocket, Kafka, RabbitMQ, SMTP)

Нативные протокольные моки (gRPC, WebSocket, Kafka, RabbitMQ, NATS, SMTP)

Mockarty умеет обслуживать gRPC- и WebSocket-моки нативно — настоящий протокольный трафик прямо с платформы, без сборки и запуска сгенерированных серверов. Вы загружаете контракт API (для gRPC), создаёте моки как обычно и направляете клиент на Mockarty.

Нативная подача работает рядом с классическими путями (сгенерированные серверы и resolve-эндпоинты). Существующие моки не меняются.

В разделе Моки → Конструктор выберите протокол до ввода ответа. Название
поля и пример меняются вместе с протоколом: ответы gRPC, MCP и GraphQL
редактируются как JSON, SOAP — как XML, а сообщения Kafka/RabbitMQ/NATS/Socket
могут быть обычным текстом. При переключении протокола редактор меняет
подсветку и подсказку, но не переписывает уже введённое содержимое. Проверьте
ответ перед сохранением или нажатием Тест.

Нативный gRPC

Как это работает

  1. Вы публикуете .proto (файл или zip-архив файлов) как gRPC-контракт в реестре контрактов.
  2. Mockarty компилирует контракт и знает каждый сервис, метод и тип сообщения.
  3. Нативный gRPC-listener отвечает на настоящие gRPC-вызовы этих методов, резолвя каждый вызов через ваши gRPC-моки — с условиями запроса, JsonPath/Faker-темплейтингом, скриптовыми ответами, задержками и кодами ошибок.
  4. Включён server reflection: инструменты вроде grpcurl и Postman находят сервисы без локального .proto.

Включение

Задайте порт до запуска Mockarty (по умолчанию listener выключен):

MOCKARTY_GRPC_NATIVE_PORT=5890 ./mockarty

Опционально: MOCKARTY_GRPC_NATIVE_NAMESPACE задаёт неймспейс по умолчанию для вызовов без явного (иначе используется стандартный).

Публикация gRPC-контракта

Контракты → Реестр → Публикация, тип спецификации gRPC, содержимое .proto. Или через API:

curl -X POST http://localhost:5770/api/v1/contract/registry \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "serviceName": "greeter",
    "specType": "grpc",
    "specContent": "syntax = \"proto3\";\npackage demo;\nservice GreeterService {\n  rpc SayHello(HelloRequest) returns (HelloReply);\n}\nmessage HelloRequest { string name = 1; }\nmessage HelloReply { string message = 1; }"
  }'

Активные gRPC-контракты подхватываются нативным listener’ом автоматически (в пределах минуты, либо сразу после открытия через API сервисов ниже).

Сервисы и скелеты сообщений

curl http://localhost:5770/api/v1/contract/registry/<contract-id>/grpc-services \
  -H "Authorization: Bearer $TOKEN"

Ответ перечисляет сервисы и методы с типом (unary, server_stream, client_stream, bidi_stream) и готовыми к редактированию JSON-скелетами запроса/ответа — скопируйте скелет ответа в payload мока и заполните значения. ИИ-агенты используют те же данные через MCP-тул grpc_contract_services.

Создание мока

Обычный gRPC-мок — сервис, метод и payload ответа:

curl -X POST http://localhost:5770/api/v1/mocks \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "id": "greeter-hello",
    "namespace": "sandbox",
    "grpc": {"service": "demo.GreeterService", "method": "SayHello"},
    "response": {"payload": {"message": "Привет, $.req.name!"}}
  }'

$.req.* читает поля декодированного gRPC-запроса; все Faker-функции тоже работают.

Вызов

grpcurl -plaintext -d '{"name":"Ада"}' localhost:5890 demo.GreeterService/SayHello
# {"message": "Привет, Ада!"}

grpcurl -plaintext localhost:5890 list        # reflection: список сервисов
grpcurl -plaintext localhost:5890 describe demo.GreeterService

Чтобы указать неймспейс или имя мок-сервера, передайте gRPC-метаданные:

Ключ метаданных Значение По умолчанию
x-mockarty-namespace неймспейс для поиска моков настроенный дефолт
x-mockarty-server фильтр по имени мок-сервера нет

При включённом мультитенантном переключателе (MOCKARTY_NATIVE_BROKERS=1, см.
Один порт — много пространств имён)
метаданные не нужны: вызывайте <namespace>.mock.example.com:5771 по TLS, и
пространство выбирает хост — grpcurl -insecure team-a.mock.example.com:5771 demo.GreeterService/SayHello.

Один вызов из API-тестера

Протестировали реальный gRPC-апстрим в API-тестере и хотите, чтобы Mockarty
отдавал этот ответ как мок? Один запрос регистрирует контракт и создаёт мок:

curl -X POST http://localhost:5770/api/v1/api-tester/grpc/mock-with-contract \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "serverAddress": "payments.internal:9090",
    "requestData": {"service": "payments.PaymentService", "method": "Charge", "body": "{\"amount\":100}"},
    "responseData": {"body": "{\"status\":\"APPROVED\"}"}
  }'

Proto-схема подтягивается через server reflection с serverAddress (или
передайте protoContent с исходником .proto). Регистрация идемпотентна:
идентичная спецификация переиспользует существующий контракт
("contractReused": true), изменённая — обновляет его на месте с новой
версией; повторные вызовы никогда не плодят дубликаты контрактов. В UI то же
самое происходит автоматически при клике Create Mock на gRPC-ответе.
AI-агентам доступен MCP-тул grpc_mock_with_contract.

Стриминг

Обслуживаются все четыре вида gRPC:

  • Server streaming — сделайте payload мока JSON-массивом; каждый элемент уходит отдельным сообщением.
  • Client streaming — клиент шлёт последовательность; условия мока матчатся по последнему сообщению; возвращается один ответ.
  • Bidirectional — каждое входящее сообщение матчится независимо и получает ответ мока (payload-массив шлёт серию сообщений на один запрос).

Ошибки

Задайте моку gRPC-код статуса (1–16) и/или сообщение об ошибке — нативный listener вернёт настоящую gRPC-ошибку:

{"response": {"statusCode": 7, "error": "нет прав для этого пользователя"}}

Детали ошибки. Приложите те же стандартные google.rpc status-details, что отдаёт генерируемый сервер — errorInfo, retryInfo, badRequest, quotaFailure, preconditionFailure, help, debugInfo, localizedMessage, requestInfo, resourceInfo — и нативный listener кладёт их в статус на проводе (клиент читает через status.Details()):

{"response": {"statusCode": 8, "error": "превышена квота",
  "errorDetails": [
    {"type": "errorInfo", "details": {"reason": "QUOTA", "domain": "shop"}},
    {"type": "retryInfo", "details": {"retryDelay": "30s"}}
  ]}}

Вызовы методов, не объявленных ни одним контрактом, или без подходящего мока возвращают понятные ошибки Unimplemented / NotFound с подсказкой, что исправить.

Декодирование захваченного payload

Вызов метода, который не объявлен ни одним контрактом, всё равно
захватывается — сырые protobuf-байты попадают на страницу
Неопределённые запросы как opaque-payload. Откройте запрос и декодируйте
его прямо там: выберите источник дескрипторов — опубликованные контракты
(по умолчанию), живой reflection-сервер или загруженный .proto (его можно
сразу сохранить в реестр контрактов для повторного использования) — и payload
превратится в читаемый JSON. Кнопка Редактировать в конструкторе заполнит
мок декодированным телом.

То же декодирование доступно по API (а AI-агентам — как MCP-инструмент
grpc_decode):

curl -X POST http://localhost:5770/api/v1/api-tester/grpc/decode \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "service": "orders.OrderService", "method": "PlaceOrder",
    "direction": "request",
    "payloadBase64": "CgVTS1UtNxAD",
    "source": {"kind": "reflection", "serverAddress": "orders.internal:9090"}
  }'
# {"service":"orders.OrderService","method":"PlaceOrder","direction":"request","json":"{\"sku\":\"SKU-7\",\"quantity\":3}"}

Без source декодирование идёт по всем загруженным контрактам. С
{"kind":"proto","protoContentBase64":"...","publish":true} загруженный
.proto дополнительно регистрируется как gRPC-контракт, а ответ содержит его
publishedContractId.

One-of, задержка и темплейтинг

Всё, что поддерживает HTTP-путь, работает и нативно: oneOf (ротация/random ответов, sequential-порядок продвигается на каждый вызов), delay (перед ответом, не переживает вызов), JsonPath/Faker ($.req.*, $.fake.*), матчинг условий и скрипт-ответы.

Нативный WebSocket

Socket-моки можно гонять по настоящему WebSocket-соединению — сгенерированный socket-сервер не нужен.

Подключение и обмен событиями

ws://localhost:5770/ws-mock/<serverName>?namespace=<namespace>

Каждое отправляемое сообщение — JSON-объект с полем event (или type); остальное — payload:

{"event": "order.created", "id": "ord-77", "total": 129.90}

Mockarty матчит socket-моки по имени сервера + событию, применяет условия и темплейтинг ($.req.* видит отправленное сообщение) и отвечает payload’ом мока одним JSON-сообщением. Каждое соединение независимо — открывайте сколько угодно одновременно, каждое получит свой сматченный, отемплейченный ответ. Задержки и коды ошибок работают как везде; неудачи приходят понятным фреймом без закрытия соединения:

{"error": {"code": 5, "message": "no socket mock for orders/order.updated in namespace sandbox — create one with event \"order.updated\""}}

Быстрая проверка из консоли браузера:

const ws = new WebSocket("ws://localhost:5770/ws-mock/orders?namespace=sandbox");
ws.onmessage = (m) => console.log("ответ:", m.data);
ws.onopen = () => ws.send(JSON.stringify({event: "order.created", id: "ord-77"}));

Стрим пачки событий

Задайте моку payload-массив — Mockarty отправит каждый элемент отдельным
сообщением по порядку: один входящий event может стримить целую
последовательность обратно, только на этом соединении ($.req.* темплейтится
в каждом элементе):

{
  "socket": {"serverName": "feed", "event": "subscribe"},
  "response": {"payload": [{"seq": 1, "id": "$.req.id"}, {"seq": 2}, {"seq": 3}]}
}

Вебхуки

Webhook-callbacks socket-мока срабатывают после отправки ответа — так же, как в
HTTP и других нативных протоколах: socket-событие может дёрнуть внешний вызов
вдобавок к ответу клиенту.

Нативные TCP / UDP сокеты

Socket-моки обслуживаются и по сырым TCP и UDP — те же моки, что отвечают
по WebSocket, без генерируемого серверного бинаря. Включите листенеры:

MOCKARTY_TCP_NATIVE_PORT=9400 MOCKARTY_UDP_NATIVE_PORT=9401 ./mockarty

Протокол: одно JSON-сообщение на строку (TCP) или на датаграмму (UDP)
с полем event (или type); ответы приходят так же. Условия, JsonPath/Faker
темплейтинг, задержки, коды ошибок, webhook-колбэки и захват неопределённых
запросов работают ровно как на WebSocket-пути, а payload-массив стримит по
одному сообщению на элемент:

# TCP: JSON построчно
printf '{"event":"order.created","id":"ord-7"}\n' | nc localhost 9400
# {"status":"accepted","id":"ord-7"}

# UDP: датаграмма туда — датаграмма (или серия) обратно
printf '{"event":"ping"}' | nc -u -w1 localhost 9401

Опциональные env: MOCKARTY_SOCKET_NATIVE_NAMESPACE,
MOCKARTY_SOCKET_NATIVE_SERVER (имя socket-сервера по умолчанию; сообщение
может переопределить его своим полем "serverName"). Несматченные события
возвращают понятный фрейм {"error":{...}} и записываются как неопределённые
запросы.

См. также

Нативный Kafka (превью)

Стандартные Kafka-клиенты могут продюсить в Mockarty как в брокер — без
сгенерированного сервера и без настоящей Kafka. Включение брокера:

MOCKARTY_GRPC_NATIVE_PORT=5890 MOCKARTY_KAFKA_NATIVE_PORT=9092 ./mockarty

Направьте продюсер на Mockarty и отправьте сообщение в топик с Kafka-моком.
Сообщение резолвится через тот же пайплайн, что и остальные Kafka-моки —
условия запроса, JsonPath/Faker-темплейтинг, скриптовые ответы и вебхук-
коллбэки
работают — а ответ мока доставляется на его output-топик, откуда его
вычитывает обычный Kafka-консьюмер (produce → мок → consume, без реальной Kafka).

Проверить мок из конструктора

Откройте мок в конструкторе и нажмите Тест. Тело сообщения подставляется
из условий мока или из полей $.req.*, на которые ссылаются ответ и ключ
сообщения, — одним нажатием уходит сообщение, на которое мок может ответить.
Результат — реакция брокера, а не HTTP-статус:

  • Мок сработал — Мок <id> называет сработавший мок;
  • Реакция говорит, куда уходит ответ — в выходной топик мока или в
    <входной топик>.mock, если он не задан, — плюс ключ сообщения, задержку и,
    для токсичного мока, что сообщение отброшено или отвечено ошибкой;
  • Payload ответа — отрендеренное сообщение (JsonPath и Faker подставлены).

Ни один мок не сработал означает, что в пространстве ничего не подошло:
проверьте топик и имя сервера, затем условия. Сообщение записано в
«Неопределённые» — оттуда его можно превратить в мок. RabbitMQ (ответ в
выходную очередь, иначе <routing key>.reply), NATS (subject ответа) и SMTP
(письмо принято или отклонено, с SMTP-кодом) показывают такой же вердикт.

Через нативный листенер

Если нативный листенер протокола запущен на этом инстансе, в панели теста
появляется переключатель Отправить через нативный листенер с адресом,
куда пойдёт сообщение, — включён по умолчанию. Нажмите Тест, и Mockarty
подключится к собственному листенеру настоящим клиентом — Kafka-продюсером,
AMQP-публикатором, NATS-запросом, SMTP-отправителем, сырым TCP-сокетом,
gRPC-вызовом — на настоящем порту (с TLS-хостом тенанта, если включён TLS),
отправит сообщение из полей теста и покажет, что вернулось по проводу:

  • Мок сработал — Мок <id> и ответ пришёл на выходной топик,
    очередь ответа или subject ответа, заголовки и payload ответа, время
    кругового пути. Нативные брокеры ставят id сработавшего мока в заголовок
    X-Mockarty-Mock-Id на каждом ответе — оттуда он и берётся;
  • Доставлено, ответа нет — листенер принял сообщение, но за таймаут никто
    не ответил: ни один мок не сработал или мок роняет ответ (токсичность);
  • Отклонено листенером — собственная ошибка мока (код ошибки Kafka,
    ошибка канала AMQP, SMTP 5xx, статус gRPC);
  • Нативный листенер выключен — переключатель неактивен и называет
    настройку, которая его включает; кнопка «Тест» тогда выполняет проверку
    резолвером, описанную выше.

Снимите переключатель, чтобы выполнить проверку резолвером внутри процесса
(только матчинг, без сети). Та же проверка доступна агентам: MCP-инструмент
test_mock с mockId Kafka / RabbitMQ / NATS / SMTP / socket / gRPC-мока
идёт через нативный листенер и возвращает вердикт, ответ и id сработавшего
мока; REST-эндпоинт — POST /api/v1/mocks/{id}/probe-native.

Дать потребителю то, что можно прочитать

Сценарий выше начинается с продюсера. Но когда проверяемый сервис сам
читает из топика, отправлять некому — пусть мок наполнит топик сам.

В конструкторе мока выберите протокол Kafka и раскройте Заготовленный
поток (для потребителей)
. Добавьте записи, которые уже должны лежать в
топике — значение и, если потребителю нужен, ключ — и выберите порядок выдачи:

  • Один раз, по порядку — записи читаются от первой к последней, после чего
    топик затихает, как топик, в который перестали писать.
  • Повторять бесконечно — набор выдаётся снова и снова, для потребителя,
    который должен продолжать получать сообщения, пока вы за ним наблюдаете.

Направьте потребитель на Mockarty и запустите его. Он прочитает записи сразу,
ничего отправлять не нужно. В значениях работают те же выражения JsonPath и
Faker, что и в любом другом ответе; они вычисляются один раз при наполнении
топика, поэтому повторное чтение того же смещения вернёт ту же запись.

Настоящее отправленное сообщение всегда в приоритете: если в топик всё-таки
что-то отправят, потребитель увидит именно его.

Спросить брокер о группах потребителей

Стандартный админ-клиент Kafka может получить список групп потребителей на
Mockarty и описать любую из них — это самый простой способ для теста
убедиться, что проверяемый сервис действительно присоединился к своей группе:

client := &kafka.Client{Addr: kafka.TCP("localhost:9092")}
groups, _ := client.ListGroups(ctx, &kafka.ListGroupsRequest{})
info, _ := client.DescribeGroups(ctx, &kafka.DescribeGroupsRequest{GroupIDs: []string{"orders-consumer"}})

Группа, к которой никто не присоединялся, возвращается как Dead без
участников — то есть «никто не подключился» это обычный ответ, а не ошибка,
которую надо отдельно разбирать.

Создание и удаление топиков

Большинство сервисов при старте запускают админ-клиент — «убедись, что мои
топики есть» — ещё до первой отправки. Mockarty на это отвечает: стандартный
админ-клиент Kafka может создавать и удалять топики.

client := &kafka.Client{Addr: kafka.TCP("localhost:9092")}
client.CreateTopics(ctx, &kafka.CreateTopicsRequest{
    Topics: []kafka.TopicConfig{{Topic: "orders.new", NumPartitions: 1, ReplicationFactor: 1}},
})

Созданный топик становится настоящим сразу: он появляется в следующем ответе
метаданных, принимает отправку и отдаёт чтение — поэтому сервис, который
создаёт топик и тут же в него пишет, ведёт себя как с настоящим брокером.
Создание уже существующего топика возвращает «уже существует», а не ошибку —
именно этого ждёт идемпотентный старт.

Удаление убирает топик вместе с накопленными в нём сообщениями. Исключение —
топики, заданные моком: их определяет мок, поэтому по протоколу они не
удаляются, меняйте или удаляйте сам мок.

Mockarty обслуживает одну партицию на топик и сообщает об этом в ответе,
сколько бы партиций вы ни запросили. Лимит выборки консьюмера соблюдается:
запросите небольшой MaxBytes — брокер уложится в него и отдаст топик за
столько выборок, сколько потребуется.

Группы потребителей хранят закоммиченные оффсеты, поэтому консьюмер, который
остановился и запустился снова, продолжит с того же места, а не перечитает топик
заново. Учтите: каждый консьюмер в группе получает весь поток — Mockarty не
делит топик между участниками группы, так что для проверки того, как два
экземпляра вашего сервиса делят нагрузку, нужен настоящий брокер.

Мок может инжектить типичные брокерные ошибки, задав статус ответа равным
Kafka-коду ошибки (например 6 NOT_LEADER_FOR_PARTITION, 3
UNKNOWN_TOPIC_OR_PARTITION, 19 NOT_ENOUGH_REPLICAS) — продюсер получит
настоящую брокерную ошибку, что незаменимо для тестирования retry/failover.

Опциональные env: MOCKARTY_KAFKA_NATIVE_ADVERTISE_HOST (хост, по которому
клиенты достучатся до брокера, если Mockarty за прокси/NAT),
MOCKARTY_KAFKA_NATIVE_NAMESPACE, MOCKARTY_KAFKA_NATIVE_SERVER.

Нативный RabbitMQ

Стандартный AMQP 0-9-1 клиент может публиковать в Mockarty как в RabbitMQ — без
сгенерированного сервера и без настоящей RabbitMQ. Включение брокера:

MOCKARTY_AMQP_NATIVE_PORT=5672 ./mockarty

Направьте AMQP-клиент на Mockarty (amqp://guest:guest@host:5672/), опубликуйте
в routing-key, совпадающий с RabbitMQ-моком (при дефолтном exchange routing-key
= имя очереди). Сообщение резолвится через тот же пайплайн, что и остальные
RabbitMQ-моки — условия, JsonPath/Faker-темплейтинг, скриптовые ответы и
вебхук-коллбэки — а ответ мока доставляется на его output-очередь, откуда его
вычитывает обычный консьюмер (basic.consume push или basic.get pull).
После basic.cancel новые ответы остаются в очереди для basic.get или
следующего консьюмера. Ответы, ожидавшие отправки консьюмеру с закрытым
соединением, возвращаются в очередь.

Заголовки сообщения. Заголовки, заданные на публикуемом сообщении (AMQP-
таблица headers), доходят до мока: используйте их в header-условиях мока
для выбора ответа и ссылайтесь на них в payload через $.header.<имя> (любое
значение — строка, boolean, число — доступно как строка).

Dead-letter. Очередь, объявленная с x-dead-letter-exchange (и при
необходимости x-dead-letter-routing-key), ведёт себя как в RabbitMQ: доставка,
которую консьюмер отклонил (reject/nack) без возврата в очередь, публикуется в
dead-letter exchange и попадает в привязанные к нему очереди (queue.bind; direct,
fanout и topic exchange либо exchange по умолчанию "" с именем очереди в ключе)
с заголовками x-first-death-queue / x-first-death-reason. С x-message-ttl
ответ, который никто не забрал за TTL, уходит в dead-letter с причиной expired.
Reject или nack с возвратом кладёт сообщение обратно в его очередь. Без
dead-letter exchange отклонённое сообщение отбрасывается. Настройки dead-letter и
привязки хранит узел, на котором клиент их объявил.

Длина очереди, приоритеты и quorum-очереди. Очередь, объявленная с
x-max-length или x-max-length-bytes, держит не больше заданного. Когда она
заполнена, самое старое сообщение отбрасывается (x-overflow: drop-head, по
умолчанию) и, если у очереди есть dead-letter exchange, уходит туда с причиной
maxlen; x-overflow: reject-publish сохраняет очередь и отбрасывает новое
сообщение, а reject-publish-dlx ещё и отправляет его в dead-letter. Очередь с
x-max-priority выдаёт сообщения с большим приоритетом раньше (приоритет ответа мок
задаёт в outputProps; приоритет выше максимума очереди считается максимальным).
Очередь с x-queue-type: quorum считает каждый возврат сообщения в заголовке
x-delivery-count, а с x-delivery-limit отправляет в dead-letter сообщение,
возвращённое больше заданного числа раз (причина delivery_limit).
x-delivery-count приходит числом, как в RabbitMQ. Если несколько
узлов делят состояние брокера, ограничения длины и приоритеты действуют на весь
кластер.

Консьюмеры, очистка и удаление. Несколько консьюмеров одной очереди получают
её сообщения по очереди. Очередь с x-single-active-consumer: true отдаёт всё
самому раннему консьюмеру, а когда он отписался или отключился — следующему.
Когда канал или соединение закрывается, сообщения, которые на нём не
подтвердили, возвращаются в свою очередь и приходят снова с пометкой
redelivered. Очередь с x-consumer-timeout (в миллисекундах) закрывает
канал, который держит её доставку неподтверждённой дольше, ошибкой 406 и так же
возвращает доставку в очередь. queue.purge отбрасывает ждущие сообщения и сообщает, сколько их
было; сообщения, которые ещё ждут подтверждения, остаются. queue.delete
отбрасывает сообщения очереди, её аргументы объявления и привязки и отменяет
её консьюмеров (клиентская библиотека закрывает их каналы доставки); с
if-unused или if-empty удаление очереди, у которой есть консьюмеры или
сообщения, отклоняется ошибкой 406, как в RabbitMQ. Очередь с x-expires
удаляется так же, если за это время её никто не читал, не слушал и не объявлял
заново. Если несколько узлов делят состояние брокера, очистка и удаление
действуют на весь кластер, а единственный активный консьюмер и x-expires
каждый узел определяет по подключённым к нему консьюмерам.

Stream-очереди. Очередь с x-queue-type: stream — это журнал: чтение ничего
не удаляет, и каждый консьюмер читает журнал с той точки, которую задал
аргументом подписки x-stream-offset: first, last, next (по умолчанию —
только новые сообщения), номер смещения, момент времени или интервал вроде 1h
(сообщения за последний час). Каждая доставка несёт свою позицию в заголовке
x-stream-offset. Журнал держит сообщения в пределах x-max-length,
x-max-length-bytes и x-max-age (например 7D), отбрасывая самые старые.
Как и в RabbitMQ, консьюмер stream-очереди обязан подтверждать сообщения
вручную и задать prefetch (basic.qos), а basic.get и queue.purge
отклоняются ошибкой 406. Журнал хранит узел, который получил сообщения.

Чего брокер НЕ делает. x-dead-letter-strategy: at-least-once
принимается, но dead-letter-сообщение, которое некуда доставить, отбрасывается,
а не хранится до появления маршрута. Узел пишет об этом одно предупреждение на
очередь. Если проверка на это опирается, ей нужен настоящий RabbitMQ.

Заголовки ответа и свойства сообщения. Заголовки ответа мока приходят
консьюмеру в AMQP-таблице headers, а сам ответ может нести стандартные
AMQP-свойства сообщения — через блок outputProps мока:

"rabbitmq": {
  "queue": "rpc.in",
  "outputQueue": "rpc.out",
  "outputProps": {
    "correlationId": "$.req.correlationId",
    "replyTo": "rpc.out",
    "contentType": "application/json",
    "deliveryMode": 2
  }
}

Значения поддерживают те же JsonPath- и Faker-выражения, что и любой ответ —
именно это делает рабочей классическую схему RPC поверх RabbitMQ: ваш клиент
публикует запрос с correlation id, мок возвращает его в ответе, и клиент
сопоставляет ответ с запросом ровно так же, как с настоящим брокером.
Доставляются correlationId, replyTo, contentType, messageId, type,
appId, deliveryMode и priority.

Именованные exchange. Публикуйте в дефолтный exchange ("", routing-key =
имя очереди) или в именованный direct-exchange, где routing-key равен имени
очереди. Мок, объявивший exchange, изолирован к нему — отвечает только на
публикацию в этот exchange и никогда в другой, случайно совпавший по routing-key.
Мок без exchange остаётся доступен и из дефолтного, и из именованного direct-
exchange. (Direct- и default-маршрутизация поддерживаются; topic/fanout-рассылка по
разноимённым очередям — нет.)

Опциональные env: MOCKARTY_AMQP_NATIVE_NAMESPACE, MOCKARTY_AMQP_NATIVE_SERVER.

Нативный NATS

Обычный NATS-клиент может подключиться к Mockarty как к настоящему NATS-серверу —
без генерируемого сервера, без реального NATS. Два способа дойти до него:

На общем TLS-порту (вообще без отдельного порта — см.
Один порт — много пространств имён).
Клиент должен сначала выполнить TLS-рукопожатие и назвать протокол в хосте:

tls://team-a.nats.mock.example.com:5771    # nats.TLSHandshakeFirst() / nats --tlsfirst

На своём порту — для классического клиента, который ждёт plaintext-строку
INFO и только потом апгрейдится:

MOCKARTY_NATS_NATIVE_PORT=4222 ./mockarty

Направьте любой NATS-клиент на nats://host:4222 и опубликуйте сообщение. Оно
резолвится через тот же пайплайн, что и остальные моки — матчинг по теме
(subject), условия на данные и заголовки, JsonPath/Faker-темплейтинг, скриптовые
ответы и токсичность — а ответ мока публикуется обратно:

# request/reply — клиент получает ответ мока на свой inbox
nats request orders.new '{"id":7}'
# {"orderId":"7","status":"accepted"}

Темы и wildcard. Subject мока — это ключ маршрутизации. Он может
использовать два NATS-wildcard, матчинг как у настоящего сервера:

  • * — ровно один токен: orders.* отвечает на orders.new, но не на orders.eu.new.
  • > — один или более хвостовых токенов: orders.> отвечает на orders.new И orders.eu.new.

Request/reply и pub/sub. Для request/reply ответ публикуется на reply-тему
запроса (inbox клиента) автоматически. Для pub/sub задайте у мока output
subject
— любой подписчик на этой теме получит ответ. Порядок выбора reply-темы:
outputSubject мока, затем replySubject, затем reply-to запроса, затем
<subject>.reply.

Заголовки сообщений. Заголовки сообщения (NATS HPUB/message headers)
доходят до мока: используйте их в header conditions и ссылайтесь в payload
через $.header.<name>. Ответ мока с response-заголовками доставляется блоком
заголовков (HMSG), который читает header-aware клиент.

Queue groups. Мок может объявить queue group; она доступна скрипту как
mk.request.attrs.queueGroup, так что скриптовый ответ может ветвиться по ней.

JetStream

Приложения на JetStream (а не на core NATS) тоже работают против мока — нужно
только объявить это явно: на уровне протокола core-запрос и JetStream-публикация
выглядят одинаково. Объявите стрим на NATS-моке:

{
  "protocol": "nats",
  "nats": {
    "subject": "orders.new",
    "jetstream": {
      "stream": "ORDERS",
      "subjects": ["orders.>"],
      "maxMsgs": 1000
    }
  },
  "response": { "payload": { "status": "accepted" } }
}

Одного мока с блоком jetstream достаточно, чтобы включить JetStream для всего
namespace. stream — имя стрима, которое ищет клиент; subjects — темы, которые
стрим забирает (те же wildcard * / >). Если subjects не указать, стрим
забирает собственный subject мока. Опционально: maxMsgs (сколько сообщений
стрим хранит для реплея), storage, retention, replicas, description,
domain — они возвращаются клиенту как конфигурация стрима.

Несколько моков могут указывать один и тот же стрим — их темы объединяются.

После этого JetStream-клиент работает обычным образом:

nats stream info ORDERS
nats publish orders.new '{"id":7}'   # подтверждено: стрим ORDERS, sequence 1
nats consumer add ORDERS workers --pull
nats consumer next ORDERS workers

Что обслуживает мок:

  • Публикация — публикация в тему стрима подтверждается настоящим ack с именем
    стрима и порядковым номером, который растёт с каждым сообщением.
  • Стримы — информация об аккаунте, создание, обновление, info, list, names
    (включая поиск «какому стриму принадлежит эта тема», который клиент делает
    перед подпиской), purge и delete. Приложение, которое само создаёт стрим на
    старте, работает; повторное создание существующего стрима идемпотентно.
  • Консьюмеры — создание, info, names, list, удаление. Работают и push-подписки
    (клиент получает сообщения на свою delivery-тему, с исходным subject сообщения
    и JetStream-метаданными), и pull-фетчи.
  • Реплей — подписчик, подключившийся после публикации, всё равно получит
    сообщения, которые держит стрим: тесту не нужно гоняться за продюсером.
  • Долговечный push-консьюмер переживает своего клиента — пока на
    delivery-тему долговечного консьюмера никто не подписан, новые сообщения ждут;
    когда клиент возвращается и подписывается с тем же durable-именем, он получает
    их — и ничего из уже полученного. (Клиент, вызвавший Unsubscribe(), удаляет
    созданный им консьюмер — как и на настоящем сервере.)
  • Подтверждения — доставленные сообщения несут ack-тему, поэтому msg.Ack()
    и msg.Metadata() на стороне клиента работают штатно.

Ответы мока на теме стрима. Мок продолжает работать и для JetStream-публикации
— условия, темплейтинг и скрипты действуют. Его ответ уходит на output subject
мока (или replySubject), потому что inbox издателя уже занят подтверждением
публикации. Так удобно моделировать схему «принял, преобразовал, опубликовал
дальше»: сообщение в orders.new даёт ответ мока в orders.processed.

Если JetStream не объявлен, клиент, обратившийся к JetStream, сразу получит
явную ошибку «JetStream не включён» вместо ожидания таймаута — а обычные core-моки
продолжают вести себя точно как раньше.

Ограничения. Мок держит сообщения стрима в памяти и ничего не сохраняет
(если только их не держит в базе общий режим кластера, описанный в кластерной
заметке): после перезапуска Mockarty стримы пусты, а каждый стрим хранит скользящее окно
(по умолчанию 1000 сообщений, меняется через maxMsgs), а не всю историю. Не
реализованы: переотправка неподтверждённых сообщений, подавление дублей, зеркала
и источники стримов, получение сохранённого сообщения по номеру, Key/Value и
Object Store. Это функции надёжности брокера — мок нужен, чтобы запустить
JetStream-код вашего приложения, а не чтобы заменить кластер NATS.

Опциональные env: MOCKARTY_NATS_NATIVE_NAMESPACE, MOCKARTY_NATS_NATIVE_SERVER,
MOCKARTY_NATS_NATIVE_SERVER_ID (анонсируется в INFO-приветствии). TLS-мультитенант
(один порт, namespace из SNI-хоста) — MOCKARTY_NATS_NATIVE_TLS=1.

Потолок соединений. Листенер отказывает в соединениях сверх
MOCKARTY_NATS_MAX_CONNECTIONS (зависит от CPU, по умолчанию 64–512), а на
тенанта — сверх MOCKARTY_BROKER_NS_MAX_CONNECTIONS (по умолчанию 100) — вне
зависимости от того, куда пришло соединение: на свой порт или на общий TLS-порт.

Нативный SMTP

Обычный почтовый клиент может слать письмо в Mockarty, как в SMTP-сервер — без
генерируемого сервера и без реального почтовика. Два способа дойти до него:

На общем TLS-порту (вообще без отдельного порта — см.
Один порт — много пространств имён).
Клиент подключается через implicit TLS (SMTPS) и называет протокол в хосте:

team-a.smtp.mock.example.com:5771     # подключиться по TLS, затем читать приветствие 220

На своём порту — для клиента, который ожидает plaintext-приветствие и
апгрейдится через STARTTLS:

MOCKARTY_SMTP_NATIVE_PORT=2525 ./mockarty

Направьте SMTP-клиент на host:2525 и отправьте сообщение. Оно проходит тот же
пайплайн, что и остальные SMTP-моки — условия по отправителю / получателю /
теме / телу / заголовкам, JsonPath/Faker-темплейтинг, скрипт-ответы и
webhook-callbacks — а ответ мока задаёт SMTP-реплай: принять письмо (250) или
отклонить 4xx/5xx (задайте статус-код мока либо верните statusCode /
statusMessage / acceptMail из payload). Письмо без совпавшего мока
принимается (250) как обычным релеем и записывается в неопределённые запросы —
из них потом можно создать мок.

Что умеет мок. HELO / EHLO, AUTH, MAIL FROM, RCPT TO, DATA,
RSET, NOOP, VRFY, QUIT, а при включённом TLS — STARTTLS. В ответе на
EHLO анонсируются 8BITMIME, AUTH PLAIN LOGIN и предел размера письма.
Письмо доходит дословно: многочастный MIME с вложениями, свёрнутые (folded)
заголовки и строки тела,
начинающиеся с точки, приходят без искажений, а все получатели письма с
несколькими адресатами доходят до мока в исходном порядке. MAIL FROM:<> —
пустой обратный адрес, который несёт каждый bounce и delivery-status
notification, — открывает обычную транзакцию, поэтому обработку bounce-писем
тоже можно тестировать.

Ограничения. Одно письмо ограничено 1 МБ, одна командная строка — 64 КБ;
письмо больше предела отклоняется кодом 552, слишком длинная команда — 500.
В одной транзакции допускается до 100 получателей, дальнейшие RCPT TO
отвечают 452. Молчащая 5 минут сессия закрывается — как и сессия, пять раз
провалившая AUTH. Не реализовано: проверка почтового ящика (VRFY отвечает
252, мок не может знать про ящик); CRAM-MD5 и другие механизмы
аутентификации «запрос-ответ»; pipelining, CHUNKING/BDAT и расширения
delivery-status. Письмо резолвится и записывается, но не хранится и не
пересылается — кроме случая proxy-мока, который релеит его на реальный
upstream-MTA.

Аутентификация (AUTH PLAIN / AUTH LOGIN). Слушатель анонсирует оба
механизма, поэтому сервис, который логинится в настоящий почтовый сервер, можно
направить на мок без правки его конфигурации — и по умолчанию принимаются
любые учётные данные
. Настраивать нечего: приложение продолжает отправлять тот
же логин и пароль, что и всегда, а письмо проходит.

Блок auth на SMTP-моке нужен, когда вы хотите протестировать саму
аутентификацию:

{
  "smtp": {
    "auth": { "username": "app", "password": "s3cret" }
  },
  "response": { "payload": { "statusCode": 250 } }
}
  • username / password — учётные данные, которых ждёт мок. Пустое поле
    означает «любой»: {"username": "app"} пускает пользователя app с любым
    паролем, {"password": "s3cret"} пускает любого пользователя с этим паролем.
    Всё, что политика не приняла, получает 535.
  • reject — любой логин получает 535. Так проверяется ветка «ошибка
    аутентификации» в вашем приложении, и придумывать неверный пароль не нужно.
    Вместе с username отклоняется только этот логин, остальные не затрагиваются.
  • required — неаутентифицированный MAIL FROM получает 530; это
    доказывает, что приложение действительно аутентифицируется.

Это ТЕСТОВЫЕ учётные данные, они хранятся в составе мока — не кладите туда
настоящий продовый секрет. Политика действует на весь namespace: если её
объявили несколько моков, они разбираются в порядке приоритета мока, и решает
первый, чей username совпал с переданным логином. Если клиент
аутентифицировался, логин попадает в данные, по которым матчится мок: добавьте
условие на путь authUser в любую группу SMTP-условий, чтобы отвечать по-разному
на разные учётные записи, и используйте $.req.authUser в payload ответа или в
скрипте. Логин также виден в записи журнала запросов. У неаутентифицированной
сессии authUser нет, поэтому такое условие для неё просто не срабатывает.

AUTH с TLS и без него. Работают все варианты. Поверх TLS учётные данные
идут в шифрованном виде — именно этого требует большинство почтовых клиентов:
например, net/smtp в Go отказывается отправлять AUTH PLAIN по
нешифрованному соединению, если сервер не localhost. На локальной машине или
в CI — а это обычное место для мока — открытый AUTH поэтому работает как есть;
если мок поднят на другом хосте, используйте общий TLS-порт (implicit TLS) или
включите MOCKARTY_SMTP_NATIVE_TLS=1 и дайте клиенту сначала сделать
STARTTLS. AUTH анонсируется во всех трёх случаях — и до handshake, и после,
кроме уже зашифрованной сессии: там STARTTLS не предлагается, апгрейдиться
некуда.

Потолок соединений. Нативный брокер-листенер отказывает в соединениях сверх
MOCKARTY_*_MAX_CONNECTIONS (зависит от CPU, по умолчанию 64–512), а на
тенанта — сверх MOCKARTY_BROKER_NS_MAX_CONNECTIONS (по умолчанию 100). Тенант
на своём потолке получает 421 до приветствия; остальные тенанты на том же
листенере это не задевает.

Потолок на тенанта виден на /metrics для каждого нативного листенера
(protocol — kafka, amqp, nats или smtp):
mockarty_native_broker_connections{protocol,namespace} (открытые соединения
тенанта; тенант без соединений исчезает из ряда),
mockarty_native_broker_namespace_cap{protocol} (действующий потолок) и
mockarty_native_broker_refused_total{protocol,namespace} (соединения, отклонённые
на потолке). Листенер, отказывающий тенантам, больше не выглядит простаивающим.

Опциональные env: MOCKARTY_SMTP_NATIVE_NAMESPACE, MOCKARTY_SMTP_NATIVE_SERVER,
MOCKARTY_SMTP_NATIVE_HOSTNAME (анонсируется в приветствии/EHLO). TLS-мультитенантность
(один порт, namespace из SNI-хоста) — MOCKARTY_SMTP_NATIVE_TLS=1.

Статус-коды — это SMTP-коды. 250 принимает, 421/451 — временный сбой,
550 отклоняет. Мок, у которого задан HTTP-подобный код ниже 211, уходит в
провод как 250 (принято), поэтому для отказа задавайте настоящий SMTP-код.
Текст статуса — одна строка: переводы строк в нём (легко получить, если
подставлять $.req.body) превращаются в пробелы, а текст длиннее 480 символов
SMTP-бюджета ответа обрезается.

Токсичность (хаос) для message-моков

Моки Kafka, RabbitMQ, SMTP и socket умеют внедрять сбои в ответ сматченного
мока
— аналог HTTP-токсичности рекордера для брокеров сообщений. Откройте у
мока секцию Токсичность (хаос) в конструкторе или задайте блок toxics на
контексте протокола:

{
  "kafka": {
    "topic": "orders.events",
    "toxics": { "delayMs": 200, "delayJitter": 100, "errorRate": 0.1, "dropRate": 0.05 }
  }
}
  • delayMs / delayJitter — задержка перед ответом плюс случайные 0..jitter.
  • errorRate (0–1) — доля сматченных вызовов, на которые вместо ответа
    внедряется ошибка протокола (код ошибки Kafka, ошибка канала AMQP, SMTP
    4xx/5xx или error-фрейм сокета). Настройте wire-ошибку через
    errorCode / errorMessage.
  • dropRate (0–1) — доля молча отбрасываемых: ответа нет совсем
    (Kafka/AMQP всё равно принимают produce/publish — как настоящий брокер — но
    ничего не буферят; SMTP отвечает 421), чтобы проверять таймауты клиента и
    at-least-once обработку.

Оставьте всё в 0 (или уберите toxics), чтобы отключить. Drop важнее error —
внедрять ошибку не во что — а задержка применяется перед любым из них.

Десктоп: нативные брокеры из коробки

В сборке Desktop нативные протоколы работают без какой-либо настройки.
Единый порт включён заранее: Kafka, RabbitMQ и TCP-сокет моки обслуживаются на
главном порту Desktop (том, что открывает браузер) — демультиплексирование
по первым байтам, отдельного порта искать не нужно. Протоколы, которым физически
нужен свой порт (говорящие первыми: NATS, SMTP; и UDP), слушают только
127.0.0.1, порты выводятся из HTTP-порта:

Протокол Где
Kafka / RabbitMQ / TCP-сокет главный порт Desktop (например localhost:5780)
gRPC главный порт Desktop, HTTP/2 без TLS (grpcurl -plaintext localhost:5780 …)
MCP главный порт + 1
NATS главный порт + 2
SMTP главный порт + 3
UDP главный порт + 4

Порт Desktop — plaintext (сертификата для установки нет), поэтому NATS и SMTP
остаются там на своих loopback-портах: общий порт для них требует TLS (см.
Почему два протокола называют себя в хосте).
На сервере с MOCKARTY_NATIVE_BROKERS=1 оба также обслуживаются на HTTPS-порту.

Каждый нативный слушатель привязан к 127.0.0.1 — снаружи недоступно ничего.
Моки, созданные без namespace, попадают в sandbox — тот же namespace, в
котором резолвит любой локальный брокер: мок из конструктора сразу работает с
реальным клиентом — направьте Kafka-консьюмера на localhost:<главный порт> и
он прочитает заготовленный топик.

gRPC в Desktop. gRPC-мок обслуживается, когда его сервис описан в .proto.
В Моки → Конструктор выберите gRPC и нажмите Загрузить .proto рядом с
выбором метода: файл проверяется, его сервисы появляются в списке, а выбор метода
заполняет сервис, метод и заготовку ответа. С этого момента любой gRPC-клиент
достаёт мок на главном порту:

grpcurl -plaintext -d '{"name":"Ada"}' localhost:5780 demo.GreeterService/SayHello

Сообщение по «сырому» TCP или UDP называет свой мок-сервер полем
"serverName", например {"serverName":"orders","event":"order.created"}.

Любая настройка (MOCKARTY_UNIFIED_PORT,
MOCKARTY_NATS_NATIVE_PORT, …) по-прежнему переопределяется перед запуском.

Примечание для сервера. На сервере (не Desktop) единый порт — opt-in через
MOCKARTY_UNIFIED_PORT=1 (HTTP) или MOCKARTY_UNIFIED_TLS=1 (HTTPS +
нативные брокеры на TLS-порту — см.
Один порт — много пространств имён),
нативные слушатели привязываются к 0.0.0.0, если
MOCKARTY_NATIVE_BIND_ADDR их не сузил. Обязательная аутентификация брокеров
(MOCKARTY_BROKER_SASL=required) работает только на TLS-путях — на
plaintext-едином порте Kafka вообще не демультиплексируется (конструктор
моков показывает причину в панели подключения).

NATS и SMTP на PLAINTEXT-едином порте не обслуживаются никогда, что бы на
нём ещё ни было. Оба — протоколы, где первым говорит СЕРВЕР: клиент молчит,
пока его не поприветствуют, поэтому нет первого байта, по которому их можно
узнать, а plaintext-порт не несёт и SNI, чтобы назвать их по имени. На
TLS-едином порте они обслуживаются — под зарезервированным семейством хостов
({пространство}.nats.<домен>, {пространство}.smtp.<домен>), потому что SNI
из ClientHello называет протокол ДО того, как сервер обязан поздороваться. Без
TLS дайте им собственные порты (MOCKARTY_NATS_NATIVE_PORT,
MOCKARTY_SMTP_NATIVE_PORT).

Один порт — много пространств имён (TLS-мультитенант)

Каждое пространство имён — отдельный тенант: один и тот же топик, очередь,
subject, маршрут или gRPC-метод в team-a и team-b никогда не пересекаются,
а клиент выбирает свой тенант хостом, к которому подключается, — больше на
клиенте не меняется ничего. Всё включается одним переключателем:

MOCKARTY_NATIVE_BROKERS=1 MOCKARTY_NATIVE_TLS_DOMAIN=mock.example.com ./mockarty

Эта одна настройка делает Mockarty брокером сразу для всех протоколов:

Протокол Куда подключается клиент Что выбирает пространство имён
HTTP, GraphQL, SOAP, MCP, SSE, WebSocket https://<namespace>.mock.example.com:5771/… хост — префикс /stubs/<namespace> не нужен
gRPC <namespace>.mock.example.com:5771 (TLS) хост — заголовок метаданных не нужен
Kafka <namespace>.mock.example.com:5771 (security.protocol=SSL) хост; брокер переанонсирует его, и реконнекты остаются в тенанте
RabbitMQ amqps://<namespace>.mock.example.com:5771/<vhost> хост; vhost — ваш
Сырые TCP-сокеты <namespace>.mock.example.com:5771 (TLS) хост
NATS tls://<namespace>.nats.mock.example.com:5771 — TLS сначала рукопожатие (nats --tlsfirst, nats.TLSHandshakeFirst()) хост. Метка nats называет протокол (см. ниже)
SMTP <namespace>.smtp.mock.example.com:5771 через implicit TLS (SMTPS) хост. Метка smtp называет протокол

Порт 5771 — это HTTPS-порт: HTTPS, gRPC, Kafka, AMQP, сырые сокеты, NATS и
SMTP
делят его. Направьте DNS (или /etc/hosts в разработке) для
*.mock.example.com, *.nats.mock.example.com и *.smtp.mock.example.com на
хост Mockarty. У UDP нет TLS-рукопожатия, поэтому UDP остаётся
одно-пространственным.

Почему два протокола называют себя в хосте

Листенер различает протоколы после TLS-рукопожатия — по первым байтам,
которые присылает клиент. Это работает для HTTP, gRPC, Kafka, RabbitMQ и сырых
сокетов: все они говорят первыми. NATS и SMTP — нет: сервер NATS отправляет
INFO …, сервер SMTP отправляет 220 … раньше, чем клиент напишет хоть
байт. Смотреть не на что, а угадывать по тишине Mockarty не будет.

Зато до того, как серверу надо определиться, эти клиенты присылают само
TLS-рукопожатие, а имя хоста в нём (SNI) — ровно тот хост, который вы
настроили. Поэтому протокол едет именно там:

team-a.mock.example.com          → HTTP / gRPC / Kafka / RabbitMQ / сырые сокеты (как раньше)
team-a.nats.mock.example.com     → NATS
team-a.smtp.mock.example.com     → SMTP (implicit TLS)

В клиенте и в данных не меняется ничего. Ваш NATS-клиент сохраняет свои
subject’ы, заголовки и JetStream-стримы; ваш мейлер — конверт и тело письма.
Дополнительную метку получает только хост, к которому вы подключаетесь. Хост,
у которого вторая метка не nats и не smtp, сохраняет своё обычное значение,
так что правило действует только там, где вы его используете.

Поэтому nats и smtp зарезервированы как вторая метка хоста тенанта,
когда единый TLS-порт включён. Если ваш домен тенантов сам начинается с такого
слова, не называйте так хост тенанта — он будет прочитан как протокол.

Какие клиенты могут использовать порт 5771

Протокол Форма клиента Порт 5771 (общий) Выделенный порт
NATS TLS-рукопожатие первым (nats://… + nats.TLSHandshakeFirst(), nats --tlsfirst, tls://) да 4222
NATS классическая — сначала plaintext INFO, апгрейд до TLS после нет — ждёт plaintext-приветствие, которого TLS-порт не отправит, и потому вообще не начинает рукопожатие 4222
SMTP implicit TLS / SMTPS (подключиться по TLS, затем читать приветствие) да —
SMTP STARTTLS (plaintext-приветствие, затем апгрейд) нет — по той же причине 2525

NATS и SMTP клиенты, которые не могут начать с TLS-рукопожатия, обслуживаются
своими выделенными портами — они остаются ровно такими, как были (включая
TLS + SNI мультитенантность). На общем порту уже зашифрованной SMTP-сессии
STARTTLS не предлагается — апгрейдиться некуда.

Даже openssl s_client -connect host:5771 -servername team-a.nats.mock.example.com
работает вообще без знания протокола: он получает NATS-баннер INFO.

Отдельные порты для клиентов, говорящих первыми

Держите выделенные листенеры, когда нужна почта через STARTTLS или
классический NATS-клиент. Оба — TLS + SNI мультитенантные, так что хост тенанта
по-прежнему выбирает пространство имён:

MOCKARTY_NATS_NATIVE_TLS=1 MOCKARTY_SMTP_NATIVE_TLS=1 \
MOCKARTY_NATIVE_TLS_DOMAIN=mock.example.com ./mockarty
  • NATS — <namespace>.mock.example.com:4222, TLS-рукопожатие первым.
  • SMTP — <namespace>.mock.example.com:2525, затем STARTTLS; пространство
    имён выбирает SNI-хост этого рукопожатия.

Перевод сервиса на Mockarty меняет только хост брокера. Ваши vhost,
очереди, топики, consumer group’ы, маршруты, сервисы и формы сообщений остаются
ровно такими, как в боевой конфигурации, — пространство выбирает один хост.
Обычный админ-хост (mock.example.com, без метки тенанта) по-прежнему отдаёт
админ-API и явные маршруты /stubs/<namespace>/….

Переключатель раскрывается в отдельные настройки, так что оператору с другой
раскладкой достаточно переопределить одну из них (заданное вами значение
никогда не трогается): HTTPS_ENABLED, MOCKARTY_UNIFIED_TLS (общий
HTTPS-порт), MOCKARTY_NATIVE_TLS (TLS+SNI на выделенных листенерах),
MOCKARTY_NATS_NATIVE_PORT (4222), MOCKARTY_SMTP_NATIVE_PORT (2525) — пустое
значение порта оставляет листенер выключенным; Kafka, AMQP и сырые сокеты можно
вынести на выделенные порты через MOCKARTY_KAFKA_NATIVE_PORT,
MOCKARTY_AMQP_NATIVE_PORT, MOCKARTY_TCP_NATIVE_PORT (тоже TLS+SNI).

Mockarty сам генерирует wildcard-сертификат для *.<domain> — плюс
*.nats.<domain> и *.smtp.<domain>, которые нужны двум двухметочным хостам
тенантов выше, — либо использует настроенный вами HTTPS-сертификат. Клиенты в разработке могут пропускать
проверку или доверять сгенерированному сертификату; в продакшене подставьте
свой wildcard-сертификат через обычные настройки HTTPS-сертификата.

Без переключателя ничего не меняется: поднятый вручную нативный листенер
остаётся plaintext и обслуживает своё единственное пространство имён
(MOCKARTY_*_NATIVE_NAMESPACE), а HTTP сохраняет маршруты /stubs/<namespace>.

Кластерная заметка. Очереди ответов AMQP, смещения и группы консюмеров Kafka,
группы NATS и потоки JetStream живут на узле, принявшем соединение, если их
не переносит в базу общий режим (ниже). Если
несколько узлов Mockarty стоят за TCP-балансером, держите таких продюсеров и
консюмеров на одном узле (sticky-балансировка по IP или
отдельный сервис на узел), чтобы консюмер читал ответы, которые вызвал его
продюсер. Sticky по IP помогает только пока обе стороны — один и тот же клиент:
как только продюсер и консюмер находятся на разных машинах, направьте обе на один
узел.
Узкое исключение — подписки core NATS: ответ мока с одного узла достигает
прямых подписчиков на других узлах при работающей кластерной шине, если тема
ответа не захватывается локальным потоком JetStream.
Queue group core NATS тоже обслуживается между узлами — и ровно один раз:
публикующий узел называет один узел, который обслужит группу, а группа, у
которой есть участник на самом публикующем узле, всегда обслуживается там.
Вход в группу и выход из неё объявляются по кластеру сразу, поэтому только
что подключившийся участник может пропустить разве что сообщение, отправленное
в тот же миг, а вышедшего перестают выбирать немедленно. На консьюмеров
JetStream это не распространяется — им по-прежнему нужен один узел, если не
включён общий режим (ниже).
При ошибке кластерной
публикации соединение продюсера закрывается, а не выдаёт локальную доставку
за успех. При транспорте PostgreSQL к ответу применяется лимит уведомления
7,5 КиБ; слишком большой ответ также закрывает соединение.

При CLUSTER_MODE=true узел говорит об этом сам: чтение статуса
GET /api/v1/native-listeners возвращает блок state с этой оговоркой, а панель
подключения в конструкторе мока показывает её рядом с адресом. Объявите
MOCKARTY_NATIVE_STATE_SCOPE=per-node, когда состояние на узел вас устраивает, —
тогда ответ перестаёт быть открытым вопросом, а не остаётся вечным
предупреждением.

MOCKARTY_NATIVE_STATE_SCOPE=shared — межузловое состояние AMQP, Kafka и
NATS JetStream (только PostgreSQL).
Когда нода работает на PostgreSQL,
общий режим запускает каждый AMQP-, Kafka- и NATS-брокер этой ноды —
выделенный слушатель (MOCKARTY_AMQP_NATIVE_PORT, MOCKARTY_KAFKA_NATIVE_PORT,
MOCKARTY_NATS_NATIVE_PORT) или единый порт — с состоянием в базе; нода,
обслуживающая только часть этих протоколов, не обязана открывать остальные:

  • AMQP-ответы хранятся долговечно: публикация на узле 1 доходит до
    потребителя на узле 2 (обрыв соединения до доставки возвращает ответ в
    очередь; просроченные ответы выметает лидер кластера).
  • Логи топиков Kafka живут в базе: ответ, который мок пишет в выходной топик
    на запись, произведённую на узле 1, читает консьюмер на узле 2; офсеты
    образуют одну последовательность, сколько бы узлов ни использовали
    продюсеры; записи stream-мока сохраняются один раз, а топик, удалённый через
    один узел, пропадает на всех. Каждый топик хранит последние 1000 записей;
    записи старше 24 часов удаляет лидер кластера. Если база не может сохранить
    ответ, продюсер получает ошибку хранения, а не успех, ответ на который никто
    не прочтёт.
  • Офсеты consumer-групп и координационные lease Kafka живут в базе: позиция
    группы, закоммиченная на узле 1, учитывается той же группой на узле 2 — на
    той же записи; коммит узла, потерявшего lease, получает ILLEGAL_GENERATION, и
    клиент перезаходит в группу.
  • Потоки NATS JetStream, их сообщения и позиции консьюмеров живут в базе:
    номера последовательности общие для кластера (публикации на двух узлах
    никогда не получают одинаковый номер), поток, созданный клиентом на узле 1,
    известен узлу 2, а pull- или push-консьюмер на узле 2 получает то, что
    опубликовано на узле 1. Долговечный консьюмер доставляет один узел за раз:
    пока узел 1 обслуживает WORKER, привязка WORKER на узле 2 отклоняется
    ошибкой «consumer … is delivered by another node of this cluster». Узел 1
    отпускает WORKER, когда останавливается, когда пропадает (через 30 секунд)
    или когда его клиент 30 секунд не пользовался WORKER на нём — это случай
    клиента, переподключившегося через балансировщик на узел 2. Следующий
    запрос информации, подписка или fetch клиента на узле 2 забирает WORKER,
    и доставка продолжается после последнего доставленного сообщения — ничего
    не повторяется и не пропускается. Списки и имена консьюмеров на каждом узле
    включают консьюмеров, которых обслуживают другие узлы. Сообщения старше 24 часов удаляет лидер
    кластера.

Нода, которая не может держать состояние в базе, отклоняет режим с названием
недостающего элемента (postgresql: false на SQLite/desktop-сборке) —
слушатели AMQP, Kafka и NATS не запускаются, вместо того чтобы обещать
невозможное. Статус-читка
GET /api/v1/native-listeners говорит, что стало общим: список shared блока
state называет каждый включённый брокер (AMQP, Kafka, NATS). Сообщения core
NATS (обычные publish/subscribe без JetStream) не хранятся вовсе; их
межузловая доставка и queue group (выше) работают при любой работающей
кластерной шине и от этого режима не зависят. Socket, gRPC и SMTP состояния брокера
не держат и от режима не зависят.

Kubernetes (Helm-чарт)

Чарт включает всё это одним блоком — admin.nativeListeners в values.yaml:

admin:
  nativeListeners:
    enabled: true
    domain: mock.example.com        # хост тенанта = {namespace}.mock.example.com
    unifiedTLS: true                # HTTPS-порт 5771 = HTTPS + gRPC + Kafka + AMQP + socket + NATS + SMTP
    nats: { enabled: true, port: 4222 }
    smtp: { enabled: true, port: 2525 }
    passthroughIngress:             # снаружи кластера (см. ниже)
      enabled: true
      host: "*.mock.example.com"

Внутри кластера клиент подключается к Service админ-ноды с хостом тенанта в
качестве TLS server name — mockarty.<namespace-релиза>.svc:5771 для
Kafka/AMQP/socket, {namespace}.nats…:5771 и {namespace}.smtp…:5771 для
NATS/SMTP, либо :4222 / :2525 для выделенных листенеров — и попадает в
пространство имён, которое называет SNI. Снаружи кластера passthrough-Ingress
передаёт сырой TLS-поток (server name и ALPN нетронуты) на тот же порт; ему
нужен ingress-nginx, запущенный с --enable-ssl-passthrough, и wildcard-DNS
*.mock.example.com, *.nats.mock.example.com и *.smtp.mock.example.com,
указывающие на ingress. Сертификат — тот, что Mockarty
генерирует сам (самоподписанный wildcard); клиентам, которые обязаны проверять
цепочку, отдайте wildcard от CA через обычные HTTPS_CERT_FILE /
HTTPS_KEY_FILE.

Логи запросов

Каждое сработавшее обращение к нативному listener’у — gRPC, WebSocket, TCP/UDP
сокет, Kafka, RabbitMQ или SMTP — записывается в Логи запросов мока
(страница мока → Логи запросов) вместе с полным сообщением и ответом мока.
Оттуда можно изучать реальный трафик и создавать новые моки из записанных
запросов — ровно как с HTTP-моками.

Каждый нативный listener записывает несматченный трафик как неопределённый
запрос — говорил ли клиент по gRPC, WebSocket, Kafka, RabbitMQ или SMTP — так
что флоу «создать мок из того, что реально к нам пришло» работает одинаково по
всем протоколам, не только по HTTP.

Прокси-режим (SMTP, Kafka, RabbitMQ, WebSocket)

Нативный SMTP / Kafka / RabbitMQ / WebSocket мок может форвардить в реальный
upstream вместо собственного ответа — async-аналог HTTP proxy-мока. Задайте моку
proxy.target:

  • SMTP — "proxy": {"target": "smtp://mail.internal:25", "recordTraffic": true}
    релеит письмо в реальный MTA и возвращает его вердикт отправителю.
  • Kafka — "proxy": {"target": "kafka://broker:9092", "recordTraffic": true}
    продюсит сообщение в реальный брокер.
  • RabbitMQ — "proxy": {"target": "amqp://guest:guest@rabbit:5672/", "recordTraffic": true}
    публикует сообщение в реальный брокер.
  • WebSocket — "proxy": {"target": "ws://upstream:8080/socket", "recordTraffic": true}
    отдаёт всё соединение в двунаправленный pipe с upstream — клиент и реальный
    сервер общаются через Mockarty, который пишет проходящий трафик.

Дозвон до upstream защищён SSRF-гардом (loopback / приватные / metadata адреса
отклоняются, если деплой явно не разрешил приватные цели) — proxy-мок нельзя
превратить в SSRF-примитив. С recordTraffic: true каждое проброшенное
сообщение записывается как неопределённый запрос (origin proxy) — один клик до
собственного мока.

Задавайте proxy на моке через REST API или MCP-тул create_mock.