WebSockets enable web applications to maintain a full-duplex connection between the backend and the frontend. This allows for many different use-cases, such as notifications, dynamic content updates (e.g., showing new comments/likes on a post), collaborative editing, etc.
At the moment, the Internet Computer does not natively support WebSocket connections and developers need to resort to work-arounds in the frontend to enable a similar functionality. This results in a poor developer experience and an overload of the backend canister.
This repository contains the implementation of a WebSocket Gateway enabling clients to establish a full-duplex connection to their backend canister on the Internet Computer via the WebSocket API.
Make sure you have the Rust toolchain installed. You can find instructions here.
-
Run the gateway:
In debug mode:
cargo run
In release mode:
cargo build --release ./target/release/ic_websocket_gateway
-
The output in the terminal should look like (some lines are omitted for brevity):
... 2024-03-14T11:19:33.649874Z INFO ic_websocket_gateway: Gateway Agent principal: lk7eq-k74k2-khrsw-n2xau-7ujb2-f5ao7-zkpqn-cbbx2-4xn3f-aibn4-uae ... 2024-03-14T11:19:33.650018Z INFO ic_websocket_gateway::manager: Start accepting incoming connections
There are some command line arguments that you can set when running the gateway:
Argument | Description | Default |
---|---|---|
--gateway-address |
The IP:port on which the gateway will listen for incoming connections. | 0.0.0.0:8080 |
--ic-network-url |
The URL of the IC network to which the gateway will connect. | http://127.0.0.1:4943 |
--polling-interval |
The interval (in milliseconds) at which the gateway will poll the canisters for new messages. | 100 |
--prometheus-endpoint |
The IP:port on which the gateway will expose Prometheus metrics. | 0.0.0.0:9090 |
--tls-certificate-pem-path |
The path to the TLS certificate file. See Obtain a TLS certificate for more details. | empty |
--tls-certificate-key-pem-path |
The path to the TLS private key file. See Obtain a TLS certificate for more details. | empty |
--opentelemetry-collector-endpoint |
OpenTelemetry collector endpoint. See Tracing telemetry for more details. | empty |
Make sure you have Docker installed.
A Dockerfile is provided. To build the image, run:
docker build -t ic-websocket-gateway .
Then, run the gateway with the following command (assuming you want to connect to the local dfx replica running on the default port):
docker run -p 8080:8080 ic-websocket-gateway --ic-network-url http://host.docker.internal:4943
A Docker image is also available at omniadevs/ic-websocket-gateway, that you can run with the following command:
docker run -p 8080:8080 omniadevs/ic-websocket-gateway --ic-network-url http://host.docker.internal:4943
Note: if you're on an ARM machine, you have to add the
--platform
flag to the command above, since the published image is built only for thex86_64
architecture:docker run --platform linux/amd64 -p 8080:8080 omniadevs/ic-websocket-gateway --ic-network-url http://host.docker.internal:4943
Have a look at the Arguments available to configure the gateway for your needs.
Make sure you have Docker Compose installed.
The following compose files are available:
The following sections describe how to use the different compose files to run the gateway with Docker Compose.
A visual representation of the containers in the compose files is provided in the docker-compose-structure.png image.
To run the gateway in a local environment with Docker Compose, follow these steps:
-
To run all the required local structure you can execute the start_local_docker_environment.sh with the command:
./scripts/start_local_docker_environment.sh
This script simply run a dfx local replica and execute the gateway with the following command:
docker compose -f docker-compose.yml -f docker-compose-local.yml --env-file .env.local up -d --build
-
To stop and clean all the local environment, the bash script stop_local_docker_environment.sh is provided. You can execute it with the command:
./scripts/stop_local_docker_environment.sh
-
The Gateway will print its principal in the container logs, just as explained in the Standalone section.
-
If you want to verify that everything started correctly, the bash script run_test_canister.sh is provided. This script assumes that the gateway is already running and reachable locally. You can execute it with the command:
./scripts/run_test_canister.sh
This configuration uses the omniadevs/ic-websocket-gateway image.
To run the gateway in a production environment with Docker Compose, follow these steps:
-
Set the environment variables:
cp .env.example .env
-
To run the docker-compose-prod.yml file you need a public domain (that you will put in the
DOMAIN_NAME
environment variable) and a TLS certificate for that domain (because it is configured to make the gateway run with TLS enabled). See Obtain a TLS certificate for more details. -
Open the
443
port (or the port that you set in theLISTEN_PORT
environment variable) on your server and make it reachable from the Internet. -
To run all the required production containers you can execute the start_prod_docker_environment.sh with the command:
./scripts/start_prod_docker_environment.sh
This script first generates the
telemetry/prometheus/prometheus-prod.yml
config file from thetelemetry/prometheus/prometheus-template.yml
template (step required to perform the environment variables substitution) and then runs the gateway with the following command:docker compose -f docker-compose.yml -f docker-compose-prod.yml --env-file .env up -d
-
To stop and clean the containers, the bash script stop_prod_docker_environment.sh is provided. You can execute it with the command:
./scripts/stop_prod_docker_environment.sh
-
The Gateway will print its principal in the container logs, just as explained in the Standalone section.
- Buy a domain name and point it to the server where you are running the gateway.
- Make sure the
.env
file is configured with the correct domain name, see above. - Obtain an SSL certificate for your domain:
This will guide you in obtaining a certificate using Certbot in Standalone mode.
./scripts/certbot_certonly.sh
Make sure you have port
80
open on your server and reachable from the Internet, otherwise certbot will not be able to verify your domain. Port80
is used only for the certificate generation and can be closed afterwards.
To renew the SSL certificate, you can run the same command as above:
./scripts/certbot_certonly.sh
The gateway uses the tracing crate for logging. There are two tracing outputs configured:
- output to stdout, which has the
info
level and can be configured with theRUST_LOG_STDOUT
env variable, see below; - output to a file, which is saved in the
data/traces/
folder and has the defaulttrace
level. The file name isgateway_{start-timestamp}.log
. It can be configured with theRUST_LOG_FILE
env variable, see below.
The RUST_LOG
environment variable enables to set different levels for each module. See the EnvFilter documentation for more details.
For example, to set the tracing level to debug
, you can run:
RUST_LOG_FILE=ic_websocket_gateway=debug RUST_LOG_STDOUT=ic_websocket_gateway=debug cargo run
The gateway uses the opentelemetry crate and Grafana for tracing telemetry. To enable tracing telemetry, you have to:
- set the
--opentelemetry-collector-endpoint
argument to point to the opentelemetry collector endpoint (leaving it empty or unset will disable tracing telemetry); - optionally set the
RUST_LOG_TELEMETRY
environment variable, which defaults totrace
, following the same principles described in the Configure logging section.
If you're deploying the gateway with Docker (see the Docker section), make sure you set the following varibales in the .env
file:
Local:
OPENTELEMETRY_COLLECTOR_ENDPOINT=grpc://otlp_collector:4317
GRAFANA_TEMPO_ENDPOINT=tempo:4318
GRAFANA_TEMPO_LOCAL=true
Production:
OPENTELEMETRY_COLLECTOR_ENDPOINT=grpc://otlp_collector:4317
GRAFANA_TEMPO_ENDPOINT=your-grafana-cloud-tempo-endpoint
GRAFANA_TEMPO_ACCESS_TOKEN=your-grafana-cloud-tempo-basic-auth-token
GRAFANA_TEMPO_LOCAL=false
You can find the Tempo endpoint and create a token, by following this guide.
For more information about how to configure the env variables properly, checkout the .env.example.
Some unit tests are provided in the tests folder. You can run them with:
./scripts/unit_test.sh
Integration tests use the IC WebSocket SDKs and are written in both Rust and Motoko. They require:
- Node.js (version 16 or higher)
- dfx, with which to run an IC local replica
- a test canister deployed on the local replica
After installing Node.js and dfx, you can run the integration tests as follows:
-
Prepare the test environment by installing dependencies and building the components by running the following command:
./scripts/prepare_integration_tests.sh
-
Set the environment variables:
cp tests/.env.example tests/.env
When running the tests, the
tests/.env
file is modified by dfx, which will add some variables. -
Run integration tests using the Rust test canister:
./scripts/integration_test_rs.sh
If you instead want to run tests using the Motoko test canister, run the following command instead:
./scripts/integration_test_mo.sh
Integration tests can be found in the tests/src/integration folder.
Tests canisters used in the integration tests can be found in the tests/src/test_canister_rs and tests/src/test_canister_mo folders.
After setting up and running the tests for the first time following the steps above, you can use the following command to run the unit and integration tests (using Rust test canister) together:
./scripts/local_test.sh
Load tests are provided in the tests/src/load folder. You can run them with:
./scripts/run_load_test.sh
This script requires you to set up the test environment manually, because you usually want to keep an eye on the logs of the different components. You have to start the local replica, start the gateway and deploy the test canister. The scripts/integration_test_rs.sh is a good reference for how to do that.
In order to enable WebSockets for a dapp running on the IC, we use a trustless intermediary - the WS Gateway - which provides a WebSocket endpoint for the frontend of the dapp, running in the user’s browser and interacts with the canister backend.
The WS Gateway is needed as a WebSocket is a one-to-one connection between client and server, but the Internet Computer does not support that due to its replicated nature. The WS Gateway relays all messages coming in via the WebSocket from the client as API canister calls for the backend and sends each message polled from the backend via the WebSocket to the corresponding client.
-
General: The WS Gateway can provide a WebSocket interface for many different dapps at the same time. Therefore, many clients can receive updates from the same or different canisters.
-
Trustless:
- all messages are signed: messages sent by the canister are certified; messages sent by the client signed using an Internet Identity either provided by the user or generated by the IC WebSocket Frontend SDK. This way, the WS Gateway cannot tamper the content of the messages;
- all messages have a sequence number to guarantee all messages are received in the correct order;
- all messages are accompanied by a timestamp to prevent the WS Gateway from delaying them;
- all messages are acknowledged by the IC WebSocket Backend CDK so that the WS Gateway cannot block them;
- the IC WebSocket Backend CDK expects keep alive messages from each connected client so that it can detect if a client is not connected anymore;
-
IMPORTANT CAVEAT: NO ENCRYPTION! No single replica can be assumed to be trusted, so the canister state cannot be assumed to be kept secret. When exchanging messages with the canister, keep in mind that in principle the messages could be seen by others on the gateway and canister side. This will be solved in the next version of IC WebSocket using VetKeys.
-
Client:
Client uses the IC WebSocket Frontend SDK to establish the IC WebSocket connection mediated by the WS Gateway in order to communicate with the backend canister using the WebSocket API. When instantiating a new IC WebSocket connection, the client can pass an identity to the SDK in order to authenticate its messages to the canister.
The client can instantiate a new IC WebSocket connection by calling the
IcWebSocket
constructor.When the client calls the
send
method of the SDK, the SDK creates a signed envelope with the content specified by the client and signed with its identity. The envelope is sent to the WS Gateway via WebSocket and specifies thews_open
method of the canister.Once receiving a message from the WS Gateway via WebSocket, the SDK validates the messages by verifying the certificate provided using the public key of the Internet Computer.
-
WS Gateway:
WS Gateway accepts WebSocket connections with multiple clients in order to relay their messages to and from the canister.
Upon receiving a signed envelope from a client, the WS Gateway relays the message to the
/canister/<canister_id>/call
endpoint of the Internet Computer. This way, the WS Gateway is transparent to the canister, which receives the request as if sent directly by the client which signed it.In order to get updates from the canister, the WS Gateway polls the caniser by sending periodic queries to the
ws_get_messages
method. Upon receiving a response to a query, the WS Gateway relays the contained message and certificate to the corresponding client using the WebSocket. -
Backend canister:
The backend canister uses the IC WebSocket Backend CDK which exposes the methods of a typical WebSocket server (
ws_open
,ws_message
,ws_error
,ws_close
) plus thews_get_messages
method polled by the WS Gateway. The backend canister must specify the callback functions which should be executed on different WebSocket events (open
,message
,error
,close
). The CDK triggers the corresponding callback upon receiving a request on one of the WebSocket methods.The CDK also implements the logic necessary to detect a possible WS Gateway misbehaviour.
In order to send an update to one of its clients, the canister calls the
ws_send
method of the CDK specifying the message that it wants to be delivered to the client. The CDK pushes this message in a FIFO queue together with all the other clients' messages. Upon receiving a query call to thews_get_messages
method, the CDK returns the messages in this queue (up to a certain limit), together with a certificate which proves to the clients that the messages are actually the ones sent from the canister even if relayed by the WS Gateway.
For more information on the messages exchanged between client, WS Gateway and canister, checkout the IC WebSocket Protocol.
MIT License. See LICENSE.
Feel free to open issues, pull requests, join our Discord and reach out to us!