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 #[serde(default)]
31 pub tls: Option<TlsConfig>,
32 #[serde(default = "default_max_body_bytes")]
35 pub max_body_bytes_request: usize,
36 #[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#[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 #[serde(default)]
72 pub client_auth: Option<ClientAuthConfig>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
80pub struct ClientAuthConfig {
81 pub mode: ClientAuthMode,
82 #[serde(default)]
83 pub trust_store: Option<ClientTrustStoreConfig>,
84}
85
86#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
88#[serde(rename_all = "lowercase")]
89pub enum ClientAuthMode {
90 None,
91 Request,
92 Require,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
98pub struct ClientTrustStoreConfig {
99 #[serde(default)]
100 pub ca_paths: Vec<PathBuf>,
101 #[serde(default)]
102 pub ca_dir: Option<PathBuf>,
103 #[serde(default)]
104 pub crls: Vec<CrlSourceConfig>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
113#[serde(tag = "kind", rename_all = "lowercase")]
114pub enum CrlSourceConfig {
115 File { path: PathBuf, fetch_failure: CrlFetchFailure },
116 Url { url: String, fetch_failure: CrlFetchFailure },
117}
118
119#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
123#[serde(rename_all = "lowercase")]
124pub enum CrlFetchFailure {
125 Tolerate,
126 Reject,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
138pub struct ListenerTlsSpec {
139 #[serde(default)]
140 pub default: Option<TlsConfig>,
141 #[serde(default)]
142 pub sni_certs: BTreeMap<String, TlsConfig>,
143 #[serde(default)]
149 pub client_auth: ClientAuthSpec,
150}
151
152impl ListenerTlsSpec {
153 #[must_use]
154 pub fn is_empty(&self) -> bool {
155 self.default.is_none()
156 && self.sni_certs.is_empty()
157 && matches!(self.client_auth, ClientAuthSpec::None)
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]
165#[serde(tag = "mode", rename_all = "lowercase")]
166pub enum ClientAuthSpec {
167 #[default]
168 None,
169 Request {
170 trust_store: ClientTrustStoreConfig,
171 },
172 Require {
173 trust_store: ClientTrustStoreConfig,
174 },
175}
176
177#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
178pub struct MiddlewareRef {
179 #[serde(rename = "use")]
180 pub name: String,
181 #[serde(default)]
182 pub args: Value,
183 #[serde(default)]
184 pub on_error: Option<OnErrorSpec>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
188pub enum OnErrorSpec {
189 Close,
190 Response(SynthResponse),
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
194pub struct SynthResponse {
195 pub status: u16,
196 #[serde(default)]
197 pub headers: Option<BTreeMap<String, String>>,
198 #[serde(default)]
199 pub body: Option<String>,
200}
201
202impl<'de> serde::Deserialize<'de> for OnErrorSpec {
203 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
204 #[derive(serde::Deserialize)]
205 #[serde(untagged)]
206 enum Raw {
207 Literal(String),
208 Response { response: SynthResponse },
209 }
210 match Raw::deserialize(de)? {
211 Raw::Literal(s) if s == "close" => Ok(Self::Close),
212 Raw::Literal(other) => Err(serde::de::Error::unknown_variant(&other, &["close"])),
213 Raw::Response { response } => Ok(Self::Response(response)),
214 }
215 }
216}
217
218#[derive(Debug, Clone, serde::Serialize)]
219pub struct TerminateSpec {
220 pub kind: FetchKind,
221 pub args: Value,
222}
223
224impl<'de> serde::Deserialize<'de> for TerminateSpec {
225 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
226 let mut v = Value::deserialize(de)?;
227 let obj = v
228 .as_object_mut()
229 .ok_or_else(|| serde::de::Error::custom("`terminate` must be a JSON object"))?;
230 let type_val = obj.remove("type").ok_or_else(|| serde::de::Error::missing_field("type"))?;
231 let Value::String(alias) = type_val else {
232 return Err(serde::de::Error::custom("`terminate.type` must be a string"));
233 };
234 let kind = fetch_kind_from_alias(&alias)
235 .ok_or_else(|| serde::de::Error::custom(format!("unknown terminate type: {alias:?}")))?;
236 if let Some(version) = http_version_from_alias(&alias)
242 && !obj.contains_key("version")
243 {
244 obj.insert("version".to_owned(), Value::String(version.to_owned()));
245 }
246 if let Some(transport) = transport_from_alias(&alias)
251 && !obj.contains_key("transport")
252 {
253 obj.insert("transport".to_owned(), Value::String(transport.to_owned()));
254 }
255 Ok(Self { kind, args: v })
256 }
257}
258
259fn fetch_kind_from_alias(alias: &str) -> Option<FetchKind> {
260 match alias {
261 "tcp_forward" | "udp_forward" => Some(FetchKind::L4Forward),
262 "http_proxy" | "http1_proxy" | "http2_proxy" | "http3_proxy" | "unix_proxy" | "cgi" => {
263 Some(FetchKind::HttpProxy)
264 }
265 "websocket" => Some(FetchKind::WebSocketUpgrade),
266 "static" | "redirect_https" => Some(FetchKind::HttpSynthesize),
267 _ => None,
268 }
269}
270
271fn http_version_from_alias(alias: &str) -> Option<&'static str> {
272 match alias {
273 "http1_proxy" => Some("h1"),
274 "http2_proxy" => Some("h2"),
275 "http3_proxy" => Some("h3"),
276 _ => None,
277 }
278}
279
280fn transport_from_alias(alias: &str) -> Option<&'static str> {
281 match alias {
282 "tcp_forward" => Some("tcp"),
283 "udp_forward" => Some("udp"),
284 _ => None,
285 }
286}
287
288#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
289pub struct SourceInfo {
290 #[serde(default)]
291 pub file: PathBuf,
292 #[serde(default)]
293 pub line: u32,
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::predicate::{CheckMap, FieldPath, Operator, Predicate, Value as PredValue};
300
301 #[test]
302 fn raw_rule_minimal_parses_with_defaults() {
303 let raw = serde_json::json!({
304 "name": "r",
305 "listen": [":443"],
306 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
307 });
308 let rule: RawRule = serde_json::from_value(raw).expect("parse minimal rule");
309 assert_eq!(rule.name, "r");
310 assert_eq!(rule.listen, vec![":443".to_string()]);
311 assert!(rule.match_predicate.is_none());
312 assert!(rule.middleware_chain.is_empty());
313 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
314 assert_eq!(rule.terminate.args, serde_json::json!({ "upstream": "127.0.0.1:8080" }));
315 assert_eq!(rule.source.file, PathBuf::new());
316 assert_eq!(rule.source.line, 0);
317 assert_eq!(rule.max_body_bytes_request, 8 * 1024 * 1024);
318 assert_eq!(rule.max_body_bytes_response, 8 * 1024 * 1024);
319 }
320
321 #[test]
322 fn raw_rule_full_populates_every_field() {
323 let raw = serde_json::json!({
324 "name": "api",
325 "listen": [":443", "0.0.0.0:80"],
326 "match": { "tls.sni": { "equals": "api.example.com" } },
327 "middleware_chain": [
328 { "use": "rate_limit", "args": { "rate": 100 } },
329 { "use": "jwt", "args": { "secret": "x" }, "on_error": "close" },
330 ],
331 "terminate": {
332 "type": "http_proxy",
333 "upstream": "127.0.0.1:8080",
334 "timeouts": { "connect": "5s" }
335 },
336 "source": { "file": "rules/30-api.json", "line": 14 },
337 });
338 let rule: RawRule = serde_json::from_value(raw).expect("parse full rule");
339 assert_eq!(rule.name, "api");
340 assert_eq!(rule.listen.len(), 2);
341 let check = match rule.match_predicate.as_ref().expect("match present") {
342 Predicate::Check(c) => c,
343 other => panic!("expected Check, got {other:?}"),
344 };
345 assert_eq!(check.path, FieldPath::TlsSni);
346 match &check.op {
347 Operator::Equals(PredValue::Str(s)) => assert_eq!(s, "api.example.com"),
348 other => panic!("unexpected op: {other:?}"),
349 }
350 assert_eq!(rule.middleware_chain.len(), 2);
351 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
352 assert_eq!(rule.terminate.kind, FetchKind::HttpProxy);
353 assert_eq!(
354 rule.terminate.args,
355 serde_json::json!({
356 "upstream": "127.0.0.1:8080",
357 "timeouts": { "connect": "5s" }
358 }),
359 );
360 assert_eq!(rule.source.file, PathBuf::from("rules/30-api.json"));
361 assert_eq!(rule.source.line, 14);
362 }
363
364 #[test]
365 fn middleware_ref_flat_form_parses_name_and_args() {
366 let raw = serde_json::json!({ "use": "rate_limit", "args": { "rate": 100 } });
367 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
368 assert_eq!(m.name, "rate_limit");
369 assert_eq!(m.args, serde_json::json!({ "rate": 100 }));
370 assert!(m.on_error.is_none());
371 }
372
373 #[test]
374 fn middleware_ref_on_error_close_form() {
375 let raw = serde_json::json!({ "use": "jwt", "args": { "secret": "x" }, "on_error": "close" });
376 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
377 assert_eq!(m.on_error, Some(OnErrorSpec::Close));
378 }
379
380 #[test]
381 fn middleware_ref_on_error_response_object_form() {
382 let raw = serde_json::json!({
383 "use": "jwt",
384 "on_error": { "response": { "status": 503, "body": "maintenance" } },
385 });
386 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
387 assert_eq!(m.name, "jwt");
388 assert_eq!(m.args, Value::Null);
389 let resp = match m.on_error.expect("on_error present") {
390 OnErrorSpec::Response(r) => r,
391 OnErrorSpec::Close => panic!("expected Response"),
392 };
393 assert_eq!(resp.status, 503);
394 assert_eq!(resp.body.as_deref(), Some("maintenance"));
395 assert!(resp.headers.is_none());
396 }
397
398 #[test]
399 fn middleware_ref_args_defaults_to_null_when_omitted() {
400 let raw = serde_json::json!({ "use": "tag" });
401 let m: MiddlewareRef = serde_json::from_value(raw).expect("parse middleware ref");
402 assert_eq!(m.args, Value::Null);
403 }
404
405 #[test]
406 fn middleware_ref_requires_use_key() {
407 let raw = serde_json::json!({});
408 let err = serde_json::from_value::<MiddlewareRef>(raw).expect_err("missing `use` must fail");
409 let _ = err.to_string();
410 }
411
412 #[test]
413 fn on_error_spec_string_invalid_variant_rejected() {
414 let raw = serde_json::json!("crash");
415 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("non-`close` literal rejected");
416 let msg = err.to_string();
417 assert!(msg.contains("close"), "error names the only valid literal: {msg}");
418 }
419
420 #[test]
421 fn on_error_spec_malformed_response_object_rejected() {
422 let raw = serde_json::json!({ "response": null });
423 let err = serde_json::from_value::<OnErrorSpec>(raw).expect_err("null response rejected");
424 let _ = err.to_string();
425 }
426
427 #[test]
428 fn on_error_spec_close_literal_parses() {
429 let raw = serde_json::json!("close");
430 let s: OnErrorSpec = serde_json::from_value(raw).expect("close literal parses");
431 assert_eq!(s, OnErrorSpec::Close);
432 }
433
434 #[test]
435 fn on_error_spec_response_object_parses() {
436 let raw = serde_json::json!({
437 "response": { "status": 503, "body": "maintenance" },
438 });
439 let s: OnErrorSpec = serde_json::from_value(raw).expect("response object parses");
440 match s {
441 OnErrorSpec::Response(r) => {
442 assert_eq!(r.status, 503);
443 assert_eq!(r.body.as_deref(), Some("maintenance"));
444 assert!(r.headers.is_none());
445 }
446 OnErrorSpec::Close => panic!("expected Response"),
447 }
448 }
449
450 #[test]
451 fn synth_response_minimal_status_only() {
452 let raw = serde_json::json!({ "status": 200 });
453 let r: SynthResponse = serde_json::from_value(raw).expect("parse status-only synth");
454 assert_eq!(r.status, 200);
455 assert!(r.headers.is_none());
456 assert!(r.body.is_none());
457 }
458
459 #[test]
460 fn synth_response_full_status_headers_body() {
461 let raw = serde_json::json!({
462 "status": 404,
463 "headers": { "content-type": "text/plain" },
464 "body": "not found",
465 });
466 let r: SynthResponse = serde_json::from_value(raw).expect("parse full synth");
467 assert_eq!(r.status, 404);
468 let headers = r.headers.as_ref().expect("headers present");
469 assert_eq!(headers.get("content-type").map(String::as_str), Some("text/plain"));
470 assert_eq!(r.body.as_deref(), Some("not found"));
471 }
472
473 #[test]
474 fn terminate_spec_alias_table_maps_to_fetch_kind() {
475 let cases: &[(&str, FetchKind)] = &[
477 ("tcp_forward", FetchKind::L4Forward),
478 ("udp_forward", FetchKind::L4Forward),
479 ("http_proxy", FetchKind::HttpProxy),
480 ("http1_proxy", FetchKind::HttpProxy),
481 ("http2_proxy", FetchKind::HttpProxy),
482 ("http3_proxy", FetchKind::HttpProxy),
483 ("unix_proxy", FetchKind::HttpProxy),
484 ("cgi", FetchKind::HttpProxy),
485 ("websocket", FetchKind::WebSocketUpgrade),
486 ("static", FetchKind::HttpSynthesize),
487 ("redirect_https", FetchKind::HttpSynthesize),
488 ];
489 for (alias, expected) in cases {
490 let raw = serde_json::json!({ "type": alias });
491 let t: TerminateSpec =
492 serde_json::from_value(raw).unwrap_or_else(|e| panic!("alias {alias} must parse: {e}"));
493 assert_eq!(t.kind, *expected, "alias {alias} must map to {expected:?}");
494 }
495 }
496
497 #[test]
498 fn terminate_spec_args_preserves_all_non_type_keys_verbatim() {
499 let raw = serde_json::json!({
502 "type": "http_proxy",
503 "upstream": "127.0.0.1:8080",
504 "timeouts": { "connect": "5s", "total": "60s" },
505 });
506 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
507 assert_eq!(t.kind, FetchKind::HttpProxy);
508 assert_eq!(
509 t.args,
510 serde_json::json!({
511 "upstream": "127.0.0.1:8080",
512 "timeouts": { "connect": "5s", "total": "60s" },
513 }),
514 );
515 }
516
517 #[test]
518 fn terminate_spec_udp_forward_alias_injects_transport_udp() {
519 let raw = serde_json::json!({ "type": "udp_forward", "upstream": "1.2.3.4:53" });
520 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
521 assert_eq!(t.kind, FetchKind::L4Forward);
522 assert_eq!(t.args["transport"], "udp");
523 assert_eq!(t.args["upstream"], "1.2.3.4:53");
524 }
525
526 #[test]
527 fn terminate_spec_tcp_forward_alias_injects_transport_tcp() {
528 let raw = serde_json::json!({ "type": "tcp_forward", "upstream": "10.0.0.5:22" });
529 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
530 assert_eq!(t.kind, FetchKind::L4Forward);
531 assert_eq!(t.args["transport"], "tcp");
532 }
533
534 #[test]
535 fn terminate_spec_explicit_transport_wins_over_alias() {
536 let raw = serde_json::json!({ "type": "udp_forward", "upstream": "x", "transport": "tcp" });
540 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
541 assert_eq!(t.args["transport"], "tcp");
542 }
543
544 #[test]
545 fn terminate_spec_alias_only_yields_empty_object_not_null() {
546 let raw = serde_json::json!({ "type": "http_proxy" });
550 let t: TerminateSpec = serde_json::from_value(raw).expect("parse");
551 assert_eq!(t.kind, FetchKind::HttpProxy);
552 assert_eq!(t.args, serde_json::Value::Object(serde_json::Map::new()));
553 assert!(t.args.is_object(), "args must be an object, got {:?}", t.args);
554 }
555
556 #[test]
557 fn terminate_spec_unknown_type_rejected_and_names_alias() {
558 let raw = serde_json::json!({ "type": "bogus" });
559 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("unknown alias rejected");
560 assert!(err.to_string().contains("bogus"), "error must name the offending alias: {err}");
561 }
562
563 #[test]
564 fn terminate_spec_missing_type_rejected_and_names_field() {
565 let raw = serde_json::json!({ "upstream": "127.0.0.1:8080" });
566 let err = serde_json::from_value::<TerminateSpec>(raw).expect_err("missing type rejected");
567 assert!(err.to_string().contains("type"), "error must name the missing field: {err}");
568 }
569
570 #[test]
571 fn source_info_default_is_empty_path_and_zero_line() {
572 let s = SourceInfo::default();
573 assert_eq!(s.file, PathBuf::new());
574 assert_eq!(s.line, 0);
575 }
576
577 #[test]
578 fn source_info_round_trip_via_json() {
579 let raw = serde_json::json!({ "file": "rules/a.json", "line": 7 });
580 let s: SourceInfo = serde_json::from_value(raw).expect("parse source info");
581 assert_eq!(s.file, PathBuf::from("rules/a.json"));
582 assert_eq!(s.line, 7);
583 }
584
585 #[test]
586 fn middleware_chain_defaults_to_empty_when_omitted() {
587 let raw = serde_json::json!({
588 "name": "r",
589 "listen": [":443"],
590 "terminate": { "type": "http_proxy" },
591 });
592 let rule: RawRule = serde_json::from_value(raw).expect("parse");
593 assert!(rule.middleware_chain.is_empty());
594 }
595
596 #[test]
597 fn middleware_ref_chain_mixes_on_error_forms() {
598 let raw = serde_json::json!({
599 "name": "r",
600 "listen": [":443"],
601 "middleware_chain": [
602 { "use": "a" },
603 { "use": "b", "on_error": "close" },
604 { "use": "c", "on_error": { "response": { "status": 500 } } },
605 ],
606 "terminate": { "type": "http_proxy" },
607 });
608 let rule: RawRule = serde_json::from_value(raw).expect("parse");
609 assert_eq!(rule.middleware_chain.len(), 3);
610 assert!(rule.middleware_chain[0].on_error.is_none());
611 assert_eq!(rule.middleware_chain[1].on_error, Some(OnErrorSpec::Close));
612 match rule.middleware_chain[2].on_error.as_ref().expect("on_error[2]") {
613 OnErrorSpec::Response(r) => {
614 assert_eq!(r.status, 500);
615 assert!(r.body.is_none());
616 assert!(r.headers.is_none());
617 }
618 OnErrorSpec::Close => panic!("expected Response at index 2"),
619 }
620 }
621
622 #[test]
623 fn raw_rule_accepts_top_level_check_predicate() {
624 let raw = serde_json::json!({
625 "name": "r",
626 "listen": [":80"],
627 "match": { "http.uri.path": { "prefix": "/api" } },
628 "terminate": { "type": "http_proxy" },
629 });
630 let rule: RawRule = serde_json::from_value(raw).expect("parse");
631 let Some(Predicate::Check(CheckMap { path, op })) = rule.match_predicate else {
632 panic!("expected Check predicate");
633 };
634 assert_eq!(path, FieldPath::HttpUriPath);
635 match op {
636 Operator::Prefix(PredValue::Str(s)) => assert_eq!(s, "/api"),
637 other => panic!("unexpected op: {other:?}"),
638 }
639 }
640
641 #[test]
642 fn raw_rule_without_tls_field_defaults_to_none() {
643 let raw = serde_json::json!({
644 "name": "r",
645 "listen": [":80"],
646 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
647 });
648 let rule: RawRule = serde_json::from_value(raw).expect("parse rule without tls");
649 assert!(rule.tls.is_none());
650 }
651
652 #[test]
653 fn raw_rule_with_tls_field_parses_paths() {
654 let raw = serde_json::json!({
655 "name": "r",
656 "listen": [":443"],
657 "terminate": { "type": "http_proxy", "upstream": "127.0.0.1:8080" },
658 "tls": { "cert_file": "/etc/vaned/certs/api.pem", "key_file": "/etc/vaned/certs/api.key" },
659 });
660 let rule: RawRule = serde_json::from_value(raw).expect("parse rule with tls");
661 let tls = rule.tls.expect("tls present");
662 assert_eq!(tls.cert_file, PathBuf::from("/etc/vaned/certs/api.pem"));
663 assert_eq!(tls.key_file, PathBuf::from("/etc/vaned/certs/api.key"));
664 }
665
666 #[test]
667 fn tls_config_round_trips_through_json() {
668 let original = TlsConfig {
669 sni: None,
670 cert_file: PathBuf::from("/srv/cert.pem"),
671 key_file: PathBuf::from("/srv/key.pem"),
672 client_auth: None,
673 };
674 let encoded = serde_json::to_string(&original).expect("serialize");
675 let decoded: TlsConfig = serde_json::from_str(&encoded).expect("deserialize");
676 assert_eq!(decoded, original);
677 }
678
679 #[test]
680 fn tls_config_with_sni_field_parses() {
681 let raw = serde_json::json!({
682 "sni": "api.example.com",
683 "cert_file": "/etc/vaned/certs/api.pem",
684 "key_file": "/etc/vaned/certs/api.key",
685 });
686 let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls with sni");
687 assert_eq!(tls.sni.as_deref(), Some("api.example.com"));
688 }
689
690 #[test]
691 fn tls_config_without_sni_parses_with_none() {
692 let raw = serde_json::json!({
695 "cert_file": "/etc/vaned/certs/default.pem",
696 "key_file": "/etc/vaned/certs/default.key",
697 });
698 let tls: TlsConfig = serde_json::from_value(raw).expect("parse tls without sni");
699 assert!(tls.sni.is_none());
700 }
701}