mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 00:42:20 +00:00
feat(push): add iOS APNs relay gateway (#43369)
* feat(push): add ios apns relay gateway * fix(shared): avoid oslog string concatenation # Conflicts: # apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayChannel.swift * fix(push): harden relay validation and invalidation * fix(push): persist app attest state before relay registration * fix(push): harden relay invalidation and url handling * feat(push): use scoped relay send grants * feat(push): configure ios relay through gateway config * feat(push): bind relay registration to gateway identity * fix(push): tighten ios relay trust flow * fix(push): bound APNs registration fields (#43369) (thanks @ngutman)
This commit is contained in:
@@ -2447,6 +2447,14 @@ See [Plugins](/tools/plugin).
|
||||
// Remove tools from the default HTTP deny list
|
||||
allow: ["gateway"],
|
||||
},
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
@@ -2472,6 +2480,11 @@ See [Plugins](/tools/plugin).
|
||||
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
|
||||
- `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`: client-side break-glass override that allows plaintext `ws://` to trusted private-network IPs; default remains loopback-only for plaintext.
|
||||
- `gateway.remote.token` / `.password` are remote-client credential fields. They do not configure gateway auth by themselves.
|
||||
- `gateway.push.apns.relay.baseUrl`: base HTTPS URL for the external APNs relay used by official/TestFlight iOS builds after they publish relay-backed registrations to the gateway. This URL must match the relay URL compiled into the iOS build.
|
||||
- `gateway.push.apns.relay.timeoutMs`: gateway-to-relay send timeout in milliseconds. Defaults to `10000`.
|
||||
- Relay-backed registrations are delegated to a specific gateway identity. The paired iOS app fetches `gateway.identity.get`, includes that identity in the relay registration, and forwards a registration-scoped send grant to the gateway. Another gateway cannot reuse that stored registration.
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` / `OPENCLAW_APNS_RELAY_TIMEOUT_MS`: temporary env overrides for the relay config above.
|
||||
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true`: development-only escape hatch for loopback HTTP relay URLs. Production relay URLs should stay on HTTPS.
|
||||
- Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
|
||||
|
||||
@@ -225,6 +225,63 @@ When validation fails:
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Enable relay-backed push for official iOS builds">
|
||||
Relay-backed push is configured in `openclaw.json`.
|
||||
|
||||
Set this in gateway config:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
// Optional. Default: 10000
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
CLI equivalent:
|
||||
|
||||
```bash
|
||||
openclaw config set gateway.push.apns.relay.baseUrl https://relay.example.com
|
||||
```
|
||||
|
||||
What this does:
|
||||
|
||||
- Lets the gateway send `push.test`, wake nudges, and reconnect wakes through the external relay.
|
||||
- Uses a registration-scoped send grant forwarded by the paired iOS app. The gateway does not need a deployment-wide relay token.
|
||||
- Binds each relay-backed registration to the gateway identity that the iOS app paired with, so another gateway cannot reuse the stored registration.
|
||||
- Keeps local/manual iOS builds on direct APNs. Relay-backed sends apply only to official distributed builds that registered through the relay.
|
||||
- Must match the relay base URL baked into the official/TestFlight iOS build, so registration and send traffic reach the same relay deployment.
|
||||
|
||||
End-to-end flow:
|
||||
|
||||
1. Install an official/TestFlight iOS build that was compiled with the same relay base URL.
|
||||
2. Configure `gateway.push.apns.relay.baseUrl` on the gateway.
|
||||
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
|
||||
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
|
||||
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
|
||||
|
||||
Operational notes:
|
||||
|
||||
- If you switch the iOS app to a different gateway, reconnect the app so it can publish a new relay registration bound to that gateway.
|
||||
- If you ship a new iOS build that points at a different relay deployment, the app refreshes its cached relay registration instead of reusing the old relay origin.
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
|
||||
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
|
||||
|
||||
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Set up heartbeat (periodic check-ins)">
|
||||
```json5
|
||||
{
|
||||
|
||||
@@ -49,6 +49,114 @@ openclaw nodes status
|
||||
openclaw gateway call node.list --params "{}"
|
||||
```
|
||||
|
||||
## Relay-backed push for official builds
|
||||
|
||||
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
|
||||
token to the gateway.
|
||||
|
||||
Gateway-side requirement:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
push: {
|
||||
apns: {
|
||||
relay: {
|
||||
baseUrl: "https://relay.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
How the flow works:
|
||||
|
||||
- The iOS app registers with the relay using App Attest and the app receipt.
|
||||
- The relay returns an opaque relay handle plus a registration-scoped send grant.
|
||||
- The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway.
|
||||
- The app forwards that relay-backed registration to the paired gateway with `push.apns.register`.
|
||||
- The gateway uses that stored relay handle for `push.test`, background wakes, and wake nudges.
|
||||
- The gateway relay base URL must match the relay URL baked into the official/TestFlight iOS build.
|
||||
- If the app later connects to a different gateway or a build with a different relay base URL, it refreshes the relay registration instead of reusing the old binding.
|
||||
|
||||
What the gateway does **not** need for this path:
|
||||
|
||||
- No deployment-wide relay token.
|
||||
- No direct APNs key for official/TestFlight relay-backed sends.
|
||||
|
||||
Expected operator flow:
|
||||
|
||||
1. Install the official/TestFlight iOS build.
|
||||
2. Set `gateway.push.apns.relay.baseUrl` on the gateway.
|
||||
3. Pair the app to the gateway and let it finish connecting.
|
||||
4. The app publishes `push.apns.register` automatically after it has an APNs token, the operator session is connected, and relay registration succeeds.
|
||||
5. After that, `push.test`, reconnect wakes, and wake nudges can use the stored relay-backed registration.
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
|
||||
|
||||
## Authentication and trust flow
|
||||
|
||||
The relay exists to enforce two constraints that direct APNs-on-gateway cannot provide for
|
||||
official iOS builds:
|
||||
|
||||
- Only genuine OpenClaw iOS builds distributed through Apple can use the hosted relay.
|
||||
- A gateway can send relay-backed pushes only for iOS devices that paired with that specific
|
||||
gateway.
|
||||
|
||||
Hop by hop:
|
||||
|
||||
1. `iOS app -> gateway`
|
||||
- The app first pairs with the gateway through the normal Gateway auth flow.
|
||||
- That gives the app an authenticated node session plus an authenticated operator session.
|
||||
- The operator session is used to call `gateway.identity.get`.
|
||||
|
||||
2. `iOS app -> relay`
|
||||
- The app calls the relay registration endpoints over HTTPS.
|
||||
- Registration includes App Attest proof plus the app receipt.
|
||||
- The relay validates the bundle ID, App Attest proof, and Apple receipt, and requires the
|
||||
official/production distribution path.
|
||||
- This is what blocks local Xcode/dev builds from using the hosted relay. A local build may be
|
||||
signed, but it does not satisfy the official Apple distribution proof the relay expects.
|
||||
|
||||
3. `gateway identity delegation`
|
||||
- Before relay registration, the app fetches the paired gateway identity from
|
||||
`gateway.identity.get`.
|
||||
- The app includes that gateway identity in the relay registration payload.
|
||||
- The relay returns a relay handle and a registration-scoped send grant that are delegated to
|
||||
that gateway identity.
|
||||
|
||||
4. `gateway -> relay`
|
||||
- The gateway stores the relay handle and send grant from `push.apns.register`.
|
||||
- On `push.test`, reconnect wakes, and wake nudges, the gateway signs the send request with its
|
||||
own device identity.
|
||||
- The relay verifies both the stored send grant and the gateway signature against the delegated
|
||||
gateway identity from registration.
|
||||
- Another gateway cannot reuse that stored registration, even if it somehow obtains the handle.
|
||||
|
||||
5. `relay -> APNs`
|
||||
- The relay owns the production APNs credentials and the raw APNs token for the official build.
|
||||
- The gateway never stores the raw APNs token for relay-backed official builds.
|
||||
- The relay sends the final push to APNs on behalf of the paired gateway.
|
||||
|
||||
Why this design was created:
|
||||
|
||||
- To keep production APNs credentials out of user gateways.
|
||||
- To avoid storing raw official-build APNs tokens on the gateway.
|
||||
- To allow hosted relay usage only for official/TestFlight OpenClaw builds.
|
||||
- To prevent one gateway from sending wake pushes to iOS devices owned by a different gateway.
|
||||
|
||||
Local/manual builds remain on direct APNs. If you are testing those builds without the relay, the
|
||||
gateway still needs direct APNs credentials:
|
||||
|
||||
```bash
|
||||
export OPENCLAW_APNS_TEAM_ID="TEAMID"
|
||||
export OPENCLAW_APNS_KEY_ID="KEYID"
|
||||
export OPENCLAW_APNS_PRIVATE_KEY_P8='-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----'
|
||||
```
|
||||
|
||||
## Discovery paths
|
||||
|
||||
### Bonjour (LAN)
|
||||
|
||||
Reference in New Issue
Block a user