1#![forbid(unsafe_code)]
16
17mod ini;
18mod parse;
19mod parse_items;
20
21use crate::{
22 parse::{is_number, parse_bool, parse_duration, parse_hex, parse_u16},
23 parse_items::{Map, MapItem},
24};
25use anyhow::{self as ah, format_err as err, Context as _};
26use letmein_proto::{Key, ResourceId, UserId, PORT};
27use sha3::{Digest, Sha3_256};
28use std::{
29 collections::HashMap,
30 path::{Path, PathBuf},
31 time::Duration,
32};
33use subtle::ConstantTimeEq as _;
34
35pub use crate::ini::{Ini, IniSectionIter};
36
37#[cfg(not(target_os = "windows"))]
39const SERVER_CONF_PATH: &str = "etc/letmeind.conf";
40#[cfg(target_os = "windows")]
41const SERVER_CONF_PATH: &str = "letmeind.conf";
42
43#[cfg(not(target_os = "windows"))]
45const CLIENT_CONF_PATH: &str = "etc/letmein.conf";
46#[cfg(target_os = "windows")]
47const CLIENT_CONF_PATH: &str = "letmein.conf";
48
49const DEFAULT_CONTROL_TIMEOUT: Duration = Duration::from_millis(5_000);
50const DEFAULT_NFT_TIMEOUT: Duration = Duration::from_millis(600_000);
51
52const MAX_CHAIN_LEN: usize = 64;
53
54#[derive(Clone, Debug, Default, Eq)]
56pub struct ConfigChecksum([u8; ConfigChecksum::SIZE]);
57
58impl ConfigChecksum {
59 pub const SIZE: usize = 32;
61
62 pub fn calculate(content: &[u8]) -> Self {
64 let mut hash = Sha3_256::new();
65 hash.update((content.len() as u64).to_be_bytes());
66 hash.update(content);
67 let digest = hash.finalize();
68 Self((*digest).try_into().expect("Unwrap sha digest"))
69 }
70
71 pub fn as_bytes(&self) -> &[u8; ConfigChecksum::SIZE] {
73 &self.0
74 }
75}
76
77impl PartialEq for ConfigChecksum {
78 fn eq(&self, other: &ConfigChecksum) -> bool {
79 self.0.ct_eq(&other.0).into()
81 }
82}
83
84impl TryFrom<&[u8]> for ConfigChecksum {
85 type Error = ah::Error;
86
87 fn try_from(data: &[u8]) -> ah::Result<Self> {
89 Ok(ConfigChecksum(data.try_into()?))
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct ControlPort {
96 pub port: u16,
97 pub tcp: bool,
98 pub udp: bool,
99}
100
101impl Default for ControlPort {
102 fn default() -> Self {
103 Self {
104 port: PORT,
105 tcp: true,
106 udp: false,
107 }
108 }
109}
110
111impl std::fmt::Display for ControlPort {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
113 write!(f, "{}(", self.port)?;
114 if self.tcp {
115 write!(f, "TCP")?;
116 if self.udp {
117 write!(f, "/")?;
118 }
119 }
120 if self.udp {
121 write!(f, "UDP")?;
122 }
123 write!(f, ")")
124 }
125}
126
127#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum Resource {
130 Port {
132 id: ResourceId,
133 port: u16,
134 tcp: bool,
135 udp: bool,
136 timeout: Option<Duration>,
137 users: Vec<UserId>,
138 },
139 Jump {
140 id: ResourceId,
141 input: Option<String>,
142 input_match_saddr: bool,
143 forward: Option<String>,
144 forward_match_saddr: bool,
145 output: Option<String>,
146 output_match_saddr: bool,
147 timeout: Option<Duration>,
148 users: Vec<UserId>,
149 },
150}
151
152impl Resource {
153 pub fn id(&self) -> ResourceId {
154 match self {
155 Self::Port { id, .. } => *id,
156 Self::Jump { id, .. } => *id,
157 }
158 }
159
160 pub fn contains_user(&self, id: UserId) -> bool {
161 let users = match self {
162 Self::Port { users, .. } => users,
163 Self::Jump { users, .. } => users,
164 };
165 if users.is_empty() {
166 true
168 } else {
169 users.contains(&id)
170 }
171 }
172
173 pub fn timeout(&self) -> Option<Duration> {
174 match self {
175 Self::Port { timeout, .. } => *timeout,
176 Self::Jump { timeout, .. } => *timeout,
177 }
178 }
179}
180
181impl std::fmt::Display for Resource {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
183 fn write_users(
184 f: &mut std::fmt::Formatter<'_>,
185 users: &[UserId],
186 ) -> Result<(), std::fmt::Error> {
187 if !users.is_empty() {
188 write!(f, " Users: ")?;
189 for (i, user) in users.iter().enumerate() {
190 if i != 0 {
191 write!(f, ", ")?;
192 }
193 write!(f, "{user}")?;
194 }
195 writeln!(f)?;
196 }
197 Ok(())
198 }
199
200 fn write_timeout(
201 f: &mut std::fmt::Formatter<'_>,
202 timeout: &Option<Duration>,
203 ) -> Result<(), std::fmt::Error> {
204 if let Some(timeout) = timeout {
205 writeln!(f, " timeout: {} s", timeout.as_secs())?;
206 }
207 Ok(())
208 }
209
210 fn write_chain(
211 f: &mut std::fmt::Formatter<'_>,
212 chain: &Option<String>,
213 match_saddr: bool,
214 name: &str,
215 ) -> Result<(), std::fmt::Error> {
216 if let Some(chain) = chain {
217 let match_saddr = if match_saddr { " match-saddr" } else { "" };
218 writeln!(f, " {name}-chain: {chain}{match_saddr}")?;
219 }
220 Ok(())
221 }
222
223 match self {
224 Self::Port {
225 id,
226 port,
227 tcp,
228 udp,
229 timeout,
230 users,
231 } => {
232 let tcpudp = if *tcp && *udp {
233 "TCP/UDP"
234 } else if *tcp {
235 "TCP"
236 } else if *udp {
237 "UDP"
238 } else {
239 ""
240 };
241 writeln!(f, "Port resource:")?;
242 writeln!(f, " id: {id}")?;
243 writeln!(f, " port: {port} {tcpudp}")?;
244 write_timeout(f, timeout)?;
245 write_users(f, users)?;
246 }
247 Self::Jump {
248 id,
249 input,
250 input_match_saddr,
251 forward,
252 forward_match_saddr,
253 output,
254 output_match_saddr,
255 timeout,
256 users,
257 } => {
258 writeln!(f, "Jump resource:")?;
259 writeln!(f, " id: {id}")?;
260 write_chain(f, input, *input_match_saddr, "input")?;
261 write_chain(f, forward, *forward_match_saddr, "forward")?;
262 write_chain(f, output, *output_match_saddr, "output")?;
263 write_timeout(f, timeout)?;
264 write_users(f, users)?;
265 }
266 }
267 Ok(())
268 }
269}
270
271#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
273pub enum ErrorPolicy {
274 #[default]
276 Always,
277
278 BasicAuth,
280
281 FullAuth,
283}
284
285impl std::fmt::Display for ErrorPolicy {
286 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
287 match self {
288 Self::Always => write!(f, "Always"),
289 Self::BasicAuth => write!(f, "Basic authentication"),
290 Self::FullAuth => write!(f, "Full challenge-response authentication"),
291 }
292 }
293}
294
295impl std::str::FromStr for ErrorPolicy {
296 type Err = ah::Error;
297
298 fn from_str(s: &str) -> Result<Self, Self::Err> {
299 match s.to_lowercase().trim() {
300 "always" => Ok(ErrorPolicy::Always),
301 "basic-auth" => Ok(ErrorPolicy::BasicAuth),
302 "full-auth" => Ok(ErrorPolicy::FullAuth),
303 other => Err(err!(
304 "Config option 'control-error-policy = {other}' is not valid. \
305 Valid values are: always, basic-auth, full-auth."
306 )),
307 }
308 }
309}
310
311#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
313pub enum Seccomp {
314 #[default]
316 Off,
317
318 Log,
323
324 Kill,
328}
329
330impl std::fmt::Display for Seccomp {
331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
332 match self {
333 Self::Off => write!(f, "Off"),
334 Self::Log => write!(f, "Logging only"),
335 Self::Kill => write!(f, "Process killing"),
336 }
337 }
338}
339
340impl std::str::FromStr for Seccomp {
341 type Err = ah::Error;
342
343 fn from_str(s: &str) -> Result<Self, Self::Err> {
344 match s.to_lowercase().trim() {
345 "off" => Ok(Self::Off),
346 "log" => Ok(Self::Log),
347 "kill" => Ok(Self::Kill),
348 other => Err(err!(
349 "Config option 'seccomp = {other}' is not valid. \
350 Valid values are: off, log, kill."
351 )),
352 }
353 }
354}
355
356fn get_debug(ini: &Ini) -> ah::Result<bool> {
357 if let Some(debug) = ini.get("GENERAL", "debug") {
358 return parse_bool(debug);
359 }
360 Ok(false)
361}
362
363fn get_port(ini: &Ini) -> ah::Result<ControlPort> {
364 if let Some(port) = ini.get("GENERAL", "port") {
365 let mut control_port = ControlPort {
366 port: PORT,
367 tcp: false,
368 udp: false,
369 };
370 let map = port.parse::<Map>().context("[GENERAL] port")?;
371 for item in map.items() {
372 match item {
373 MapItem::KeyValues(k, _) => {
374 return Err(err!("[GENERAL] port: Unknown option: {k}"));
375 }
376 MapItem::Values(vs) => {
377 if vs.len() == 1 && is_number(&vs[0]) {
378 control_port.port = parse_u16(&vs[0])?;
379 } else {
380 for v in vs {
381 match &v.to_lowercase()[..] {
382 "tcp" => control_port.tcp = true,
383 "udp" => control_port.udp = true,
384 v => {
385 return Err(err!("[GENERAL] port: Unknown option: {v}"));
386 }
387 }
388 }
389 }
390 }
391 }
392 }
393 if !control_port.tcp && !control_port.udp {
394 control_port.tcp = true;
396 }
397 return Ok(control_port);
398 }
399 Ok(Default::default())
400}
401
402fn get_control_timeout(ini: &Ini) -> ah::Result<Duration> {
403 if let Some(timeout) = ini.get("GENERAL", "control-timeout") {
404 return parse_duration(timeout);
405 }
406 Ok(DEFAULT_CONTROL_TIMEOUT)
407}
408
409fn get_control_error_policy(ini: &Ini) -> ah::Result<ErrorPolicy> {
410 if let Some(policy) = ini.get("GENERAL", "control-error-policy") {
411 return policy.parse();
412 }
413 Ok(Default::default())
414}
415
416fn get_seccomp(ini: &Ini) -> ah::Result<Seccomp> {
417 if let Some(seccomp) = ini.get("GENERAL", "seccomp") {
418 return seccomp.parse();
419 }
420 Ok(Default::default())
421}
422
423fn get_keys(ini: &Ini) -> ah::Result<HashMap<UserId, Key>> {
424 let mut keys = HashMap::new();
425 if let Some(options) = ini.options_iter("KEYS") {
426 for (id, key) in options {
427 let id = id.parse().context("[KEYS]")?;
428 let key = parse_hex(key).context("[KEYS]")?;
429 if key == [0; std::mem::size_of::<Key>()] {
430 return Err(err!("Invalid key {id}: Key is all zeros (00)"));
431 }
432 if key == [0xFF; std::mem::size_of::<Key>()] {
433 return Err(err!("Invalid key {id}: Key is all ones (FF)"));
434 }
435 if keys.contains_key(&id) {
436 return Err(err!("[KEYS] Multiple definitions of key '{id}'"));
437 }
438 keys.insert(id, key);
439 }
440 }
441 Ok(keys)
442}
443
444fn extract_users(id: ResourceId, users: &[String]) -> ah::Result<Vec<UserId>> {
445 let mut ret = Vec::with_capacity(users.len());
446 for user in users {
447 if let Ok(user) = user.parse() {
448 ret.push(user);
449 } else {
450 return Err(err!("[RESOURCE] '{id}': 'user' id is invalid"));
451 }
452 }
453 Ok(ret)
454}
455
456fn extract_resource_port(
457 id: ResourceId,
458 resources: &mut HashMap<ResourceId, Resource>,
459 map: &Map,
460) -> ah::Result<()> {
461 let mut port: Option<u16> = None;
462 let mut timeout: Option<Duration> = None;
463 let mut users: Vec<String> = vec![];
464 let mut tcp = false;
465 let mut udp = false;
466
467 for item in map.items() {
468 match item {
469 MapItem::KeyValues(k, vs) => {
470 if k == "port" {
471 if vs.len() == 1 {
472 if port.is_some() {
473 return Err(err!("[RESOURCE] multiple 'port' values"));
474 }
475 port = Some(parse_u16(&vs[0]).context("[RESOURCES] port")?);
476 } else {
477 return Err(err!("[RESOURCE] invalid 'port' option"));
478 }
479 } else if k == "timeout" {
480 if vs.len() == 1 {
481 if timeout.is_some() {
482 return Err(err!("[RESOURCE] multiple 'timeout' values"));
483 }
484 timeout = Some(parse_duration(&vs[0]).context("[RESOURCES] timeout")?);
485 } else {
486 return Err(err!("[RESOURCE] invalid 'timeout' option"));
487 }
488 } else if k == "users" {
489 if !users.is_empty() {
490 return Err(err!("[RESOURCE] multiple 'users' values"));
491 }
492 users = vs.clone();
493 } else {
494 return Err(err!("[RESOURCE] unknown option: {k}"));
495 }
496 }
497 MapItem::Values(vs) => {
498 for v in vs {
499 match &v.to_lowercase()[..] {
500 "tcp" => {
501 tcp = true;
502 }
503 "udp" => {
504 udp = true;
505 }
506 v => {
507 return Err(err!("[RESOURCE] unknown option: {v}"));
508 }
509 }
510 }
511 }
512 }
513 }
514 if !tcp && !udp {
515 tcp = true;
517 }
518 let Some(port) = port else {
519 return Err(err!("[RESOURCE] '{id}': No 'port' value present"));
520 };
521
522 let users = extract_users(id, &users)?;
523
524 for (_, res) in resources.iter() {
525 match res {
526 Resource::Port { port: res_port, .. } => {
527 if *res_port == port {
528 return Err(err!(
529 "[RESOURCE] Multiple definitions of resource port '{port}'"
530 ));
531 }
532 }
533 Resource::Jump { .. } => (),
534 }
535 }
536
537 resources.insert(
538 id,
539 Resource::Port {
540 id,
541 port,
542 tcp,
543 udp,
544 timeout,
545 users,
546 },
547 );
548 Ok(())
549}
550
551fn extract_resource_jump(
552 id: ResourceId,
553 resources: &mut HashMap<ResourceId, Resource>,
554 map: &Map,
555) -> ah::Result<()> {
556 let mut input: Option<String> = None;
557 let mut input_match_saddr = false;
558 let mut forward: Option<String> = None;
559 let mut forward_match_saddr = false;
560 let mut output: Option<String> = None;
561 let mut output_match_saddr = false;
562 let mut timeout: Option<Duration> = None;
563 let mut users: Vec<String> = vec![];
564
565 for item in map.items() {
566 match item {
567 MapItem::KeyValues(k, vs) => {
568 if k == "jump" {
569 return Err(err!("[RESOURCE] invalid 'jump' option"));
570 } else if k == "input" {
571 if vs.len() == 1 {
572 input = Some(vs[0].trim().to_string());
573 } else {
574 return Err(err!("[RESOURCE] invalid 'input' option"));
575 }
576 } else if k == "input-match" {
577 if vs.len() == 1 && vs[0].trim() == "saddr" {
578 input_match_saddr = true;
579 } else {
580 return Err(err!("[RESOURCE] invalid 'input-match' option"));
581 }
582 } else if k == "forward" {
583 if vs.len() == 1 {
584 forward = Some(vs[0].trim().to_string());
585 } else {
586 return Err(err!("[RESOURCE] invalid 'forward' option"));
587 }
588 } else if k == "forward-match" {
589 if vs.len() == 1 && vs[0].trim() == "saddr" {
590 forward_match_saddr = true;
591 } else {
592 return Err(err!("[RESOURCE] invalid 'forward-match' option"));
593 }
594 } else if k == "output" {
595 if vs.len() == 1 {
596 output = Some(vs[0].trim().to_string());
597 } else {
598 return Err(err!("[RESOURCE] invalid 'output' option"));
599 }
600 } else if k == "output-match" {
601 if vs.len() == 1 && vs[0].trim() == "saddr" {
602 output_match_saddr = true;
603 } else {
604 return Err(err!("[RESOURCE] invalid 'output-match' option"));
605 }
606 } else if k == "timeout" {
607 if vs.len() == 1 {
608 if timeout.is_some() {
609 return Err(err!("[RESOURCE] multiple 'timeout' values"));
610 }
611 timeout = Some(parse_duration(&vs[0]).context("[RESOURCES] timeout")?);
612 } else {
613 return Err(err!("[RESOURCE] invalid 'timeout' option"));
614 }
615 } else if k == "users" {
616 if !users.is_empty() {
617 return Err(err!("[RESOURCE] multiple 'users' values"));
618 }
619 users = vs.clone();
620 } else {
621 return Err(err!("[RESOURCE] unknown option: {k}"));
622 }
623 }
624 MapItem::Values(vs) => {
625 if vs.len() == 1 && vs[0].trim() == "jump" {
626 } else {
628 return Err(err!("[RESOURCE] unknown values: {vs:?}"));
629 }
630 }
631 }
632 }
633
634 if input.is_none() && forward.is_none() && output.is_none() {
635 return Err(err!(
636 "[RESOURCE] '{id}': 'jump' resource has no 'input', 'forward' or 'output' target."
637 ));
638 }
639
640 let users = extract_users(id, &users)?;
641
642 resources.insert(
643 id,
644 Resource::Jump {
645 id,
646 input,
647 input_match_saddr,
648 forward,
649 forward_match_saddr,
650 output,
651 output_match_saddr,
652 timeout,
653 users,
654 },
655 );
656 Ok(())
657}
658
659fn get_resources(ini: &Ini) -> ah::Result<HashMap<ResourceId, Resource>> {
660 let mut resources = HashMap::new();
661 if let Some(options) = ini.options_iter("RESOURCES") {
662 for (id, resource) in options {
663 let id: ResourceId = id.parse().context("[RESOURCES]")?;
664
665 for res_id in resources.keys() {
666 if *res_id == id {
667 return Err(err!(
668 "[RESOURCE] Multiple definitions of resource ID '{id}'"
669 ));
670 }
671 }
672
673 let map = resource.parse::<Map>().context("[RESOURCES]")?;
674 let mut is_port = false;
675 let mut is_jump = false;
676
677 for item in map.items() {
678 match item.key() {
679 Some("port") => is_port = true,
680 Some("jump") => is_jump = true,
681 _ => (),
682 }
683 }
684
685 if is_port && !is_jump {
686 extract_resource_port(id, &mut resources, &map)?;
687 } else if !is_port && is_jump {
688 extract_resource_jump(id, &mut resources, &map)?;
689 } else {
690 return Err(err!(
691 "[RESOURCE] Resource ID '{id}' is not a 'port' resource."
692 ));
693 };
694 }
695 }
696 Ok(resources)
697}
698
699fn get_default_user(ini: &Ini) -> ah::Result<UserId> {
700 if let Some(default_user) = ini.get("CLIENT", "default-user") {
701 return default_user.parse();
702 }
703 Ok(Default::default())
704}
705
706fn get_nft_exe(ini: &Ini) -> ah::Result<PathBuf> {
707 if let Some(nft_exe) = ini.get("NFTABLES", "exe") {
708 return Ok(nft_exe.trim().into());
709 }
710 Ok("nft".into())
711}
712
713fn get_nft_family(ini: &Ini) -> ah::Result<String> {
714 if let Some(nft_family) = ini.get("NFTABLES", "family") {
715 let nft_family = nft_family.trim();
716 Ok(match nft_family {
717 "inet" | "ip" | "ip6" => nft_family,
718 nft_family => {
719 return Err(err!("[NFTABLES] family={nft_family} is invalid"));
720 }
721 }
722 .to_string())
723 } else {
724 Ok("".to_string())
725 }
726}
727
728fn get_nft_table(ini: &Ini) -> ah::Result<String> {
729 if let Some(nft_table) = ini.get("NFTABLES", "table") {
730 Ok(nft_table.trim().to_string())
731 } else {
732 Ok("".to_string())
733 }
734}
735
736fn get_nft_chain(ini: &Ini, field: &str) -> ah::Result<String> {
737 if let Some(chain) = ini.get("NFTABLES", field) {
738 let chain = chain.trim().to_string();
739 if chain.len() > MAX_CHAIN_LEN {
740 Err(err!(
741 "[NFTABLES] {} is {} bytes long. \
742 Which exceeds the maximum of {} bytes. \
743 Please choose a smaller chain name.",
744 field,
745 chain.len(),
746 MAX_CHAIN_LEN
747 ))
748 } else {
749 Ok(chain)
750 }
751 } else {
752 Ok("".to_string())
753 }
754}
755
756fn get_nft_chain_input(ini: &Ini) -> ah::Result<String> {
757 get_nft_chain(ini, "chain-input")
758}
759
760fn get_nft_chain_forward(ini: &Ini) -> ah::Result<String> {
761 get_nft_chain(ini, "chain-forward")
762}
763
764fn get_nft_chain_output(ini: &Ini) -> ah::Result<String> {
765 get_nft_chain(ini, "chain-output")
766}
767
768fn get_nft_timeout(ini: &Ini) -> ah::Result<Duration> {
769 if let Some(nft_timeout) = ini.get("NFTABLES", "timeout") {
770 parse_duration(nft_timeout)
771 } else {
772 Ok(DEFAULT_NFT_TIMEOUT)
773 }
774}
775
776#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
778pub enum ConfigVariant {
779 #[default]
781 Server,
782 Client,
784}
785
786#[derive(Clone, Default, Debug)]
788pub struct Config {
789 checksum: ConfigChecksum,
790 variant: ConfigVariant,
791 path: Option<PathBuf>,
792 debug: bool,
793 port: ControlPort,
794 control_timeout: Duration,
795 control_error_policy: ErrorPolicy,
796 seccomp: Seccomp,
797 keys: HashMap<UserId, Key>,
798 resources: HashMap<ResourceId, Resource>,
799 default_user: UserId,
800 nft_exe: PathBuf,
801 nft_family: String,
802 nft_table: String,
803 nft_chain_input: String,
804 nft_chain_forward: String,
805 nft_chain_output: String,
806 nft_timeout: Duration,
807}
808
809impl Config {
810 pub fn new(variant: ConfigVariant) -> Self {
812 Self {
813 checksum: Default::default(),
814 variant,
815 control_timeout: DEFAULT_CONTROL_TIMEOUT,
816 nft_timeout: DEFAULT_NFT_TIMEOUT,
817 ..Default::default()
818 }
819 }
820
821 pub fn get_default_path(variant: ConfigVariant) -> PathBuf {
823 let prefix = match option_env!("LETMEIN_CONF_PREFIX") {
826 Some(env_prefix) => env_prefix,
827 None => {
828 #[cfg(not(target_os = "windows"))]
829 let prefix = "/";
830 #[cfg(target_os = "windows")]
831 let prefix = "";
832 prefix
833 }
834 };
835
836 let mut path = PathBuf::new();
837 path.push(prefix);
838 match variant {
839 ConfigVariant::Client => {
840 path.push(CLIENT_CONF_PATH);
841 }
842 ConfigVariant::Server => {
843 path.push(SERVER_CONF_PATH);
844 }
845 }
846 path
847 }
848
849 pub fn get_path(&self) -> Option<&Path> {
851 self.path.as_deref()
852 }
853
854 pub fn load(&mut self, path: &Path) -> ah::Result<()> {
856 if let Ok(ini) = Ini::new_from_file(path) {
857 self.load_ini(&ini)?;
858 } else if self.variant == ConfigVariant::Server {
859 return Err(err!("Failed to load configuration {path:?}"));
860 }
861 self.path = Some(path.to_path_buf());
862 Ok(())
863 }
864
865 pub fn load_ini(&mut self, ini: &Ini) -> ah::Result<()> {
867 let mut default_user = Default::default();
868 let mut nft_exe = Default::default();
869 let mut nft_family = Default::default();
870 let mut nft_table = Default::default();
871 let mut nft_chain_input = Default::default();
872 let mut nft_chain_forward = Default::default();
873 let mut nft_chain_output = Default::default();
874 let mut nft_timeout = DEFAULT_NFT_TIMEOUT;
875
876 let debug = get_debug(ini)?;
877 let port = get_port(ini)?;
878 let control_timeout = get_control_timeout(ini)?;
879 let control_error_policy = get_control_error_policy(ini)?;
880 let seccomp = get_seccomp(ini)?;
881 let keys = get_keys(ini)?;
882 let resources = get_resources(ini)?;
883 if self.variant == ConfigVariant::Client {
884 default_user = get_default_user(ini)?;
885 }
886 if self.variant == ConfigVariant::Server {
887 nft_exe = get_nft_exe(ini)?;
888 nft_family = get_nft_family(ini)?;
889 nft_table = get_nft_table(ini)?;
890 nft_chain_input = get_nft_chain_input(ini)?;
891 nft_chain_forward = get_nft_chain_forward(ini)?;
892 nft_chain_output = get_nft_chain_output(ini)?;
893 nft_timeout = get_nft_timeout(ini)?;
894 }
895
896 self.checksum = ini.checksum().clone();
897 self.debug = debug;
898 self.port = port;
899 self.control_timeout = control_timeout;
900 self.control_error_policy = control_error_policy;
901 self.seccomp = seccomp;
902 self.keys = keys;
903 self.resources = resources;
904 self.default_user = default_user;
905 self.nft_exe = nft_exe;
906 self.nft_family = nft_family;
907 self.nft_table = nft_table;
908 self.nft_chain_input = nft_chain_input;
909 self.nft_chain_forward = nft_chain_forward;
910 self.nft_chain_output = nft_chain_output;
911 self.nft_timeout = nft_timeout;
912 Ok(())
913 }
914
915 pub fn checksum(&self) -> &ConfigChecksum {
917 &self.checksum
918 }
919
920 pub fn debug(&self) -> bool {
922 self.debug
923 }
924
925 pub fn port(&self) -> ControlPort {
927 self.port
928 }
929
930 pub fn control_timeout(&self) -> Duration {
932 self.control_timeout
933 }
934
935 pub fn control_error_policy(&self) -> ErrorPolicy {
937 self.control_error_policy
938 }
939
940 pub fn seccomp(&self) -> Seccomp {
942 self.seccomp
943 }
944
945 pub fn users(&self) -> Vec<UserId> {
947 let mut users: Vec<UserId> = self.keys.keys().cloned().collect();
948 users.sort();
949 users
950 }
951
952 pub fn key(&self, id: UserId) -> Option<&Key> {
954 self.keys.get(&id)
955 }
956
957 pub fn resources(&self) -> Vec<Resource> {
959 let mut resources: Vec<Resource> = self.resources.values().cloned().collect();
960 resources.sort_by_key(|r| r.id());
961 resources
962 }
963
964 pub fn resource(&self, id: ResourceId) -> Option<&Resource> {
966 self.resources.get(&id)
967 }
968
969 pub fn resource_id_by_port(&self, port: u16, user_id: Option<UserId>) -> Option<ResourceId> {
971 for (k, v) in &self.resources {
972 match v {
973 Resource::Port { port: p, .. } => {
974 if *p == port {
975 if let Some(user_id) = user_id {
976 if v.contains_user(user_id) {
977 return Some(*k);
978 }
979 } else {
980 return Some(*k);
981 }
982 }
983 }
984 Resource::Jump { .. } => (),
985 }
986 }
987 None
988 }
989
990 pub fn default_user(&self) -> UserId {
992 self.default_user
993 }
994
995 pub fn nft_exe(&self) -> &Path {
997 &self.nft_exe
998 }
999
1000 pub fn nft_family(&self) -> &str {
1002 &self.nft_family
1003 }
1004
1005 pub fn nft_table(&self) -> &str {
1007 &self.nft_table
1008 }
1009
1010 pub fn nft_chain_input(&self) -> &str {
1012 &self.nft_chain_input
1013 }
1014
1015 pub fn nft_chain_forward(&self) -> &str {
1017 &self.nft_chain_forward
1018 }
1019
1020 pub fn nft_chain_output(&self) -> &str {
1022 &self.nft_chain_output
1023 }
1024
1025 pub fn nft_timeout(&self) -> Duration {
1027 self.nft_timeout
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034
1035 #[test]
1036 fn test_general() {
1037 let mut ini = Ini::new();
1038 let cs_empty = ini.checksum().clone();
1039 ini.parse_str(
1040 "[GENERAL]\ndebug = true\nport = 1234\ncontrol-timeout=1.5\n\
1041 control-error-policy= basic-auth \nseccomp = kill",
1042 )
1043 .unwrap();
1044 let cs_parsed = ini.checksum().clone();
1045 assert_ne!(cs_empty, cs_parsed);
1046 assert!(get_debug(&ini).unwrap());
1047 assert_eq!(
1048 get_port(&ini).unwrap(),
1049 ControlPort {
1050 port: 1234,
1051 tcp: true,
1052 udp: false
1053 }
1054 );
1055 assert_eq!(
1056 get_control_timeout(&ini).unwrap(),
1057 Duration::from_millis(1500)
1058 );
1059 assert_eq!(
1060 get_control_error_policy(&ini).unwrap(),
1061 ErrorPolicy::BasicAuth
1062 );
1063 assert_eq!(get_seccomp(&ini).unwrap(), Seccomp::Kill);
1064 }
1065
1066 #[test]
1067 fn test_port() {
1068 let mut ini = Ini::new();
1069 ini.parse_str("[GENERAL]\nport=1234").unwrap();
1070 let cs_a = ini.checksum().clone();
1071 assert_eq!(
1072 get_port(&ini).unwrap(),
1073 ControlPort {
1074 port: 1234,
1075 tcp: true,
1076 udp: false
1077 }
1078 );
1079 ini.parse_str("[GENERAL]\nport=1234 / TCP ").unwrap();
1080 let cs_b = ini.checksum().clone();
1081 assert_eq!(
1082 get_port(&ini).unwrap(),
1083 ControlPort {
1084 port: 1234,
1085 tcp: true,
1086 udp: false
1087 }
1088 );
1089 ini.parse_str("[GENERAL]\nport=1234/UDP").unwrap();
1090 let cs_c = ini.checksum().clone();
1091 assert_eq!(
1092 get_port(&ini).unwrap(),
1093 ControlPort {
1094 port: 1234,
1095 tcp: false,
1096 udp: true
1097 }
1098 );
1099 ini.parse_str("[GENERAL]\nport=1234/ UDP , TCP").unwrap();
1100 assert_eq!(
1101 get_port(&ini).unwrap(),
1102 ControlPort {
1103 port: 1234,
1104 tcp: true,
1105 udp: true
1106 }
1107 );
1108 ini.parse_str("[GENERAL]\nport=udp, tcp / 1234").unwrap();
1109 assert_eq!(
1110 get_port(&ini).unwrap(),
1111 ControlPort {
1112 port: 1234,
1113 tcp: true,
1114 udp: true
1115 }
1116 );
1117 assert_ne!(cs_a, cs_b);
1118 assert_ne!(cs_b, cs_c);
1119 }
1120
1121 #[test]
1122 fn test_keys() {
1123 let mut ini = Ini::new();
1124 ini.parse_str(
1125 "[KEYS]\nABCD1234 = 998877665544332211009988776655443322110099887766554433221100CDEF\n",
1126 )
1127 .unwrap();
1128 let keys = get_keys(&ini).unwrap();
1129 assert_eq!(
1130 keys.get(&0xABCD1234.into()).unwrap(),
1131 &[
1132 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x99, 0x88, 0x77, 0x66,
1133 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22,
1134 0x11, 0x00, 0xCD, 0xEF
1135 ]
1136 );
1137 }
1138
1139 #[test]
1140 fn test_resources_port() {
1141 let mut ini = Ini::new();
1142 ini.parse_str("[RESOURCES]\n9876ABCD = port : 4096\n")
1143 .unwrap();
1144 let resources = get_resources(&ini).unwrap();
1145 assert_eq!(
1146 resources.get(&0x9876ABCD.into()).unwrap(),
1147 &Resource::Port {
1148 id: 0x9876ABCD.into(),
1149 port: 4096,
1150 tcp: true,
1151 udp: false,
1152 timeout: None,
1153 users: vec![]
1154 }
1155 );
1156
1157 let mut ini = Ini::new();
1158 ini.parse_str("[RESOURCES]\n9876ABCD = port : 4096 / TCP\n")
1159 .unwrap();
1160 let resources = get_resources(&ini).unwrap();
1161 assert_eq!(
1162 resources.get(&0x9876ABCD.into()).unwrap(),
1163 &Resource::Port {
1164 id: 0x9876ABCD.into(),
1165 port: 4096,
1166 tcp: true,
1167 udp: false,
1168 timeout: None,
1169 users: vec![]
1170 }
1171 );
1172
1173 let mut ini = Ini::new();
1174 ini.parse_str("[RESOURCES]\n9876ABCD = port : 4096 / udp / users: 1, 2 ,3 \n")
1175 .unwrap();
1176 let resources = get_resources(&ini).unwrap();
1177 assert_eq!(
1178 resources.get(&0x9876ABCD.into()).unwrap(),
1179 &Resource::Port {
1180 id: 0x9876ABCD.into(),
1181 port: 4096,
1182 tcp: false,
1183 udp: true,
1184 timeout: None,
1185 users: vec![1.into(), 2.into(), 3.into()]
1186 }
1187 );
1188
1189 let mut ini = Ini::new();
1190 ini.parse_str("[RESOURCES]\n9876ABCD = port : 4096 / udp, tcp / users: 4 / timeout:42\n")
1191 .unwrap();
1192 let resources = get_resources(&ini).unwrap();
1193 assert_eq!(
1194 resources.get(&0x9876ABCD.into()).unwrap(),
1195 &Resource::Port {
1196 id: 0x9876ABCD.into(),
1197 port: 4096,
1198 tcp: true,
1199 udp: true,
1200 timeout: Some(Duration::from_secs(42)),
1201 users: vec![4.into()]
1202 }
1203 );
1204 }
1205
1206 #[test]
1207 fn test_resources_jump() {
1208 let mut ini = Ini::new();
1209 ini.parse_str("[RESOURCES]\n1234FEDC = jump / input: FOO\n")
1210 .unwrap();
1211 let resources = get_resources(&ini).unwrap();
1212 assert_eq!(
1213 resources.get(&0x1234FEDC.into()).unwrap(),
1214 &Resource::Jump {
1215 id: 0x1234FEDC.into(),
1216 input: Some("FOO".to_string()),
1217 input_match_saddr: false,
1218 forward: None,
1219 forward_match_saddr: false,
1220 output: None,
1221 output_match_saddr: false,
1222 timeout: None,
1223 users: vec![]
1224 }
1225 );
1226
1227 let mut ini = Ini::new();
1228 ini.parse_str(
1229 "[RESOURCES]\n1234FEDC = jump / input: FOO / forward:BAR / input-match: saddr\n",
1230 )
1231 .unwrap();
1232 let resources = get_resources(&ini).unwrap();
1233 assert_eq!(
1234 resources.get(&0x1234FEDC.into()).unwrap(),
1235 &Resource::Jump {
1236 id: 0x1234FEDC.into(),
1237 input: Some("FOO".to_string()),
1238 input_match_saddr: true,
1239 forward: Some("BAR".to_string()),
1240 forward_match_saddr: false,
1241 output: None,
1242 output_match_saddr: false,
1243 timeout: None,
1244 users: vec![]
1245 }
1246 );
1247
1248 let mut ini = Ini::new();
1249 ini.parse_str("[RESOURCES]\n1234FEDC = jump / input: FOO / forward:BAR / output: BIZ / users: 1, 10, 100 / output-match: saddr\n")
1250 .unwrap();
1251 let resources = get_resources(&ini).unwrap();
1252 assert_eq!(
1253 resources.get(&0x1234FEDC.into()).unwrap(),
1254 &Resource::Jump {
1255 id: 0x1234FEDC.into(),
1256 input: Some("FOO".to_string()),
1257 input_match_saddr: false,
1258 forward: Some("BAR".to_string()),
1259 forward_match_saddr: false,
1260 output: Some("BIZ".to_string()),
1261 output_match_saddr: true,
1262 timeout: None,
1263 users: vec![0x1.into(), 0x10.into(), 0x100.into()]
1264 }
1265 );
1266
1267 let mut ini = Ini::new();
1268 ini.parse_str(
1269 "[RESOURCES]\n1234FEDC = jump / forward:BAR / forward-match: saddr / timeout: 3\n",
1270 )
1271 .unwrap();
1272 let resources = get_resources(&ini).unwrap();
1273 assert_eq!(
1274 resources.get(&0x1234FEDC.into()).unwrap(),
1275 &Resource::Jump {
1276 id: 0x1234FEDC.into(),
1277 input: None,
1278 input_match_saddr: false,
1279 forward: Some("BAR".to_string()),
1280 forward_match_saddr: true,
1281 output: None,
1282 output_match_saddr: false,
1283 timeout: Some(Duration::from_secs(3)),
1284 users: vec![]
1285 }
1286 );
1287 }
1288
1289 #[test]
1290 fn test_client() {
1291 let mut ini = Ini::new();
1292 ini.parse_str("[CLIENT]\ndefault-user = 123\n").unwrap();
1293 let default_user = get_default_user(&ini).unwrap();
1294 assert_eq!(default_user, 0x123.into());
1295 }
1296
1297 #[test]
1298 fn test_nft() {
1299 let mut ini = Ini::new();
1300 ini.parse_str(
1301 "[NFTABLES]\nexe = mynft \nfamily = ip6\ntable = myfilter\nchain-input = myLETMEIN-INPUT\ntimeout = 50\n",
1302 )
1303 .unwrap();
1304 let nft_exe = get_nft_exe(&ini).unwrap();
1305 let nft_family = get_nft_family(&ini).unwrap();
1306 let nft_table = get_nft_table(&ini).unwrap();
1307 let nft_chain_input = get_nft_chain_input(&ini).unwrap();
1308 let nft_timeout = get_nft_timeout(&ini).unwrap();
1309 assert_eq!(nft_exe, Path::new("mynft"));
1310 assert_eq!(nft_family, "ip6");
1311 assert_eq!(nft_table, "myfilter");
1312 assert_eq!(nft_chain_input, "myLETMEIN-INPUT");
1313 assert_eq!(nft_timeout, Duration::from_secs(50));
1314 }
1315
1316 #[test]
1317 fn test_checksum() {
1318 let checksum = ConfigChecksum::calculate(b"foo");
1319 assert_eq!(
1320 checksum.as_bytes(),
1321 &[
1322 169, 20, 32, 235, 39, 155, 209, 150, 21, 4, 157, 0, 214, 7, 7, 53, 175, 241, 233,
1323 40, 193, 191, 156, 101, 63, 41, 34, 51, 17, 221, 76, 170
1324 ]
1325 );
1326 }
1327}
1328
1329