Skip to content

Commit

Permalink
feat: filter RPC methods from daemon (#5254)
Browse files Browse the repository at this point in the history
  • Loading branch information
LesnyRumcajs authored Feb 12, 2025
1 parent 76a9ade commit e959888
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 125 deletions.
29 changes: 28 additions & 1 deletion .github/workflows/forest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Integration tests
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: '${{ github.ref != ''refs/heads/main'' }}'
'on':
on:
workflow_dispatch:
merge_group:
pull_request:
Expand Down Expand Up @@ -185,6 +185,32 @@ jobs:
chmod +x ~/.cargo/bin/forest*
- run: ./scripts/tests/calibnet_stateless_mode_check.sh
timeout-minutes: '${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}'
calibnet-stateless-rpc-check:
needs:
- build-ubuntu
name: Calibnet stateless RPC check
runs-on: ubuntu-24.04
steps:
- run: lscpu
- uses: actions/cache@v4
with:
path: '${{ env.FIL_PROOFS_PARAMETER_CACHE }}'
key: proof-params-keys
- name: Checkout Sources
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: 'forest-${{ runner.os }}'
path: ~/.cargo/bin
- uses: actions/download-artifact@v4
with:
name: 'forest-${{ runner.os }}'
path: ~/.cargo/bin
- name: Set permissions
run: |
chmod +x ~/.cargo/bin/forest*
- run: ./scripts/tests/calibnet_stateless_rpc_check.sh
timeout-minutes: '${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}'
state-migrations-check:
needs:
- build-ubuntu
Expand Down Expand Up @@ -558,6 +584,7 @@ jobs:
- forest-cli-check
- calibnet-check
- calibnet-stateless-mode-check
- calibnet-stateless-rpc-check
- state-migrations-check
- calibnet-wallet-check
- calibnet-export-check
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@

- [#4769](https://github.com/ChainSafe/forest/issues/4769) Add delegated address support to `forest-wallet new` command.

- [#5147](https://github.com/ChainSafe/forest/issues/5147) Add support for the `--rpc-filter-list` flag to the `forest` daemon. This flag allows users to specify a list of RPC methods to whitelist or blacklist.

- [#4709](https://github.com/ChainSafe/forest/issues/4709) Add support for `Filecoin.EthTraceReplayBlockTransactions` RPC method.

### Changed
Expand Down
1 change: 1 addition & 0 deletions docs/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ M2
macOS
Mainnet
mainnet
namespace
NV22
NV23
NV24
Expand Down
108 changes: 108 additions & 0 deletions docs/docs/users/guides/methods_filtering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
title: RPC methods filtering
---

# RPC methods filtering

## Why filter RPC methods?

When running a Filecoin node, you might want to restrict the RPC methods that are available to the clients. This can be useful for security reasons, to limit the exposure of the node to the internet, or to reduce the load on the node by disabling unnecessary methods.

:::note
[JWT authentication](../knowledge_base/jwt_handling.md) is a different way to restrict access to the node. It allows you to authorize certain operations on the node using JWTs. However, JWT restrictions are hard-coded in the node and cannot be changed dynamically. If you want to make sure that a certain read-only method is not available to the clients, you can use the method filtering feature.

The methods are first filtered by the method filtering feature, and then the JWT authentication is applied. If a method is disallowed by the method filtering, the JWT token will not be checked for this method.
:::

## How to filter RPC methods

You need to run `forest` with the `--rpc-filter-list <PATH-TO-FILTER-LIST>` argument. If the filter list is not provided, all methods are allowed by default.

### Example

In this example, will disallow the `Filecoin.ChainExport` method which is used to export the chain to a file. This method should not be available to the clients due to its impact (compute, disk space, etc.) on the node.

1. Create a filter list file, for example, `filter-list.txt`:

```plaintext
# Disabling the snapshot exporting
!Filecoin.ChainExport
```

2. Run `forest` with the `--rpc-filter-list` argument:

```shell
forest --chain calibnet --encrypt-keystore false --rpc-filter-list filter-list.txt
```

3. Try to export the snapshot using the `forest-cli`:

```shell
forest-cli snapshot-export
```

You should see the following error:

```console
Getting ready to export...
Error: ErrorObject { code: ServerError(403), message: "Forbidden", data: None }

Caused by:
ErrorObject { code: ServerError(403), message: "Forbidden", data: None }
```

## Filter list format

The filter list is a text file where each line represents a method that should be allowed or disallowed. The format is as follows:

- `!` at the beginning of the line means that the method is disallowed.
- `#` at the beginning of the line is a comment and is ignored.
- no prefix means that all the methods containing this name are allowed.

If there is a single allowed method (no prefix), all non-matching methods are disallowed by default.

:::warning
Some methods have aliases, so you need to filter all of them. This is most prominent in the `Filecoin.Eth.*` namespace. They are implemented for compatibility with Lotus, see [here](https://github.com/filecoin-project/lotus/blob/a9718c841e1fced8afc6e9fee2db2a2b565acc42/api/eth_aliases.go).
:::

## Example filter lists

Allow only the `Filecoin.StateCall` method. All other methods are disallowed:

```plaintext
Filecoin.StateCall
```

Disallow the `Filecoin.ChainExport` method. All other methods are allowed:

```plaintext
!Filecoin.ChainExport
```

Disallow the `Filecoin.EthGasPrice`, `Filecoin.EthEstimateGas`, and their aliases. All other methods are allowed:

```plaintext
!Filecoin.EthGasPrice
!eth_gasPrice
!Filecoin.EthEstimateGas
!eth_estimateGas
```

Allow all the methods in the `Filecoin.Chain` namespace. Disallow the `Filecoin.ChainExport` method. This will allow methods such as `Filecoin.ChainGetTipSet` and `Filecoin.ChainGetBlock` but disallow the `Filecoin.ChainExport` method:

```plaintext
Filecoin.Chain
!Filecoin.ChainExport
```

## Public RPC node recommendations

If you are running a public RPC node, it is recommended to filter certain methods (even those not requiring a JWT token) to reduce the load on the node and to improve security. Here is a list of methods that you might want to consider filtering:

```plaintext
# Creates a snapshot of the chain and writes it to a file. Very resource-intensive.
!Filecoin.ChainExport
# Potentially resource-intensive.
!Filecoin.EthCall
!eth_call
```
72 changes: 72 additions & 0 deletions scripts/tests/calibnet_stateless_rpc_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash
set -euxo pipefail

# This script tests RPC on a stateless node. This is done to avoid downloading the snapshot for this test and speed up the CI.

source "$(dirname "$0")/harness.sh"

# Run a stateless node with a filter list as an argument.
function forest_run_node_stateless_detached_with_filter_list {
pkill -9 forest || true
local filter_list=$1

$FOREST_PATH --detach --chain calibnet --encrypt-keystore false --log-dir "$LOG_DIRECTORY" --save-token ./admin_token --skip-load-actors --stateless --rpc-filter-list "$filter_list"

ADMIN_TOKEN=$(cat admin_token)
FULLNODE_API_INFO="$ADMIN_TOKEN:/ip4/127.0.0.1/tcp/2345/http"

export ADMIN_TOKEN
export FULLNODE_API_INFO
}

# Tests the RPC method `Filecoin.ChainHead` and checks if the status code matches the expected code.
function test_rpc {
local expected_code=$1

# Test the RPC, get status code
status_code=$(curl --silent -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","id":2,"method":"Filecoin.ChainHead","params": [ ] }' \
"http://127.0.0.1:2345/rpc/v1" | jq '.error.code')

# check if the expected code is returned
if [ "$status_code" != "$expected_code" ]; then
echo "Expected status code $expected_code, got $status_code"
exit 1
fi
}

# No filter list - all RPCs are allowed. This is the default behavior.

cat <<- EOF > "$TMP_DIR"/filter-list
# Cthulhu fhtagn
EOF

forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list"
test_rpc null # null means there is no error

# Filter list with the `ChainHead` RPC disallowed. Should return 403.

cat <<- EOF > "$TMP_DIR"/filter-list
!Filecoin.ChainHead
EOF

forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list"
test_rpc 403

# Filter list with a single other RPC allowed. `ChainHead` should be disallowed and return 403.
# Note - this method is required for the test harness.
cat <<- EOF > "$TMP_DIR"/filter-list
Filecoin.Shutdown
EOF

forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list"
test_rpc 403

# Filter list with a single other RPC allowed, along with `ChainHead`. Should succeed.
cat <<- EOF > "$TMP_DIR"/filter-list
Filecoin.Shutdown
Filecoin.ChainHead
EOF

forest_run_node_stateless_detached_with_filter_list "$TMP_DIR/filter-list"
test_rpc null # null means there is no error
4 changes: 4 additions & 0 deletions src/cli_shared/cli/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub struct Client {
pub metrics_address: SocketAddr,
/// RPC bind, e.g. 127.0.0.1:1234
pub rpc_address: SocketAddr,
/// Path to a list of RPC methods to allow/disallow.
pub rpc_filter_list: Option<PathBuf>,
/// Healthcheck bind, e.g. 127.0.0.1:2346
pub healthcheck_address: SocketAddr,
/// Load actors from the bundle file (possibly generating it if it doesn't exist)
pub load_actors: bool,
Expand All @@ -87,6 +90,7 @@ impl Default for Client {
encrypt_keystore: true,
metrics_address: FromStr::from_str("0.0.0.0:6116").unwrap(),
rpc_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), crate::rpc::DEFAULT_PORT),
rpc_filter_list: None,
healthcheck_address: SocketAddr::new(
IpAddr::V4(Ipv4Addr::LOCALHOST),
crate::health::DEFAULT_HEALTHCHECK_PORT,
Expand Down
4 changes: 4 additions & 0 deletions src/cli_shared/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub struct CliOpts {
/// Address used for RPC. By defaults binds on localhost on port 2345.
#[arg(long)]
pub rpc_address: Option<SocketAddr>,
/// Path to a list of RPC methods to allow/disallow.
#[arg(long)]
pub rpc_filter_list: Option<PathBuf>,
/// Disable healthcheck endpoints
#[arg(long)]
pub no_healthcheck: bool,
Expand Down Expand Up @@ -165,6 +168,7 @@ impl CliOpts {
}
if self.rpc.unwrap_or(cfg.client.enable_rpc) {
cfg.client.enable_rpc = true;
cfg.client.rpc_filter_list = self.rpc_filter_list.clone();
if let Some(rpc_address) = self.rpc_address {
cfg.client.rpc_address = rpc_address;
}
Expand Down
7 changes: 7 additions & 0 deletions src/daemon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ pub(super) async fn start(
let keystore_rpc = Arc::clone(&keystore);
let rpc_state_manager = Arc::clone(&state_manager);
let rpc_address = config.client.rpc_address;
let filter_list = config
.client
.rpc_filter_list
.as_ref()
.map(|path| crate::rpc::FilterList::new_from_file(path))
.transpose()?;

info!("JSON-RPC endpoint will listen at {rpc_address}");

Expand All @@ -418,6 +424,7 @@ pub(super) async fn start(
tipset_send: tipset_sender,
},
rpc_address,
filter_list,
)
.await
});
Expand Down
71 changes: 71 additions & 0 deletions src/rpc/filter_layer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2019-2025 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use std::sync::Arc;

use futures::future::BoxFuture;
use futures::FutureExt;
use jsonrpsee::server::middleware::rpc::RpcServiceT;
use jsonrpsee::types::ErrorObject;
use jsonrpsee::MethodResponse;
use tower::Layer;

use super::FilterList;

/// JSON-RPC middleware layer for filtering RPC methods based on their name.
#[derive(Clone, Default)]
pub(super) struct FilterLayer {
filter_list: Arc<FilterList>,
}

impl FilterLayer {
pub fn new(filter_list: FilterList) -> Self {
Self {
filter_list: Arc::new(filter_list),
}
}
}

impl<S> Layer<S> for FilterLayer {
type Service = Filtering<S>;

fn layer(&self, service: S) -> Self::Service {
Filtering {
service,
filter_list: self.filter_list.clone(),
}
}
}

#[derive(Clone)]
pub(super) struct Filtering<S> {
service: S,
filter_list: Arc<FilterList>,
}

impl<'a, S> RpcServiceT<'a> for Filtering<S>
where
S: RpcServiceT<'a> + Send + Sync + Clone + 'static,
{
type Future = BoxFuture<'a, MethodResponse>;

fn call(&self, req: jsonrpsee::types::Request<'a>) -> Self::Future {
let service = self.service.clone();
let authorized = self.filter_list.authorize(req.method_name());
async move {
if authorized {
service.call(req).await
} else {
MethodResponse::error(
req.id(),
ErrorObject::borrowed(
http::StatusCode::FORBIDDEN.as_u16() as _,
"Forbidden",
None,
),
)
}
}
.boxed()
}
}
Loading

0 comments on commit e959888

Please sign in to comment.