通过 ModemManager 收发、保存并转发短信的 Linux 服务。
A Linux service for receiving, storing, sending, and forwarding SMS through ModemManager.
sms-relayed 适用于插有 SIM 卡的 OpenWrt 路由器、Debian 网关、随身 Wi-Fi、USB 蜂窝网卡和其他 Linux 设备。它通过系统 D-Bus 连接 ModemManager,监听新短信并转发到多个通知渠道,同时提供短信历史、Web 管理控制台、命令行发送和设备状态管理。
项目由 Rust 后端和嵌入二进制的 React 前端组成,不需要单独部署 Web 服务器。
- 实时接收 ModemManager 的新短信,持久化到 SQLite,并避免重复入库。
- 将短信转发到 Bark、Telegram、企业微信、钉钉、Lark 自定义机器人或自定义 HTTP Webhook。
- 支持多个命名 profile,同一条短信可投递到多个渠道,并记录投递结果、重试和延迟。
- 从常见验证码短信中提取 4–7 位字母数字验证码;Bark 通知可自动复制验证码。
- 通过 CLI 或密码保护的 Web 控制台发送短信。
- 在 Web 控制台中搜索、筛选、标记已读、删除和导出 CSV/JSON。
- 查看 Modem 状态,执行启用、禁用和确认后的重置操作。
- 在 Web Modem 页面查看可选的 SMS over IMS 辅助状态;该状态不影响主健康判定。
- 可选的历史保留策略,分批清理超过指定天数且没有待处理投递的消息。
- 支持 OpenWrt procd 与 systemd 服务。
- 支持带 SHA-256 校验、原子替换和服务重启的自更新。
- Linux 与可用的系统 D-Bus。
- ModemManager,以及一个支持短信功能的 Modem。
- 服务用户需要访问 ModemManager 系统 D-Bus;路由器上通常以
root运行。 mmcli是 Web Modem 状态、健康诊断和设备控制的运行时依赖。没有mmcli时,短信收发仍通过 D-Bus 工作,但相关状态会显示为unknown。- SMS over IMS 状态需要带 IMS/IMSA 命令的
qmicli。Debian 12 可按运维说明私有安装 1.36.0,不替换系统 libqmi。 - 官方 Release 当前提供静态 Linux x86_64、aarch64 和 armv7 二进制。
在 OpenWrt 的 root shell 中,或在有写入 /usr/bin 和 /etc 权限的 Linux 环境中运行:
curl -fsSL https://19yo.de/smsrelay | sh
# or if you don't like shortlink:
curl -fsSL https://raw.githubusercontent.com/frankwei98/sms-relayed/main/install.sh | sh安装脚本会:
- 检测系统和架构并下载最新 GitHub Release。
- 安装二进制到
/usr/bin/sms-relayed。 - 创建
/etc/sms-relayed。 - 注册 OpenWrt procd 或 systemd 服务。
- 在交互式终端中启动配置向导;配置存在后启用并启动服务。
如果当前账号需要提权,可以先下载脚本、检查内容,再用 sudo sh install.sh 执行。
常用安装变量:
| 变量 | 默认值 | 说明 |
|---|---|---|
SMS_RELAYED_VERSION |
latest |
安装指定 Release tag。 |
SMS_RELAYED_BIN_DIR |
/usr/bin |
二进制安装目录。 |
SMS_RELAYED_CONFIG_DIR |
/etc/sms-relayed |
配置和默认数据库目录。 |
SMS_RELAYED_START |
1 |
设为 0 时不自动启动服务。 |
SMS_RELAYED_CONFIG_ONLY |
0 |
设为 1 时不安装二进制,并使用已有二进制运行配置流程。 |
SMS_RELAYED_UPDATE |
自动/交互确认 | 已安装二进制存在时,设为 1 强制更新,设为 0 跳过。 |
SMS_RELAYED_ROOT |
空 | 将文件写入指定根目录,用于镜像或离线文件系统准备;不会启动服务。 |
默认配置路径是 /etc/sms-relayed/config.toml。推荐运行交互式向导:
sudo sms-relayed setup
sudo sms-relayed config check
sudo sms-relayed config show也可以使用全局 --config 参数指定其他路径:
sms-relayed --config /path/to/config.toml config check完整示例:
[app]
device_name = "router-sim"
modem_path = "/org/freedesktop/ModemManager1/Modem/0"
[sms]
ignore_storage = ["sm"]
code_keywords = ["验证码", "verification", "code", "인증", "代码", "随机码"]
[forward]
enabled = ["bark.personal", "telegram.main"]
[delivery]
concurrency = 2
[channels.bark.personal]
server_url = "https://api.day.app"
key = "..."
[channels.telegram.main]
bot_token = "..."
chat_id = "..."
api_base = "https://api.telegram.org"
[channels.wecom.default]
corp_id = "..."
agent_id = "..."
secret = "..."
to_user = "@all"
[channels.dingtalk.default]
access_token = "..."
secret = "..."
[channels.lark.default]
webhook_url = "https://open.larksuite.com/open-apis/bot/v2/hook/..."
secret = "..."
[channels.webhook.default]
method = "post"
url = "https://example.com/api/message"
content_type = "application/json"
body = """
{
"sender": {SENDER_JSON},
"message": {MESSAGE_JSON},
"datetime": {DATETIME_JSON}
}
"""
[channels.webhook.default.headers]
Authorization = "Bearer ..."
[api]
enabled = true
bind = "0.0.0.0"
port = 8080
enable_ipv6 = false
trusted_proxies = []
password = "change-this-password"
database_path = "/etc/sms-relayed/sms-relayed.sqlite"
[http]
connect_timeout_secs = 10
request_timeout_secs = 30
[retention]
enabled = false
max_age_days = 90
batch_size = 500
[monitoring]
enabled = true配置说明:
forward.enabled使用渠道类型.profile名称,例如telegram.main。同一渠道可定义多个 profile。- 支持的渠道类型为
bark、telegram、wecom、dingtalk、lark和webhook。 - Bark 使用 API v2:将
server_url设为服务根地址,程序会向<server_url>/push发送 JSON 请求。 - Lark 使用群聊自定义机器人的完整
webhook_url发送文本消息;启用签名校验时填写secret,未启用时可留空。Webhook URL 和签名密钥都属于敏感凭据。Lark 返回限流错误时会进入现有投递重试流程。 delivery.concurrency控制同时执行的转发任务数,默认值为2,有效范围为1到16。- 新 delivery 在数据库事务提交后会立即唤醒 worker;worker 启动时扫描一次,并保留固定 30 秒安全扫描。重试按最近的
next_attempt_at精确唤醒。 api.enabled = true时必须设置非空api.password。- 反向代理部署时,可将直接连接到服务的代理 IP 明确列入
api.trusted_proxies,例如["127.0.0.1", "::1"]。默认不信任任何代理;只有可信代理发送的X-Forwarded-For才用于登录限流,并从右向左选择最近的非可信地址,防止客户端伪造更左侧的地址。配置错误或格式无效时会回退到 TCP peer IP。 - 配置文件包含凭据;程序写入配置时会将权限设为
0600。 - 修改配置后需要重启服务才能生效。
启用 [api] 后,访问:
http://<设备 IP>:8080/
控制台包含:
- SMS 收件箱/发件箱、号码会话、搜索、筛选和未读状态。
- SMS 发送、单条/批量删除、CSV/JSON 流式导出。
- Modem 状态与启用、禁用、重置操作。
- 各转发 profile 最近五次已完成投递的结果与延迟;
Dispatch是任务到期后的调度等待,Request是渠道请求耗时,迁移前的历史调度延迟显示为—。 - 配置编辑、验证、保存和服务重启。
- 通过 SSE 推送新消息和状态变化。
主要 API:
| 路径 | 用途 |
|---|---|
/api/auth/login, /api/auth/logout, /api/auth/me |
会话认证。 |
/api/messages, /api/conversations |
消息与会话查询。 |
/api/messages/send |
发送短信。 |
/api/messages/export |
导出 CSV 或 JSON。 |
/api/events |
SSE 事件流。 |
/api/config, /api/config/check |
配置读取、保存和验证。 |
/api/status, /api/service/restart |
服务状态与重启。 |
/api/modem/* |
Modem 状态和控制。 |
/api/forwarding/attempts |
转发投递状态。 |
/api/health |
无需登录的服务与 Modem 健康检查。 |
POST /api/messages/send 必须携带 1–200 字符的 Idempotency-Key 请求头。请求超时后重试时必须复用同一个 key;相同 key 搭配不同号码或正文会返回 409。消息被保留策略删除后,该 key 的重放保证随消息一起结束;手动删除消息留下的 key 最多保留 30 天。
/api/health 会隐藏对象路径、运营商、信号、SIM 标识、号码、短信正文和原始命令输出。Web 服务当前直接提供 HTTP;不要将它裸露到公网,请放在可信网络、VPN 或带 TLS 和访问控制的反向代理后面。
sms-relayed [--config <path>]
sms-relayed [--config <path>] setup
sms-relayed [--config <path>] run
sms-relayed [--config <path>] send
sms-relayed update
sms-relayed [--config <path>] config check
sms-relayed [--config <path>] config show
- 无子命令:在交互式终端中打开配置向导;非交互环境会返回错误。
setup:打开配置向导。run:启动短信监听、持久化、投递 worker、保留策略 worker,以及可选 Web API。send:交互式输入号码和正文,确认后发送并保存记录。update:查询最新 Release,校验配套 SHA-256,验证下载文件的版本/commit,原子替换二进制并重启已检测到的服务。config check:加载并验证配置。config show:输出脱敏后的配置摘要。
自更新当前仅支持官方发布的 Linux x86_64、aarch64 与 armv7 资产。已是相同 commit 时不会替换或重启。更新系统目录通常需要以有权限的账号运行,例如 sudo sms-relayed update。
OpenWrt:
/etc/init.d/sms-relayed enable
/etc/init.d/sms-relayed start
/etc/init.d/sms-relayed restart
/etc/init.d/sms-relayed status
logread | grep sms-relayedsystemd:
sudo systemctl enable --now sms-relayed
sudo systemctl restart sms-relayed
systemctl status sms-relayed
journalctl -u sms-relayedWebhook 是一个刻意保持小巧、自由的 HTTP 模板模块,仅支持 GET 和 POST。可在 URL 和 POST body 中使用 MESSAGE、SENDER、DATETIME:
{MESSAGE}:原文替换;{MESSAGE_URL}:百分号编码;{MESSAGE_JSON}:包含外层引号的完整 JSON 字符串字面量。SENDER、DATETIME支持相同的_URL和_JSON后缀。- 替换只执行一次。短信内容里的
{SENDER}等文字不会被再次展开。 headers是静态键值;所有 header 值和 Webhook URL 在配置摘要及日志中都会脱敏。
表单 POST 可直接配置:
content_type = "application/x-www-form-urlencoded"
body = "sender={SENDER_URL}&message={MESSAGE_URL}&datetime={DATETIME_URL}"GET 可把变量放进 URL,例如 https://example.com/send/{SENDER_URL}/{MESSAGE_URL},但发送者和短信内容可能出现在代理、服务端或监控系统的访问日志中。首个版本不生成 multipart/form-data。
2xx 表示成功;408、425、429 和 5xx 会重试;其他 4xx 及所有 3xx 为永久失败,重定向不会自动跟随;连接、DNS 和超时错误会重试。投递采用至少一次语义,重试可能造成重复请求,接收 API 如有需要应自行实现幂等。Webhook 可访问 HTTP、局域网、localhost 和私网地址;兼容性及目标 API 的安全策略由用户负责。
Shell 转发已移除。旧 shell.<name> 或 [channels.shell.*] 配置会明确报错,需迁移为 webhook.<name>。
需要稳定版 Rust、Node.js 和 pnpm。发布构建应先生成前端资产,再编译 Rust:
cd frontend
pnpm install --frozen-lockfile
pnpm build
cd ..
cargo build --release --locked常用检查:
cargo fmt --check
cargo test
cargo build
cd frontend
pnpm check
pnpm test
pnpm build如果 frontend/dist 不存在,build.rs 会生成一个提示页面,便于 Rust 开发构建;正式打包前必须运行前端构建。
Sentry 错误监测在缺少配置时默认关闭;配置向导会询问是否启用,并默认选择 yes。后端服务和生产前端使用独立项目。上报会移除请求、用户、breadcrumb、主机名、上下文变量和异常正文,不发送短信正文、电话号码或配置凭据;重复运行时错误会限流。
- 运行时:设置
[monitoring] enabled = true同时允许后端和生产前端上报。 - 后端:设置
SMS_RELAYED_SENTRY_DSN可覆盖 DSN,设为空字符串可关闭。 - 前端:构建时设置
VITE_SENTRY_DSN可覆盖 DSN,设置VITE_SENTRY_ENABLED=false可关闭。 - 前端开发模式不发送 Sentry 事件。
本项目采用 GNU GPL v3 only(GPL-3.0-only)。项目名称、Logo 和官方 Release 名称另受 Trademark Policy 约束。允许将本软件集成进硬件产品并如实说明,但修改版或第三方构建不得冒充官方 Release。
参考资料:
sms-relayed is designed for OpenWrt routers, Debian gateways, portable hotspots, USB cellular modems, and other Linux devices with a SIM card. It connects to ModemManager over the system D-Bus, receives and forwards SMS messages, and provides message history, a web dashboard, interactive sending, and modem status controls.
The project consists of a Rust backend and a React frontend embedded in the binary. No separate web server is required.
- Receive new messages from ModemManager, persist them in SQLite, and suppress duplicate inserts.
- Forward messages to Bark, Telegram, WeCom, DingTalk, Lark custom bots, or a custom HTTP webhook.
- Configure multiple named profiles, deliver one message to multiple channels, and record outcomes, retries, and latency.
- Extract 4–7 character alphanumeric codes from common verification messages; Bark can copy detected codes automatically.
- Send SMS from the CLI or the password-protected web dashboard.
- Search, filter, mark, delete, and export message history as CSV or JSON.
- Inspect modem health and enable, disable, or explicitly reset the modem.
- Inspect an optional SMS over IMS auxiliary status without changing main health.
- Optionally delete old terminal messages in batches while retaining messages with pending deliveries.
- Run under OpenWrt procd or systemd.
- Self-update with SHA-256 verification, atomic replacement, and service restart.
- Linux with a working system D-Bus.
- ModemManager and an SMS-capable modem.
- Permission to access ModemManager on the system bus; router installations commonly run as
root. mmcliis required for dashboard modem diagnostics, health details, and modem controls. SMS receive/send still uses D-Bus without it, but those status features reportunknown.- SMS over IMS status requires qmicli with IMS/IMSA actions. On Debian 12, follow the operations guide to install a private 1.36.0 without replacing system libqmi.
- Official releases currently provide static Linux binaries for x86_64, aarch64, and armv7.
Run this from an OpenWrt root shell or another Linux account that can write to /usr/bin and /etc:
curl -fsSL https://raw.githubusercontent.com/frankwei98/sms-relayed/main/install.sh | shThe installer detects the platform, downloads the latest release, installs /usr/bin/sms-relayed, creates /etc/sms-relayed, registers a procd or systemd service, and opens the setup wizard when a TTY is available. It starts the service once a configuration exists.
If elevation is required, download and inspect the script first, then run it with sudo sh install.sh.
Installer variables:
| Variable | Default | Purpose |
|---|---|---|
SMS_RELAYED_VERSION |
latest |
Install a specific release tag. |
SMS_RELAYED_BIN_DIR |
/usr/bin |
Binary installation directory. |
SMS_RELAYED_CONFIG_DIR |
/etc/sms-relayed |
Configuration and default database directory. |
SMS_RELAYED_START |
1 |
Set to 0 to avoid starting the service. |
SMS_RELAYED_CONFIG_ONLY |
0 |
Set to 1 to skip binary installation and configure with an existing binary. |
SMS_RELAYED_UPDATE |
automatic/prompt | Set to 1 to replace an existing binary or 0 to skip it. |
SMS_RELAYED_ROOT |
empty | Write into an alternate root for image/offline filesystem preparation; services are not started. |
The default configuration path is /etc/sms-relayed/config.toml. The setup wizard is the recommended starting point:
sudo sms-relayed setup
sudo sms-relayed config check
sudo sms-relayed config showUse the global --config option for another path:
sms-relayed --config /path/to/config.toml config checkComplete example:
[app]
device_name = "router-sim"
modem_path = "/org/freedesktop/ModemManager1/Modem/0"
[sms]
ignore_storage = ["sm"]
code_keywords = ["验证码", "verification", "code", "인증", "代码", "随机码"]
[forward]
enabled = ["bark.personal", "telegram.main"]
[delivery]
concurrency = 2
[channels.bark.personal]
server_url = "https://api.day.app"
key = "..."
[channels.telegram.main]
bot_token = "..."
chat_id = "..."
api_base = "https://api.telegram.org"
[channels.wecom.default]
corp_id = "..."
agent_id = "..."
secret = "..."
to_user = "@all"
[channels.dingtalk.default]
access_token = "..."
secret = "..."
[channels.lark.default]
webhook_url = "https://open.larksuite.com/open-apis/bot/v2/hook/..."
secret = "..."
[channels.webhook.default]
method = "post"
url = "https://example.com/api/message"
content_type = "application/json"
body = """
{
"sender": {SENDER_JSON},
"message": {MESSAGE_JSON},
"datetime": {DATETIME_JSON}
}
"""
[channels.webhook.default.headers]
Authorization = "Bearer ..."
[api]
enabled = true
bind = "0.0.0.0"
port = 8080
enable_ipv6 = false
trusted_proxies = []
password = "change-this-password"
database_path = "/etc/sms-relayed/sms-relayed.sqlite"
[http]
connect_timeout_secs = 10
request_timeout_secs = 30
[retention]
enabled = false
max_age_days = 90
batch_size = 500
[monitoring]
enabled = trueImportant rules:
forward.enabledcontainschannel.profilereferences such astelegram.main; multiple profiles of the same channel are supported.- Channel types are
bark,telegram,wecom,dingtalk,lark, andwebhook. - Bark uses API v2: set
server_urlto the server root and sms-relayed sends JSON to<server_url>/push. - Lark sends text messages to the complete custom bot
webhook_url. Setsecretwhen signature verification is enabled, or leave it empty otherwise. Treat both values as credentials. Lark rate-limit responses use the existing delivery retry flow. delivery.concurrencycontrols concurrent forwarding jobs. It defaults to2and accepts values from1through16.- A committed delivery wakes the worker immediately. The worker also scans on startup, keeps a fixed 30-second safety scan, and wakes precisely for the earliest
next_attempt_atretry deadline. - A non-empty
api.passwordis required whenapi.enabled = true. - For reverse-proxy deployments, explicitly list the proxy IPs that connect directly to the service in
api.trusted_proxies, for example["127.0.0.1", "::1"]. No proxy is trusted by default. Login rate limiting usesX-Forwarded-Foronly from a trusted proxy and selects the nearest untrusted address from right to left, preventing clients from spoofing addresses farther left. Invalid header data safely falls back to the TCP peer IP. - The configuration contains credentials. Files written by sms-relayed are restricted to mode
0600. - Restart the service after changing the configuration.
With [api] enabled, open:
http://<device-ip>:8080/
The dashboard provides inbox/outbox conversations, search and filters, read state, sending, deletion, streamed CSV/JSON exports, modem diagnostics and controls, recent forwarding results, configuration editing, validation, service restart, and live SSE updates. Forwarding timing separates queue Dispatch delay from channel Request latency; pre-migration dispatch history is shown as —.
Main API groups:
| Path | Purpose |
|---|---|
/api/auth/login, /api/auth/logout, /api/auth/me |
Session authentication. |
/api/messages, /api/conversations |
Message and conversation queries. |
/api/messages/send |
Send an SMS. |
/api/messages/export |
Export CSV or JSON. |
/api/events |
SSE event stream. |
/api/config, /api/config/check |
Read, save, and validate configuration. |
/api/status, /api/service/restart |
Service status and restart. |
/api/modem/* |
Modem status and controls. |
/api/forwarding/attempts |
Forwarding delivery status. |
/api/health |
Public service and modem health check. |
POST /api/messages/send requires an Idempotency-Key header containing 1–200 characters. Reuse the same key when retrying a timed-out request; using one key with a different number or body returns 409. The replay guarantee ends when message retention deletes the corresponding message. Keys left by manual deletion are retained for at most 30 days.
/api/health omits object paths, operator details, signal values, SIM identifiers, phone numbers, message bodies, and raw command output. The web service currently speaks plain HTTP. Do not expose it directly to the public Internet; use a trusted network, VPN, or a reverse proxy with TLS and access control.
sms-relayed [--config <path>]
sms-relayed [--config <path>] setup
sms-relayed [--config <path>] run
sms-relayed [--config <path>] send
sms-relayed update
sms-relayed [--config <path>] config check
sms-relayed [--config <path>] config show
- No subcommand opens the setup wizard on an interactive terminal and fails in non-interactive mode.
setupopens the configuration wizard.runstarts SMS monitoring, persistence, the delivery and retention workers, and the optional web API.sendprompts for a recipient and message, asks for confirmation, sends it, and stores the result.updatequeries the latest release, verifies its matching SHA-256 file and embedded version/commit, atomically replaces the binary, and restarts a detected service.config checkloads and validates the configuration.config showprints a redacted configuration summary.
Self-update supports the official Linux x86_64, aarch64, and armv7 assets. A build at the same commit is left untouched and the service is not restarted. Updating a system directory usually requires sufficient permissions, for example sudo sms-relayed update.
OpenWrt:
/etc/init.d/sms-relayed enable
/etc/init.d/sms-relayed start
/etc/init.d/sms-relayed restart
/etc/init.d/sms-relayed status
logread | grep sms-relayedsystemd:
sudo systemctl enable --now sms-relayed
sudo systemctl restart sms-relayed
systemctl status sms-relayed
journalctl -u sms-relayedWebhook is deliberately a small, free-form HTTP template module supporting GET and POST only. URL and POST body templates can use MESSAGE, SENDER, and DATETIME:
{MESSAGE}inserts the raw value,{MESSAGE_URL}percent-encodes it, and{MESSAGE_JSON}inserts a complete JSON string literal including its outer quotes.SENDERandDATETIMEsupport the same_URLand_JSONsuffixes.- Replacement is one pass. Token-like text inside an SMS is never expanded again.
headersare static key/value pairs. Header values and webhook URLs are redacted from configuration summaries and logs.
For a form POST:
content_type = "application/x-www-form-urlencoded"
body = "sender={SENDER_URL}&message={MESSAGE_URL}&datetime={DATETIME_URL}"A GET can place variables in its URL, for example https://example.com/send/{SENDER_URL}/{MESSAGE_URL}, but sender and message data may then appear in proxy, server, or monitoring access logs. The first version does not generate multipart/form-data.
2xx is success; 408, 425, 429, and 5xx are retried; other 4xx and every 3xx are permanent failures, and redirects are not followed. Connection, DNS, and timeout failures are retried. Delivery is at least once, so retries can duplicate requests; receiving APIs should implement idempotency when needed. Webhooks may target HTTP, LAN, localhost, and private addresses. Compatibility and the target API's security policy remain the user's responsibility.
Shell forwarding has been removed. Old shell.<name> or [channels.shell.*] configurations fail with an explicit migration error and must be replaced with webhook.<name>.
Stable Rust, Node.js, and pnpm are required. Build frontend assets before the Rust release binary:
cd frontend
pnpm install --frozen-lockfile
pnpm build
cd ..
cargo build --release --lockedUseful checks:
cargo fmt --check
cargo test
cargo build
cd frontend
pnpm check
pnpm test
pnpm buildIf frontend/dist is missing, build.rs creates a fallback page for Rust development builds. Always build the frontend before packaging a release.
Sentry error monitoring defaults to off when the setting is absent. The setup wizard asks whether to enable it and defaults that answer to yes. The backend and production frontend use separate projects. Reports strip requests, users, breadcrumbs, host names, contextual variables, and exception text. SMS bodies, phone numbers, and configuration credentials are not sent, and repeated operational errors are rate-limited.
- Runtime: set
[monitoring] enabled = trueto allow both backend and production frontend reporting. - Backend: override with
SMS_RELAYED_SENTRY_DSN, or set it to an empty string to disable reporting. - Frontend: override at build time with
VITE_SENTRY_DSN, or setVITE_SENTRY_ENABLED=falseto disable reporting. - Frontend development mode does not send Sentry events.
sms-relayed is licensed under GNU GPL v3 only (GPL-3.0-only). The project name, logos, and official release names are separately covered by the Trademark Policy. You may integrate the software into hardware and describe that inclusion truthfully, but modified or third-party builds must not be presented as official releases.
References: