Skip to content
Open
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions proposals/3079-low-bandwidth-csapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
# MSC3079: Low Bandwidth Client-Server API

The Client-Server API is not bandwidth efficient, relying on HTTP and human-readable JSON as a data
format. It is desirable to provide low bandwidth alternatives for mobile or low power devices. This
proposal specifies a CoAP/CBOR alternative to the existing HTTP/JSON API. This proposal
*will not replace* the existing HTTP/JSON APIs. Both protocols are expected to co-exist together.
This MSC has the potential to have a significant beneficial impact on mobile devices power
(and hence battery) consumption. It would also open up the possibility of the `/sync` API being used
in the background of mobile devices as a form of Matrix Push Notification, removing the need for
proprietary push systems such as GCM/APNS. Furthermore, it allows Matrix to run faster on
low bitrate links (e.g 1-5Kbps).

## Proposal

The current Client-Server API stack, along with the proposed alternatives are as follows:
- JSON -> CBOR
- HTTP -> CoAP
- HTTP long-polling -> CoAP OBSERVE (optional)
- TLS/TCP -> DTLS/UDP

The rest of this proposal breaks down each option and fleshes out the implementation/rationale.

### JSON -> CBOR

JSON is human-readable and not bandwidth efficient at all. It is extensible (you can add custom keys)
and this is a property which MUST be preserved. This makes static schema based data formats such as
Protocol Buffers inappropriate for use. CBOR is widely used and is standardised as
[RFC 8949: Concise Binary Object Representation](https://tools.ietf.org/html/rfc8949). Implementations
exist in [many languages](https://cbor.io/impls.html). Any valid JSON can be represented as CBOR with
[sensible representations](https://www.rfc-editor.org/rfc/rfc8949.html#name-converting-from-json-to-cbo).

This proposal goes beyond just "Use CBOR" however. This proposal also specifies integer keys which act
as shorthand for long string keys. This compresses data more efficiently. This shorthand may look similar
to:
```
{
"custom_key": "value",
8: 18572837543,
}
```
Where `8` is statically mapped to a string key e.g `origin_server_ts`. This means it is possible for
a key to be defined twice: once as a number and once as a string. If this happens, the string key
MUST be used. It is NOT required that keys which have integers are represented as such; the integer
mapping is entirely optional. String representations of numbers e.g `"8"` MUST NOT be expanded, and
should be treated literally. As JSON does not allow integer keys, this prevents any ambiguity when
converting from CBOR to JSON. The complete key enum list is in Appendix A and represents version 1
of the CBOR key mappings.

Clients MUST set the `Content-Type` header to `application/cbor` when sending CBOR objects.
Similarly, servers MUST set the `Content-Type` header to `application/cbor` when responding with CBOR
to requests. Servers MUST NOT respond to requests with `Content-Type: application/json` with CBOR,
unless the `Accept` header in the client's request includes `application/cbor`.

TODO: Define test objects to and from JSON

### HTTP -> CoAP

CoAP is defined in [RFC 7252: Contrained Application Protocol](https://tools.ietf.org/html/rfc7252)
and is intended to be bandwidth efficient. It defines mappings to and from HTTP. Implementations exist
in [many languages](https://coap.technology/impls.html). This allows the existing HTTP CS API to be mapped
to CoAP request/responses.

This proposal goes beyond just "Use CoAP" however. This proposal also specifies single character path
enums which act as shorthand for certain commonly used CS API paths. This compresses data more efficiently.
This shorthand may look similar to:
```
/9/!7mqP7DYBUOmwAweF:localhost/m.room.message/$.AAABeH6obLU
```
which is the collapsed form of:
```
/_matrix/client/r0/rooms/!7mqP7DYBUOmwAweF:localhost/send/m.room.message/$.AAABeH6obLU
```
Where `9` is statically mapped to the path regexp `/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`.
The path parameters in the mapping should be read left to right to determine where they should exist in the
shortened form. In this example, the short mapping is `/9/{roomId}/{eventType}/{txnId}`. Paths which do
not have any path parameters only have one path segment e.g `/_matrix/client/r0/sync` is just `/7`.
This path mapping is optional, and servers MUST accept the full paths. The complete path enum list is in
Appendix B and represents version 1 of the CoAP path mappings.

#### Access Tokens

CoAP defines many mappings but notably has no Option mapping for `Authorization` headers. This proposal marks
the option ID 256 as `Authorization` in accordance with the [CoAP Option Registry](https://tools.ietf.org/html/rfc7252#section-12.2)
which mandates that options IDs 0-255 are reserved for IETF review. This Option should have the same Critical,
UnSafe, NoCacheKey, Format and Length values as the `Uri-Query` Option.

In addition, this Option MAY be omitted on subsequent requests, in which case the server MUST use the last
used value for this Option. This reduces bandwidth usage by only sending the access token once per connection.
Upon establishing a new CoAP connection (determined by the underlying transport), the client MUST send the
Option again. If this Option is set on subsequent requests, servers MUST use the Option in the latest request
and discard the previously stored Option value. This makes this functionality optional from a client's perspective,
but required from a server's perspective.


### HTTP long-polling -> CoAP OBSERVE

*This feature is entirely optional. Clients can continue using HTTP long-polling with CoAP requests.*

The CS API currently relies on long-polling to retrieve new events from the server. This is bandwidth inefficient
as the background traffic is high when there are no events. This proposal allows the `/sync` endpoint to be observed
using [RFC 7641: CoAP OBSERVE](https://tools.ietf.org/html/rfc7641). The registration SHOULD be keyed off the tuple
of (access_token, coap token ID). This allows the same device to have multiple OBSERVE registrations. All pushed
events MUST be sent as Confirmable messages to ensure that the client received the events. The pushed events MUST
look identical to a normal `/sync` response, which includes the `next_batch` token. When the client disconnects and
reconnects with a new registration, they can use the `next_batch` token to consume where they left off. If a client
doesn't acknowledge an event despite multiple attempts to do so, the registration SHOULD be removed to avoid consuming
server resources and network bandwidth.

Clients MAY re-register with the `/sync` endpoint if they believe their registration was deleted e.g due to loss of
internet connectivity. Servers SHOULD retry sending events in the event of a re-register request, if the registration
is active but backing off. This ensures that servers proactively send events to the client in a timely manner.

Large `/sync` responses cannot be sent as a single message. In this case, RFC 7641 mandates that the client performs
a GET request to the resource (`/sync`) and performs blockwise transfers to retrieve the data. Servers MUST ensure
that the blockwise data returned from this request matches the data being pushed e.g by remembering which `/sync`
response was last pushed to the client.

### TLS/TCP -> DTLS/UDP

This proposal allows servers to listen on UDP ports for CoAP requests. It is recommended that the port listened
on matches the same TCP port for the same functionality, e.g port 8008/udp for the CS API. Management and configuration
of the DTLS connection is out of scope for this proposal.

CoAP requests MUST be supported on this UDP port.

### Versioning

In order to aid discovery, servers SHOULD add an extra key to `/_matrix/client/r0/versions` which indicates
which low bandwidth features are supported on this server. This object looks like:
```
"low_bandwidth": {
"dtls": 8008, // advertise that this server runs a UDP listener for DTLS here.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"dtls": 8008, // advertise that this server runs a UDP listener for DTLS here.
"dtls_port": 8008, // advertise that this server runs a UDP listener for DTLS here.

Also, couldn't it be possible here to specify a host+port for the endpoint seperately?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm debating if that information should live in .well-known as well... Not sure.

"cbor_enum_version": 1, // which table is used for integer keys. This proposal is version 1. Omission indicates no support.
"coap_enum_version": 1, // which table is used for coap path enums. This proposal is version 1. Omission indicates no support.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add enum shorthands for these fields as well?

Copy link
Copy Markdown
Member Author

@kegsay kegsay Apr 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's tricky, because if the client doesn't support the enums then they can't read the fields to determine the versions of things they may support e.g CoAP path enums. So probably not, though that would be a good reason to make these keys shorter!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the enum is backwards compatible and these values are defined in the first version, it should work I think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client could cache the response of this for longer time, if low-bandwidth is enabled and it isn't getting errors back from the server; so trying to save every single byte on the version response doesn't make much sense to soru

```

### Summary of required features

Many features in this proposal are optional. The fundamental features required in order for a server to declare itself
as "low-bandwidth" in the `/versions` response are:
- Accept `application/cbor`. CBOR integer keys are optional.
- Accept CoAP requests with access token Option stickiness. CoAP path enums are optional.
- Accept DTLS/UDP requests.

## Potential issues

Browsers are currently unsupported due to their inability to send UDP traffic.
[RFC 8323: CoAP over TCP, TLS, and WebSockets](https://tools.ietf.org/html/rfc8323) may provide a
way for browsers to participate over WebSockets but this is out of scope for this proposal.

## Alternatives

HTTP/3 over QUIC (which is UDP) was considered but rejected due to large initial connection sizes, which
mandate 1200 bytes of padding.

## Security considerations

DTLS connections should have the same security guarantees as TLS connections. Weak ciphers should not be used.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extremely vague, some examples or links to articles which give a better opinion on this would be appreciated.

Copy link
Copy Markdown
Contributor

@ShadowJonathan ShadowJonathan Dec 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recently came across the official IANA TLS Parameters register, which provides a section of cipher suites, which are then annotated with "DTLS Readiness" and "Recommendation", maybe referencing that, and/or providing one or two "must support" ciphers, could work.

Personally, for those "must support" ciphers, i'll recommend;

TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

But i'll also mention TLS_ECDHE_ECDSA_WITH_AES_128_CCM for it's ease of implementation and performance on embedded devices. Note however that it comes with a decreased security guarantee due to the use of CCM, which has been regarded as weak.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommended cipher suites change over time which is why I'm not naming any specific ones. I don't mind indirecting to say "if it's on this list then you should probably support it" but I'm wary about mandating any one cipher suite in this document as that is really not the place of this MSC.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not mandating, but giving implementation suggestions to start off with for interoperability, such as those specific cipher suites.

Though, maybe this is going too much into detail, I think there's not a lot of DTLS implementations out there to really have this matter all that much, if one could potentially know all of them at once at this point.


If a DTLS connection is hijacked, an attacker can send requests without the need for an access token.

OBSERVE could potentially send encrypted traffic to addresses which, in the worst case scenario, are attacker
controlled. Without the DTLS session keys this traffic should remain secure.


## Unstable prefix

The `/versions` response should look like this whilst the proposal is in review:
```json
{
"org.matrix.msc3079.low_bandwidth": {
"dtls": 8008,
"cbor_enum_version": 1,
"coap_enum_version": 1,
}
}
```

### Appendix A: CBOR integer keys

| Key Name | Integer |
|----------|---------|
|event_id| 1|
|type| 2|
|content| 3|
|state_key| 4|
|room_id| 5|
|sender| 6|
|user_id| 7|
|origin_server_ts| 8|
|unsigned| 9|
|prev_content| 10|
|state| 11|
|timeline| 12|
|events| 13|
|limited| 14|
|prev_batch| 15|
|transaction_id| 16|
|age| 17|
|redacted_because| 18|
|next_batch| 19|
|presence| 20|
|avatar_url| 21|
|account_data| 22|
|rooms| 23|
|join| 24|
|membership| 25|
|displayname| 26|
|body| 27|
|msgtype| 28|
|format| 29|
|formatted_body| 30|
|ephemeral| 31|
|invite_state| 32|
|leave| 33|
|third_party_invite| 34|
|is_direct| 35|
|hashes| 36|
|signatures| 37|
|depth| 38|
|prev_events| 39|
|prev_state| 40|
|auth_events| 41|
|origin| 42|
|creator| 43|
|join_rule| 44|
|history_visibility| 45|
|ban| 46|
|events_default| 47|
|kick| 48|
|redact| 49|
|state_default| 50|
|users| 51|
|users_default| 52|
|reason| 53|
|visibility| 54|
|room_alias_name| 55|
|name| 56|
|topic| 57|
|invite| 58|
|invite_3pid| 59|
|room_version| 60|
|creation_content| 61|
|initial_state| 62|
|preset| 63|
|servers| 64|
|identifier| 65|
|user| 66|
|medium| 67|
|address| 68|
|password| 69|
|token| 70|
|device_id| 71|
|initial_device_display_name| 72|
|access_token| 73|
|home_server| 74|
|well_known| 75|
|base_url| 76|
|device_lists| 77|
|to_device| 78|
|peek| 79|
|last_seen_ip| 80|
|display_name| 81|
|typing| 82|
|last_seen_ts| 83|
|algorithm| 84|
|sender_key| 85|
|session_id| 86|
|ciphertext| 87|
|one_time_keys| 88|
|timeout| 89|
|recent_rooms| 90|
|chunk| 91|
|m.fully_read| 92|
|device_keys| 93|
|failures| 94|
|device_display_name| 95|
|prev_sender| 96|
|replaces_state| 97|
|changed| 98|
|unstable_features| 99|
|versions| 100|
|devices| 101|
|errcode| 102|
|error| 103|
|room_alias| 104|

### Appendix B: CoAP Path Enums

| Path Enum | Matrix Path Regexp |
|-----------|--------------------|
|0| /_matrix/client/versions|
|1| /_matrix/client/r0/login|
|2| /_matrix/client/r0/capabilities|
|3| /_matrix/client/r0/logout|
|4| /_matrix/client/r0/register|
|5| /_matrix/client/r0/user/{userId}/filter|
|6| /_matrix/client/r0/user/{userId}/filter/{filterId}|
|7| /_matrix/client/r0/sync|
|8| /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}|
|9| /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}|
|A| /_matrix/client/r0/rooms/{roomId}/event/{eventId}|
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explicitly document which paths are reserved for this mode? For example is everything that isn't _matrix as the first component reserved? That seems a little broad. Is anything that doesn't start with _ reserved?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? I'm being explicit on a per path basis. Anything not in this table doesn't have an enum version. I see no benefit to saying "stuff under X may be enummed" without being more specific.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think you mean to be explicit on the enum path e.g for reverse proxies to let through paths according to a pattern or something? Not that reverse proxies would be able to read this..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is basically the idea. I think there will be a number of use cases including proxying, monitoring as well as just understandably to a causal reader. it would be useful to sort these into "full path" or "enummed path". Furthermore as we are reserving more enumed paths and adding more paths to the specification it would make sense to ensure that these sets don't ever collide without needing to explicitly check between the two. For example if there is a path /id I don't know if we have moved onto 2 letter enummed paths or just a short full path without reading the spec.

Additionally I assume that the /_matrix prefix was to allow the possibility of serving a homepage, .well-known and similar on the same HTTP host, it would be nice to know what set of paths an operator needs to keep free for these enummed paths.

|B| /_matrix/client/r0/rooms/{roomId}/state|
|C| /_matrix/client/r0/rooms/{roomId}/members|
|D| /_matrix/client/r0/rooms/{roomId}/joined_members|
|E| /_matrix/client/r0/rooms/{roomId}/messages|
|F| /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}|
|G| /_matrix/client/r0/createRoom|
|H| /_matrix/client/r0/directory/room/{roomAlias}|
|I| /_matrix/client/r0/joined_rooms|
|J| /_matrix/client/r0/rooms/{roomId}/invite|
|K| /_matrix/client/r0/rooms/{roomId}/join|
|L| /_matrix/client/r0/join/{roomIdOrAlias}|
|M| /_matrix/client/r0/rooms/{roomId}/leave|
|N| /_matrix/client/r0/rooms/{roomId}/forget|
|O| /_matrix/client/r0/rooms/{roomId}/kick|
|P| /_matrix/client/r0/rooms/{roomId}/ban|
|Q| /_matrix/client/r0/rooms/{roomId}/unban|
|R| /_matrix/client/r0/directory/list/room/{roomId}|
|S| /_matrix/client/r0/publicRooms|
|T| /_matrix/client/r0/user_directory/search|
|U| /_matrix/client/r0/profile/{userId}/displayname|
|V| /_matrix/client/r0/profile/{userId}/avatar_url|
|W| /_matrix/client/r0/profile/{userId}|
|X| /_matrix/client/r0/voip/turnServer|
|Y| /_matrix/client/r0/rooms/{roomId}/typing/{userId}|
|Z| /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}|
|a| /_matrix/client/r0/rooms/{roomId}/read_markers|
|b| /_matrix/client/r0/presence/{userId}/status|
|c| /_matrix/client/r0/sendToDevice/{eventType}/{txnId}|
|d| /_matrix/client/r0/devices|
|e| /_matrix/client/r0/devices/{deviceId}|
|f| /_matrix/client/r0/delete_devices|
|g| /_matrix/client/r0/keys/upload|
|h| /_matrix/client/r0/keys/query|
|i| /_matrix/client/r0/keys/claim|
|j| /_matrix/client/r0/keys/changes|
|k| /_matrix/client/r0/pushers|
|l| /_matrix/client/r0/pushers/set|
|m| /_matrix/client/r0/notifications|
|n| /_matrix/client/r0/pushrules/|
|o| /_matrix/client/r0/search|
|p| /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags|
|q| /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}|
|r| /_matrix/client/r0/user/{userId}/account_data/{type}|
|s| /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}|
|t| /_matrix/client/r0/rooms/{roomId}/context/{eventId}|
|u| /_matrix/client/r0/rooms/{roomId}/report/{eventId}|