Skip to main content

linera_rpc/grpc/
mod.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4mod client;
5mod conversions;
6mod node_provider;
7/// A pool of reusable gRPC transport channels.
8pub mod pool;
9#[cfg(with_server)]
10mod server;
11/// Transport-level configuration and channel construction for gRPC.
12pub mod transport;
13
14pub use client::*;
15pub use conversions::*;
16pub use node_provider::*;
17#[cfg(with_server)]
18pub use server::*;
19
20/// The gRPC service and message types generated from `proto/rpc.proto`.
21pub mod api {
22    // Generated gRPC bindings; the generated items cannot carry doc comments.
23    #![allow(missing_docs)]
24    tonic::include_proto!("rpc.v1");
25}
26
27#[derive(thiserror::Error, Debug)]
28#[allow(missing_docs)]
29pub enum GrpcError {
30    #[error("failed to connect to address: {0}")]
31    ConnectionFailed(#[from] transport::Error),
32
33    #[error("failed to execute task to completion: {0}")]
34    Join(#[from] futures::channel::oneshot::Canceled),
35
36    #[error("failed to parse socket address: {0}")]
37    SocketAddr(#[from] std::net::AddrParseError),
38
39    #[cfg(with_server)]
40    #[error(transparent)]
41    Reflection(#[from] tonic_reflection::server::Error),
42}
43
44const MEBIBYTE: usize = 1024 * 1024;
45/// The maximum gRPC message size, in bytes.
46pub const GRPC_MAX_MESSAGE_SIZE: usize = 16 * MEBIBYTE;
47
48/// Limit of gRPC message size up to which we will try to populate with data when estimating.
49/// We leave 30% of buffer for the rest of the message and potential underestimation.
50pub const GRPC_CHUNKED_MESSAGE_FILL_LIMIT: usize = GRPC_MAX_MESSAGE_SIZE * 7 / 10;
51
52/// Prometheus label for the gRPC method name.
53pub const METHOD_NAME_LABEL: &str = "method_name";
54
55/// Prometheus label for distinguishing organic vs synthetic (benchmark) traffic.
56pub const TRAFFIC_TYPE_LABEL: &str = "traffic_type";
57
58/// Prometheus label for the error variant name, e.g. `"WorkerError::UnexpectedBlockHeight"`.
59pub const ERROR_TYPE_LABEL: &str = "error_type";
60
61/// Maximum length of a single proto identifier accepted as a service or method name.
62/// Real proto identifiers are far shorter than this; the cap guards against attacker
63/// input being recorded verbatim as a Prometheus label value.
64const MAX_PROTO_IDENT_LEN: usize = 128;
65
66/// Returns `true` if `s` is a valid protobuf identifier (`[A-Za-z][A-Za-z0-9_]*`)
67/// within the length cap.
68fn is_proto_identifier(s: &str) -> bool {
69    if s.is_empty() || s.len() > MAX_PROTO_IDENT_LEN {
70        return false;
71    }
72    let mut bytes = s.bytes();
73    let first = bytes.next().expect("non-empty checked above");
74    first.is_ascii_alphabetic() && bytes.all(|b| b.is_ascii_alphanumeric() || b == b'_')
75}
76
77/// Returns `true` if `s` is a fully-qualified proto Service-Name: two or more
78/// proto identifiers joined by dots, e.g. `package.Service` or `outer.inner.Service`.
79fn is_proto_service_name(s: &str) -> bool {
80    let mut parts = s.split('.');
81    let Some(first) = parts.next() else {
82        return false;
83    };
84    let Some(second) = parts.next() else {
85        return false;
86    };
87    is_proto_identifier(first) && is_proto_identifier(second) && parts.all(is_proto_identifier)
88}
89
90/// Extracts the gRPC method name from a request URI path.
91///
92/// gRPC paths follow the HTTP/2 form `"/" Service-Name "/" {method name}`, where
93/// `Service-Name` is `{proto package} "." {service name}` and the method name is a
94/// single proto identifier. Anything that does not match this exact shape — health
95/// probes, browser requests, bot scans probing for `.env` files, etc. — is mapped
96/// to `"non_grpc"` so the value is safe to use as a Prometheus label without
97/// blowing up cardinality or label-value length.
98pub fn extract_grpc_method_name(path: &str) -> &str {
99    let mut parts = path.splitn(3, '/');
100    let (Some(""), Some(service), Some(method)) = (parts.next(), parts.next(), parts.next()) else {
101        return "non_grpc";
102    };
103    if is_proto_service_name(service) && is_proto_identifier(method) {
104        method
105    } else {
106        "non_grpc"
107    }
108}
109
110#[cfg(test)]
111mod method_name_tests {
112    use super::*;
113
114    #[test]
115    fn grpc_unary_method() {
116        assert_eq!(
117            extract_grpc_method_name("/rpc.v1.ValidatorNode/HandleBlockProposal"),
118            "HandleBlockProposal"
119        );
120    }
121
122    #[test]
123    fn grpc_streaming_method() {
124        assert_eq!(
125            extract_grpc_method_name("/rpc.v1.ValidatorNode/SubscribeToNotifications"),
126            "SubscribeToNotifications"
127        );
128    }
129
130    #[test]
131    fn health_check_path() {
132        assert_eq!(
133            extract_grpc_method_name("/grpc.health.v1.Health/Check"),
134            "Check"
135        );
136    }
137
138    #[test]
139    fn non_grpc_root_path() {
140        assert_eq!(extract_grpc_method_name("/"), "non_grpc");
141    }
142
143    #[test]
144    fn non_grpc_plain_path() {
145        assert_eq!(extract_grpc_method_name("/healthz"), "non_grpc");
146    }
147
148    #[test]
149    fn non_grpc_no_dot_in_service() {
150        assert_eq!(extract_grpc_method_name("/NoDotService/Method"), "non_grpc");
151    }
152
153    #[test]
154    fn empty_path() {
155        assert_eq!(extract_grpc_method_name(""), "non_grpc");
156    }
157
158    #[test]
159    fn dot_env_bot_scan_does_not_leak_into_label() {
160        assert_eq!(
161            extract_grpc_method_name(
162                "/.env.local/.env.production/.env.staging/.env.development/.env.test"
163            ),
164            "non_grpc"
165        );
166    }
167
168    #[test]
169    fn service_starting_with_dot_is_rejected() {
170        assert_eq!(extract_grpc_method_name("/.foo.bar/Method"), "non_grpc");
171    }
172
173    #[test]
174    fn method_with_extra_path_segments_is_rejected() {
175        assert_eq!(
176            extract_grpc_method_name("/foo.bar/Method/extra"),
177            "non_grpc"
178        );
179    }
180
181    #[test]
182    fn method_with_invalid_characters_is_rejected() {
183        assert_eq!(extract_grpc_method_name("/foo.bar/Method-x"), "non_grpc");
184        assert_eq!(extract_grpc_method_name("/foo.bar/Method?x"), "non_grpc");
185        assert_eq!(extract_grpc_method_name("/foo.bar/.Method"), "non_grpc");
186    }
187
188    #[test]
189    fn identifier_starting_with_underscore_is_rejected() {
190        assert_eq!(extract_grpc_method_name("/foo.bar/_Method"), "non_grpc");
191        assert_eq!(extract_grpc_method_name("/_foo.bar/Method"), "non_grpc");
192        assert_eq!(
193            extract_grpc_method_name("/foo.bar/________________"),
194            "non_grpc"
195        );
196    }
197
198    #[test]
199    fn empty_method_segment_is_rejected() {
200        assert_eq!(extract_grpc_method_name("/foo.bar/"), "non_grpc");
201    }
202
203    #[test]
204    fn empty_service_segment_is_rejected() {
205        assert_eq!(extract_grpc_method_name("//Method"), "non_grpc");
206    }
207
208    #[test]
209    fn service_with_consecutive_dots_is_rejected() {
210        assert_eq!(extract_grpc_method_name("/foo..bar/Method"), "non_grpc");
211    }
212
213    #[test]
214    fn overlong_method_is_rejected() {
215        let long_method = "M".repeat(MAX_PROTO_IDENT_LEN + 1);
216        let path = format!("/foo.bar/{long_method}");
217        assert_eq!(extract_grpc_method_name(&path), "non_grpc");
218    }
219
220    #[test]
221    fn path_without_leading_slash_is_rejected() {
222        assert_eq!(extract_grpc_method_name("foo.bar/Method"), "non_grpc");
223    }
224}