Add matrix-rustpush-bridge (iMessage)

Add the matrix-rustpush-bridge role, a Matrix <-> iMessage bridge built
on the mautrix-go bridgev2 framework using RustPush (OpenBubbles backend).

Unlike the existing mautrix-imessage/wsproxy bridge, it talks directly to
Apple's push notification service, so it needs neither a running Mac nor a
wsproxy on the homeserver. Each user supplies a hardware key extracted from a
Mac through the bridge bot's login flow.

The bridge uses its own bot username and puppet namespace (rustpushbot,
rustpush_*) so it does not collide with the wsproxy iMessage bridge.

This bridge is in early development and may have stability issues.
This commit is contained in:
Jason LaGuidice
2026-06-24 01:17:09 -07:00
committed by GitHub
parent 6f57ab8ba1
commit 11cd178cb2
19 changed files with 1068 additions and 0 deletions
@@ -0,0 +1,110 @@
{#
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason LaGuidice
SPDX-License-Identifier: AGPL-3.0-or-later
#}
# ── Stage 1: builder ─────────────────────────────────────────────────────────
FROM ubuntu:24.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
cmake protobuf-compiler build-essential pkg-config \
git curl ca-certificates \
libolm-dev libclang-dev libssl-dev libunicorn-dev libheif-dev zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
# Rust — install to default ~/.cargo so the Makefile's $(HOME)/.cargo/bin path resolves
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain stable
ENV PATH=/root/.cargo/bin:$PATH
# Go — arch-aware, fetches latest stable with fallback
ARG TARGETARCH
RUN set -e; \
GOARCH="${TARGETARCH:-amd64}"; \
GO_VERSION=$(curl -fsSL 'https://go.dev/dl/?mode=json' \
| grep -o '"version":"go[0-9.]*"' | head -1 \
| sed 's/"version":"//;s/"//'); \
: "${GO_VERSION:=go1.25.0}"; \
curl -fsSL "https://go.dev/dl/${GO_VERSION}.linux-${GOARCH}.tar.gz" \
| tar -C /usr/local -xz
ENV PATH=/usr/local/go/bin:$PATH \
GOTOOLCHAIN=local
WORKDIR /build
# ── Rust build layers ─────────────────────────────────────────────────────────
# Copy files that determine whether the clone+patch layer is valid.
# Changing the SHA pin, Makefile, or open-absinthe overlay invalidates this layer.
COPY third_party/rustpush-upstream.sha third_party/
COPY rustpush/ rustpush/
COPY Makefile .
# Clone upstream rustpush at the pinned SHA, apply all patches, overlay open-absinthe.
RUN make ensure-rustpush-source
# Copy Rust crate sources. Changing these invalidates only the Rust build layer,
# not the clone layer above.
COPY pkg/rustpushgo/ pkg/rustpushgo/
COPY nac-validation/ nac-validation/
# Build the Rust static library (~3 min; cached when Rust source is unchanged).
# hardware-key enables the unicorn-based x86 NAC emulator required on Linux
# (both amd64 and arm64 — unicorn supports cross-arch x86 emulation).
RUN cd pkg/rustpushgo && \
cargo build --release --features hardware-key && \
cp target/release/librustpushgo.a /build/librustpushgo.a
# ── Go build layers ───────────────────────────────────────────────────────────
# Download modules first so this layer is cached by go.mod/go.sum.
COPY go.mod go.sum ./
RUN go mod download
# Copy Go source.
COPY cmd/ cmd/
COPY pkg/connector/ pkg/connector/
COPY imessage/ imessage/
COPY ipc/ ipc/
# Build the bridge binary.
ARG BUILD_VERSION=dev
ARG BUILD_COMMIT=unknown
RUN BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) && \
CGO_LDFLAGS="-L/build" \
go build \
-ldflags "-X main.Tag=${BUILD_VERSION} -X main.Commit=${BUILD_COMMIT} -X main.BuildTime=${BUILD_TIME}" \
-o /build/matrix-rustpush \
./cmd/matrix-rustpush/
# ── Stage 2: runtime ─────────────────────────────────────────────────────────
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Runtime shared libraries the bridge binary needs at startup.
# libunicorn2 — unicorn-engine x86 NAC emulator (hardware-key feature)
# libheif1 — HEIC/HEIF conversion (linked at compile time even when disabled)
# libolm3 — Matrix OLM encryption (mautrix bridgev2 framework)
# libssl3 — OpenSSL (rustpush openssl crate dynamic link)
# ffmpeg — video transcoding
RUN apt-get update && apt-get install -y --no-install-recommends \
libunicorn2 libheif1 libolm3 libssl3 ffmpeg \
ca-certificates openssl curl \
&& curl -fsSL 'https://www.apple.com/appleca/AppleIncRootCertificate.cer' \
-o /tmp/AppleRootCA.cer \
&& openssl x509 -inform DER -in /tmp/AppleRootCA.cer \
-out /usr/local/share/ca-certificates/AppleRootCA.crt \
&& update-ca-certificates \
&& rm /tmp/AppleRootCA.cer \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/matrix-rustpush /usr/local/bin/matrix-rustpush
WORKDIR /data
VOLUME /data
EXPOSE 29332
ENTRYPOINT ["matrix-rustpush", "-c", "/data/config.yaml"]
@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason LaGuidice
SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,209 @@
#jinja2: lstrip_blocks: True
# Network-specific config options (iMessage via RustPush)
network:
# Displayname template for iMessage contacts.
# Available variables:
# .FirstName, .LastName, .Nickname
# .Phone, .Email, .ID
displayname_template: {{ matrix_rustpush_bridge_network_displayname_template | to_json }}
# How many days back to look for chats during initial sync.
# Default is 365 (1 year). Set to 0 to use the default.
initial_sync_days: {{ matrix_rustpush_bridge_initial_sync_days | to_json }}
# Set to false to disable CloudKit backfill globally
cloudkit_backfill: {{ matrix_rustpush_bridge_cloudkit_backfill | to_json }}
backfill_source: cloudkit
# Enable or disable video transcoding
video_transcoding: {{ matrix_rustpush_bridge_video_transcoding | to_json }}
# Enable or disable HEIC conversion
heic_conversion: {{ matrix_rustpush_bridge_heic_conversion | to_json }}
heic_jpeg_quality: 95
# Set to true to disable Facetime support globally
disable_facetime: {{ matrix_rustpush_bridge_disable_facetime | to_json }}
# Set to false to disable Statuskit support globally
statuskit_notifications: {{ matrix_rustpush_bridge_statuskit_notifications | to_json }}
statuskit_share_on_startup: {{ matrix_rustpush_bridge_statuskit_share_on_startup | to_json }}
# Config options that affect the central bridge module.
bridge:
# The prefix for commands. Only required in non-management rooms.
command_prefix: {{ matrix_rustpush_bridge_bridge_command_prefix | to_json }}
# Should the bridge create a space for each login containing the rooms that account is in?
personal_filtering_spaces: true
# Whether the bridge should set names and avatars explicitly for DM portals.
private_chat_portal_meta: true
# Should events be handled asynchronously within portal rooms?
async_events: false
# Should every user have their own portals rather than sharing them?
split_portals: false
# Should the bridge resend `m.bridge` events to all portals on startup?
resend_bridge_info: false
# Should leaving Matrix rooms be bridged as leaving groups on the remote network?
bridge_matrix_leave: false
# Should room tags only be synced when creating the portal?
tag_only_on_create: true
# List of tags to allow bridging.
only_bridge_tags: [m.favourite, m.lowpriority]
# Should room mute status only be synced when creating the portal?
mute_only_on_create: true
# What should be done to portal rooms when a user logs out or is logged out?
cleanup_on_logout:
enabled: false
manual:
private: nothing
relayed: nothing
shared_no_users: nothing
shared_has_users: nothing
bad_credentials:
private: nothing
relayed: nothing
shared_no_users: nothing
shared_has_users: nothing
# Settings for relay mode
relay:
enabled: false
admin_only: true
default_relays: []
message_formats:
m.text: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b>: {{ .Message }}{% endraw %}"
m.notice: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b>: {{ .Message }}{% endraw %}"
m.emote: "{% raw %}* <b>{{ .Sender.DisambiguatedName }}</b> {{ .Message }}{% endraw %}"
m.file: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b> sent a file{{ if .Caption }}: {{ .Caption }}{{ end }}{% endraw %}"
m.image: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b> sent an image{{ if .Caption }}: {{ .Caption }}{{ end }}{% endraw %}"
m.audio: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b> sent an audio file{{ if .Caption }}: {{ .Caption }}{{ end }}{% endraw %}"
m.video: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b> sent a video{{ if .Caption }}: {{ .Caption }}{{ end }}{% endraw %}"
m.location: "{% raw %}<b>{{ .Sender.DisambiguatedName }}</b> sent a location{{ if .Caption }}: {{ .Caption }}{{ end }}{% endraw %}"
displayname_format: "{% raw %}{{ .DisambiguatedName }}{% endraw %}"
# Permissions for using the bridge.
permissions: {{ matrix_rustpush_bridge_bridge_permissions | to_json }}
# Config for the bridge's database.
database:
type: postgres
uri: {{ matrix_rustpush_bridge_database_uri | to_json }}
max_open_conns: 5
max_idle_conns: 1
max_conn_idle_time: null
max_conn_lifetime: null
# Homeserver details.
homeserver:
address: {{ matrix_rustpush_bridge_homeserver_address | to_json }}
domain: {{ matrix_rustpush_bridge_homeserver_domain | to_json }}
software: standard
status_endpoint:
message_send_checkpoint_endpoint:
async_media: {{ matrix_rustpush_bridge_homeserver_async_media | to_json }}
websocket: false
ping_interval_seconds: 0
# Application service host/registration related details.
appservice:
address: {{ matrix_rustpush_bridge_appservice_address | to_json }}
public_address: {{ matrix_rustpush_bridge_appservice_public_address | to_json }}
hostname: 0.0.0.0
port: 8081
id: rustpush-bridge
bot:
username: {{ matrix_rustpush_bridge_appservice_bot_username | to_json }}
displayname: {{ matrix_rustpush_bridge_appservice_bot_displayname | to_json(ensure_ascii=False) }}
avatar: {{ matrix_rustpush_bridge_appservice_bot_avatar | to_json }}
ephemeral_events: true
async_transactions: false
as_token: {{ matrix_rustpush_bridge_appservice_token | to_json }}
hs_token: {{ matrix_rustpush_bridge_homeserver_token | to_json }}
# Localpart template of MXIDs for remote users.
username_template: {{ matrix_rustpush_bridge_appservice_username_template | to_json }}
# Config options that affect the Matrix connector of the bridge.
matrix:
message_status_events: false
delivery_receipts: false
message_error_notices: true
sync_direct_chat_list: true
federate_rooms: {{ matrix_rustpush_bridge_matrix_federate_rooms | to_json }}
upload_file_threshold: 5242880
# Segment-compatible analytics endpoint for tracking some events.
analytics:
token: null
url: https://api.segment.io/v1/track
user_id: null
# Settings for provisioning API
provisioning:
prefix: /_matrix/provision
shared_secret: {{ matrix_rustpush_bridge_provisioning_shared_secret | to_json }}
allow_matrix_auth: true
debug_endpoints: false
# Settings for backfilling messages.
backfill:
enabled: {{ matrix_rustpush_bridge_backfill_enabled | to_json }}
max_initial_messages: {{ matrix_rustpush_bridge_backfill_max_initial_messages | to_json }}
max_catchup_messages: {{ matrix_rustpush_bridge_backfill_max_catchup_messages | to_json }}
unread_hours_threshold: 720
threads:
max_initial_messages: 50
queue:
enabled: false
batch_size: 100
batch_delay: 20
max_batches: -1
max_batches_override: {}
# Settings for enabling double puppeting
double_puppet:
servers: {}
allow_discovery: false
secrets: {{ matrix_rustpush_bridge_double_puppet_secrets | to_json }}
# End-to-bridge encryption support options.
encryption:
allow: {{ matrix_rustpush_bridge_bridge_encryption_allow | to_json }}
default: {{ matrix_rustpush_bridge_bridge_encryption_default | to_json }}
require: {{ matrix_rustpush_bridge_bridge_encryption_require | to_json }}
appservice: {{ matrix_rustpush_bridge_bridge_encryption_appservice | to_json }}
msc4190: {{ matrix_rustpush_bridge_msc4190_enabled | to_json }}
self_sign: {{ matrix_rustpush_bridge_self_sign_enabled | to_json }}
allow_key_sharing: {{ matrix_rustpush_bridge_bridge_encryption_key_sharing_allow | to_json }}
pickle_key: {{ matrix_rustpush_bridge_bridge_encryption_pickle_key | to_json }}
delete_keys:
delete_outbound_on_ack: false
dont_store_outbound: false
ratchet_on_decrypt: false
delete_fully_used_on_decrypt: false
delete_prev_on_new_session: false
delete_on_device_delete: false
periodically_delete_expired: false
delete_outdated_inbound: false
verification_levels:
receive: unverified
send: unverified
share: cross-signed-tofu
rotation:
enable_custom: false
milliseconds: 604800000
messages: 100
disable_device_change_key_rotation: false
# Logging config.
logging:
min_level: {{ matrix_rustpush_bridge_logging_level | to_json }}
writers:
- type: stdout
format: pretty-colored
@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason LaGuidice
SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,53 @@
{#
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason LaGuidice
SPDX-License-Identifier: AGPL-3.0-or-later
#}
{% if matrix_rustpush_bridge_container_labels_traefik_enabled %}
traefik.enable=true
{% if matrix_rustpush_bridge_container_labels_traefik_docker_network %}
traefik.docker.network={{ matrix_rustpush_bridge_container_labels_traefik_docker_network }}
{% endif %}
traefik.http.services.matrix-rustpush-bridge-metrics.loadbalancer.server.port=8000
{% if matrix_rustpush_bridge_container_labels_metrics_enabled %}
############################################################
# #
# Metrics #
# #
############################################################
{% if matrix_rustpush_bridge_container_labels_metrics_middleware_basic_auth_enabled %}
traefik.http.middlewares.matrix-rustpush-bridge-metrics-basic-auth.basicauth.users={{ matrix_rustpush_bridge_container_labels_metrics_middleware_basic_auth_users }}
traefik.http.routers.matrix-rustpush-bridge-metrics.middlewares=matrix-rustpush-bridge-metrics-basic-auth
{% endif %}
traefik.http.routers.matrix-rustpush-bridge-metrics.rule={{ matrix_rustpush_bridge_container_labels_metrics_traefik_rule }}
{% if matrix_rustpush_bridge_container_labels_metrics_traefik_priority | int > 0 %}
traefik.http.routers.matrix-rustpush-bridge-metrics.priority={{ matrix_rustpush_bridge_container_labels_metrics_traefik_priority }}
{% endif %}
traefik.http.routers.matrix-rustpush-bridge-metrics.service=matrix-rustpush-bridge-metrics
traefik.http.routers.matrix-rustpush-bridge-metrics.entrypoints={{ matrix_rustpush_bridge_container_labels_metrics_traefik_entrypoints }}
traefik.http.routers.matrix-rustpush-bridge-metrics.tls={{ matrix_rustpush_bridge_container_labels_metrics_traefik_tls | to_json }}
{% if matrix_rustpush_bridge_container_labels_metrics_traefik_tls %}
traefik.http.routers.matrix-rustpush-bridge-metrics.tls.certResolver={{ matrix_rustpush_bridge_container_labels_metrics_traefik_tls_certResolver }}
{% endif %}
############################################################
# #
# /Metrics #
# #
############################################################
{% endif %}
{% endif %}
{{ matrix_rustpush_bridge_container_labels_additional_labels }}
@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason LaGuidice
SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,51 @@
#jinja2: lstrip_blocks: True
[Unit]
Description=Matrix RustPush bridge
{% for service in matrix_rustpush_bridge_systemd_required_services_list %}
Requires={{ service }}
After={{ service }}
{% endfor %}
{% for service in matrix_rustpush_bridge_systemd_wanted_services_list %}
Wants={{ service }}
{% endfor %}
DefaultDependencies=no
[Service]
Type=simple
Environment="HOME={{ devture_systemd_docker_base_systemd_unit_home_path }}"
ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-rustpush-bridge 2>/dev/null || true'
ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-rustpush-bridge 2>/dev/null || true'
ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} create \
--rm \
--name=matrix-rustpush-bridge \
--log-driver=none \
--user={{ matrix_user_uid }}:{{ matrix_user_gid }} \
--cap-drop=ALL \
--network={{ matrix_rustpush_bridge_container_network }} \
--env HOME=/data \
{% if matrix_rustpush_bridge_rust_log %} --env RUST_LOG={{ matrix_rustpush_bridge_rust_log }} \
{% endif %} --mount type=bind,src={{ matrix_rustpush_bridge_config_path }},dst=/config \
--mount type=bind,src={{ matrix_rustpush_bridge_data_path }},dst=/data \
--label-file={{ matrix_rustpush_bridge_base_path }}/labels \
--entrypoint /usr/local/bin/matrix-rustpush \
{% for arg in matrix_rustpush_bridge_container_extra_arguments %}
{{ arg }} \
{% endfor %}
{{ matrix_rustpush_bridge_container_image }} \
-c /config/config.yaml -r /config/registration.yaml
{% for network in matrix_rustpush_bridge_container_additional_networks %}
ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} network connect {{ network }} matrix-rustpush-bridge
{% endfor %}
ExecStart={{ devture_systemd_docker_base_host_command_docker }} start --attach matrix-rustpush-bridge
ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-rustpush-bridge 2>/dev/null || true'
ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-rustpush-bridge 2>/dev/null || true'
Restart=always
RestartSec=30
SyslogIdentifier=matrix-rustpush-bridge
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason LaGuidice
SPDX-License-Identifier: AGPL-3.0-or-later