// Copyright 2021 Ian Jackson and contributors to Hippotat
-// SPDX-License-Identifier: AGPL-3.0-or-later
+// SPDX-License-Identifier: GPL-3.0-or-later
// There is NO WARRANTY.
use crate::prelude::*;
use configparser::ini::Ini;
+#[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 success_report_interval: Duration,
+ #[client] pub url: Uri,
+ #[client] pub vroutes: Vec<IpNet>,
+
+ // Computed, rather than looked up. Client only:
+ #[computed] pub effective_http_timeout: Duration,
+}
+
+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
+success_report_interval = 3600
+
+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 {
/// Top-level config file or directory
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(pub)]
+impl u32 {
+ fn sat(self) -> usize { self.try_into().unwrap_or(usize::MAX) }
+}
- // Ordinary settings, used by server only:
- pub max_clock_skew: Duration,
+#[ext]
+impl<'s> Option<&'s str> {
+ #[throws(AE)]
+ fn value(self) -> &'s str {
+ self.ok_or_else(|| anyhow!("value needed"))?
+ }
+}
- // 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(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(***)")? }
}
#[derive(Debug,Clone,Hash,Eq,PartialEq)]
pub enum SectionName {
- Link { server: ServerName, client: ClientName },
+ Link(LinkName),
Client(ClientName),
Server(ServerName), // includes SERVER, which is slightly special
+ ServerLimit(ServerName),
+ GlobalLimit,
Common,
- Default,
}
pub use SectionName as SN;
#[derive(Debug,Clone)]
-struct RawVal { val: Option<String>, loc: Arc<PathBuf> }
+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,
+}
+
+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 {
+ keys_allowed: HashMap<&'static str, SectionKindList>,
sections: HashMap<SectionName, SectionMap>,
}
fn from_str(s: &str) -> Self {
match s {
"COMMON" => return SN::Common,
- "DEFAULT" => return SN::Default,
+ "LIMIT" => return SN::GlobalLimit,
_ => { }
};
if let Ok(n@ ServerName(_)) = s.parse() { return SN::Server(n) }
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 { server, client }
+ 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]" )?,
+ }
}
}
if let Some(anyway) = anyway.ok(&y) { return Some(anyway) }
y.context("read")?;
+ 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).map_err(|e| anyhow!("{}", e)).context("parse as INI")?;
throw!(anyhow!("INI file contains settings outside a section"));
}
- let loc = Arc::new(path.to_owned());
+ let loc = Arc::new(path_for_loc.to_owned());
for (sn, vars) in map {
let sn = sn.parse().dcontext(&sn)?;
- self.sections.entry(sn)
- .or_default()
- .extend(
- vars.into_iter()
- .map(|(k,val)| (k, RawVal { val, loc: loc.clone() }))
- );
+
+ 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))
+ }
+ }
+
+ 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() });
+ }
}
- None
}
#[throws(AE)] // AE includes path
}
}
+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() }
+}
-#[throws(AE)]
-pub fn read() {
- let opts = config::Opts::from_args();
- (||{
+#[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),
+ }
+ }
+}
+
+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 computed<T>(&self, _key: &'static str) -> T
+ where T: Default
+ {
+ 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")
+ )?;
+ }
+
+ let check_batch = {
+ let mtu = self.mtu;
+ move |max_batch, key| {
+ if max_batch/2 < mtu {
+ throw!(anyhow!("max batch {:?} ({}) must be >= 2 x mtu ({}) \
+ (to allow for SLIP ESC-encoding)",
+ key, max_batch, mtu))
+ }
+ Ok::<_,AE>(())
+ }
+ };
+
+ 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()
+ }
+
+ self.effective_http_timeout = {
+ let a = self.http_timeout;
+ let b = self.http_timeout_grace;
+ a.checked_add(b).ok_or_else(
+ || anyhow!("calculate effective http timeout ({:?} + {:?})", a, b)
+ )?
+ };
+
+ check_batch(self.max_batch_up, "max_batch_up")?;
+ },
+
+ LinkEnd::Server => {
+ if self.addrs.is_empty() {
+ throw!(anyhow!("missing 'addrs' setting"))
+ }
+ check_batch(self.max_batch_down, "max_batch_down")?;
+ },
+
+ // xxx check target vs max req outstanding
+ }
+
+ #[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(opts: &Opts, end: LinkEnd) -> Vec<InstanceConfig> {
+ 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>(())
+ 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);
+ }
+
+ ics
}