1 // Copyright 2021 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: GPL-3.0-or-later
3 // There is NO WARRANTY.
7 #[derive(hippotat_macros::ResolveConfig)]
9 pub struct InstanceConfig {
10 // Exceptional settings
11 #[special(special_link, SKL::None)] pub link: LinkName,
12 #[per_client] pub secret: Secret,
13 #[global] #[special(special_ipif, SKL::PerClient)] pub ipif: String,
16 #[limited] pub max_batch_down: u32,
17 #[limited] pub max_queue_time: Duration,
18 #[limited] pub http_timeout: Duration,
19 #[limited] pub target_requests_outstanding: u32,
21 // Ordinary settings, used by both, not client-specifi:
22 #[global] pub addrs: Vec<IpAddr>,
23 #[global] pub vnetwork: Vec<IpNet>,
24 #[global] pub vaddr: IpAddr,
25 #[global] pub vrelay: IpAddr,
26 #[global] pub port: u16,
27 #[global] pub mtu: u32,
29 // Ordinary settings, used by server only:
30 #[server] #[per_client] pub max_clock_skew: Duration,
31 #[server] #[global] pub ifname_server: String,
33 // Ordinary settings, used by client only:
34 #[client] pub http_timeout_grace: Duration,
35 #[client] pub max_requests_outstanding: u32,
36 #[client] pub max_batch_up: u32,
37 #[client] pub http_retry: Duration,
38 #[client] pub success_report_interval: Duration,
39 #[client] pub url: Uri,
40 #[client] pub vroutes: Vec<IpNet>,
41 #[client] pub ifname_client: String,
43 // Computed, rather than looked up. Client only:
44 #[computed] pub effective_http_timeout: Duration,
47 static DEFAULT_CONFIG: &str = r#"
49 max_batch_down = 65536
51 target_requests_outstanding = 3
53 http_timeout_grace = 5
54 max_requests_outstanding = 6
59 ifname_client = hippo%d
60 ifname_server = shippo%d
62 success_report_interval = 3600
64 ipif = userv root ipif %{local},%{peer},%{mtu},slip,%{ifname} '%{rnets}'
68 vnetwork = 172.24.230.192
71 max_batch_down = 262144
74 target_requests_outstanding = 10
77 #[derive(StructOpt,Debug)]
79 /// Top-level config file or directory
81 /// Look for `main.cfg`, `config.d` and `secrets.d` here.
83 /// Or if this is a file, just read that file.
84 #[structopt(long, default_value="/etc/hippotat")]
87 /// Additional config files or dirs, which can override the others
88 #[structopt(long, multiple=true, number_of_values=1)]
89 pub extra_config: Vec<PathBuf>,
94 fn sat(self) -> usize { self.try_into().unwrap_or(usize::MAX) }
98 impl<'s> Option<&'s str> {
100 fn value(self) -> &'s str {
101 self.ok_or_else(|| anyhow!("value needed"))?
106 pub struct Secret(pub String);
107 impl Parseable for Secret {
109 fn parse(s: Option<&str>) -> Self {
111 if s.is_empty() { throw!(anyhow!("secret value cannot be empty")) }
115 fn default() -> Self { Secret(default()) }
117 impl Debug for Secret {
118 #[throws(fmt::Error)]
119 fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "Secret(***)")? }
122 #[derive(Debug,Clone,Hash,Eq,PartialEq)]
123 pub enum SectionName {
126 Server(ServerName), // includes SERVER, which is slightly special
127 ServerLimit(ServerName),
131 pub use SectionName as SN;
134 struct RawValRef<'v,'l,'s> {
135 raw: Option<&'v str>, // todo: not Option any more
138 section: &'s SectionName,
141 impl<'v> RawValRef<'v,'_,'_> {
143 fn try_map<F,T>(&self, f: F) -> T
144 where F: FnOnce(Option<&'v str>) -> Result<T, AE> {
146 .with_context(|| format!(r#"file {:?}, section {}, key "{}""#,
147 self.loc, self.section, self.key))?
155 static OUTSIDE_SECTION: &str = "[";
156 static SPECIAL_SERVER_SECTION: &str = "SERVER";
161 keys_allowed: HashMap<&'static str, SectionKindList>,
162 sections: HashMap<SectionName, ini::Section>,
165 type OkAnyway<'f,A> = &'f dyn Fn(ErrorKind) -> Option<A>;
167 impl<'f,A> OkAnyway<'f,A> {
168 fn ok<T>(self, r: &Result<T, io::Error>) -> Option<A> {
169 let e = r.as_ref().err()?;
176 impl FromStr for SectionName {
179 fn from_str(s: &str) -> Self {
181 "COMMON" => return SN::Common,
182 "LIMIT" => return SN::GlobalLimit,
185 if let Ok(n@ ServerName(_)) = s.parse() { return SN::Server(n) }
186 if let Ok(n@ ClientName(_)) = s.parse() { return SN::Client(n) }
187 let (server, client) = s.split_ascii_whitespace().collect_tuple()
188 .ok_or_else(|| anyhow!(
189 "bad section name {:?} \
190 (must be COMMON, DEFAULT, <server>, <client>, or <server> <client>",
193 let server = server.parse().context("server name in link section name")?;
194 if client == "LIMIT" { return SN::ServerLimit(server) }
195 let client = client.parse().context("client name in link section name")?;
196 SN::Link(LinkName { server, client })
199 impl Display for InstanceConfig {
200 #[throws(fmt::Error)]
201 fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.link, f)? }
204 impl Display for SectionName {
205 #[throws(fmt::Error)]
206 fn fmt(&self, f: &mut fmt::Formatter) {
208 SN::Link (ref l) => Display::fmt(l, f)?,
209 SN::Client(ref c) => write!(f, "[{}]" , c)?,
210 SN::Server(ref s) => write!(f, "[{}]" , s)?,
211 SN::ServerLimit(ref s) => write!(f, "[{} LIMIT] ", s)?,
212 SN::GlobalLimit => write!(f, "[LIMIT]" )?,
213 SN::Common => write!(f, "[COMMON]" )?,
221 keys_allowed: HashMap<&'static str, SectionKindList>
222 ) -> Self { Aggregate {
227 #[throws(AE)] // AE does not include path
228 fn read_file<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
230 let f = fs::File::open(path);
231 if let Some(anyway) = anyway.ok(&f) { return Some(anyway) }
232 let mut f = f.context("open")?;
234 let mut s = String::new();
235 let y = f.read_to_string(&mut s);
236 if let Some(anyway) = anyway.ok(&y) { return Some(anyway) }
239 self.read_string(s, path)?;
243 #[throws(AE)] // AE does not include path
244 fn read_string(&mut self, s: String, path_for_loc: &Path) {
245 let mut map: ini::Parsed = default();
246 ini::read(&mut map, &mut s.as_bytes(), path_for_loc)
247 .context("parse as INI")?;
248 if map.get(OUTSIDE_SECTION).is_some() {
249 throw!(anyhow!("INI file contains settings outside a section"));
252 for (sn, section) in map {
253 let sn = sn.parse().dcontext(&sn)?;
254 let vars = §ion.values;
256 for key in vars.keys() {
257 let skl = if key == "server" {
260 *self.keys_allowed.get(key.as_str()).ok_or_else(
261 || anyhow!("unknown configuration key {:?}", key)
264 if ! skl.contains(&sn, self.end) {
265 throw!(anyhow!("configuration key {:?} not applicable \
266 in this kind of section: {}", key, &sn))
270 let ent = self.sections.entry(sn)
271 .or_insert_with(|| ini::Section {
272 loc: section.loc.clone(),
276 for (key, ini::Val { val: raw, loc }) in vars {
277 let val = if raw.starts_with('\'') || raw.starts_with('"') {
279 if raw.contains('\\') {
281 anyhow!("quoted value contains backslash, not supported")
284 let quote = &raw[0..1];
286 let unq = raw[1..].strip_suffix(quote)
288 || anyhow!("mismatched quotes around quoted value")
291 if unq.contains(quote) {
293 "quoted value contains quote (escaping not supported)"
299 .with_context(|| format!("key {:?}", key))
300 .dcontext(path_for_loc)?
304 let key = key.replace('-',"_");
305 ent.values.insert(key, ini::Val { val, loc: loc.clone() });
310 #[throws(AE)] // AE includes path
311 fn read_dir_d<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
313 let dir = fs::read_dir(path);
314 if let Some(anyway) = anyway.ok(&dir) { return Some(anyway) }
315 let dir = dir.context("open directory").dcontext(path)?;
317 let ent = ent.context("read directory").dcontext(path)?;
318 let leaf = ent.file_name();
319 let leaf = leaf.to_str();
320 let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
321 if leaf.len() == 0 { continue }
322 if ! leaf.chars().all(
323 |c| c=='-' || c=='_' || c.is_ascii_alphanumeric()
326 // OK we want this one
327 let ent = ent.path();
328 self.read_file(&ent, &|_| None::<Void>).dcontext(&ent)?;
333 #[throws(AE)] // AE includes everything
334 fn read_toplevel(&mut self, toplevel: &Path) {
335 enum Anyway { None, Dir }
336 match self.read_file(toplevel, &|k| match k {
337 EK::NotFound => Some(Anyway::None),
338 EK::IsADirectory => Some(Anyway::Dir),
341 .dcontext(toplevel).context("top-level config directory (or file)")?
343 None | Some(Anyway::None) => { },
345 Some(Anyway::Dir) => {
347 let anyway_none = |k| match k {
348 EK::NotFound => Some(AnywayNone),
352 let mk = |leaf: &str| {
353 [ toplevel, &PathBuf::from(leaf) ]
354 .iter().collect::<PathBuf>()
357 for &(try_main, desc) in &[
358 ("main.cfg", "main config file"),
359 ("master.cfg", "obsolete-named main config file"),
361 let main = mk(try_main);
363 match self.read_file(&main, &anyway_none)
364 .dcontext(main).context(desc)?
367 Some(AnywayNone) => { },
371 for &(try_dir, desc) in &[
372 ("config.d", "per-link config directory"),
373 ("secrets.d", "per-link secrets directory"),
375 let dir = mk(try_dir);
376 match self.read_dir_d(&dir, &anyway_none).context(desc)? {
378 Some(AnywayNone) => { },
385 #[throws(AE)] // AE includes extra, but does that this is extra
386 fn read_extra(&mut self, extra: &Path) {
389 match self.read_file(extra, &|k| match k {
390 EK::IsADirectory => Some(AnywayDir),
397 self.read_dir_d(extra, &|_| None::<Void>)?;
405 fn instances(&self, only_server: Option<&ServerName>) -> BTreeSet<LinkName> {
406 let mut links: BTreeSet<LinkName> = default();
408 let mut secrets_anyserver: BTreeSet<&ClientName> = default();
409 let mut secrets_anyclient: BTreeSet<&ServerName> = default();
410 let mut secret_global = false;
412 let mut putative_servers = BTreeSet::new();
413 let mut putative_clients = BTreeSet::new();
415 let mut note_server = |s| {
416 if let Some(only) = only_server { if s != only { return false } }
417 putative_servers.insert(s);
420 let mut note_client = |c| {
421 putative_clients.insert(c);
424 for (section, vars) in &self.sections {
425 let has_secret = || vars.values.contains_key("secret");
426 //dbg!(§ion, has_secret());
430 if ! note_server(&l.server) { continue }
431 note_client(&l.client);
432 if has_secret() { links.insert(l.clone()); }
434 SN::Server(ref s) => {
435 if ! note_server(s) { continue }
436 if has_secret() { secrets_anyclient.insert(s); }
438 SN::Client(ref c) => {
440 if has_secret() { secrets_anyserver.insert(c); }
443 if has_secret() { secret_global = true; }
449 //dbg!(&putative_servers, &putative_clients);
450 //dbg!(&secrets_anyserver, &secrets_anyclient, &secret_global);
452 // Add links which are justified by blanket secrets
453 for (client, server) in iproduct!(
454 putative_clients.into_iter().filter(
456 || secrets_anyserver.contains(c)
457 || ! secrets_anyclient.is_empty()
459 putative_servers.iter().cloned().filter(
461 || secrets_anyclient.contains(s)
462 || ! secrets_anyserver.is_empty()
465 links.insert(LinkName {
466 client: client.clone(),
467 server: server.clone(),
475 struct ResolveContext<'c> {
479 all_sections: Vec<SectionName>,
482 trait Parseable: Sized {
483 fn parse(s: Option<&str>) -> Result<Self, AE>;
484 fn default() -> Result<Self, AE> {
485 Err(anyhow!("setting must be specified"))
488 fn default_for_key(key: &str) -> Self {
489 Self::default().with_context(|| key.to_string())?
493 impl Parseable for Duration {
495 fn parse(s: Option<&str>) -> Duration {
496 // todo: would be nice to parse with humantime maybe
497 Duration::from_secs( s.value()?.parse()? )
500 macro_rules! parseable_from_str { ($t:ty $(, $def:expr)? ) => {
501 impl Parseable for $t {
503 fn parse(s: Option<&str>) -> $t { s.value()?.parse()? }
504 $( #[throws(AE)] fn default() -> Self { $def } )?
507 parseable_from_str!{u16, default() }
508 parseable_from_str!{u32, default() }
509 parseable_from_str!{String, default() }
510 parseable_from_str!{IpNet, default() }
511 parseable_from_str!{IpAddr, Ipv4Addr::UNSPECIFIED.into() }
512 parseable_from_str!{Uri, default() }
514 impl<T:Parseable> Parseable for Vec<T> {
516 fn parse(s: Option<&str>) -> Vec<T> {
518 .split_ascii_whitespace()
519 .map(|s| Parseable::parse(Some(s)))
520 .collect::<Result<Vec<_>,_>>()?
523 fn default() -> Self { default() }
527 #[derive(Debug,Copy,Clone,Eq,PartialEq)]
528 enum SectionKindList {
536 use SectionKindList as SKL;
539 fn special_server_section() -> Self { SN::Server(ServerName(
540 SPECIAL_SERVER_SECTION.into()
544 impl SectionKindList {
545 fn contains(self, s: &SectionName, end: LinkEnd) -> bool {
548 (SKL::Global, LinkEnd::Client) => matches!(s, SN::Link(_)
553 (SKL::Limits,_) => matches!(s, SN::ServerLimit(_)
556 (SKL::Global, LinkEnd::Server) => matches!(s, SN::Common
559 (SKL::Limited,_) => SKL::PerClient.contains(s, end)
560 | SKL::Limits .contains(s, end),
562 (SKL::ServerName,_) => matches!(s, SN::Common)
563 | matches!(s, SN::Server(ServerName(name))
564 if name == SPECIAL_SERVER_SECTION),
565 (SKL::None,_) => false,
571 fn lookup_raw<'a,'s,S>(&'a self, key: &'static str, sections: S)
572 -> Option<RawValRef<'a,'a,'s>>
573 where S: Iterator<Item=&'s SectionName>
575 for section in sections {
576 if let Some(val) = self.sections
578 .and_then(|s: &ini::Section| s.values.get(key))
580 return Some(RawValRef {
591 pub fn establish_server_name(&self) -> ServerName {
593 let raw = match self.lookup_raw(
595 [ &SectionName::Common, &SN::special_server_section() ].iter().cloned()
597 Some(raw) => raw.try_map(|os| os.value())?,
598 None => SPECIAL_SERVER_SECTION,
600 ServerName(raw.into())
604 impl<'c> ResolveContext<'c> {
605 fn first_of_raw(&'c self, key: &'static str, sections: SectionKindList)
606 -> Option<RawValRef<'c,'c,'c>> {
609 self.all_sections.iter()
610 .filter(|s| sections.contains(s, self.end))
615 fn first_of<T>(&self, key: &'static str, sections: SectionKindList)
619 match self.first_of_raw(key, sections) {
621 Some(raw) => Some(raw.try_map(Parseable::parse)?),
626 pub fn ordinary<T>(&self, key: &'static str, skl: SKL) -> T
629 match self.first_of(key, skl)? {
631 None => Parseable::default_for_key(key)?,
636 pub fn limited<T>(&self, key: &'static str, skl: SKL) -> T
637 where T: Parseable + Ord
639 assert_eq!(skl, SKL::Limited);
640 let val = self.ordinary(key, SKL::PerClient)?;
641 if let Some(limit) = self.first_of(key, SKL::Limits)? {
649 pub fn client<T>(&self, key: &'static str, skl: SKL) -> T
650 where T: Parseable + Default {
652 LinkEnd::Client => self.ordinary(key, skl)?,
653 LinkEnd::Server => default(),
657 pub fn server<T>(&self, key: &'static str, skl: SKL) -> T
658 where T: Parseable + Default {
660 LinkEnd::Server => self.ordinary(key, skl)?,
661 LinkEnd::Client => default(),
666 pub fn computed<T>(&self, _key: &'static str, skl: SKL) -> T
669 assert_eq!(skl, SKL::None);
674 pub fn special_ipif(&self, key: &'static str, skl: SKL) -> String {
675 assert_eq!(skl, SKL::PerClient); // we tolerate it in per-client sections
677 LinkEnd::Client => self.ordinary(key, SKL::PerClient)?,
678 LinkEnd::Server => self.ordinary(key, SKL::Global)?,
683 pub fn special_link(&self, _key: &'static str, skl: SKL) -> LinkName {
684 assert_eq!(skl, SKL::None);
689 impl InstanceConfig {
691 fn complete(&mut self, end: LinkEnd) {
692 let mut vhosts = self.vnetwork.iter()
693 .map(|n| n.hosts()).flatten()
694 .filter({ let vaddr = self.vaddr; move |v| v != &vaddr });
696 if self.vaddr.is_unspecified() {
697 self.vaddr = vhosts.next().ok_or_else(
698 || anyhow!("vnetwork too small to generate vaddrr")
701 if self.vrelay.is_unspecified() {
702 self.vrelay = vhosts.next().ok_or_else(
703 || anyhow!("vnetwork too small to generate vrelay")
709 move |max_batch, key| {
710 if max_batch/2 < mtu {
711 throw!(anyhow!("max batch {:?} ({}) must be >= 2 x mtu ({}) \
712 (to allow for SLIP ESC-encoding)",
713 key, max_batch, mtu))
721 if &self.url == &default::<Uri>() {
722 let addr = self.addrs.get(0).ok_or_else(
723 || anyhow!("client needs addrs or url set")
728 IpAddr::V4(a) => format!("{}", a),
729 IpAddr::V6(a) => format!("[{}]", a),
733 p => format!(":{}", p),
738 self.effective_http_timeout = {
739 let a = self.http_timeout;
740 let b = self.http_timeout_grace;
741 a.checked_add(b).ok_or_else(
742 || anyhow!("calculate effective http timeout ({:?} + {:?})", a, b)
747 let t = self.target_requests_outstanding;
748 let m = self.max_requests_outstanding;
749 if t > m { throw!(anyhow!(
750 "target_requests_outstanding ({}) > max_requests_outstanding ({})",
755 check_batch(self.max_batch_up, "max_batch_up")?;
759 if self.addrs.is_empty() {
760 throw!(anyhow!("missing 'addrs' setting"))
762 check_batch(self.max_batch_down, "max_batch_down")?;
767 fn subst(var: &mut String,
768 kv: &mut dyn Iterator<Item=(&'static str, &dyn Display)>
771 .map(|(k,v)| (k.to_string(), v.to_string()))
772 .collect::<HashMap<String, String>>();
773 let bad = parking_lot::Mutex::new(vec![]);
774 *var = regex_replace_all!(
775 r#"%(?:%|\((\w+)\)s|\{(\w+)\}|.)"#,
777 |whole, k1, k2| (|| Ok::<_,String>({
778 if whole == "%%" { "%" }
779 else if let Some(&k) = [k1,k2].iter().find(|&&s| s != "") {
780 substs.get(k).ok_or_else(
781 || format!("unknown key %({})s", k)
784 throw!(format!("bad percent escape {:?}", &whole));
786 }))().unwrap_or_else(|e| { bad.lock().push(e); "" })
788 let bad = bad.into_inner();
789 if ! bad.is_empty() {
790 throw!(anyhow!("substitution failed: {}", bad.iter().format("; ")));
796 type DD<'d> = &'d dyn Display;
797 fn dv<T:Display>(v: &[T]) -> String {
798 format!("{}", v.iter().format(" "))
800 let mut ipif = mem::take(&mut self.ipif); // lets us borrow all of self
801 let s = &self; // just for abbreviation, below
802 let vnetwork = dv(&s.vnetwork);
803 let vroutes = dv(&s.vroutes);
805 let keys = &["local", "peer", "rnets", "ifname"];
806 let values = match end {
807 Server => [&s.vaddr as DD , &s.vrelay, &vnetwork, &s.ifname_server],
808 Client => [&s.link.client as DD, &s.vaddr, &vroutes, &s.ifname_client],
811 ( "mtu", &s.mtu as DD ),
816 &mut keys.iter().cloned()
818 .chain(always.iter().cloned()),
825 trait ResolveGlobal<'i> where Self: 'i {
826 fn resolve<I>(it: I) -> Self
827 where I: Iterator<Item=&'i Self>;
829 impl<'i,T> ResolveGlobal<'i> for T where T: Eq + Clone + Debug + 'i {
830 fn resolve<I>(mut it: I) -> Self
831 where I: Iterator<Item=&'i Self>
833 let first = it.next().expect("empty instances no global!");
834 for x in it { assert_eq!(x, first); }
840 pub fn read(opts: &Opts, end: LinkEnd) -> Vec<InstanceConfig> {
842 let mut agg = Aggregate::new(
844 InstanceConfig::FIELDS.iter().cloned().collect(),
847 agg.read_string(DEFAULT_CONFIG.into(),
848 "<build-in defaults>".as_ref()).unwrap();
850 agg.read_toplevel(&opts.config)?;
851 for extra in &opts.extra_config {
852 agg.read_extra(extra).context("extra config")?;
855 //eprintln!("GOT {:#?}", agg);
858 })().context("read configuration")?;
860 let server_name = match end {
861 LinkEnd::Server => Some(agg.establish_server_name()?),
862 LinkEnd::Client => None,
865 let instances = agg.instances(server_name.as_ref());
866 let mut ics = vec![];
869 for link in instances {
870 let rctx = ResolveContext {
875 SN::Link(link.clone()),
876 SN::Client(link.client.clone()),
877 SN::Server(link.server.clone()),
879 SN::ServerLimit(link.server.clone()),
884 if rctx.first_of_raw("secret", SKL::PerClient).is_none() { continue }
886 let mut ic = InstanceConfig::resolve_instance(&rctx)
887 .with_context(|| format!("resolve config for {}", &link))?;
890 .with_context(|| format!("complete config for {}", &link))?;
898 pub fn startup<F,T>(progname: &str, end: LinkEnd,
899 opts: &Opts, logopts: &LogOpts,
901 -> (Vec<InstanceConfig>,T)
902 where F: FnOnce(&[InstanceConfig]) -> Result<T,AE>
906 let ics = config::read(opts, end)?;
907 if ics.is_empty() { throw!(anyhow!("no associations, quitting")); }
913 })().unwrap_or_else(|e| {
914 eprintln!("{}: startup error: {}", progname, &e);