Пример docker-compose для тестовой SMS-лаборатории (SMPP сервер + клиент)
Опубликовано:
Используемые термины: SMPP, Docker.
В данной инструкции мы развернем в docker небольшой стенд, состоящий из SMSC (SMPP-сервер) и Kannel — ESME (клиент). Такая лаборатория пригодится для отладки интеграции с SMS-агрегатором до подключения боевого канала — можно проверить формат запросов, склейку длинных сообщений и обработку DLR без затрат на реальную отправку. При этом, реальный оператор не нужен, все SMS живут внутри тестовой среды.
Стенд только для тестов, в публичную сеть порты лучше не открывать.
Пример файла docker-compose
Дополнительные конфигурационные файлы
Запуск стенда
Пробная отправка СМС
Отправка сообщения с получением DLR отчета
Остановка и удаление стенда
Решение возможных проблем
Файл docker-compose.yml
Создадим каталог для рабочих файлов и конфигов. Сразу перейдем в него:
mkdir -p /opt/smpp-lab/conf && cd /opt/smpp-lab
Соберем три сервиса вместе — SMPPSim, bearerbox и smsbox — и опишем зависимости между ними:
vi docker-compose.yml
services:
smppsim:
image: eagafonov/smppsim:latest
container_name: smppsim
hostname: smppsim
restart: unless-stopped
environment:
TZ: Europe/Moscow
ports:
- "2775:2775"
- "8884:88"
volumes:
- ./conf/smppsim.props:/opt/local/SMPPSim/conf/smppsim.props:ro
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:88 || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
bearerbox:
build:
context: .
dockerfile: Dockerfile.kannel
image: smpp-lab-kannel:local
container_name: kannel-bearerbox
hostname: bearerbox
restart: unless-stopped
command: ["/usr/sbin/bearerbox", "-v", "0", "/etc/kannel/kannel.conf"]
environment:
TZ: Europe/Moscow
ports:
- "13000:13000"
volumes:
- ./conf/kannel.conf:/etc/kannel/kannel.conf:ro
depends_on:
smppsim:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:13000/status?password=kannel || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 15s
smsbox:
image: smpp-lab-kannel:local
container_name: kannel-smsbox
hostname: smsbox
restart: unless-stopped
command: ["/usr/sbin/smsbox", "-v", "0", "/etc/kannel/kannel.conf"]
environment:
TZ: Europe/Moscow
ports:
- "13013:13013"
volumes:
- ./conf/kannel.conf:/etc/kannel/kannel.conf:ro
depends_on:
bearerbox:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://bearerbox:13000/status?password=kannel | grep -q \"smsbox:smsbox\" || exit 1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
* где:
- eagafonov/smppsim — готовый SMPPSim (Selenium Software); конфиг монтируем в /opt/local/SMPPSim/conf/smppsim.props.
- 2775 — SMPP с хоста; ваш код/клиент биндится на 127.0.0.1:2775 как smppclient2 / password.
- 8884:88 — веб SMPPSim на хосте 8884, внутри контейнера слушает 88 (штатный HTTP_PORT образа).
- bearerbox — ядро Kannel, держит SMPP-сессию с симулятором.
- smsbox — HTTP /cgi-bin/sendsms на 13013; стартует после живого status у bearerbox.
- smsbox-port 13001 — только внутри сети compose, на хост не публикуем.
Конфигурационные файлы
Создадим 2 конфигурации для сервера и клиента, а также dockerfile для kannel.
SMPPSim (сервер)
Опишем настройки симулятора:
vi conf/smppsim.props
SMPP_PORT=2775
SMPP_CONNECTION_HANDLERS=10
CONNECTION_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardConnectionHandler
PROTOCOL_HANDLER_CLASS=com.seleniumsoftware.SMPPSim.StandardProtocolHandler
LIFE_CYCLE_MANAGER=com.seleniumsoftware.SMPPSim.DeterministicLifeCycleManager
MESSAGE_STATE_CHECK_FREQUENCY=2000
MAX_TIME_ENROUTE=3000
DELAY_DELIVERY_RECEIPTS_BY=1000
PERCENTAGE_THAT_TRANSITION=100
PERCENTAGE_DELIVERED=100
PERCENTAGE_UNDELIVERABLE=0
PERCENTAGE_ACCEPTED=0
PERCENTAGE_REJECTED=0
DISCARD_FROM_QUEUE_AFTER=60000
HTTP_PORT=88
HTTP_THREADS=1
DOCROOT=www
AUTHORISED_FILES=/index.htm,/inject_mo.htm,/favicon.ico,/images/logo.gif,/images/pda2.gif,/user-guide.htm,/css/style.css,/images/dots.gif,/images/homepage.gif,/images/inject_mo.gif
INJECT_MO_PAGE=/inject_mo.htm
SYSTEM_IDS=smppclient1,smppclient2
PASSWORDS=password,password
OUTBIND_ENABLED=false
DELIVERY_MESSAGES_PER_MINUTE=0
LOOPBACK=FALSE
ESME_TO_ESME=false
OUTBOUND_QUEUE_MAX_SIZE=1000
INBOUND_QUEUE_MAX_SIZE=1000
DELAYED_INBOUND_QUEUE_PROCESSING_PERIOD=60
DELAYED_INBOUND_QUEUE_MAX_ATTEMPTS=100
DECODE_PDUS_IN_LOG=true
DELIVERY_RECEIPT_OPTIONAL_PARAMS=true
SMSCID=SMPPSim
* где:
- SMPP_PORT — порт SMSC для BIND/SUBMIT_SM/DELIVER_SM.
- SYSTEM_IDS / PASSWORDS — пары логин/пароль ESME; Kannel берет smppclient1, второй слот — для внешнего SMPP-клиента.
- DeterministicLifeCycleManager — финальный статус SMS задает первая цифра номера получателя: 1 EXPIRED, 2 DELETED, 3 UNDELIVERABLE, 4 ACCEPTED, 5 REJECTED, любая другая — DELIVERED.
- HTTP_PORT — веб-морда внутри контейнера (инъекция MO); на хост публикуем как 8884:88.
- LOOPBACK=FALSE — исходящее SMS не возвращается обратно как входящее; для эха MO поставьте TRUE.
- DECODE_PDUS_IN_LOG — в логах будут разборы PDU, а не только hex.
Конфигурация Kannel
Опишем настройки bearerbox, smsbox и подключение к SMSC:
vi conf/kannel.conf
group = core
admin-port = 13000
smsbox-port = 13001
admin-password = kannel
status-password = kannel
admin-allow-ip = "*.*.*.*"
box-allow-ip = "*.*.*.*"
log-file = "/var/log/kannel/bearerbox.log"
log-level = 0
access-log = "/var/log/kannel/access.log"
group = smsc
smsc = smpp
smsc-id = smppsim
host = smppsim
port = 2775
transceiver-mode = true
smsc-username = smppclient1
smsc-password = password
system-type = VMA
interface-version = 34
enquire-link-interval = 30
msg-id-type = 0
throughput = 20
source-addr-ton = 1
source-addr-npi = 1
dest-addr-ton = 1
dest-addr-npi = 1
group = smsbox
smsbox-id = smsbox
bearerbox-host = bearerbox
sendsms-port = 13013
sendsms-interface = 0.0.0.0
global-sender = 1234
mo-recode = true
log-file = "/var/log/kannel/smsbox.log"
log-level = 0
group = sendsms-user
username = tester
password = foobar
concatenation = true
max-messages = 10
default-smsc = smppsim
forced-smsc = smppsim
user-allow-ip = "*.*.*.*"
group = sms-service
keyword = default
text = "lab-echo: %a"
max-messages = 1
catch-all = true
* где:
- admin-allow-ip / box-allow-ip — без маски *.*.*.* статус с хоста и smsbox из соседнего контейнера не достучатся (по умолчанию только 127.0.0.1).
- host = smppsim — DNS-имя сервиса в compose, не localhost.
- transceiver-mode — один BIND_TRANSCEIVER вместо пары transmitter+receiver; иначе DLR и MO не придут на этот bind.
- smsc-username / smsc-password — должны совпасть с первой парой в smppsim.props.
- msg-id-type = 0 — message_id в submit_sm_resp как hex-строка; иначе Kannel не склеит DLR с исходным SMS.
- throughput — потолок MT в секунду на стороне Kannel; имитация лимита агрегатора.
- concatenation / max-messages — длинный текст режется на сегменты с UDH.
- sms-service — ответ на входящее MO; %a подставит текст входящего.
Dockerfile Kannel
Готового образа с нужной версией Kannel под задачу нет, поэтому соберем свой на базе Debian:
vi Dockerfile.kannel
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive TZ=Europe/Moscow
RUN printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d \
&& chmod +x /usr/sbin/policy-rc.d \
&& apt-get update \
&& apt-get install -y --no-install-recommends kannel curl ca-certificates \
&& mkdir -p /var/log/kannel \
&& chown -R kannel: /var/log/kannel /etc/kannel \
&& chmod 755 /var/log/kannel /etc/kannel \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /etc/kannel
USER kannel
* где:
- policy-rc.d exit 101 — пакет kannel не пытается поднять init-скрипты внутри образа.
- kannel — пакет Debian с бинарниками /usr/sbin/bearerbox и /usr/sbin/smsbox.
- curl — для healthcheck в compose.
- CMD не задается, так как образ будет использоваться в docker-compose для двух сервисов. Каждый из них будет запускать процесс своей командой.
Запуск контейнеров и проверка статуса
Соберем образ Kannel и поднимем стек:
docker-compose build
docker-compose up -d
Проверим состояние контейнеров:
docker-compose ps
* все три сервиса в состоянии Up; у bearerbox healthcheck — healthy.
Стек поднят. Запросим статус bearerbox и убедимся, что он подключился к симулятору:
curl -sS "http://127.0.0.1:13000/status?password=kannel"
В выводе ищем SMSC smppsim в статусе online / connected:
SMSC connections:
smppsim[smppsim] SMPP:smppsim:2775/2775:smppclient1:VMA (online 89s, rcvd: sms 0 (0.00,0.00,0.00) / dlr 0 (0.00,0.00,0.00), sent: sms 0 (0.00,0.00,0.00) / dlr 0 (0.00,0.00,0.00), failed 0, queued 0 msgs)
* если still connecting — 10–20 секунд на BIND, затем повторяем запрос.
Проверим логи обоих сервисов:
docker-compose logs --tail=80 smppsim
docker-compose logs --tail=80 bearerbox
* в логах симулятора должно быть BIND_TRANSCEIVER и успешный bind для smppclient1; в bearerbox — подключение к SMSC smppsim.
Bind установлен, можно отправлять сообщения.
Отправка SMS
Отправим тестовое сообщение через HTTP API Kannel:
curl -sS -G "http://127.0.0.1:13013/cgi-bin/sendsms" --data-urlencode "username=tester" --data-urlencode "password=foobar" --data-urlencode "from=1234" --data-urlencode "to=79001234567" --data-urlencode "text=Hello SMPP lab"
* ответ Kannel мы должны увидеть код: 0: Accepted for delivery. Номер на 7 у детерминированного lifecycle — финальный статус DELIVERED.
Для кириллицы понадобятся отдельные параметры кодировки:
curl -sS -G "http://127.0.0.1:13013/cgi-bin/sendsms" --data-urlencode "username=tester" --data-urlencode "password=foobar" --data-urlencode "from=1234" --data-urlencode "to=79001234567" --data-urlencode "charset=utf-8" --data-urlencode "coding=2" --data-urlencode "text=Привет лаборатория"
* где:
- charset=utf-8 — как передаем текст в HTTP.
- coding=2 — UCS-2 в SUBMIT_SM; без этого кириллица уходит GSM 7-bit и превращается в «???».
Проверим склейку длинного сообщения на сегменты:
curl -sS -G "http://127.0.0.1:13013/cgi-bin/sendsms" --data-urlencode "username=tester" --data-urlencode "password=foobar" --data-urlencode "from=1234" --data-urlencode "to=79001234567" --data-urlencode "charset=utf-8" --data-urlencode "coding=2" --data-urlencode "text=Длинное SMS для проверки склейки сегментов: Kannel режет текст на части, в SUBMIT_SM появляется UDH, симулятор принимает каждый сегмент отдельно."
* в логах smppsim должно быть несколько SUBMIT_SM подряд и ненулевой esm_class / UDH у сегментов.
docker-compose logs --tail=100 smppsim
* ищем SUBMIT_SM, текст/UCS-2 и message id в submit_sm_resp.
Отчеты DLR
В двух словах, отчет DLR — это ответ от сервера клиенту о конечном статусе доставки сообщения. То есть, по нему мы можем с большой уверенностью сказать, была ли выполнена доставка или нет.
Отправим сообщение с запросом отчета о доставке и укажем адрес, куда Kannel пришлет DLR:
curl -sS -G "http://127.0.0.1:13013/cgi-bin/sendsms" --data-urlencode "username=tester" --data-urlencode "password=foobar" --data-urlencode "from=1234" --data-urlencode "to=79001234567" --data-urlencode "text=DLR delivered" --data-urlencode "dlr-mask=31" --data-urlencode "dlr-url=http://smsbox:13013/dlr-receiver?id=%F&status=%d"
Проверим и сценарий недоставки — для этого достаточно поменять первую цифру номера получателя:
curl -sS -G "http://127.0.0.1:13013/cgi-bin/sendsms" --data-urlencode "username=tester" --data-urlencode "password=foobar" --data-urlencode "from=1234" --data-urlencode "to=39990000001" --data-urlencode "text=DLR failed" --data-urlencode "dlr-mask=31" --data-urlencode "dlr-url=http://smsbox:13013/dlr-receiver?id=%F&status=%d"
* где:
- dlr-mask=31 — все классы отчетов (delivered / failed / buffered / submit / reject).
- 79001234567 — DELIVERED, через ~1–3 с в логах DELIVER_SM с receipt DELIVRD.
- 39990000001 — первая цифра 3 → UNDELIVERABLE / UNDELIV.
Проверим логи bearerbox:
docker-compose logs --tail=80 bearerbox
* dlr-url в примерах выше указывает на demo-путь smsbox:13013/dlr-receiver, которого нет в конфиге smsbox — Kannel отправит запрос и получит 404. Для реальной обработки DLR нужен свой HTTP-приемник на этом адресе; в рамках теста достаточно смотреть статус доставки в логах bearerbox, как показано выше.
Остановка и очистка
Для остановки стенда без удаления данных достаточно остановить контейнеры:
docker-compose stop
Если лаборатория больше не нужна, удалим контейнеры и сеть вместе с образом Kannel:
docker-compose down --rmi local
Решение проблем
Рассмотрим несколько примеров ошибок, с которыми можно столкнуться при развертывании стенда.
SMSC still connecting / Bind Failed
Сообщения не уходят, а в логах мы видим что-то на подобие:
SMSC still connecting
Причина: не совпали system_id/password либо Kannel ходит на localhost, а не на сервис smppsim.
Решение: сверить smsc-username/smsc-password с SYSTEM_IDS/PASSWORDS; host должен быть smppsim. Пересоздать стек:
docker-compose down && docker-compose up -d
0: Accepted for delivery, DLR нет
При отправке сообщения мы получаем статус, подтверждающий отправку, но нет отчета DLR.
Причина: неверный msg-id-type или bind не transceiver.
Решение: оставить msg-id-type = 0 и transceiver-mode = true, в sendsms передавать dlr-mask=31.
Кириллица в логах как ???
Если сообщение было отправлено на русском, то в логах мы видим вместо текста различные закорючки.
Причина: HTTP отдал UTF-8, а в SMPP ушел GSM 7-bit (coding=0).
Решение: в sendsms всегда charset=utf-8 и coding=2.
Authorization failed for sendsms
При попытке отправить сообщение мы видим ошибку:
Authorization failed for sendsms
Причина: не те username/password HTTP-пользователя или IP не в user-allow-ip.
Решение: tester / foobar и user-allow-ip = "*.*.*.*".