1use std::ffi::OsString;
6use std::net::IpAddr;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone)]
11pub struct Args {
12 pub host: IpAddr,
14 pub host_explicit: bool,
22 pub port: u16,
24 pub port_explicit: bool,
31 pub config: Option<PathBuf>,
33 pub api_key: Option<String>,
35 pub no_auth: bool,
37 pub require_auth: bool,
39 pub capabilities: Vec<String>,
41 pub preset: Option<String>,
43 pub no_rate_limit: bool,
45 pub tunnel: bool,
47 pub tunnel_command: Option<String>,
49 pub relay: bool,
51 pub relay_url: Option<String>,
53 pub enroll_token: Option<String>,
55 pub public_base: Option<String>,
57 pub device_name: Option<String>,
59 pub tls_cert: Option<PathBuf>,
61 pub tls_key: Option<PathBuf>,
63 pub tls_self_signed: bool,
65 pub relay_fingerprint: Option<String>,
67 pub relay_ca: Option<PathBuf>,
69 pub allow_hosts: Vec<String>,
71 pub audit_log: Option<PathBuf>,
73 pub fs_root: Option<PathBuf>,
75 pub fs_chunk_size: Option<usize>,
77 pub audit_max_bytes_raw: Option<u64>,
84 pub kill_orphans: bool,
90 pub cors_allow_any: bool,
92 pub log_level: Option<String>,
94 pub version: bool,
96 pub help: bool,
98 pub check_update: bool,
100 pub update: bool,
102 pub no_update_check: bool,
104}
105
106impl Default for Args {
107 fn default() -> Self {
108 Self {
109 host: "127.0.0.1".parse().unwrap(),
110 port: 3000,
111 host_explicit: false,
112 port_explicit: false,
113 config: None,
114 api_key: None,
115 no_auth: false,
116 require_auth: false,
117 capabilities: Vec::new(),
118 preset: None,
119 no_rate_limit: false,
120 tunnel: false,
121 tunnel_command: None,
122 relay: false,
123 relay_url: None,
124 enroll_token: None,
125 public_base: None,
126 device_name: None,
127 tls_cert: None,
128 tls_key: None,
129 tls_self_signed: false,
130 relay_fingerprint: None,
131 relay_ca: None,
132 allow_hosts: Vec::new(),
133 audit_log: None,
134 audit_max_bytes_raw: None,
135 fs_root: None,
136 fs_chunk_size: None,
137 kill_orphans: false,
138 cors_allow_any: false,
139 log_level: None,
140 version: false,
141 help: false,
142 check_update: false,
143 update: false,
144 no_update_check: false,
145 }
146 }
147}
148
149impl Args {
150 pub fn audit_rotation_limit(&self) -> Option<u64> {
166 match self.audit_max_bytes_raw {
167 None => Some(crate::audit::DEFAULT_MAX_BYTES),
168 Some(0) => None,
169 Some(n) => Some(n),
170 }
171 }
172}
173
174pub fn parse_args() -> Result<Args, ArgsError> {
176 parse_args_from(std::env::args_os())
177}
178
179pub fn parse_args_from<I>(args: I) -> Result<Args, ArgsError>
181where
182 I: IntoIterator<Item = OsString>,
183{
184 use lexopt::prelude::*;
185
186 let mut result = Args::default();
187 let mut parser = lexopt::Parser::from_iter(args);
188
189 while let Some(arg) = parser.next()? {
190 match arg {
191 Short('h') | Long("help") => {
192 result.help = true;
193 }
194 Short('V') | Long("version") => {
195 result.version = true;
196 }
197 Short('H') | Long("host") => {
198 let value: String = parser.value()?.parse()?;
199 result.host = value
200 .parse()
201 .map_err(|_| ArgsError::InvalidValue("host", value))?;
202 result.host_explicit = true;
203 }
204 Short('p') | Long("port") => {
205 let value: String = parser.value()?.parse()?;
206 result.port = value
207 .parse()
208 .map_err(|_| ArgsError::InvalidValue("port", value))?;
209 result.port_explicit = true;
210 }
211 Short('c') | Long("config") => {
212 result.config = Some(parser.value()?.parse()?);
213 }
214 Short('k') | Long("api-key") => {
215 result.api_key = Some(parser.value()?.parse()?);
216 }
217 Long("no-auth") => {
218 result.no_auth = true;
219 }
220 Long("require-auth") => {
221 result.require_auth = true;
222 }
223 Long("capabilities") => {
224 let value: String = parser.value()?.parse()?;
226 result.capabilities.extend(
227 value
228 .split(',')
229 .map(|s| s.trim())
230 .filter(|s| !s.is_empty())
231 .map(String::from),
232 );
233 }
234 Long("preset") => {
235 result.preset = Some(parser.value()?.parse()?);
236 }
237 Long("no-rate-limit") => {
238 result.no_rate_limit = true;
239 }
240 Long("tunnel") => {
241 result.tunnel = true;
242 }
243 Long("tunnel-command") => {
244 result.tunnel_command = Some(parser.value()?.parse()?);
245 }
246 Long("relay") => {
247 result.relay_url = Some(parser.value()?.parse()?);
248 }
249 Long("enroll-token") => {
250 result.enroll_token = Some(parser.value()?.parse()?);
251 }
252 Long("public-base") => {
253 result.public_base = Some(parser.value()?.parse()?);
254 }
255 Long("device-name") => {
256 result.device_name = Some(parser.value()?.parse()?);
257 }
258 Long("tls-cert") => {
259 result.tls_cert = Some(parser.value()?.parse()?);
260 }
261 Long("tls-key") => {
262 result.tls_key = Some(parser.value()?.parse()?);
263 }
264 Long("tls-self-signed") => {
265 result.tls_self_signed = true;
266 }
267 Long("relay-fingerprint") => {
268 result.relay_fingerprint = Some(parser.value()?.parse()?);
269 }
270 Long("relay-ca") => {
271 result.relay_ca = Some(parser.value()?.parse()?);
272 }
273 Long("allow-host") => {
274 let value: String = parser.value()?.parse()?;
275 result.allow_hosts.push(value);
276 }
277 Long("audit-log") => {
278 result.audit_log = Some(parser.value()?.parse()?);
279 }
280 Long("audit-max-bytes") => {
281 let value: String = parser.value()?.parse()?;
282 result.audit_max_bytes_raw = Some(
283 value
284 .parse()
285 .map_err(|_| ArgsError::InvalidValue("audit-max-bytes", value))?,
286 );
287 }
288 Long("kill-orphans") => {
289 result.kill_orphans = true;
290 }
291 Long("cors-allow-any") => {
292 result.cors_allow_any = true;
293 }
294 Long("fs-root") => {
295 result.fs_root = Some(parser.value()?.parse()?);
296 }
297 Long("fs-chunk-size") => {
298 let value: String = parser.value()?.parse()?;
299 result.fs_chunk_size = Some(
300 value
301 .parse()
302 .map_err(|_| ArgsError::InvalidValue("fs-chunk-size", value))?,
303 );
304 }
305 Short('l') | Long("log-level") => {
306 result.log_level = Some(parser.value()?.parse()?);
307 }
308 #[cfg(feature = "self-update")]
309 Long("check-update") => {
310 result.check_update = true;
311 }
312 #[cfg(feature = "self-update")]
313 Long("update") => {
314 result.update = true;
315 }
316 #[cfg(feature = "self-update")]
317 Long("no-update-check") => {
318 result.no_update_check = true;
319 }
320 Value(val) if val == "relay" && !result.relay => {
324 result.relay = true;
325 }
326 Value(val) => {
327 return Err(ArgsError::UnexpectedArgument(val.to_string_lossy().into()));
328 }
329 _ => return Err(arg.unexpected().into()),
330 }
331 }
332
333 if result.tls_cert.is_some() != result.tls_key.is_some() {
336 return Err(ArgsError::Conflicting("--tls-cert", "--tls-key"));
337 }
338
339 if result.tls_self_signed && result.tls_cert.is_none() {
341 let defaults = (
342 std::path::PathBuf::from("shell-tunnel-cert.pem"),
343 std::path::PathBuf::from("shell-tunnel-key.pem"),
344 );
345 result.tls_cert = Some(defaults.0);
346 result.tls_key = Some(defaults.1);
347 }
348
349 if result.relay && (result.tunnel || result.tunnel_command.is_some()) {
352 return Err(ArgsError::Conflicting("relay", "--tunnel"));
353 }
354 if result.relay_url.is_some() && result.tunnel {
355 return Err(ArgsError::Conflicting("--relay", "--tunnel"));
356 }
357 if result.relay_url.is_some() && result.tunnel_command.is_some() {
358 return Err(ArgsError::Conflicting("--relay", "--tunnel-command"));
359 }
360
361 if result.tunnel && result.tunnel_command.is_some() {
364 return Err(ArgsError::Conflicting("--tunnel", "--tunnel-command"));
365 }
366
367 Ok(result)
368}
369
370pub fn print_help() {
372 let version = env!("CARGO_PKG_VERSION");
373
374 #[cfg(feature = "self-update")]
376 let update_opts = " --check-update Check for updates and exit\n --update Download and install latest version\n --no-update-check Disable automatic update check on startup\n";
377 #[cfg(not(feature = "self-update"))]
378 let update_opts = "";
379
380 #[cfg(feature = "self-update")]
381 let update_examples = "\n # Check for updates\n shell-tunnel --check-update\n\n # Self-update to latest version\n shell-tunnel --update\n";
382 #[cfg(not(feature = "self-update"))]
383 let update_examples = "";
384
385 println!(
386 r#"shell-tunnel {version}
387Ultra-lightweight remote shell gateway with a REST/WebSocket API
388
389USAGE:
390 shell-tunnel [OPTIONS] Serve a shell gateway
391 shell-tunnel relay [OPTIONS] Serve a relay that devices dial out to
392
393OPTIONS:
394 -H, --host <ADDR> Host address to bind [default: 127.0.0.1]
395 -p, --port <PORT> Port to listen on [default: 3000]
396 -c, --config <FILE> Path to configuration file (JSON)
397 -k, --api-key <KEY> API key callers present to run commands here. Adds to
398 any keys a config file lists rather than replacing
399 them; edit the file to retire a key
400 -l, --log-level <LVL> Log level (error, warn, info, debug, trace)
401 --no-auth Disable authentication (refused when reachable)
402 --require-auth Require auth, auto-generating an API key if none given
403 and printing it on stdout (never in the log, which a
404 log level can silence)
405 --capabilities <C> Scope issued token(s): comma-separated capabilities
406 (e.g. exec,session.read). Default: full-control, or
407 operator when the server is reachable
408 --preset <NAME> Scope issued token(s) by role preset
409 (operator | file-write | file-read | full-control)
410 --no-rate-limit Disable rate limiting. No X-RateLimit-* headers are
411 then sent — there is no budget to report
412 --tunnel Expose publicly via a Cloudflare quick tunnel
413 (requires `cloudflared`; implies authentication)
414 --tunnel-command <C>
415 Expose publicly by running an arbitrary tunnel
416 command (ngrok, bore, frp, ...); its printed URL
417 is used. Implies authentication
418 --relay <URL> Attach to a self-hosted relay (dial out, no inbound
419 port). Needs the relay's --enroll-token; implies
420 authentication. The local port is chosen for you
421 unless -p says otherwise
422 --device-name <N> Claim a stable name on the relay, so the device URL
423 survives reconnects [default: this machine's name]
424 --relay-fingerprint <FP>
425 Expect exactly this certificate from the relay, as
426 printed by `shell-tunnel relay --tls-self-signed`.
427 Nothing to copy but the string, and the certificate
428 need not name the address being dialled
429 --relay-ca <FILE> Also trust this PEM authority when dialling a relay
430 (the alternative to a fingerprint, for a private CA)
431 --allow-host <HOST> Also answer to this host name. A loopback-bound
432 server that is not published otherwise answers only
433 to localhost, which is what stops DNS rebinding.
434 Published, nothing is host-checked. Repeatable
435 --audit-log <FILE> Append executions, denied requests, and file
436 operations to this file (JSON per line; the token
437 itself is never written)
438 [default: off; shell-tunnel-audit.jsonl when reachable]
439 --audit-max-bytes <N>
440 Rotate the audit trail to <FILE>.1 past this size,
441 keeping one generation. 0 never rotates
442 [default: 67108864 (64 MiB)]
443 --kill-orphans Kill anything a command leaves running when the
444 command ends. Off by default, because a daemon
445 started on purpose is meant to outlive the request
446 --cors-allow-any Allow any CORS origin (opt-in; for browser UIs)
447 --fs-root <PATH> Confine the file API to this directory. Without it
448 the API reaches everything this account can
449 --fs-chunk-size <N> Upload chunk size advertised to callers, in bytes.
450 Default 4194304; 262144 when --relay is given,
451 because a relayed chunk must also finish inside the
452 relay's 120s request deadline
453
454TLS OPTIONS (with `relay`):
455 --tls-self-signed Serve HTTPS with a self-signed certificate,
456 generating one on first run and reusing it after.
457 Needs no paths; devices trust it with the
458 --relay-fingerprint the banner prints. Its names are
459 fixed when it is generated, so adding --public-base
460 later does not add that name — the banner says which
461 names it actually covers
462 --tls-cert <FILE> PEM certificate chain [default with --tls-self-signed:
463 shell-tunnel-cert.pem]
464 --tls-key <FILE> PEM private key matching the certificate
465
466 A gateway does not serve HTTPS and refuses these
467 flags at startup: reach it through a tunnel or a
468 relay, which carry their own TLS, or put a reverse
469 proxy in front. Its own socket is plaintext.
470 With a proxy, pass --require-auth: the proxy does
471 not change the bind address, so a loopback-bound
472 gateway still counts itself local and leaves
473 authentication off while the proxy publishes it.
474 Forget it and the server warns once, on the first
475 request carrying a proxy header — but a proxy that
476 forwards none of them leaves nothing to warn about
477
478RELAY OPTIONS (with `relay`):
479 --enroll-token <T> Secret devices present to attach to this relay
480 (generated if unset). Distinct from --api-key, which
481 is what callers present to a device
482 --public-base <URL> Public base URL of this relay. A URL with no port
483 uses this relay's listen port; name a port only when
484 a proxy remaps it [default: http://<bind address>]
485
486OTHER OPTIONS:
487{update_opts} -h, --help Print help
488 -V, --version Print version
489
490ENVIRONMENT VARIABLES:
491 SHELL_TUNNEL_HOST Bind address, unless -H names one
492 SHELL_TUNNEL_PORT Port, unless -p names one
493 SHELL_TUNNEL_API_KEY Adds an API key and turns auth on. Keys from the
494 config file stay valid alongside it
495 SHELL_TUNNEL_LOG_LEVEL Log level (overrides config)
496 RUST_LOG Alternative log level setting
497
498EXAMPLES:
499 # Start with defaults (localhost:3000, no auth)
500 shell-tunnel
501
502 # Start on all interfaces with API key
503 shell-tunnel -H 0.0.0.0 -p 8080 -k my-secret-key
504
505 # Start with config file
506 shell-tunnel -c /etc/shell-tunnel/config.json
507
508 # Development mode (no security)
509 shell-tunnel --no-auth --no-rate-limit
510
511 # Publish on the internet with a generated key (no account needed)
512 shell-tunnel --tunnel
513
514 # Publish using a different tunnel client
515 shell-tunnel --tunnel-command "ngrok http 3000"
516
517 # Attach to a relay under a stable name
518 shell-tunnel --relay https://relay.example.com --enroll-token <t> --device-name box
519
520 # Run a relay with HTTPS, generating a certificate on first run.
521 # --public-base names the host; the URL uses this relay's port (8443).
522 shell-tunnel relay -H 0.0.0.0 -p 8443 --tls-self-signed --public-base https://relay.example.com
523
524 # Behind a proxy that forwards 443 here, name the port devices dial
525 shell-tunnel relay -H 0.0.0.0 -p 8443 --public-base https://relay.example.com:443
526
527 # Issue a token that can only read files, confined to one directory
528 shell-tunnel -k readonly-key --preset file-read --fs-root /srv/deploy
529
530 # Issue a token scoped to specific capabilities
531 shell-tunnel -k ci-key --capabilities exec,session.read
532{update_examples}"#
533 );
534}
535
536pub fn print_version() {
538 println!("shell-tunnel {}", env!("CARGO_PKG_VERSION"));
539}
540
541#[derive(Debug)]
543pub enum ArgsError {
544 Lexopt(lexopt::Error),
546 InvalidValue(&'static str, String),
548 UnexpectedArgument(String),
550 Conflicting(&'static str, &'static str),
552}
553
554impl std::fmt::Display for ArgsError {
555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
556 match self {
557 Self::Lexopt(e) => write!(f, "{}", e),
558 Self::InvalidValue(name, value) => {
559 write!(f, "invalid value for --{}: '{}'", name, value)
560 }
561 Self::UnexpectedArgument(arg) => {
562 write!(f, "unexpected argument: '{}'", arg)
563 }
564 Self::Conflicting(a, b) if a.starts_with("--tls") => {
565 write!(f, "{} and {} must be given together", a, b)
566 }
567 Self::Conflicting(a, b) => {
568 write!(f, "{} and {} cannot be used together", a, b)
569 }
570 }
571 }
572}
573
574impl std::error::Error for ArgsError {}
575
576impl From<lexopt::Error> for ArgsError {
577 fn from(e: lexopt::Error) -> Self {
578 Self::Lexopt(e)
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 fn args(args: &[&str]) -> Vec<OsString> {
587 std::iter::once("shell-tunnel")
588 .chain(args.iter().copied())
589 .map(OsString::from)
590 .collect()
591 }
592
593 #[test]
594 fn test_default_args() {
595 let result = parse_args_from(args(&[])).unwrap();
596 assert_eq!(result.host.to_string(), "127.0.0.1");
597 assert_eq!(result.port, 3000);
598 assert!(!result.no_auth);
599 }
600
601 #[test]
602 fn test_host_port() {
603 let result = parse_args_from(args(&["-H", "0.0.0.0", "-p", "8080"])).unwrap();
604 assert_eq!(result.host.to_string(), "0.0.0.0");
605 assert_eq!(result.port, 8080);
606 }
607
608 #[test]
609 fn test_long_options() {
610 let result = parse_args_from(args(&["--host", "192.168.1.1", "--port", "9000"])).unwrap();
611 assert_eq!(result.host.to_string(), "192.168.1.1");
612 assert_eq!(result.port, 9000);
613 }
614
615 #[test]
616 fn test_api_key() {
617 let result = parse_args_from(args(&["-k", "my-secret"])).unwrap();
618 assert_eq!(result.api_key, Some("my-secret".to_string()));
619 }
620
621 #[test]
622 fn test_config_file() {
623 let result = parse_args_from(args(&["-c", "/etc/config.json"])).unwrap();
624 assert_eq!(result.config, Some(PathBuf::from("/etc/config.json")));
625 }
626
627 #[test]
628 fn test_no_auth() {
629 let result = parse_args_from(args(&["--no-auth"])).unwrap();
630 assert!(result.no_auth);
631 }
632
633 #[test]
634 fn test_require_auth() {
635 let result = parse_args_from(args(&["--require-auth"])).unwrap();
636 assert!(result.require_auth);
637 assert!(!Args::default().require_auth);
638 }
639
640 #[test]
641 fn test_no_rate_limit() {
642 let result = parse_args_from(args(&["--no-rate-limit"])).unwrap();
643 assert!(result.no_rate_limit);
644 }
645
646 #[test]
647 fn test_capabilities_csv() {
648 let result = parse_args_from(args(&["--capabilities", "exec,session.read"])).unwrap();
649 assert_eq!(result.capabilities, vec!["exec", "session.read"]);
650 assert!(Args::default().capabilities.is_empty());
651 }
652
653 #[test]
654 fn test_capabilities_trims_and_ignores_blanks() {
655 let result = parse_args_from(args(&["--capabilities", " exec , , session.read "])).unwrap();
656 assert_eq!(result.capabilities, vec!["exec", "session.read"]);
657 }
658
659 #[test]
660 fn test_capabilities_repeated_accumulate() {
661 let result = parse_args_from(args(&[
662 "--capabilities",
663 "exec",
664 "--capabilities",
665 "session.read,session.manage",
666 ]))
667 .unwrap();
668 assert_eq!(
669 result.capabilities,
670 vec!["exec", "session.read", "session.manage"]
671 );
672 }
673
674 #[test]
675 fn test_preset() {
676 let result = parse_args_from(args(&["--preset", "operator"])).unwrap();
677 assert_eq!(result.preset, Some("operator".to_string()));
678 assert!(Args::default().preset.is_none());
679 }
680
681 #[test]
682 fn test_help_flag() {
683 let result = parse_args_from(args(&["-h"])).unwrap();
684 assert!(result.help);
685
686 let result = parse_args_from(args(&["--help"])).unwrap();
687 assert!(result.help);
688 }
689
690 #[test]
691 fn test_version_flag() {
692 let result = parse_args_from(args(&["-V"])).unwrap();
693 assert!(result.version);
694
695 let result = parse_args_from(args(&["--version"])).unwrap();
696 assert!(result.version);
697 }
698
699 #[test]
700 fn test_log_level() {
701 let result = parse_args_from(args(&["-l", "debug"])).unwrap();
702 assert_eq!(result.log_level, Some("debug".to_string()));
703 }
704
705 #[test]
706 fn test_invalid_port() {
707 let result = parse_args_from(args(&["-p", "invalid"]));
708 assert!(result.is_err());
709 }
710
711 #[test]
712 fn test_invalid_host() {
713 let result = parse_args_from(args(&["-H", "not-an-ip"]));
714 assert!(result.is_err());
715 }
716
717 #[test]
718 fn test_combined_options() {
719 let result = parse_args_from(args(&[
720 "-H",
721 "0.0.0.0",
722 "-p",
723 "8080",
724 "-k",
725 "secret",
726 "-l",
727 "debug",
728 "--no-rate-limit",
729 ]))
730 .unwrap();
731
732 assert_eq!(result.host.to_string(), "0.0.0.0");
733 assert_eq!(result.port, 8080);
734 assert_eq!(result.api_key, Some("secret".to_string()));
735 assert_eq!(result.log_level, Some("debug".to_string()));
736 assert!(result.no_rate_limit);
737 assert!(!result.no_auth);
738 }
739
740 #[test]
741 fn test_tunnel_flag() {
742 let result = parse_args_from(vec![
743 OsString::from("shell-tunnel"),
744 OsString::from("--tunnel"),
745 ])
746 .unwrap();
747 assert!(result.tunnel);
748 assert!(result.tunnel_command.is_none());
749 }
750
751 #[test]
752 fn test_tunnel_command_flag() {
753 let result = parse_args_from(vec![
754 OsString::from("shell-tunnel"),
755 OsString::from("--tunnel-command"),
756 OsString::from("ngrok http 3000"),
757 ])
758 .unwrap();
759 assert_eq!(result.tunnel_command.as_deref(), Some("ngrok http 3000"));
760 assert!(!result.tunnel);
761 }
762
763 #[test]
764 fn test_tunnel_paths_are_mutually_exclusive() {
765 let err = parse_args_from(vec![
766 OsString::from("shell-tunnel"),
767 OsString::from("--tunnel"),
768 OsString::from("--tunnel-command"),
769 OsString::from("bore local 3000 --to bore.pub"),
770 ])
771 .unwrap_err();
772 let msg = err.to_string();
773 assert!(msg.contains("--tunnel"), "{msg}");
774 assert!(msg.contains("cannot be used together"), "{msg}");
775 }
776
777 #[test]
778 fn test_no_tunnel_by_default() {
779 let result = parse_args_from(vec![OsString::from("shell-tunnel")]).unwrap();
780 assert!(!result.tunnel);
781 assert!(result.tunnel_command.is_none());
782 }
783
784 #[test]
792 fn the_audit_rotation_limit_maps_absent_zero_and_a_size_differently() {
793 let absent = parse_args_from(vec![OsString::from("shell-tunnel")]).unwrap();
794 assert_eq!(
795 absent.audit_rotation_limit(),
796 Some(crate::audit::DEFAULT_MAX_BYTES),
797 "an operator who said nothing gets the bounded default"
798 );
799
800 let zero = parse_args_from(vec![
801 OsString::from("shell-tunnel"),
802 OsString::from("--audit-max-bytes"),
803 OsString::from("0"),
804 ])
805 .unwrap();
806 assert_eq!(
807 zero.audit_rotation_limit(),
808 None,
809 "0 is the opt-out to the old unbounded behaviour, not a zero-byte limit"
810 );
811
812 let sized = parse_args_from(vec![
813 OsString::from("shell-tunnel"),
814 OsString::from("--audit-max-bytes"),
815 OsString::from("4096"),
816 ])
817 .unwrap();
818 assert_eq!(sized.audit_rotation_limit(), Some(4096));
819 }
820}