MMS
1xBTS ships an optional MMS stack built around Mbuni, the open-source OMA/WAP MMSC. It’s enabled through the mms compose profile and reuses the existing SMS pipeline for the M-Notification.ind WAP Push that tells handsets a new MMS is available.
Bringing it up
docker compose --profile mms up -d --buildThe mms profile adds a single container that runs Mbuni alongside an in-tree mbuni-msc-bridge shim. The bridge terminates Mbuni’s Kannel-style sendsms HTTP API on loopback and forwards each call to MscManagementService.SendSms so the MSC pages the destination handset over A1 in the same request — no SMSC retry-sweep wait.
You’ll also need two listeners bound so the containers can reach them over the docker bridge. Put these in the local override files — the committed config/msc.json and config/management.json templates stay untouched.
config/msc.local.json, so the mbuni container reaches the MSC:
{ "mgmt_grpc_addr": "0.0.0.0:17017" }config/management.local.json, so the fou-nat container can look up which subscriber sent each message (see Sender attribution below):
{ "grpc_listen_addr": "0.0.0.0:17016" }Handset configuration
Two values must be provisioned on the MS:
- MMSC URL:
http://mmsc.local.1xbts.org/ - MMS proxy: (leave empty)
The handset resolves mmsc.local.1xbts.org via the captive DNS at the PDSN gateway (10.55.0.1), reaches the Mbuni container through the compose network, and uses HTTP for both upload (M-Send.req POST) and retrieval (M-Retrieve.conf GET). Leave the MMS proxy empty: the built-in WAP 1.x gateway is for browsing and is not on the MMS path.
Modern Android
Settings → Mobile networks → Access Point Names → Add APN
| Field | Value |
|---|---|
| Name | 1xBTS MMS |
| APN | internet (any non-empty string) |
| MMSC | http://mmsc.local.1xbts.org/ |
| MMS proxy | (empty) |
| MMS port | 80 |
| APN type | default,mms |
| APN protocol | IPv4 |
| MMS protocol | IPv4 |
Set this as the default carrier APN, then power-cycle data.
Older feature phones
For LG VX/Cosmos/Env, Kyocera slider/clamshell, and older Samsung clamshells, dial the model-specific service menu (commonly 2945#*#, ##78737#, or ##778#) and set the MMSC URL to http://mmsc.local.1xbts.org/ with the proxy field blank.
Carrier MMSC hostnames
Many CDMA handsets have a carrier MMSC URL baked into firmware (mms.vtext.com, mmsc.mobile.att.net, mmsc.sprintpcs.com, etc.) and can’t be reprovisioned without OMA Client Provisioning. The captive Unbound in fou-nat redirects those hostnames to the local Mbuni container at startup, so a Verizon-bundled handset that dials mms.vtext.com still lands on the cell’s MMSC. Override the list with MMSC_HIJACK_HOSTS on the fou-nat service. HTTP only — HTTPS upgrade is out of scope.
Sender attribution
A handset’s MMS upload does not carry a trustworthy sender number. 1xBTS fills it in at the packet-data gateway: every MMSC request is matched to the packet session it came from, and the sender’s phone number is attached before the message reaches the MMSC. The handset cannot override it. If the session lookup fails, the message is still accepted and the MMSC falls back to its own default sender.
Delivery flow
- The handset uploads the message to the MMSC hostname. The gateway attaches the sender’s number and forwards it to Mbuni, which queues it and acknowledges.
- Mbuni resolves the recipient as a local subscriber and hands a WAP Push notification to the MSC, which delivers it as an SMS: on the paging channel if the handset is idle, or on its traffic channel if it has one.
- The handset’s WAP Push receiver opens packet data and fetches the message from the MMSC.
Notifications are ordinary SMS deliveries, so they show up in the message log and follow the same retry rules as any other SMS.
MS-to-MS messages
MO SMS and MMS for a destination that resolves to a provisioned subscriber are forwarded as MT automatically. After the MO submission is recorded the MSC re-runs HLR resolution on the destination phone number and, if a subscriber matches, queues an MT delivery on the spot through send_sms. Offline recipients are handled by the regular retry sweep — the MT submission stays in Accepted and re-pages when the handset re-registers. MO submissions whose destination has no provisioned subscriber are marked failed.
MMSC tuning
docker/mbuni/mbuni.conf.template ships with intervals tuned for tight retry on a small cell:
| Setting | Value | Effect |
|---|---|---|
queue-run-interval | 10s | Global relay queue scanner |
mm1-queue-run-interval | 10s | MM1 delivery queue scanner (WAP Push dispatch) |
send-attempt-back-off | 10s | Minimum gap between retries for the same item |
maximum-send-attempts | 5 | Overall retry cap |
mm1-maximum-notify-attempts | 3 | M-Notification.ind retry cap |
default-message-expiry | 86400s | Body expiry in the relay queue |
content-adaptation | false | Skip the UAProf fetch that adds ~75s to each retrieval |
The bridge container’s /etc/hosts blackholes well-known carrier UAProf hosts (uaprof.vtext.com, uaprof.mobile.att.net, etc.) so a handset that advertises x-wap-profile doesn’t make Mbuni hang on an unreachable HTTP lookup. Override with UAPROF_BLACKHOLE_HOSTS on the mbuni service.
Testing — sending an MT MMS
Mbuni ships an mmssend CLI that injects a MIME-formatted MMS as if it had arrived over MM3/MM4. Prepare a small payload on the host (mms-test.txt):
Subject: hello from 1xBTSContent-Type: text/plain
Hello, MMS.Sender and recipient are passed on the command line. The recipient must be a registered subscriber phone number:
docker compose --profile mms cp mms-test.txt mbuni:/tmp/docker compose --profile mms exec mbuni \ /opt/mbuni/bin/mmssend \ -f 12025550100/TYPE=PLMN \ -t 5551234/TYPE=PLMN \ -m /tmp/mms-test.txt \ /opt/mbuni/etc/mbuni.confmmssend prints Queued: <message-id> on success. The MS receives a WAP Push notification, opens packet data, fetches the M-Retrieve.conf URL, and renders the message.
Known limitations
- Modern iPhones on a Verizon carrier bundle don’t reach the MMSC. iOS prefers IMS/RCS over the legacy WAP MMS path and the carrier bundle hides the MMS APN field. The DNS hijack resolves correctly but the iPhone never opens a TCP connection to our cell’s MMSC. Lumia, Windows Phone, older Android, and the LG/Kyocera/Samsung feature phones use the legacy path and work end-to-end.
- HTTPS MMSC URLs are out of scope. The DNS hijack is HTTP-only — TLS interception with a trusted cert chain isn’t shipped.
- Outbound (off-net) MMS isn’t implemented. Anything addressed to a number with no HLR subscriber is marked
failed. There is no SMPP/SMTP egress. - Sustained MO upload speed is limited by R-FCH (~7 kbps net IP). A 70 KB MMS takes a minute or longer to POST; some handsets time the upload out client-side before Mbuni answers M-Send.conf, then retry. The notification still gets queued because Mbuni accepts the upload server-side.
Inspecting the queue
docker exec 1xbts-mbuni ls -la /var/spool/mbuni/global/r/au/ # accepted MO MMS waiting to be relayeddocker exec 1xbts-mbuni ls -la /var/spool/mbuni/mm1/ # outbound MM1 notifications
# Inside a queue file:# F 14805558888/TYPE=PLMN — sender (the subscriber's phone number)# R 15558675309/TYPE=PLMN — recipient# N 2 — attempts so far# X 1779803541 — next retry epoch# D 1779890000 — expiry epoch