Skip to main content

clawft_kernel/infer_proxy/
types.rs

1//! Shared types of the stable inference address (card mesh-placement-19).
2
3use std::time::Duration;
4
5use async_trait::async_trait;
6
7use crate::mesh::MeshStream;
8use crate::mesh_admit::{Grant, PeerClass};
9
10/// Hard limits. Every one bounds something a client or a peer controls.
11#[derive(Debug, Clone)]
12pub struct ProxyLimits {
13    /// Request line plus headers.
14    pub max_head_bytes: usize,
15    /// Header lines.
16    pub max_headers: usize,
17    /// Request body.
18    pub max_request_body: usize,
19    /// Request body over the mesh (sent as hex inside a control message).
20    pub max_mesh_request_body: usize,
21    /// Response body, summed over all chunks.
22    pub max_response_body: u64,
23    /// Time to read a request head.
24    pub head_timeout: Duration,
25    /// Time to read a request body.
26    pub body_timeout: Duration,
27    /// Time to connect to an upstream.
28    pub connect_timeout: Duration,
29    /// Longest silence between upstream (or mesh) chunks.
30    pub stall_timeout: Duration,
31    /// Wall-clock cap on one forwarded request.
32    pub request_timeout: Duration,
33    /// Concurrent connections per listener.
34    pub max_connections: usize,
35    /// Concurrent connections per client address on an exposed listener.
36    pub max_connections_per_ip: usize,
37}
38
39impl Default for ProxyLimits {
40    fn default() -> Self {
41        Self {
42            max_head_bytes: 16 * 1024,
43            max_headers: 64,
44            max_request_body: 8 * 1024 * 1024,
45            max_mesh_request_body: 2 * 1024 * 1024,
46            max_response_body: 64 * 1024 * 1024,
47            head_timeout: Duration::from_secs(10),
48            body_timeout: Duration::from_secs(30),
49            connect_timeout: Duration::from_secs(5),
50            stall_timeout: Duration::from_secs(120),
51            request_timeout: Duration::from_secs(900),
52            max_connections: 32,
53            max_connections_per_ip: 8,
54        }
55    }
56}
57
58/// Methods the proxy forwards. Anything else is refused.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Method {
61    /// GET.
62    Get,
63    /// POST.
64    Post,
65}
66
67impl Method {
68    /// Wire name.
69    pub fn as_str(self) -> &'static str {
70        match self {
71            Self::Get => "GET",
72            Self::Post => "POST",
73        }
74    }
75}
76
77/// A request that passed validation: nothing here can name a host.
78#[derive(Debug, Clone)]
79pub struct ProxyRequest {
80    /// Inference role the request is for.
81    pub role: String,
82    /// Method.
83    pub method: Method,
84    /// Origin-form path and query, allowlisted.
85    pub path: String,
86    /// `Content-Type`.
87    pub content_type: Option<String>,
88    /// `Accept`.
89    pub accept: Option<String>,
90    /// `Authorization`: forwarded to a local server only, never over the mesh.
91    pub authorization: Option<String>,
92    /// Body.
93    pub body: Vec<u8>,
94}
95
96/// Where a role is served right now.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum Target {
99    /// A loopback instance on this node (`http://127.0.0.1:PORT`).
100    Local {
101        /// Base URL, verified loopback.
102        base: String,
103    },
104    /// An instance on an admitted peer, reached over the mesh.
105    Remote {
106        /// Peer node id.
107        node_id: String,
108    },
109}
110
111/// Why a request was not served.
112#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
113pub enum ProxyError {
114    /// Malformed request.
115    #[error("bad request: {0}")]
116    BadRequest(String),
117    /// Missing or wrong bearer token on an exposed listener.
118    #[error("unauthorized")]
119    Unauthorized,
120    /// The request is not one the proxy forwards (path, host, origin).
121    #[error("forbidden: {0}")]
122    Forbidden(String),
123    /// Method other than GET or POST.
124    #[error("method not allowed")]
125    MethodNotAllowed,
126    /// Request body or head over the limit.
127    #[error("too large: {0}")]
128    TooLarge(String),
129    /// A framing the proxy does not implement (chunked request bodies).
130    #[error("not implemented: {0}")]
131    Unsupported(String),
132    /// Nothing serves the role.
133    #[error("no instance serves role '{0}'")]
134    NoInstance(String),
135    /// The upstream failed or answered garbage.
136    #[error("upstream: {0}")]
137    Upstream(String),
138    /// The mesh path failed.
139    #[error("mesh: {0}")]
140    Mesh(String),
141    /// A timeout elapsed.
142    #[error("timeout: {0}")]
143    Timeout(String),
144    /// The peer is not an admitted, verified peer.
145    #[error("refused: {0}")]
146    Refused(String),
147    /// A bind address or endpoint that is not loopback.
148    #[error("not loopback: {0}")]
149    NotLoopback(String),
150    /// The port is held by something else.
151    #[error("port {0} is in use")]
152    Occupied(u16),
153    /// Local I/O.
154    #[error("io: {0}")]
155    Io(String),
156}
157
158impl ProxyError {
159    /// HTTP status for a failure before any response byte was sent.
160    pub fn status(&self) -> u16 {
161        match self {
162            Self::BadRequest(_) => 400,
163            Self::Unauthorized => 401,
164            Self::Forbidden(_) | Self::Refused(_) => 403,
165            Self::MethodNotAllowed => 405,
166            Self::TooLarge(_) => 413,
167            Self::Unsupported(_) => 501,
168            Self::NoInstance(_) => 503,
169            Self::Upstream(_) | Self::Mesh(_) => 502,
170            Self::Timeout(_) => 504,
171            Self::NotLoopback(_) | Self::Occupied(_) | Self::Io(_) => 500,
172        }
173    }
174}
175
176/// Receives a response as it is produced (so a streamed completion is
177/// relayed as it arrives, not buffered).
178#[async_trait]
179pub trait ResponseSink: Send {
180    /// The status line and content type. Called once, first.
181    async fn head(&mut self, status: u16, content_type: Option<&str>) -> Result<(), ProxyError>;
182    /// A piece of the body.
183    async fn chunk(&mut self, data: &[u8]) -> Result<(), ProxyError>;
184}
185
186/// A peer's standing is good enough to serve inference: admission was
187/// enforced (not observe or off), the peer is a full node (not a leaf or
188/// legacy device), and its scope is trusted.
189pub fn qualifies(g: &Grant) -> bool {
190    g.admitted && g.class == PeerClass::Node && g.trust_scope && g.observed.is_none()
191}
192
193/// The mesh side of the proxy. The implementation is the only authority on
194/// who is admitted: `dial` must return a stream only to a peer that passed
195/// admission and whose id was verified against the connection (an
196/// authenticated Noise channel), never to an address a client named.
197#[async_trait]
198pub trait MeshDialer: Send + Sync + 'static {
199    /// The admission grant of the connected peer `node_id`, if any.
200    fn standing(&self, node_id: &str) -> Option<Grant>;
201    /// Whether `node_id` is an enforced-admission full node with a trusted
202    /// scope right now.
203    fn is_admitted(&self, node_id: &str) -> bool {
204        self.standing(node_id).is_some_and(|g| qualifies(&g))
205    }
206    /// An authenticated stream to the peer.
207    async fn dial(&self, node_id: &str) -> Result<Box<dyn MeshStream>, ProxyError>;
208}
209
210/// Where security-relevant proxy decisions are recorded (the chain, in
211/// production). Per-request traffic is not recorded, only binds, refusals
212/// and exposure changes.
213pub trait ProxyAudit: Send + Sync + 'static {
214    /// Record `kind` with `payload`.
215    fn record(&self, kind: &str, payload: serde_json::Value);
216}
217
218/// Audit into the ExoChain.
219#[cfg(feature = "exochain")]
220pub struct ChainAudit(pub std::sync::Arc<crate::chain::ChainManager>);
221
222#[cfg(feature = "exochain")]
223impl ProxyAudit for ChainAudit {
224    fn record(&self, kind: &str, payload: serde_json::Value) {
225        self.0.append("infer_proxy", kind, Some(payload));
226    }
227}