chiark / gitweb /
config: Substitutions prefer %{...} to %(...)s, document, etc.
[hippotat.git] / src / config.rs
index 1d6caf29813d9356aa9955ec292a30d6de14a0a2..4b94b3ed73357db4659e2c3fde777e4e67b13e66 100644 (file)
@@ -6,7 +6,70 @@ use crate::prelude::*;
 
 use configparser::ini::Ini;
 
-static MAIN_CONFIGS: [&str;_] = ["main.cfg", "config.d", "secrets.d"];
+#[derive(hippotat_macros::ResolveConfig)]
+#[derive(Debug,Clone)]
+pub struct InstanceConfig {
+  // Exceptional settings
+  #[special(special_link, SKL::ServerName)]   pub link:   LinkName,
+  pub                                             secret: Secret,
+  #[special(special_ipif, SKL::Ordinary)]     pub ipif:   String,
+
+  // Capped settings:
+  #[limited]    pub max_batch_down:               u32,
+  #[limited]    pub max_queue_time:               Duration,
+  #[limited]    pub http_timeout:                 Duration,
+  #[limited]    pub target_requests_outstanding:  u32,
+
+  // Ordinary settings:
+  pub addrs:                        Vec<IpAddr>,
+  pub vnetwork:                     Vec<IpNet>,
+  pub vaddr:                        IpAddr,
+  pub vrelay:                       IpAddr,
+  pub port:                         u16,
+  pub mtu:                          u32,
+  pub ifname_server:                String,
+  pub ifname_client:                String,
+
+  // Ordinary settings, used by server only:
+  #[server]  pub max_clock_skew:               Duration,
+
+  // Ordinary settings, used by client only:
+  #[client]  pub http_timeout_grace:           Duration,
+  #[client]  pub max_requests_outstanding:     u32,
+  #[client]  pub max_batch_up:                 u32,
+  #[client]  pub http_retry:                   Duration,
+  #[client]  pub url:                          Uri,
+  #[client]  pub vroutes:                      Vec<IpNet>,
+}
+
+static DEFAULT_CONFIG: &str = r#"
+[COMMON]
+max_batch_down = 65536
+max_queue_time = 10
+target_requests_outstanding = 3
+http_timeout = 30
+http_timeout_grace = 5
+max_requests_outstanding = 6
+max_batch_up = 4000
+http_retry = 5
+port = 80
+vroutes = ''
+ifname_client = hippo%d
+ifname_server = shippo%d
+max_clock_skew = 300
+
+ipif = userv root ipif %{local},%{peer},%{mtu},slip,%{ifname} '%{rnets}'
+
+mtu = 1500
+
+vnetwork = 172.24.230.192
+
+[LIMIT]
+max_batch_down = 262144
+max_queue_time = 121
+http_timeout = 121
+target_requests_outstanding = 10
+"#;
 
 #[derive(StructOpt,Debug)]
 pub struct Opts {
@@ -23,133 +86,258 @@ pub struct Opts {
   pub extra_config: Vec<PathBuf>,
 }
 
-pub struct CidrString(pub String);
-
-pub struct InstanceConfig {
-  // Exceptional settings
-  pub server:                       String,
-  pub secret:                       String,
-  pub ipif:                         String,
-
-  // Capped settings:
-  pub max_batch_down:               u32,
-  pub max_queue_time:               Duration,
-  pub http_timeout:                 Duration,
-
-  // Ordinary settings:
-  pub target_requests_outstanding:  u32,
-  pub addrs:                        Vec<IpAddr>,
-  pub vnetwork:                     Vec<CidrString>,
-  pub vaddr:                        Vec<IpAddr>,
-  pub vrelay:                       IpAddr,
-  pub port:                         u16,
-  pub mtu:                          u32,
-  pub ifname_server:                String,
-  pub ifname_client:                String,
+#[ext]
+impl<'s> Option<&'s str> {
+  #[throws(AE)]
+  fn value(self) -> &'s str {
+    self.ok_or_else(|| anyhow!("value needed"))?
+  }
+}
 
-  // Ordinary settings, used by server only:
-  pub max_clock_skew:               Duration,
+#[derive(Clone)]
+pub struct Secret(pub String);
+impl Parseable for Secret {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Self {
+    let s = s.value()?;
+    if s.is_empty() { throw!(anyhow!("secret value cannot be empty")) }
+    Secret(s.into())
+  }
+  #[throws(AE)]
+  fn default() -> Self { Secret(default()) }
+}
+impl Debug for Secret {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "Secret(***)")? }
+}
 
