1 // Copyright 2021 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: GPL-3.0-or-later
3 // There is NO WARRANTY.
7 use configparser::ini::Ini;
9 #[derive(hippotat_macros::ResolveConfig)]
10 #[derive(Debug,Clone)]
11 pub struct InstanceConfig {
12 // Exceptional settings
13 #[special(special_link, SKL::ServerName)] pub link: LinkName,
15 #[special(special_ipif, SKL::Ordinary)] pub ipif: String,
18 #[limited] pub max_batch_down: u32,
19 #[limited] pub max_queue_time: Duration,
20 #[limited] pub http_timeout: Duration,
21 #[limited] pub target_requests_outstanding: u32,
24 pub addrs: Vec<IpAddr>,
25 pub vnetwork: Vec<IpNet>,
30 pub ifname_server: String,
31 pub ifname_client: String,
33 // Ordinary settings, used by server only:
34 #[server] pub max_clock_skew: Duration,
36 // Ordinary settings, used by client only:
37 #[client] pub http_timeout_grace: Duration,
38 #[client] pub max_requests_outstanding: u32,
39 #[client] pub max_batch_up: u32,
40 #[client] pub http_retry: Duration,
41 #[client] pub url: Uri,
42 #[client] pub vroutes: Vec<IpNet>,
44 // Computed, rather than looked up. Client only:
45 #[computed] pub effective_http_timeout: Duration,
48 static DEFAULT_CONFIG: &str = r#"
50 max_batch_down = 65536
52 target_requests_outstanding = 3
54 http_timeout_grace = 5
55 max_requests_outstanding = 6
60 ifname_client = hippo%d
61 ifname_server = shippo%d
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;
133 #[derive(Debug,Clone)]
134 struct RawVal { raw: Option<String>, loc: Arc<PathBuf> }
135 type SectionMap = HashMap<String, RawVal>;
138 struct RawValRef<'v,'l,'s> {
139 raw: Option<&'v str>,
142 section: &'s SectionName,
145 impl<'v> RawValRef<'v,'_,'_> {
147 fn try_map<F,T>(&self, f: F) -> T
148 where F: FnOnce(Option<&'v str>) -> Result<T, AE> {
150 .with_context(|| format!(r#"file {:?}, section {}, key "{}""#,
151 self.loc, self.section, self.key))?
159 static OUTSIDE_SECTION: &str = "[";
160 static SPECIAL_SERVER_SECTION: &str = "SERVER";
162 #[derive(Default,Debug)]
164 keys_allowed: HashMap<&'static str, SectionKindList>,
165 sections: HashMap<SectionName, SectionMap>,
168 type OkAnyway<'f,A> = &'f dyn Fn(ErrorKind) -> Option<A>;
170 impl<'f,A> OkAnyway<'f,A> {
171 fn ok<T>(self, r: &Result<T, io::Error>) -> Option<A> {
172 let e = r.as_ref().err()?;
179 impl FromStr for SectionName {
182 fn from_str(s: &str) -> Self {
184 "COMMON" => return SN::Common,
185 "LIMIT" => return SN::GlobalLimit,
188 if let Ok(n@ ServerName(_)) = s.parse() { return SN::Server(n) }
189 if let Ok(n@ ClientName(_)) = s.parse() { return SN::Client(n) }
190 let (server, client) = s.split_ascii_whitespace().collect_tuple()
191 .ok_or_else(|| anyhow!(
192 "bad section name {:?} \
193 (must be COMMON, DEFAULT, <server>, <client>, or <server> <client>",
196 let server = server.parse().context("server name in link section name")?;
197 if client == "LIMIT" { return SN::ServerLimit(server) }
198 let client = client.parse().context("client name in link section name")?;
199 SN::Link(LinkName { server, client })
202 impl Display for InstanceConfig {
203 #[throws(fmt::Error)]
204 fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.link, f)? }
207 impl Display for SectionName {
208 #[throws(fmt::Error)]
209 fn fmt(&self, f: &mut fmt::Formatter) {
211 SN::Link (ref l) => Display::fmt(l, f)?,
212 SN::Client(ref c) => write!(f, "[{}]" , c)?,
213 SN::Server(ref s) => write!(f, "[{}]" , s)?,
214 SN::ServerLimit(ref s) => write!(f, "[{} LIMIT] ", s)?,
215 SN::GlobalLimit => write!(f, "[LIMIT]" )?,
216 SN::Common => write!(f, "[COMMON]" )?,
222 #[throws(AE)] // AE does not include path
223 fn read_file<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
225 let f = fs::File::open(path);
226 if let Some(anyway) = anyway.ok(&f) { return Some(anyway) }
227 let mut f = f.context("open")?;
229 let mut s = String::new();
230 let y = f.read_to_string(&mut s);
231 if let Some(anyway) = anyway.ok(&y) { return Some(anyway) }
234 self.read_string(s, path)?;
238 #[throws(AE)] // AE does not include path
239 fn read_string(&mut self, s: String, path_for_loc: &Path) {
240 let mut ini = Ini::new_cs();
241 ini.set_default_section(OUTSIDE_SECTION);
242 ini.read(s).map_err(|e| anyhow!("{}", e)).context("parse as INI")?;
243 let map = mem::take(ini.get_mut_map());
244 if map.get(OUTSIDE_SECTION).is_some() {
245 throw!(anyhow!("INI file contains settings outside a section"));
248 let loc = Arc::new(path_for_loc.to_owned());
250 for (sn, vars) in map {
251 let sn = sn.parse().dcontext(&sn)?;
253 for key in vars.keys() {
254 let skl = self.keys_allowed.get(key.as_str()).ok_or_else(
255 || anyhow!("unknown configuration key {:?}", key)
257 if ! skl.contains(&sn) {
258 throw!(anyhow!("configuration key {:?} not applicable \
259 in this kind of section {:?}", key, &sn))
263 let ent = self.sections.entry(sn).or_default();
264 for (key, raw) in vars {
265 let raw = match raw {
266 Some(raw) if raw.starts_with('\'') || raw.starts_with('"') => Some(
268 if raw.contains('\\') {
270 anyhow!("quoted value contains backslash, not supported")
273 let unq = raw[1..].strip_suffix(&raw[0..1])
275 || anyhow!("mismatched quotes around quoted value")
280 .with_context(|| format!("key {:?}", key))
281 .dcontext(path_for_loc)?
285 let key = key.replace('-',"_");
286 ent.insert(key, RawVal { raw, loc: loc.clone() });
291 #[throws(AE)] // AE includes path
292 fn read_dir_d<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
294 let dir = fs::read_dir(path);
295 if let Some(anyway) = anyway.ok(&dir) { return Some(anyway) }
296 let dir = dir.context("open directory").dcontext(path)?;
298 let ent = ent.context("read directory").dcontext(path)?;
299 let leaf = ent.file_name();
300 let leaf = leaf.to_str();
301 let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
302 if leaf.len() == 0 { continue }
303 if ! leaf.chars().all(
304 |c| c=='-' || c=='_' || c.is_ascii_alphanumeric()
307 // OK we want this one
308 let ent = ent.path();
309 self.read_file(&ent, &|_| None::<Void>).dcontext(&ent)?;
314 #[throws(AE)] // AE includes everything
315 fn read_toplevel(&mut self, toplevel: &Path) {
316 enum Anyway { None, Dir }
317 match self.read_file(toplevel, &|k| match k {
318 EK::NotFound => Some(Anyway::None),
319 EK::IsADirectory => Some(Anyway::Dir),
322 .dcontext(toplevel).context("top-level config directory (or file)")?
324 None | Some(Anyway::None) => { },
326 Some(Anyway::Dir) => {
328 let anyway_none = |k| match k {
329 EK::NotFound => Some(AnywayNone),
333 let mk = |leaf: &str| {
334 [ toplevel, &PathBuf::from(leaf) ]
335 .iter().collect::<PathBuf>()
338 for &(try_main, desc) in &[
339 ("main.cfg", "main config file"),
340 ("master.cfg", "obsolete-named main config file"),
342 let main = mk(try_main);
344 match self.read_file(&main, &anyway_none)
345 .dcontext(main).context(desc)?
348 Some(AnywayNone) => { },
352 for &(try_dir, desc) in &[
353 ("config.d", "per-link config directory"),
354 ("secrets.d", "per-link secrets directory"),
356 let dir = mk(try_dir);
357 match self.read_dir_d(&dir, &anyway_none).context(desc)? {
359 Some(AnywayNone) => { },
366 #[throws(AE)] // AE includes extra, but does that this is extra
367 fn read_extra(&mut self, extra: &Path) {
370 match self.read_file(extra, &|k| match k {
371 EK::IsADirectory => Some(AnywayDir),
378 self.read_dir_d(extra, &|_| None::<Void>)?;
386 fn instances(&self, only_server: Option<&ServerName>) -> BTreeSet<LinkName> {
387 let mut links: BTreeSet<LinkName> = default();
389 let mut secrets_anyserver: BTreeSet<&ClientName> = default();
390 let mut secrets_anyclient: BTreeSet<&ServerName> = default();
391 let mut secret_global = false;
393 let mut putative_servers = BTreeSet::new();
394 let mut putative_clients = BTreeSet::new();
396 let mut note_server = |s| {
397 if let Some(only) = only_server { if s != only { return false } }
398 putative_servers.insert(s);
401 let mut note_client = |c| {
402 putative_clients.insert(c);
405 for (section, vars) in &self.sections {
406 let has_secret = || vars.contains_key("secret");
410 if ! note_server(&l.server) { continue }
411 note_client(&l.client);
412 if has_secret() { links.insert(l.clone()); }
414 SN::Server(ref s) => {
415 if ! note_server(s) { continue }
416 if has_secret() { secrets_anyclient.insert(s); }
418 SN::Client(ref c) => {
420 if has_secret() { secrets_anyserver.insert(c); }
423 if has_secret() { secret_global = true; }
429 // Add links which are justified by blanket secrets
430 for (client, server) in iproduct!(
431 putative_clients.into_iter().filter(
432 |c| secret_global || secrets_anyserver.contains(c)
434 putative_servers.iter().cloned().filter(
435 |s| secret_global || secrets_anyclient.contains(s)
438 links.insert(LinkName {
439 client: client.clone(),
440 server: server.clone(),
448 struct ResolveContext<'c> {
452 all_sections: Vec<SectionName>,
455 trait Parseable: Sized {
456 fn parse(s: Option<&str>) -> Result<Self, AE>;
457 fn default() -> Result<Self, AE> {
458 Err(anyhow!("setting must be specified"))
461 fn default_for_key(key: &str) -> Self {
462 Self::default().with_context(|| key.to_string())?
466 impl Parseable for Duration {
468 fn parse(s: Option<&str>) -> Duration {
469 // todo: would be nice to parse with humantime maybe
470 Duration::from_secs( s.value()?.parse()? )
473 macro_rules! parseable_from_str { ($t:ty $(, $def:expr)? ) => {
474 impl Parseable for $t {
476 fn parse(s: Option<&str>) -> $t { s.value()?.parse()? }
477 $( #[throws(AE)] fn default() -> Self { $def } )?
480 parseable_from_str!{u16, default() }
481 parseable_from_str!{u32, default() }
482 parseable_from_str!{String, default() }
483 parseable_from_str!{IpNet, default() }
484 parseable_from_str!{IpAddr, Ipv4Addr::UNSPECIFIED.into() }
485 parseable_from_str!{Uri, default() }
487 impl<T:Parseable> Parseable for Vec<T> {
489 fn parse(s: Option<&str>) -> Vec<T> {
491 .split_ascii_whitespace()
492 .map(|s| Parseable::parse(Some(s)))
493 .collect::<Result<Vec<_>,_>>()?
496 fn default() -> Self { default() }
500 #[derive(Debug,Copy,Clone)]
501 enum SectionKindList {
508 use SectionKindList as SKL;
511 fn special_server_section() -> Self { SN::Server(ServerName(
512 SPECIAL_SERVER_SECTION.into()
516 impl SectionKindList {
517 fn contains(self, s: &SectionName) -> bool {
519 SKL::Ordinary => matches!(s, SN::Link(_)
524 SKL::Limits => matches!(s, SN::ServerLimit(_)
527 SKL::ClientAgnostic => matches!(s, SN::Common
530 SKL::Limited => SKL::Ordinary.contains(s)
531 | SKL::Limits .contains(s),
533 SKL::ServerName => matches!(s, SN::Common)
534 | matches!(s, SN::Server(ServerName(name))
535 if name == SPECIAL_SERVER_SECTION),
541 fn lookup_raw<'a,'s,S>(&'a self, key: &'static str, sections: S)
542 -> Option<RawValRef<'a,'a,'s>>
543 where S: Iterator<Item=&'s SectionName>
545 for section in sections {
546 if let Some(raw) = self.sections
548 .and_then(|vars: &SectionMap| vars.get(key))
550 return Some(RawValRef {
551 raw: raw.raw.as_deref(),
561 pub fn establish_server_name(&self) -> ServerName {
563 let raw = match self.lookup_raw(
565 [ &SectionName::Common, &SN::special_server_section() ].iter().cloned()
567 Some(raw) => raw.try_map(|os| os.value())?,
568 None => SPECIAL_SERVER_SECTION,
570 ServerName(raw.into())
574 impl<'c> ResolveContext<'c> {
575 fn first_of_raw(&'c self, key: &'static str, sections: SectionKindList)
576 -> Option<RawValRef<'c,'c,'c>> {
579 self.all_sections.iter()
580 .filter(|s| sections.contains(s))
585 fn first_of<T>(&self, key: &'static str, sections: SectionKindList)
589 match self.first_of_raw(key, sections) {
591 Some(raw) => Some(raw.try_map(Parseable::parse)?),
596 pub fn ordinary<T>(&self, key: &'static str) -> T
599 match self.first_of(key, SKL::Ordinary)? {
601 None => Parseable::default_for_key(key)?,
606 pub fn limited<T>(&self, key: &'static str) -> T
607 where T: Parseable + Ord
609 let val = self.ordinary(key)?;
610 if let Some(limit) = self.first_of(key, SKL::Limits)? {
618 pub fn client<T>(&self, key: &'static str) -> T
619 where T: Parseable + Default {
621 LinkEnd::Client => self.ordinary(key)?,
622 LinkEnd::Server => default(),
626 pub fn server<T>(&self, key: &'static str) -> T
627 where T: Parseable + Default {
629 LinkEnd::Server => self.ordinary(key)?,
630 LinkEnd::Client => default(),
635 pub fn computed<T>(&self, _key: &'static str) -> T
642 pub fn special_ipif(&self, key: &'static str) -> String {
644 LinkEnd::Client => self.ordinary(key)?,
646 self.first_of(key, SKL::ClientAgnostic)?
653 pub fn special_link(&self, _key: &'static str) -> LinkName {
658 impl InstanceConfig {
660 fn complete(&mut self, end: LinkEnd) {
661 let mut vhosts = self.vnetwork.iter()
662 .map(|n| n.hosts()).flatten()
663 .filter({ let vaddr = self.vaddr; move |v| v != &vaddr });
665 if self.vaddr.is_unspecified() {
666 self.vaddr = vhosts.next().ok_or_else(
667 || anyhow!("vnetwork too small to generate vaddrr")
670 if self.vrelay.is_unspecified() {
671 self.vrelay = vhosts.next().ok_or_else(
672 || anyhow!("vnetwork too small to generate vrelay")
678 move |max_batch, key| {
679 if max_batch/2 < mtu {
680 throw!(anyhow!("max batch {:?} ({}) must be >= 2 x mtu ({}) \
681 (to allow for SLIP ESC-encoding)",
682 key, max_batch, mtu))
690 if &self.url == &default::<Uri>() {
691 let addr = self.addrs.get(0).ok_or_else(
692 || anyhow!("client needs addrs or url set")
697 IpAddr::V4(a) => format!("{}", a),
698 IpAddr::V6(a) => format!("[{}]", a),
702 p => format!(":{}", p),
707 self.effective_http_timeout = {
708 let a = self.http_timeout;
709 let b = self.http_timeout_grace;
710 a.checked_add(b).ok_or_else(
711 || anyhow!("calculate effective http timeout ({:?} + {:?})", a, b)
715 check_batch(self.max_batch_up, "max_batch_up")?;
719 if self.addrs.is_empty() {
720 throw!(anyhow!("missing 'addrs' setting"))
722 check_batch(self.max_batch_down, "max_batch_down")?;
725 // xxx check target vs max req outstanding
729 fn subst(var: &mut String,
730 kv: &mut dyn Iterator<Item=(&'static str, &dyn Display)>
733 .map(|(k,v)| (k.to_string(), v.to_string()))
734 .collect::<HashMap<String, String>>();
735 let bad = parking_lot::Mutex::new(vec![]);
736 *var = regex_replace_all!(
737 r#"%(?:%|\((\w+)\)s|\{(\w+)\}|.)"#,
739 |whole, k1, k2| (|| Ok::<_,String>({
740 if whole == "%%" { "%" }
741 else if let Some(&k) = [k1,k2].iter().find(|&&s| s != "") {
742 substs.get(k).ok_or_else(
743 || format!("unknown key %({})s", k)
746 throw!(format!("bad percent escape {:?}", &whole));
748 }))().unwrap_or_else(|e| { bad.lock().push(e); "" })
750 let bad = bad.into_inner();
751 if ! bad.is_empty() {
752 throw!(anyhow!("substitution failed: {}", bad.iter().format("; ")));
758 type DD<'d> = &'d dyn Display;
759 fn dv<T:Display>(v: &[T]) -> String {
760 format!("{}", v.iter().format(" "))
762 let mut ipif = mem::take(&mut self.ipif); // lets us borrow all of self
763 let s = &self; // just for abbreviation, below
764 let vnetwork = dv(&s.vnetwork);
765 let vroutes = dv(&s.vroutes);
767 let keys = &["local", "peer", "rnets", "ifname"];
768 let values = match end {
769 Server => [&s.vaddr as DD , &s.vrelay, &vnetwork, &s.ifname_server],
770 Client => [&s.link.client as DD, &s.vaddr, &vroutes, &s.ifname_client],
773 ( "mtu", &s.mtu as DD ),
778 &mut keys.iter().cloned()
780 .chain(always.iter().cloned()),
788 pub fn read(opts: &Opts, end: LinkEnd) -> Vec<InstanceConfig> {
790 let mut agg = Aggregate::default();
791 agg.keys_allowed.extend(
792 InstanceConfig::FIELDS.iter().cloned()
795 agg.read_string(DEFAULT_CONFIG.into(),
796 "<build-in defaults>".as_ref()).unwrap();
798 agg.read_toplevel(&opts.config)?;
799 for extra in &opts.extra_config {
800 agg.read_extra(extra).context("extra config")?;
803 //eprintln!("GOT {:#?}", agg);
806 })().context("read configuration")?;
808 let server_name = match end {
809 LinkEnd::Server => Some(agg.establish_server_name()?),
810 LinkEnd::Client => None,
813 let instances = agg.instances(server_name.as_ref());
814 let mut ics = vec![];
816 for link in instances {
817 let rctx = ResolveContext {
822 SN::Link(link.clone()),
823 SN::Client(link.client.clone()),
824 SN::Server(link.server.clone()),
826 SN::ServerLimit(link.server.clone()),
831 let mut ic = InstanceConfig::resolve_instance(&rctx)
832 .with_context(|| format!("resolve config for {}", &link))?;
835 .with_context(|| format!("complete config for {}", &link))?;