Skip to main content

vane_core/
rule.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde_json::Value;
5
6use crate::fetch::FetchKind;
7use crate::predicate::Predicate;
8
9pub type ListenSpec = String;
10
11#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
12pub struct RawRule {
13	pub name: String,
14	pub listen: Vec<ListenSpec>,
15	#[serde(default, rename = "match")]
16	pub match_predicate: Option<Predicate>,
17	#[serde(default)]
18	pub middleware_chain: Vec<MiddlewareRef>,
19	pub terminate: TerminateSpec,
20	/// Optional TLS termination config. When set, the listener wraps
21	/// each accepted TCP stream in a `rustls` server-side handshake
22	/// before driving the L7 sub-graph; cleartext sockets get
23	/// `Box<dyn AsyncReadWrite>` instead of raw `TcpStream`.
24	///
25	/// `lower_port` enforces consistency: every rule on the same
26	/// listener must agree on `tls` (all `None` or all the same
27	/// `Some(_)`); L4-only listeners cannot carry TLS (terminate +
28	/// re-emit cleartext is not a useful proxy shape — it leaks the
29	/// upstream traffic).
30	#[serde(default)]
31	pub tls: Option<TlsConfig>,
32	/// Maximum bytes to buffer for request body `LazyBuffer` collection.
33	/// Default 8 MiB. Exceeding this produces 413 Payload Too Large.
34	#[serde(default = "default_max_body_bytes")]
35	pub max_body_bytes_request: usize,
36	/// Maximum bytes to buffer for response body `LazyBuffer` collection.
37	/// Default 8 MiB. Exceeding this produces 502 Bad Gateway.
38	#[serde(default = "default_max_body_bytes")]
39	pub max_body_bytes_response: usize,
40	#[serde(default)]
41	pub source: SourceInfo,
42}
43
44fn default_max_body_bytes() -> usize {
45	8 * 1024 * 1024
46}
47
48/// Listener-side TLS termination config — paths to the cert chain +
49/// private key in PEM, plus an optional SNI hostname this cert serves.
50///
51/// `sni: None` marks the cert as the listener's _default_ — used when
52/// the `ClientHello` has no SNI extension, or when the SNI doesn't
53/// match any of the listener's `Some(_)` entries. A listener has at
54/// most one default cert.
55///
56/// SNI hostnames are normalised to ASCII-lowercase at every ingest
57/// boundary per 08-tls.md § _SNI normalization_; comparison against
58/// rustls's already-lowercased `ClientHello::server_name()` is then
59/// byte-for-byte.
60#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
61pub struct TlsConfig {
62	#[serde(default)]
63	pub sni: Option<String>,
64	pub cert_file: PathBuf,
65	pub key_file: PathBuf,
66}
67
68/// Per-listener cert pool — produced by `compile/lower` from every
69/// rule on the bind address that carries a `tls` block, after
70/// hash-consing identical entries and rejecting conflicts.
71///
72/// At most one `default` cert (sni-less); any number of SNI-keyed
73/// certs. The engine's link stage compiles this into a single
74/// `rustls::ServerConfig` whose cert resolver picks by SNI with
75/// `default` as the fallback for unmatched / missing SNI.
76#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
77pub struct ListenerTlsSpec {
78	#[serde(default)]
79	pub default: Option<TlsConfig>,
80	#[serde(default)]
81	pub sni_certs: BTreeMap<String, TlsConfig>,
82}
83
84impl ListenerTlsSpec {
85	#[must_use]
86	pub fn is_empty(&self) -> bool {
87		self.default.is_none() && self.sni_certs.is_empty()
88	}
89}
90
91#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
92pub struct MiddlewareRef {
93	#[serde(rename = "use")]
94	pub name: String,
95	#[serde(default)]
96	pub args: Value,
97	#[serde(default)]
98	pub on_error: Option<OnErrorSpec>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
102pub enum OnErrorSpec {
103	Close,
104	Response(SynthResponse),
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
108pub struct SynthResponse {
109	pub status: u16,
110	#[serde(default)]
111	pub headers: Option<BTreeMap<String, String>>,
112	#[serde(default)]
113	pub body: Option<String>,
114}
115
116impl<'de> serde::Deserialize<'de> for OnErrorSpec {
117	fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
118		#[derive(serde::Deserialize)]
119		#[serde(untagged)]
120		enum Raw {
121			Literal(String),
122			Response { response: SynthResponse },
123		}
124		match Raw::deserialize(de)? {
125			Raw::Literal(s) if s == "close" => Ok(Self::Close),
126			Raw::Literal(other) => Err(serde::de::Error::unknown_variant(&other, &["close"])),
127			Raw::Response { response } => Ok(Self::Response(response)),
128		}
129	}
130}
131
132#[derive(Debug, Clone, serde::Serialize)]
133pub struct TerminateSpec {
134	pub kind: FetchKind,
135	pub args: Value,
136}
137
138impl<'de> serde::Deserialize<'de> for TerminateSpec {
139	fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
140		let mut v = Value::deserialize(de)?;
141		let obj = v
142			.as_object_mut()
143			.ok_or_else(|| serde::de::Error::custom("`terminate` must be a JSON object"))?;
144		let type_val = obj.remove("type").ok_or_else(|| serde::de::Error::missing_field("type"))?;
145		let Value::String(alias) = type_val else {
146			return Err(serde::de::Error::custom("`terminate.type` must be a string"));
147		};
148		let kind = fetch_kind_from_alias(&alias)
149			.ok_or_else(|| serde::de::Error::custom(format!("unknown terminate type: {alias:?}")))?;
150		Ok(Self { kind, args: v })
151	}
152}
153
154fn fetch_kind_from_alias(alias: &str) -> Option<FetchKind> {
155	match alias {
156		"tcp_forward" | "udp_forward" => Some(FetchKind::L4Forward),
157		"http_proxy" | "http1_proxy" | "http2_proxy" | "http3_proxy" | "unix_proxy" | "cgi" => {
158			Some(FetchKind::HttpProxy)
159		}
160		"websocket" => Some(FetchKind::WebSocketUpgrade),
161		"static" | "redirect_https" => Some(FetchKind::HttpSynthesize),
162		_ => None,
163	}
164}
165
166#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
167pub struct SourceInfo {
168	#[serde(default)]
169	pub file: PathBuf,
170	#[serde(default)]
171	pub line: u32,
172}
173
174#[cfg(test)]
175mod tests {
176	use super::*;
177	use crate::predicate::{CheckMap, FieldPath, Operator, Predicate, Value as PredValue};
178
179	#[test]
180	fn raw_rule_minimal_parses_with_defaults() {
181		let raw = serde_json::json!({
182			"name": "r",
183			"listen": [":443"],
184			"terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
185		});
186		let rule: RawRule = serde_json::from_value(raw).expect("parse minimal rule");
187		assert_eq!(rule.name, "r");
188		assert_eq!(rule.listen, vec![":443".to_string()]);
189		assert!(rule.match_predicate.is_none());
190		assert!(rule.middleware_chain.is_empty());
191		assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
192		assert_eq!(rule.terminate.args, serde_json::json!({ "upstream": "127.0.0.1:8080" }));
193		assert_eq!(rule.source.file, PathBuf::new());
194		assert_eq!(rule.source.line, 0);
195		assert_eq!(rule.max_body_bytes_request, 8 * 1024 * 1024);
196		assert_eq!(rule.max_body_bytes_response, 8 * 1024 * 1024);
197	}
198
199	#[test]
200	fn raw_rule_full_populates_every_field() {
201		let raw = serde_json::json!({
202			"name": "api",
203			"listen": [":443", "0.0.0.0:80"],
204			"match": { "tls.sni": { "equals": "api.example.com" } },
205			"middleware_chain": [
206				{ "use": "rate_limit", "args": { "rate": 100 } },
207				{ "use": "jwt", "args": { "secret": "x" }, "on_error": "close" },
208			],
209			"terminate": {
210				"type": "http_proxy",
211				"upstream": "127.0.0.1:8080",
212				"timeouts": { "connect": "5s" }
213			},
214			"source": { "file": "rules/30-api.json", "line": 14 },
215		});
216		let rule: RawRule = serde_json::from_value(raw).expect("parse full rule");
217		assert_eq!(rule.name, "api");
218		assert_eq!(rule.listen.len(), 2);
219		let check = match rule.match_predicate.as_ref().expect("match present") {
220			Predicate::Check(c) => c,
221			other => panic!("expected Check, got {other:?}"),
222		};
223		assert_eq!(check.path, FieldPath::TlsSni);
224		match &check.op {
225			Operator::Equals(PredValue::Str(s)) => assert_eq!(s, "api.example.com"),
226			other => panic!("unexpected op: {other:?}"),
227		}
228		assert_eq!(rule.middleware_chain.len(), 2);
229		assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
230		assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
231		assert_eq!(
232			rule.terminate.args,
233			serde_json::json!({
234				"upstream": "127.0.0.1:8080",
235				"timeouts": { "connect": "5s" }
236			}),
237		);
238		assert_eq!(rule.source.file, PathBuf::from("rules/30-api.json"));
239		assert_eq!(rule.source.line, 14);
240	}
241
242	#[test]
243	fn middleware_ref_flat_form_parses_name_and_args() {
244		let raw = serde_json::json!({ "use": "rate_limit", "args": { "rate": 100 } });
245		let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
246		assert_eq!(m.name, "rate_limit");
247		assert_eq!(m.args, serde_json::json!({ "rate": 100 }));
248		assert!(m.on_error.is_none());
249	}
250
251	#[test]
252	fn middleware_ref_on_error_close_form() {
253		let raw = serde_json::json!({ "use": "jwt", "args": { "secret": "x" }, "on_error": "close" });
254		let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
255		assert_eq!(m.on_error, Some(OnErrorSpec::Close));
256	}
257
258	#[test]
259	fn middleware_ref_on_error_response_object_form() {
260		let raw = serde_json::json!({
261			"use": "jwt",
262			"on_error": { "response": { "status": 503, "body": "maintenance" } },
263		});
264		let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
265		assert_eq!(m.name, "jwt");
266		assert_eq!(m.args, Value::Null);
267		let resp = match m.on_error.expect("on_error present") {
268			OnErrorSpec::Response(r) => r,
269			OnErrorSpec::Close => panic!("expected Response"),
270		};
271		assert_eq!(resp.status, 503);
272		assert_eq!(resp.body.as_deref(), Some("maintenance"));
273		assert!(resp.headers.is_none());
274	}
275
276	#[test]
277	fn middleware_ref_args_defaults_to_null_when_omitted() {
278		let raw = serde_json::json!({ "use": "tag" });
279		let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
280		assert_eq!(m.args, Value::Null);
281	}
282
283	#[test]
284	fn middleware_ref_requires_use_key() {
285		let raw = serde_json::json!({});
286		let err = serde_json::from_value::<MiddlewareRef>(raw).expect_err("missing `use` must fail");
287		let _ = err.to_string();
288	}
289
290	#[test]
291	fn on_error_spec_string_invalid_variant_rejected() {
292		let raw = serde_json::json!("crash");
293		let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("non-`close` literal rejected");
294		let msg = err.to_string();
295		assert!(msg.contains("close"), "error names the only valid literal: {msg}");
296	}
297
298	#[test]
299	fn on_error_spec_malformed_response_object_rejected() {
300		let raw = serde_json::json!({ "response": null });
301		let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("null response rejected");
302		let _ = err.to_string();
303	}
304
305	#[test]
306	fn on_error_spec_close_literal_parses() {
307		let raw = serde_json::json!("close");
308		let s: OnErrorSpec = serde_json::from_value(raw).expect("close literal parses");
309		assert_eq!(s, OnErrorSpec::Close);
310	}
311
312	#[test]
313	fn on_error_spec_response_object_parses() {
314		let raw = serde_json::json!({
315			"response": { "status": 503, "body": "maintenance" },
316		});
317		let s: OnErrorSpec = serde_json::from_value(raw).expect("response object parses");
318		match s {
319			OnErrorSpec::Response(r) => {
320				assert_eq!(r.status, 503);
321				assert_eq!(r.body.as_deref(), Some("maintenance"));
322				assert!(r.headers.is_none());
323			}
324			OnErrorSpec::Close => panic!("expected Response"),
325		}
326	}
327
328	#[test]
329	fn synth_response_minimal_status_only() {
330		let raw = serde_json::json!({ "status": 200 });
331		let r: SynthResponse = serde_json::from_value(raw).expect("parse status-only synth");
332		assert_eq!(r.status, 200);
333		assert!(r.headers.is_none());
334		assert!(r.body.is_none());
335	}
336
337	#[test]
338	fn synth_response_full_status_headers_body() {
339		let raw = serde_json::json!({
340			"status": 404,
341			"headers": { "content-type": "text/plain" },
342			"body": "not found",
343		});
344		let r: SynthResponse = serde_json::from_value(raw).expect("parse full synth");
345		assert_eq!(r.status, 404);
346		let headers = r.headers.as_ref().expect("headers present");
347		assert_eq!(headers.get("content-type").map(String::as_str), Some("text/plain"));
348		assert_eq!(r.body.as_deref(), Some("not found"));
349	}
350
351	#[test]
352	fn terminate_spec_alias_table_maps_to_fetch_kind() {
353		// Every row of 05-terminator.md § _Variant ergonomics in config_.
354		let cases: &[(&str, FetchKind)] = &[
355			("tcp_forward", FetchKind::L4Forward),
356			("udp_forward", FetchKind::L4Forward),
357			("http_proxy", FetchKind::HttpProxy),
358			("http1_proxy", FetchKind::HttpProxy),
359			("http2_proxy", FetchKind::HttpProxy),
360			("http3_proxy", FetchKind::HttpProxy),
361			("unix_proxy", FetchKind::HttpProxy),
362			("cgi", FetchKind::HttpProxy),
363			("websocket", FetchKind::WebSocketUpgrade),
364			("static", FetchKind::HttpSynthesize),
365			("redirect_https", FetchKind::HttpSynthesize),
366		];
367		for (alias, expected) in cases {
368			let raw = serde_json::json!({ "type": alias });
369			let t: TerminateSpec =
370				serde_json::from_value(raw).unwrap_or_else(|e| panic!("alias {alias} must parse: {e}"));
371			assert_eq!(t.kind, *expected, "alias {alias} must map to {expected:?}");
372		}
373	}
374
375	#[test]
376	fn terminate_spec_args_preserves_all_non_type_keys_verbatim() {
377		// 14-presets.md § _RawRule shape_: "every other key goes into `args`
378		// verbatim". Covers top-level scalars AND nested objects.
379		let raw = serde_json::json!({
380			"type": "http_proxy",
381			"upstream": "127.0.0.1:8080",
382			"timeouts": { "connect": "5s", "total": "60s" },
383		});
384		let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
385		assert_eq!(t.kind, FetchKind::HttpProxy);
386		assert_eq!(
387			t.args,
388			serde_json::json!({
389				"upstream": "127.0.0.1:8080",
390				"timeouts": { "connect": "5s", "total": "60s" },
391			}),
392		);
393	}
394
395	#[test]
396	fn terminate_spec_alias_only_yields_empty_object_not_null() {
397		// 14-presets.md § _RawRule shape_: the custom Deserialize removes `type`
398		// from a JSON object and keeps the rest. An alias-only terminate leaves
399		// an empty object behind — NOT Value::Null.
400		let raw = serde_json::json!({ "type": "http_proxy" });
401		let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
402		assert_eq!(t.kind, FetchKind::HttpProxy);
403		assert_eq!(t.args, serde_json::Value::Object(serde_json::Map::new()));
404		assert!(t.args.is_object(), "args must be an object, got {:?}", t.args);
405	}
406
407	#[test]
408	fn terminate_spec_unknown_type_rejected_and_names_alias() {
409		let raw = serde_json::json!({ "type": "bogus" });
410		let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("unknown alias rejected");
411		assert!(err.to_string().contains("bogus"), "error must name the offending alias: {err}");
412	}
413
414	#[test]
415	fn terminate_spec_missing_type_rejected_and_names_field() {
416		let raw = serde_json::json!({ "upstream": "127.0.0.1:8080" });
417		let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("missing type rejected");
418		assert!(err.to_string().contains("type"), "error must name the missing field: {err}");
419	}
420
421	#[test]
422	fn source_info_default_is_empty_path_and_zero_line() {
423		let s = SourceInfo::default();
424		assert_eq!(s.file, PathBuf::new());
425		assert_eq!(s.line, 0);
426	}
427
428	#[test]
429	fn source_info_round_trip_via_json() {
430		let raw = serde_json::json!({ "file": "rules/a.json", "line": 7 });
431		let s: SourceInfo = serde_json::from_value(raw).expect("parse source info");
432		assert_eq!(s.file, PathBuf::from("rules/a.json"));
433		assert_eq!(s.line, 7);
434	}
435
436	#[test]
437	fn middleware_chain_defaults_to_empty_when_omitted() {
438		let raw = serde_json::json!({
439			"name": "r",
440			"listen": [":443"],
441			"terminate": { "type": "http_proxy" },
442		});
443		let rule: RawRule = serde_json::from_value(raw).expect("parse");
444		assert!(rule.middleware_chain.is_empty());
445	}
446
447	#[test]
448	fn middleware_ref_chain_mixes_on_error_forms() {
449		let raw = serde_json::json!({
450			"name": "r",
451			"listen": [":443"],
452			"middleware_chain": [
453				{ "use": "a" },
454				{ "use": "b", "on_error": "close" },
455				{ "use": "c", "on_error": { "response": { "status": 500 } } },
456			],
457			"terminate": { "type": "http_proxy" },
458		});
459		let rule: RawRule = serde_json::from_value(raw).expect("parse");
460		assert_eq!(rule.middleware_chain.len(), 3);
461		assert!(rule.middleware_chain[0].on_error.is_none());
462		assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
463		match rule.middleware_chain[2].on_error.as_ref().expect("on_error[2]") {
464			OnErrorSpec::Response(r) => {
465				assert_eq!(r.status, 500);
466				assert!(r.body.is_none());
467				assert!(r.headers.is_none());
468			}
469			OnErrorSpec::Close => panic!("expected Response at index 2"),
470		}
471	}
472
473	#[test]
474	fn raw_rule_accepts_top_level_check_predicate() {
475		let raw = serde_json::json!({
476			"name": "r",
477			"listen": [":80"],
478			"match": { "http.uri.path": { "prefix": "/api" } },
479			"terminate": { "type": "http_proxy" },
480		});
481		let rule: RawRule = serde_json::from_value(raw).expect("parse");
482		let Some(Predicate::Check(CheckMap { path, op })) = rule.match_predicate else {
483			panic!("expected Check predicate");
484		};
485		assert_eq!(path, FieldPath::HttpUriPath);
486		match op {
487			Operator::Prefix(PredValue::Str(s)) => assert_eq!(s, "/api"),
488			other => panic!("unexpected op: {other:?}"),
489		}
490	}
491
492	#[test]
493	fn raw_rule_without_tls_field_defaults_to_none() {
494		let raw = serde_json::json!({
495			"name": "r",
496			"listen": [":80"],
497			"terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
498		});
499		let rule: RawRule = serde_json::from_value(raw).expect("parse rule without tls");
500		assert!(rule.tls.is_none());
501	}
502
503	#[test]
504	fn raw_rule_with_tls_field_parses_paths() {
505		let raw = serde_json::json!({
506			"name": "r",
507			"listen": [":443"],
508			"terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
509			"tls": { "cert_file": "/etc/vaned/certs/api.pem", "key_file": "/etc/vaned/certs/api.key" },
510		});
511		let rule: RawRule = serde_json::from_value(raw).expect("parse rule with tls");
512		let tls = rule.tls.expect("tls present");
513		assert_eq!(tls.cert_file, PathBuf::from("/etc/vaned/certs/api.pem"));
514		assert_eq!(tls.key_file, PathBuf::from("/etc/vaned/certs/api.key"));
515	}
516
517	#[test]
518	fn tls_config_round_trips_through_json() {
519		let original = TlsConfig {
520			sni: None,
521			cert_file: PathBuf::from("/srv/cert.pem"),
522			key_file: PathBuf::from("/srv/key.pem"),
523		};
524		let encoded = serde_json::to_string(&original).expect("serialize");
525		let decoded: TlsConfig = serde_json::from_str(&encoded).expect("deserialize");
526		assert_eq!(decoded, original);
527	}
528
529	#[test]
530	fn tls_config_with_sni_field_parses() {
531		let raw = serde_json::json!({
532			"sni": "api.example.com",
533			"cert_file": "/etc/vaned/certs/api.pem",
534			"key_file": "/etc/vaned/certs/api.key",
535		});
536		let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls with sni");
537		assert_eq!(tls.sni.as_deref(), Some("api.example.com"));
538	}
539
540	#[test]
541	fn tls_config_without_sni_parses_with_none() {
542		// Wire-compat with TLS part 1 — files written before the `sni`
543		// field existed must still deserialise.
544		let raw = serde_json::json!({
545			"cert_file": "/etc/vaned/certs/default.pem",
546			"key_file": "/etc/vaned/certs/default.key",
547		});
548		let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls without sni");
549		assert!(tls.sni.is_none());
550	}
551}