-  // Ordinary settings, used by client only:
-  pub http_timeout_grace:           Duration,
-  pub max_requests_outstanding:     u32,
-  pub max_batch_up:                 u32,
-  pub http_retry:                   Duration,
-  pub url:                          Uri,
-  pub vroutes:                      Vec<CidrString>,
+#[derive(Debug,Clone,Hash,Eq,PartialEq)]
+pub enum SectionName {
+  Link(LinkName),
+  Client(ClientName),
+  Server(ServerName), // includes SERVER, which is slightly special
+  ServerLimit(ServerName),
+  GlobalLimit,
+  Common,
+}
+pub use SectionName as SN;
+
+#[derive(Debug,Clone)]
+struct RawVal { raw: Option<String>, loc: Arc<PathBuf> }
+type SectionMap = HashMap<String, RawVal>;
+
+#[derive(Debug)]
+struct RawValRef<'v,'l,'s> {
+  raw: Option<&'v str>,
+  key: &'static str,
+  loc: &'l Path,
+  section: &'s SectionName,
 }
 
-type SectionMap = HashMap<String, Option<String>>;
+impl<'v> RawValRef<'v,'_,'_> {
+  #[throws(AE)]
+  fn try_map<F,T>(&self, f: F) -> T
+  where F: FnOnce(Option<&'v str>) -> Result<T, AE> {
+    f(self.raw)
+      .with_context(|| format!(r#"file {:?}, section {}, key "{}""#,
+                               self.loc, self.section, self.key))?
+  }
+}
 
 pub struct Config {
-  opts: Opts,
+  pub opts: Opts,
 }
 
 static OUTSIDE_SECTION: &str = "[";
+static SPECIAL_SERVER_SECTION: &str = "SERVER";
 
+#[derive(Default,Debug)]
 struct Aggregate {
-  sections: HashMap<String, SectionMap>,
+  keys_allowed: HashMap<&'static str, SectionKindList>,
+  sections: HashMap<SectionName, SectionMap>,
+}
+
+type OkAnyway<'f,A> = &'f dyn Fn(ErrorKind) -> Option<A>;
+#[ext]
+impl<'f,A> OkAnyway<'f,A> {
+  fn ok<T>(self, r: &Result<T, io::Error>) -> Option<A> {
+    let e = r.as_ref().err()?;
+    let k = e.kind();
+    let a = self(k)?;
+    Some(a)
+  }
+}
+
+impl FromStr for SectionName {
+  type Err = AE;
+  #[throws(AE)]
+  fn from_str(s: &str) -> Self {
+    match s {
+      "COMMON" => return SN::Common,
+      "LIMIT" => return SN::GlobalLimit,
+      _ => { }
+    };
+    if let Ok(n@ ServerName(_)) = s.parse() { return SN::Server(n) }
+    if let Ok(n@ ClientName(_)) = s.parse() { return SN::Client(n) }
+    let (server, client) = s.split_ascii_whitespace().collect_tuple()
+      .ok_or_else(|| anyhow!(
+        "bad section name {:?} \
+         (must be COMMON, DEFAULT, <server>, <client>, or <server> <client>",
+        s
+      ))?;
+    let server = server.parse().context("server name in link section name")?;
+    if client == "LIMIT" { return SN::ServerLimit(server) }
+    let client = client.parse().context("client name in link section name")?;
+    SN::Link(LinkName { server, client })
+  }
+}
+impl Display for InstanceConfig {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.link, f)? }
+}
+
+impl Display for SectionName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    match self {
+      SN::Link  (ref l)      => Display::fmt(l, f)?,
+      SN::Client(ref c)      => write!(f, "[{}]"       , c)?,
+      SN::Server(ref s)      => write!(f, "[{}]"       , s)?,
+      SN::ServerLimit(ref s) => write!(f, "[{} LIMIT] ", s)?,
+      SN::GlobalLimit        => write!(f, "[LIMIT]"       )?,
+      SN::Common             => write!(f, "[COMMON]"      )?,
+    }
+  }
 }
 
 impl Aggregate {
   #[throws(AE)] // AE does not include path
-  fn read_file(&mut self, path: Path,
-               ekok: &Fn(ErrorKind) -> Result<(),()>)
-               -> Result<(), io::ErrorKind>
+  fn read_file<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
   {
-    let f = match File::open(path) {
-      Err(e) if ekok(e.kind()) => return Err(e.kind()),
-      r => r.context("open")?;
-    }
+    let f = fs::File::open(path);
+    if let Some(anyway) = anyway.ok(&f) { return Some(anyway) }
+    let mut f = f.context("open")?;
 
     let mut s = String::new();
-    f.read_to_string(&mut s).context("read")?;
+    let y = f.read_to_string(&mut s);
+    if let Some(anyway) = anyway.ok(&y) { return Some(anyway) }
+    y.context("read")?;
 
-    let mut ini = configparser::ini::Ini::new_cs();
+    self.read_string(s, path)?;
+    None
+  }
+
+  #[throws(AE)] // AE does not include path
+  fn read_string(&mut self, s: String, path_for_loc: &Path) {
+    let mut ini = Ini::new_cs();
     ini.set_default_section(OUTSIDE_SECTION);
-    ini.read(s).context("parse as INI");
-    let map = mem::take(ini.get_mut_map);
+    ini.read(s).map_err(|e| anyhow!("{}", e)).context("parse as INI")?;
+    let map = mem::take(ini.get_mut_map());
     if map.get(OUTSIDE_SECTION).is_some() {
       throw!(anyhow!("INI file contains settings outside a section"));
     }
 
-    // xxx parse section names here
-    // xxx save Arc<PathBuf> where we found each item
+    let loc = Arc::new(path_for_loc.to_owned());
+
+    for (sn, vars) in map {
+      let sn = sn.parse().dcontext(&sn)?;
+
+      for key in vars.keys() {
+        let skl = self.keys_allowed.get(key.as_str()).ok_or_else(
+          || anyhow!("unknown configuration key {:?}", key)
+        )?;
+        if ! skl.contains(&sn) {
+          throw!(anyhow!("configuration key {:?} not applicable \
+                          in this kind of section {:?}", key, &sn))
+        }
+      }
 
-    self.sections.extend(map.drain());
-    Ok(())
+      let ent = self.sections.entry(sn).or_default();
+      for (key, raw) in vars {
+        let raw = match raw {
+          Some(raw) if raw.starts_with('\'') || raw.starts_with('"') => Some(
+            (||{
+              if raw.contains('\\') {
+                throw!(
+                  anyhow!("quoted value contains backslash, not supported")
+                );
+              }
+              let unq = raw[1..].strip_suffix(&raw[0..1])
+                .ok_or_else(
+                  || anyhow!("mismatched quotes around quoted value")
+                )?
+                .to_owned();
+              Ok::<_,AE>(unq)
+            })()
+              .with_context(|| format!("key {:?}", key))
+              .dcontext(path_for_loc)?
+          ),
+          x => x,
+        };
+        let key = key.replace('-',"_");
+        ent.insert(key, RawVal { raw, loc: loc.clone() });
+      }
+    }
   }
 
   #[throws(AE)] // AE includes path
-  fn read_dir_d(&mut self, path: Path
-                ekok: &Fn(ErrorKind))
-                -> Result<(), io::ErrorKind>
+  fn read_dir_d<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
   {
-    let wc = || format("{:?}", path);
-    for ent in match fs::read_dir(path) {
-      Err(e) if ekok(e.kind()) => return Err(e.kind()),
-      r => r.context("open directory").with_context(wc)?;
-    } {
-      let ent = ent.context("read directory").with_context(wc)?;
-      let leaf = ent.file_name().as_str();
+    let dir = fs::read_dir(path);
+    if let Some(anyway) = anyway.ok(&dir) { return Some(anyway) }
+    let dir = dir.context("open directory").dcontext(path)?;
+    for ent in dir {
+      let ent = ent.context("read directory").dcontext(path)?;
+      let leaf = ent.file_name();
+      let leaf = leaf.to_str();
       let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
-      if leaf.length() == 0 { continue }
+      if leaf.len() == 0 { continue }
       if ! leaf.chars().all(
-        |c| c=='-' || c=='_' || c.is_ascii_alphenumeric()
+        |c| c=='-' || c=='_' || c.is_ascii_alphanumeric()
       ) { continue }
 
       // OK we want this one
-      self.read_file(&ent.path, &|_| false)
-        .with_context(format!("{:?}", &ent.path))??;
+      let ent = ent.path();
+      self.read_file(&ent, &|_| None::<Void>).dcontext(&ent)?;
     }
+    None
   }
 
   #[throws(AE)] // AE includes everything
   fn read_toplevel(&mut self, toplevel: &Path) {
-    match se;lf.read_file(
-      toplevel,
-      |k| matches!(k, EK::NotFound || EK::IsDirectory)
-    )
-      .with_context(format!("{:?}", toplevel))
-      .context("top-level config directory (or file)")?
+    enum Anyway { None, Dir }
+    match self.read_file(toplevel, &|k| match k {
+      EK::NotFound => Some(Anyway::None),
+      EK::IsADirectory => Some(Anyway::Dir),
+      _ => None,
+    })
+      .dcontext(toplevel).context("top-level config directory (or file)")?
     {
-      Err(EK::NotFound) => { },
+      None | Some(Anyway::None) => { },
 
-      Err(EK::IsDirectory) => {
-        let mk = |leaf| [ toplevel, leaf ].iter().collect::<PathBuf>();
+      Some(Anyway::Dir) => {
+        struct AnywayNone;
+        let anyway_none = |k| match k {
+          EK::NotFound => Some(AnywayNone),
+          _ => None,
+        };
+
+        let mk = |leaf: &str| {
+          [ toplevel, &PathBuf::from(leaf) ]
+            .iter().collect::<PathBuf>()
+        };
 
         for &(try_main, desc) in &[
           ("main.cfg", "main config file"),
           ("master.cfg", "obsolete-named main config file"),
         ] {
           let main = mk(try_main);
-          match self.read_file(&main, |e| e == EK::NotFound)
-            .with_context(format!("{:?}", &main))
-            .context(desc)?
+
+          match self.read_file(&main, &anyway_none)
+            .dcontext(main).context(desc)?
           {
-            Ok(()) => break,
-            Err(EK::NotFound) => { },
-            x => panic!("huh? {:?}", &x),
+            None => break,
+            Some(AnywayNone) => { },
           }
         }
 
@@ -158,10 +346,9 @@ impl Aggregate {
           ("secrets.d", "per-link secrets directory"),
         ] {
           let dir = mk(try_dir);
-          match agg.read_dir(&dir, |k| k == EK::NotFound).context(desc)? {
-            Ok(()) => { },
-            Err(EK::NotFound) => { },
-            x => panic!("huh? {:?}", &x),
+          match self.read_dir_d(&dir, &anyway_none).context(desc)? {
+            None => { },
+            Some(AnywayNone) => { },
           }
         }
       }
@@ -170,34 +357,447 @@ impl Aggregate {
 
   #[throws(AE)] // AE includes extra, but does that this is extra
   fn read_extra(&mut self, extra: &Path) {
+    struct AnywayDir;
 
-    match self.read_file(extra, |k| k == EK::IsDirectory)
-      .with_context(format!("{:?}", extra))?
+    match self.read_file(extra, &|k| match k {
+      EK::IsADirectory => Some(AnywayDir),
+      _ => None,
+    })
+      .dcontext(extra)?
     {
-      Ok(()) => return,
-      Err(EK::IsDirectory) => { }
-      x => panic!("huh? {:?}", &x),
+      None => return,
+      Some(AnywayDir) => {
+        self.read_dir_d(extra, &|_| None::<Void>)?;
+      }
+    }
+
+  }
+}
+
+impl Aggregate {
+  fn instances(&self, only_server: Option<&ServerName>) -> BTreeSet<LinkName> {
+    let mut links:              BTreeSet<LinkName> = default();
+
+    let mut secrets_anyserver:  BTreeSet<&ClientName> = default();
+    let mut secrets_anyclient:  BTreeSet<&ServerName> = default();
+    let mut secret_global       = false;
+
+    let mut putative_servers   = BTreeSet::new();
+    let mut putative_clients   = BTreeSet::new();
+
+    let mut note_server = |s| {
+      if let Some(only) = only_server { if s != only { return false } }
+      putative_servers.insert(s);
+      true
+    };
+    let mut note_client = |c| {
+      putative_clients.insert(c);
+    };
+
+    for (section, vars) in &self.sections {
+      let has_secret = || vars.contains_key("secret");
+
+      match section {
+        SN::Link(l) => {
+          if ! note_server(&l.server) { continue }
+          note_client(&l.client);
+          if has_secret() { links.insert(l.clone()); }
+        },
+        SN::Server(ref s) => {
+          if ! note_server(s) { continue }
+          if has_secret() { secrets_anyclient.insert(s); }
+        },
+        SN::Client(ref c) => {
+          note_client(c);
+          if has_secret() { secrets_anyserver.insert(c); }
+        },
+        SN::Common => {
+          if has_secret() { secret_global = true; }
+        },
+        _ => { },
+      }
+    }
+
+    // Add links which are justified by blanket secrets
+    for (client, server) in iproduct!(
+      putative_clients.into_iter().filter(
+        |c| secret_global || secrets_anyserver.contains(c)
+      ),
+      putative_servers.iter().cloned().filter(
+        |s| secret_global || secrets_anyclient.contains(s)
+      )
+    ) {
+      links.insert(LinkName {
+        client: client.clone(),
+        server: server.clone(),
+      });
+    }
+
+    links
+  }
+}
+
+struct ResolveContext<'c> {
+  agg: &'c Aggregate,
+  link: &'c LinkName,
+  end: LinkEnd,
+  all_sections: Vec<SectionName>,
+}
+
+trait Parseable: Sized {
+  fn parse(s: Option<&str>) -> Result<Self, AE>;
+  fn default() -> Result<Self, AE> {
+    Err(anyhow!("setting must be specified"))
+  }
+  #[throws(AE)]
+  fn default_for_key(key: &str) -> Self {
+    Self::default().with_context(|| key.to_string())?
+  }
+}
+
+impl Parseable for Duration {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Duration {
+    // todo: would be nice to parse with humantime maybe
+    Duration::from_secs( s.value()?.parse()? )
+  }
+}
+macro_rules! parseable_from_str { ($t:ty $(, $def:expr)? ) => {
+  impl Parseable for $t {
+    #[throws(AE)]
+    fn parse(s: Option<&str>) -> $t { s.value()?.parse()? }
+    $( #[throws(AE)] fn default() -> Self { $def } )?
+  }
+} }
+parseable_from_str!{u16, default() }
+parseable_from_str!{u32, default() }
+parseable_from_str!{String, default() }
+parseable_from_str!{IpNet, default() }
+parseable_from_str!{IpAddr, Ipv4Addr::UNSPECIFIED.into() }
+parseable_from_str!{Uri, default() }
+
+impl<T:Parseable> Parseable for Vec<T> {
+  #[throws(AE)]
+  fn parse(s: Option<&str>) -> Vec<T> {
+    s.value()?
+      .split_ascii_whitespace()
+      .map(|s| Parseable::parse(Some(s)))
+      .collect::<Result<Vec<_>,_>>()?
+  }
+  #[throws(AE)]
+  fn default() -> Self { default() }
+}
+
+
+#[derive(Debug,Copy,Clone)]
+enum SectionKindList {
+  Ordinary,
+  Limited,
+  Limits,
+  ClientAgnostic,
+  ServerName,
+}
+use SectionKindList as SKL;
+
+impl SectionName {
+  fn special_server_section() -> Self { SN::Server(ServerName(
+    SPECIAL_SERVER_SECTION.into()
+  )) }
+}
+
+impl SectionKindList {
+  fn contains(self, s: &SectionName) -> bool {
+    match self {
+      SKL::Ordinary       => matches!(s, SN::Link(_)
+                                       | SN::Client(_)
+                                       | SN::Server(_)
+                                       | SN::Common),
+
+      SKL::Limits         => matches!(s, SN::ServerLimit(_)
+                                       | SN::GlobalLimit),
+
+      SKL::ClientAgnostic => matches!(s, SN::Common
+                                       | SN::Server(_)),
+
+      SKL::Limited        => SKL::Ordinary.contains(s)
+                           | SKL::Limits  .contains(s),
+
+      SKL::ServerName     => matches!(s, SN::Common)
+                           | matches!(s, SN::Server(ServerName(name))
+                                         if name == SPECIAL_SERVER_SECTION),
     }
+  }
+}
 
-    self.read_dir(extra, |_| false)??;
+impl Aggregate {
+  fn lookup_raw<'a,'s,S>(&'a self, key: &'static str, sections: S)
+                       -> Option<RawValRef<'a,'a,'s>>
+  where S: Iterator<Item=&'s SectionName>
+  {
+    for section in sections {
+      if let Some(raw) = self.sections
+        .get(section)
+        .and_then(|vars: &SectionMap| vars.get(key))
+      {
+        return Some(RawValRef {
+          raw: raw.raw.as_deref(),
+          loc: &raw.loc,
+          section, key,
+        })
+      }
+    }
+    None
+  }
+
+  #[throws(AE)]
+  pub fn establish_server_name(&self) -> ServerName {
+    let key = "server";
+    let raw = match self.lookup_raw(
+      key,
+      [ &SectionName::Common, &SN::special_server_section() ].iter().cloned()
+    ) {
+      Some(raw) => raw.try_map(|os| os.value())?,
+      None => SPECIAL_SERVER_SECTION,
+    };
+    ServerName(raw.into())
   }
 }
 
+impl<'c> ResolveContext<'c> {
+  fn first_of_raw(&'c self, key: &'static str, sections: SectionKindList)
+                  -> Option<RawValRef<'c,'c,'c>> {
+    self.agg.lookup_raw(
+      key,
+      self.all_sections.iter()
+        .filter(|s| sections.contains(s))
+    )
+  }
+
+  #[throws(AE)]
+  fn first_of<T>(&self, key: &'static str, sections: SectionKindList)
+                 -> Option<T>
+  where T: Parseable
+  {
+    match self.first_of_raw(key, sections) {
+      None => None,
+      Some(raw) => Some(raw.try_map(Parseable::parse)?),
+    }
+  }
+
+  #[throws(AE)]
+  pub fn ordinary<T>(&self, key: &'static str) -> T
+  where T: Parseable
+  {
+    match self.first_of(key, SKL::Ordinary)? {
+      Some(y) => y,
+      None => Parseable::default_for_key(key)?,
+    }
+  }
+
+  #[throws(AE)]
+  pub fn limited<T>(&self, key: &'static str) -> T
+  where T: Parseable + Ord
+  {
+    let val = self.ordinary(key)?;
+    if let Some(limit) = self.first_of(key, SKL::Limits)? {
+      min(val, limit)
+    } else {
+      val
+    }
+  }
+
+  #[throws(AE)]
+  pub fn client<T>(&self, key: &'static str) -> T
+  where T: Parseable + Default {
+    match self.end {
+      LinkEnd::Client => self.ordinary(key)?,
+      LinkEnd::Server => default(),
+    }
+  }
+  #[throws(AE)]
+  pub fn server<T>(&self, key: &'static str) -> T
+  where T: Parseable + Default {
+    match self.end {
+      LinkEnd::Server => self.ordinary(key)?,
+      LinkEnd::Client => default(),
+    }
+  }
+
+  #[throws(AE)]
+  pub fn special_ipif(&self, key: &'static str) -> String {
+    match self.end {
+      LinkEnd::Client => self.ordinary(key)?,
+      LinkEnd::Server => {
+        self.first_of(key, SKL::ClientAgnostic)?
+          .unwrap_or_default()
+      },
+    }
+  }
+
+  #[throws(AE)]
+  pub fn special_link(&self, _key: &'static str) -> LinkName {
+    self.link.clone()
+  }
+}
+
+impl InstanceConfig {
+  #[throws(AE)]
+  fn complete(&mut self, end: LinkEnd) {
+    let mut vhosts = self.vnetwork.iter()
+      .map(|n| n.hosts()).flatten()
+      .filter({ let vaddr = self.vaddr; move |v| v != &vaddr });
+
+    if self.vaddr.is_unspecified() {
+      self.vaddr = vhosts.next().ok_or_else(
+        || anyhow!("vnetwork too small to generate vaddrr")
+      )?;
+    }
+    if self.vrelay.is_unspecified() {
+      self.vrelay = vhosts.next().ok_or_else(
+        || anyhow!("vnetwork too small to generate vrelay")
+      )?;
+    }
+
+    match end {
+      LinkEnd::Client => {
+        if &self.url == &default::<Uri>() {
+          let addr = self.addrs.get(0).ok_or_else(
+            || anyhow!("client needs addrs or url set")
+          )?;
+          self.url = format!(
+            "http://{}{}/",
+            match addr {
+              IpAddr::V4(a) => format!("{}", a),
+              IpAddr::V6(a) => format!("[{}]", a),
+            },
+            match self.port {
+              80 => format!(""),
+              p => format!(":{}", p),
+            })
+            .parse().unwrap()
+        }
+      },
+
+      LinkEnd::Server => {
+        if self.addrs.is_empty() {
+          throw!(anyhow!("missing 'addrs' setting"))
+        }
+      },
+    }
+
+    #[throws(AE)]
+    fn subst(var: &mut String,
+             kv: &mut dyn Iterator<Item=(&'static str, &dyn Display)>
+    ) {
+      let substs = kv
+        .map(|(k,v)| (k.to_string(), v.to_string()))
+        .collect::<HashMap<String, String>>();
+      let bad = parking_lot::Mutex::new(vec![]);
+      *var = regex_replace_all!(
+        r#"%(?:%|\((\w+)\)s|\{(\w+)\}|.)"#,
+        &var,
+        |whole, k1, k2| (|| Ok::<_,String>({
+          if whole == "%%" { "%" }
+          else if let Some(&k) = [k1,k2].iter().find(|&&s| s != "") {
+            substs.get(k).ok_or_else(
+              || format!("unknown key %({})s", k)
+            )?
+          } else {
+            throw!(format!("bad percent escape {:?}", &whole));
+          }
+        }))().unwrap_or_else(|e| { bad.lock().push(e); "" })
+      ).into_owned();
+      let bad = bad.into_inner();
+      if ! bad.is_empty() {
+        throw!(anyhow!("substitution failed: {}", bad.iter().format("; ")));
+      }
+    }
+
+    {
+      use LinkEnd::*;
+      type DD<'d> = &'d dyn Display;
+      fn dv<T:Display>(v: &[T]) -> String {
+        format!("{}", v.iter().format(" "))
+      }
+      let mut ipif = mem::take(&mut self.ipif); // lets us borrow all of self
+      let s = &self; // just for abbreviation, below
+      let vnetwork = dv(&s.vnetwork);
+      let vroutes  = dv(&s.vroutes);
+
+      let keys = &["local",       "peer",    "rnets",   "ifname"];
+      let values = match end {
+ Server => [&s.vaddr as DD      , &s.vrelay, &vnetwork, &s.ifname_server],
+ Client => [&s.link.client as DD, &s.vaddr,  &vroutes,  &s.ifname_client],
+      };
+      let always = [
+        ( "mtu",     &s.mtu as DD ),
+      ];
+
+      subst(
+        &mut ipif,
+        &mut keys.iter().cloned()
+          .zip_eq(values)
+          .chain(always.iter().cloned()),
+      ).context("ipif")?;
+      self.ipif = ipif;
+    }
+  }
+}
 
 #[throws(AE)]
-pub fn read() {
+pub fn read(end: LinkEnd) -> Vec<InstanceConfig> {
   let opts = config::Opts::from_args();
 
-  (||{
-    let agg = Aggregate::default();
+  let agg = (||{
+    let mut agg = Aggregate::default();
+    agg.keys_allowed.extend(
+      InstanceConfig::FIELDS.iter().cloned()
+    );
+
+    agg.read_string(DEFAULT_CONFIG.into(),
+                    "<build-in defaults>".as_ref()).unwrap();
 
     agg.read_toplevel(&opts.config)?;
     for extra in &opts.extra_config {
       agg.read_extra(extra).context("extra config")?;
     }
 
-    eprintln!("GOT {:?}", agg);
+    //eprintln!("GOT {:#?}", agg);
+
+    Ok::<_,AE>(agg)
+  })().context("read configuration")?;
+
+  let server_name = match end {
+    LinkEnd::Server => Some(agg.establish_server_name()?),
+    LinkEnd::Client => None,
+  };
+
+  let instances = agg.instances(server_name.as_ref());
+  let mut ics = vec![];
+
+  for link in instances {
+    let rctx = ResolveContext {
+      agg: &agg,
+      link: &link,
+      end,
+      all_sections: vec![
+        SN::Link(link.clone()),
+        SN::Client(link.client.clone()),
+        SN::Server(link.server.clone()),
+        SN::Common,
+        SN::ServerLimit(link.server.clone()),
+        SN::GlobalLimit,
+      ],
+    };
+
+    let mut ic = InstanceConfig::resolve_instance(&rctx)
+      .with_context(|| format!("resolve config for {}", &link))?;
+
+    ic.complete(end)
+      .with_context(|| format!("complete config for {}", &link))?;
+
+    ics.push(ic);
+  }
 
-    Ok::<_,AE>();
-  }).context("read configuration");
+  ics
 }