--- /dev/null
+((rust-mode . ((rust-indent-offset . 2))))
-rndaddtoentcnt
+/target
+/docs/html
+/docs/doctrees
+/stamp
--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bytes"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
+
+[[package]]
+name = "cc"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
+
+[[package]]
+name = "cervine"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f0db89834ef04fc63d2f136327b42d532b45def0345213d28690a3446c7bdb5"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "configparser"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7201ee416d124d589a820111ba755930df8b75855321a9a1b87312a0597ec8f"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "env_logger"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "extend"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5c89e2933a4ec753dc007a4d6a7f9b6dc8e89b8fe89cabc252ccddf39c08bb1"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "fehler"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635"
+dependencies = [
+ "fehler-macros",
+]
+
+[[package]]
+name = "fehler-macros"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "futures"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57"
+dependencies = [
+ "autocfg",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53"
+
+[[package]]
+name = "futures-task"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
+
+[[package]]
+name = "futures-util"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
+dependencies = [
+ "autocfg",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "proc-macro-hack",
+ "proc-macro-nested",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hippotat"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "base64",
+ "cervine",
+ "configparser",
+ "env_logger",
+ "extend",
+ "fehler",
+ "futures",
+ "hippotat-macros",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "itertools",
+ "lazy-regex",
+ "log",
+ "parking_lot",
+ "regex",
+ "sha2",
+ "structopt",
+ "thiserror",
+ "tokio",
+ "void",
+]
+
+[[package]]
+name = "hippotat-macros"
+version = "0.0.0"
+dependencies = [
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "http"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68"
+
+[[package]]
+name = "httpdate"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "hyper"
+version = "0.14.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7728a72c4c7d72665fde02204bcbd93b247721025b222ef78606f14513e0fd03"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+
+[[package]]
+name = "itertools"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "lazy-regex"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17d198f91272f6e788a5c0bd5d741cf778da4e5bc761ec67b32d5d3b0db34a54"
+dependencies = [
+ "lazy-regex-proc_macros",
+ "once_cell",
+ "regex",
+]
+
+[[package]]
+name = "lazy-regex-proc_macros"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c12938b1b92cf5be22940527e15b79fd0c7e706e34bc70816f6a72b3484f84e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790"
+
+[[package]]
+name = "lock_api"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
+
+[[package]]
+name = "mio"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "openssl"
+version = "0.10.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
+
+[[package]]
+name = "proc-macro-nested"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
+dependencies = [
+ "lazy_static",
+ "winapi",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "security-framework"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
+
+[[package]]
+name = "smallvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+
+[[package]]
+name = "socket2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "structopt"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b041cdcb67226aca307e6e7be44c8806423d83e018bd662360a93dabce4d71"
+dependencies = [
+ "clap",
+ "lazy_static",
+ "structopt-derive",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7813934aecf5f51a54775e00068c237de98489463968231a51746bbbc03f9c10"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2602b8af3767c285202012822834005f596c811042315fa7e9f5b12b2a43207"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
+[[package]]
+name = "tracing"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
+[[package]]
+name = "typenum"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
--- /dev/null
+# Copyright 2021 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+[package]
+name = "hippotat"
+version = "0.0.0"
+edition = "2018"
+description="Asinine HTTP-over-IP"
+license="GPL-3.0-or-later"
+repository="https://salsa.debian.org/iwj/hippotat"
+
+[workspace]
+members = ["macros"]
+
+[[bin]]
+name="hippotat"
+path="src/bin/client.rs"
+
+[[bin]]
+name="hippotatd"
+path="src/bin/server.rs"
+
+[dependencies]
+
+hippotat-macros = { path = "macros" }
+
+# versions specified here are mostly just guesses at what is needed
+# (or currently available):
+anyhow = "1"
+base64 = "0.13"
+configparser = "2"
+env_logger = "0.9"
+futures = "0.3"
+hyper = { version = "0.14", features = ["full"] }
+hyper-tls = "0.5"
+ipnet = "2"
+itertools = "0.10"
+parking_lot = "0.11"
+regex = "1.5"
+log = "0.4"
+sha2 = "0.9"
+structopt = "0.3"
+tokio = { version = "1", features = ["full"] }
+thiserror = "1"
+void = "1"
+
+# Not in sid:
+extend = "1" # no deps not in sid
+fehler = "1" # no deps (other than fehler-macros, obvs)
+lazy-regex = "2" # no deps not in sid
+cervine = "0.0" # no (non-dev)-deps not in sid
--- /dev/null
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
-rndaddtoentcnt: rndaddtoentcnt.c
- $(CC) rndaddtoentcnt.c -o rndaddtoentcnt
+# Copyright 2020-2021 Ian Jackson and contributors to Otter
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+SHELL=/bin/bash
+
+default: all
+
+CARGO ?= cargo
+TARGET_DIR ?= target
+
+SPHINXBUILD ?= sphinx-build
+
+ifneq (,$(wildcard ../Cargo.nail))
+
+NAILING_CARGO ?= nailing-cargo
+CARGO = $(NAILING_CARGO)
+BUILD_SUBDIR ?= ../Build
+TARGET_DIR = $(BUILD_SUBDIR)/$(notdir $(PWD))/target
+NAILING_CARGO_JUST_RUN ?= $(NAILING_CARGO) --just-run -q ---
+
+else
+
+endif # Cargo.nail
+
+rsrcs = $(shell $(foreach x,$(MAKEFILE_FIND_X),set -$x;)\
+ find -H $1 \( -name Cargo.toml -o -name Cargo.lock -o -name Cargo.lock.example -o -name \*.rs \) )
+stamp=@mkdir -p stamp; touch $@
+
+all: cargo-build doc
+
+cargo-build: stamp/cargo-build
+
+stamp/cargo-build: $(call rsrcs,.)
+ $(NAILING_CARGO) build $(CARGO_BUILD_OPTIONS)
+ $(stamp)
+
+doc: docs/html/index.html
+ @echo 'Documentation can now be found here:'
+ @echo ' file://$(PWD)/$<'
+
+docs/html/index.html: docs/conf.py $(wildcard docs/*.md docs/*.rst docs/*.png)
+ $(SPHINXBUILD) -M html docs docs $(SPHINXOPTS)
-.PHONY: clean
clean:
- rm -f *.o rndaddtoentcnt
+ rm -rf stamp/* doc/html
+ $(NAILING_CARGO) clean
+
+.PHONY: cargo-build all doc clean
--- /dev/null
+Server maintains a queue of outbound packets for each user
+
+Packets which are older than the applicable max_queue_time are discarded
+
+Each incoming request to the server takes up to max_batch_down bytes
+from the queue and returns them as the POST response body payload
+
+Each incoming request contains up to max_batch_up bytes of payload.
+It's a multipart/form-data.
+
+Authentication: clock-based lifetime-limited bearer tokens.
+
+Encryption and integrity checking: none. Use a real VPN over this!
+
+Routing assistance: none in hippotat; can be requested on client
+ from userv-ipif via `vroutes' parameter. Use with secnet polypath
+ ideally uses the special support in secnet 0.4.x.
+
+Client form parameters (multipart/form-data):
+ m metadata, newline-separated list (text file) of
+ client ip address (textual)
+ token
+ target_requests_outstanding
+ http_timeout
+ max_batch_down
+ d data (SLIP format, with SLIP_ESC and `-' swapped)
+
+
+Authentication token is:
+ <time_t in hex with no leading 0s> <hmac in base64>
+(separated by a single space). The hmac is
+ HMAC(secret, <time_t in hex>)
+and the hash function is SHA256
+
+
+Possible future nonce-based authentication:
+
+server keeps big nonce counter for each client
+meaning is:
+ nonce counter is most recent nonce client has sent
+also server keeps bitmap of the previous ?64 nonces,
+ whether client has sent them
+
+difficult because client-generated nonces would have to never go
+backwaards which basically means never-rewinding state on the client.
-### rndaddtoentcnt
-
-Seeding the random number generator by writing to /dev/urandom does not update the entropy count.
-
-This utility makes the RNDADDTOENTCNT ioctl call needed to do this.
-
-Used in startup scripts after initializing /dev/urandom with a presaved seed.
-
-Example:
-
- dd if=/path/to/some/random-seed-file of=/dev/urandom bs=512 count=1
-
- /path/to/rdnaddtoentcnt <entropy-bit-count>
-
-where entropy-bit-count is a number between 1 and (8 * 512) depending on how much you trust the seed file.
+Introduction
+============
--- /dev/null
+../README.md
\ No newline at end of file
--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Hippotat'
+copyright = '2021 Ian Jackson and the contributors to Hippotat'
+author = 'Ian Jackson et al'
+
+# The short X.Y version
+version = ''
+# The full version, including alpha/beta/rc tags
+release = ''
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'recommonmark',
+ 'sphinx.ext.autosectionlabel',
+]
+autosectionlabel_prefix_document = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+#source_suffix = ['.rst', '.md']
+#source_suffix = '.rst'
+
+# https://github.com/readthedocs/recommonmark (retrieved 8.4.2021)
+from recommonmark.parser import CommonMarkParser
+source_parsers = {
+ '.md': CommonMarkParser,
+}
+source_suffix = {
+ '.rst': 'restructuredtext',
+ '.txt': 'markdown',
+ '.md': 'markdown',
+}
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'classic'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself. Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Hippotatdoc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'Hippotat.tex', 'Hippotat Documentation',
+ 'Ian Jackson and the contributors to Hippotat', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'otter', 'Hippotat Documentation',
+ [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'Hippotat', 'Hippotat Documentation',
+ author, 'Hippotat', 'Online Table Top Environment Renderer',
+ 'Games'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
--- /dev/null
+Configuration scheme
+====================
+
+Configuration is in an INI-like format. Sections start with lines
+``[...]``. Every setting must be in a section. ``#`` and ``;``
+comment lines are supported. Settings ``nmae = value``. Whitespace
+around the name and value is ignored.
+
+The configuration files are resolved to a configuration for each
+pairwise link between a client and a server.
+
+Sections
+--------
+
+The same config key may appear in multiple sections; for example, in a
+section specific to a link, as well as one for all links to a server.
+
+Unless otherwise specified, any particular config setting for a
+particular link is the value from the first of the following
+applicable sections, or failing that the built-in default:
+
+ * ``[<servername> <client>]``
+ * ``[<client>]``
+ * ``[<servername>]`` (often ``[SERVER]``)
+ * ``[COMMON]``
+
+``<client>`` is the client's virtual address in IPv4 or IPv6 literal
+syntax (without any surrounding ``[..]``.
+
+``<servername>`` must be in the syntrax of a valid lowercase DNS
+hostname (and not look like an address), or be literally ``SERVER``.
+
+There are also these special sections:
+
+ * ``[<servername> LIMIT]``
+ * ``[LIMIT]``
+
+Files
+-----
+
+Both client and server read the files
+
+ * ``/etc/hippotat/main.cfg`` (if it exists)
+ * ``/etc/hippotat/config.d/*``
+ * ``/etc/hippotat/secrets.d/*``
+
+Here ``*`` means all contained files whose names consists of only
+ascii alphanumerics plus ``-`` and ``_``.
+
+The ``--config`` option can be used to override the directory (usually
+``/etc/hippotat``). Additonally each ``--extra-config`` option names
+an existing file or directory to be processed analagously.
+
+The ini file format sections from these files are all unioned. Later
+files (in the list above, or alphabetically later) can override
+settings from earlier ones.
+
+Note that although it is conventional for information for a particular
+server or client to be in a file named after that endpoint, there is
+no semantic link: all the files are always read and the appropriate
+section from each is applied to every link.
+
+(If ``main.cfg`` does not exist, ``master.cfg`` will be tried for
+backward compatibility reasons.)
--- /dev/null
+Hippotat - Asinine IP over HTTP
+===============================
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ README
+ config.rst
+ settings.rst
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
--- /dev/null
+Configuration settings
+======================
+
+Exceptional settings
+--------------------
+
+``server``
+ Specifies ``<servername>``.
+ Is looked up in ``[SERVER]`` and ``[COMMON]`` only.
+ If not specified there, it is ``SERVER``.
+
+ Used by server to select the appropriate parts of the
+ rest of the configuration. Ignored by the client.
+
+``secret``
+ Looked up in the usual way, but used by client and server to
+ determine which possible peerings to try to set up, and which to
+ ignore.
+
+ We define the sets of putative clients and servers, as follows:
+ all those, for which there is any section (even an empty one)
+ whose name is based on ``<client>`` or ``<servername>`` (as applicable).
+ (``LIMIT`` sections do not count.)
+
+ The server queue packets for, and accept requests from, each
+ putative client for which the config search yields a secret.
+
+ Each client will create a local interface, and try to communicate
+ with the server, for each possible pair (putative server,
+ putative client) for which the config search yields a secret.
+
+ The value is a string, fed directly into HMAC.
+
+``ipif``
+ Command to run to create and communicate with local network
+ interface. Passed to sh -c. Must speak SLIP on stdin/stdout.
+ The following interpolations aare substituted:
+
+ ============== ============ ============ =============== =================
+ Input ``%{local}`` ``%{peer}`` ``%{rnets}`` ``%{ifname}``
+ ============== ============ ============ =============== =================
+ **on server** ``vaddr`` ``vrelay`` ``vnetwork`` ``ifname_server``
+ **on client** ``client`` ``vaddr`` ``vroutes`` ``ifname_client``
+ ============== ============ ============ =============== =================
+
+ **Always:** ``%{mtu}``, and ``%%`` to indicate a literal ``%``.
+
+ (For compatibility with older hippotat, ``%(var)s`` is supported too
+ but this is deprecated since the extra ``s`` is confusing.)
+
+ On server: applies to all clients; not looked up in client-specific sections.
+ On client: may be different for different servers.
+
+ [string; ``userv root ipif %{local},%{peer},%{mtu},slip '%{rnets}'``]
+
+
+Capped settings
+---------------
+
+Values in ``[<server> LIMIT]`` and ``[LIMIT]`` are a cap (maximum) on
+those from the other sections (including ``COMMON``). If a larger
+value is obtained, it is (silently) reduced to the limit value.
+
+
+``max_batch_down``
+ Size limit for response payloads.
+
+ On client, incoming response bodies are limited to this plus
+ a fixed constant metadata overhead of 10000 bytes.
+ Server uses minimum of client and server value (old servers
+ just uses server's value).
+
+ [``65536`` (bytes); ``LIMIT``: ``262144``]
+
+``max_queue_time``
+ Discard packets after they have been queued this long
+ waiting for http.
+
+ On server: setting applies to downward packets.
+ On client: setting applies to upward packets.
+
+ [``10`` (s); ``LIMIT``: ``121``]
+
+``http_timeout``
+ On server: return with empty payload any http request oustanding
+ for this long.
+
+ On client: give up on any http request outstanding for
+ for this long plus ``http_timeout_grace``.
+
+ Warning messages about link problems, printed by the client,
+ are rate limited to no more than one per effective timeout.
+
+ Client's effective timeout must be at least server's (checked).
+
+ [``30`` (s); ``LIMIT``: ``121``]
+
+target_requests_outstanding
+ On client: try to keep this many requests outstanding, to
+ allow for downbound data transfer.
+ On server: whenever number of outstanding requests for
+ a client exceeds this, returns oldest with empty payload.
+ Must match between client and server (checked).
+ [``3``; ``LIMIT``: ``10``]
+
+
+Ordinary settings, used by both, not client-specific
+----------------------------------------------------
+
+These are not looked up in the client-specific config sections.
+
+``addrs``
+ Public IP (v4 or v6) address(es) of the server; space-separated.
+ On server: mandatory; used for bind.
+ On client: used only to construct default ``url``.
+ No default.
+
+``vnetwork``
+ Private network range. Must contain all
+ ``<client>``s. Must contain ``vaddr`` and ``vrelay``, and is used
+ to compute their defaults. [CIDR syntax (``<prefix>/<length>``);
+ ``172.24.230.192/28``]
+
+``vaddr``
+ Address of server's virtual interface.
+ [default: first host entry in ``vnetwork``, so ``172.24.230.193``]
+
+``vrelay``
+ Virtual point-to-point address used for tunnel routing
+ (does not appear in packets).
+ [default: first host entry in ``vnetwork`` other than ``vaddr``,
+ so ``172.24.230.194``]
+
+``port``
+ Public port number of the server.
+ On server: used for bind.
+ On client: used only to construct default url.
+ [``80``]
+
+``mtu``
+ Of virtual interface.
+ Must match exactly at each end - *this is not checked*.
+ [``1500`` (bytes)]
+
+``ifname_server``
+ | Virtual interface name on the server. [``shippo%d``]
+ | Any ``%d`` is interpolated (by the kernel).
+
+``ifname_client``
+ | Virtual interface name on the client. [``hippo%d``]
+ | Any ``%d`` is interpolated (by the kernel).
+
+
+Ordinary settings, used by server only
+--------------------------------------
+
+``max_clock_skew``
+ Permissible clock skew between client and server.
+ Hippotat will not work if clock skew is more than this.
+ Conversely: when moving client from one public network to
+ another, the first network can deny service to the client for
+ this period after the client leaves the first network.
+ [``300`` (s)]
+
+
+Ordinary settings, used by client only
+--------------------------------------
+
+``http_timeout_grace``
+ See ``http_timeout``. [``5`` (s)]
+
+``max_requests_outstanding``
+ Client will hold off sending more requests than this to
+ server even if it has data to send. [``6``]
+
+``max_batch_up``
+ Size limit for request upbound payloads. [``4000`` (bytes)]
+
+``success_report_interval``
+ If nonzero, report success periodically. Otherwise just
+ report it when we first have success. [``3600`` (s)]
+
+``http_retry``
+ If a request fails, wait this long before considering it
+ "finished" - to limit rate of futile requests (and also
+ to limit rate of moaning on stderr). [``5`` s]
+
+``url``
+ Public url of server.
+ [``http://<first-entry-in-addrs>:<port>/``]
+
+``vroutes``
+ Additional virtual addresses to be found at the server
+ end, space-separated. Routes to those will be created on
+ the client. ``vrelay`` is included implicitly.
+ [CIDR syntax, space separated; default: none]
--- /dev/null
+# Copyright 2021 Ian Jackson and contributors to Hippotat
+# SPDX-License-Identifier: GPL-3.0-or-later
+# There is NO WARRANTY.
+
+[package]
+name = "hippotat-macros"
+version = "0.0.0"
+edition = "2018"
+description="Asinine HTTP-over-IP, proc-macros"
+license="GPL-3.0-or-later"
+repository="https://salsa.debian.org/iwj/hippotat"
+
+[dependencies]
+itertools = "0.10"
+syn = "1"
+proc-macro2 = "1"
+quote = "1"
+
+[lib]
+path = "macros.rs"
+proc-macro = true
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use syn::{parse_macro_input, parse_quote};
+use syn::{Data, DataStruct, DeriveInput, LitStr, Meta, NestedMeta};
+use quote::{quote, quote_spanned, ToTokens};
+use proc_macro2::{Literal, TokenStream};
+
+use itertools::Itertools;
+
+/// Generates config resolver method
+///
+/// Atrributes:
+///
+/// * `limited`, `server`, `client`: cooked sets of settings;
+/// default `SKL` is `Ordinary` except for `limited`
+/// * `special(method, SKL)`
+///
+/// Generated code
+///
+/// ```no_run
+/// impl<'c> ResolveContext<'c> {
+///
+/// const FIELDS: &'static [(&'static str, SectionKindList)] = &[ ... ];
+///
+/// #[throws(AE)]
+/// fn resolve_instance(&self) -> InstanceConfig {
+/// InstanceConfig {
+/// ...
+/// max_batch_down: self.limited("max_batch_down")?,
+/// ...
+/// }
+/// }
+/// }
+/// ```
+#[proc_macro_derive(ResolveConfig, attributes(
+ limited, server, client, computed, special
+))]
+pub fn resolve(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let input = parse_macro_input!(input as DeriveInput);
+
+ let fields = match input.data {
+ Data::Struct(DataStruct { fields: syn::Fields::Named(ref f),.. }) => f,
+ _ => panic!(),
+ };
+
+ let target = &input.ident;
+
+ let mut names = vec![];
+ let mut output = vec![];
+ for field in &fields.named {
+ //dbg!(field);
+ let fname = &field.ident.as_ref().unwrap();
+ let fname_span = fname.span();
+ let mut skl = quote_spanned!{fname_span=> SectionKindList::Ordinary };
+ let mut method = quote_spanned!{fname_span=> ordinary };
+ for attr in &field.attrs {
+ if attr.tokens.is_empty() {
+ let atspan = attr.path.segments.last().unwrap().ident.span();
+ method = attr.path.to_token_stream();
+ if &attr.path == &parse_quote!{ limited } {
+ skl = quote_spanned!{atspan=> SectionKindList::Limited };
+ }
+ } else if &attr.path == &parse_quote!{ special } {
+ let meta = match attr.parse_meta().unwrap() {
+ Meta::List(list) => list,
+ _ => panic!(),
+ };
+ let (tmethod, tskl) = meta.nested.iter().collect_tuple().unwrap();
+ fn get_path(meta: &NestedMeta) -> TokenStream {
+ match meta {
+ NestedMeta::Meta(Meta::Path(ref path)) => path.to_token_stream(),
+ _ => panic!(),
+ }
+ }
+ method = get_path(tmethod);
+ skl = get_path(tskl);
+ }
+ }
+ let fname_string = fname.to_string();
+ let fname_lit = Literal::string( &fname_string );
+
+ names.push(quote!{
+ (#fname_lit, #skl),
+ });
+ //dbg!(&method);
+ output.push(quote!{
+ #fname: rctx. #method ( #fname_lit )?,
+ });
+ //eprintln!("{:?} method={:?} skl={:?}", field.ident, method, skl);
+ }
+ //dbg!(&output);
+
+ let output = quote! {
+ impl #target {
+ const FIELDS: &'static [(&'static str, SectionKindList)]
+ = &[ #( #names )* ];
+
+ fn resolve_instance(rctx: &ResolveContext)
+ -> ::std::result::Result<#target, anyhow::Error>
+ {
+ ::std::result::Result::Ok(#target {
+ #( #output )*
+ })
+ }
+ }
+ };
+ //eprintln!("{}", &output);
+ output.into()
+}
+
+#[proc_macro]
+pub fn into_crlfs(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let input: proc_macro2::TokenStream = input.into();
+ let token: LitStr = syn::parse2(input).expect("expected literal");
+ let input = token.value();
+ let output = input.split_inclusive('\n')
+ .map(|s| s.trim_start_matches(&[' ','\t'][..]))
+ .map(|s| match s.strip_suffix("\n") {
+ None => [s, ""],
+ Some(l) => [l, "\r\n"],
+ })
+ .flatten()
+ .collect::<String>();
+ //dbg!(&output);
+ let output = LitStr::new(&output, token.span());
+ let output = quote!(#output);
+ output.into()
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use hippotat::prelude::*;
+use hippotat_macros::into_crlfs;
+
+const MAX_BATCH_DOWN_RESP_OVERHEAD: usize = 10_000;
+
+#[derive(StructOpt,Debug)]
+pub struct Opts {
+ #[structopt(flatten)]
+ log: LogOpts,
+
+ #[structopt(flatten)]
+ config: config::Opts,
+}
+
+type OutstandingRequest<'r> = Pin<Box<
+ dyn Future<Output=Option<Box<[u8]>>> + Send + 'r
+ >>;
+
+impl<T> HCC for T where
+ T: hyper::client::connect::Connect + Clone + Send + Sync + 'static { }
+trait HCC: hyper::client::connect::Connect + Clone + Send + Sync + 'static { }
+
+struct ClientContext<'c,C> {
+ ic: &'c InstanceConfig,
+ hclient: &'c Arc<hyper::Client<C>>,
+ reporter: &'c parking_lot::Mutex<Reporter<'c>>,
+}
+
+#[derive(Debug)]
+struct TxQueued {
+ expires: Instant,
+ data: Box<[u8]>,
+}
+
+#[throws(AE)]
+fn submit_request<'r, 'c:'r, C:HCC>(
+ c: &'c ClientContext<C>,
+ req_num: &mut ReqNum,
+ reqs: &mut Vec<OutstandingRequest<'r>>,
+ upbound: FramesData,
+) {
+ let show_timeout = c.ic.http_timeout
+ .saturating_add(Duration::from_nanos(999_999_999))
+ .as_secs();
+
+ let time_t = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_else(|_| Duration::default()) // clock is being weird
+ .as_secs();
+ let time_t = format!("{:x}", time_t);
+ let hmac = token_hmac(c.ic.secret.0.as_bytes(), time_t.as_bytes());
+ let mut token = time_t;
+ write!(token, " ").unwrap();
+ base64::encode_config_buf(&hmac, BASE64_CONFIG, &mut token);
+
+ let req_num = { *req_num += 1; *req_num };
+
+ let prefix1 = format!(into_crlfs!(
+ r#"--b
+ Content-Type: text/plain; charset="utf-8"
+ Content-Disposition: form-data; name="m"
+
+ {}
+ {}
+ {}
+ {}
+ {}"#),
+ &c.ic.link.client,
+ token,
+ c.ic.target_requests_outstanding,
+ show_timeout,
+ c.ic.max_batch_down,
+ );
+
+ let prefix2 = format!(into_crlfs!(
+ r#"
+ --b
+ Content-Type: application/octet-stream
+ Content-Disposition: form-data; name="d"
+
+ "#),
+ );
+ let suffix = format!(into_crlfs!(
+ r#"
+ --b--
+ "#),
+ );
+
+ macro_rules! content { {
+ $out:ty,
+ $iter:ident,
+ $into:ident,
+ } => {
+ itertools::chain![
+ array::IntoIter::new([
+ prefix1.$into(),
+ prefix2.$into(),
+ ]).take(
+ if upbound.is_empty() { 1 } else { 2 }
+ ),
+ Itertools::intersperse(
+ upbound.$iter().map(|u| { let out: $out = u.$into(); out }),
+ SLIP_END_SLICE.$into()
+ ),
+ [ suffix.$into() ],
+ ]
+ }}
+
+ let body_len: usize = content!(
+ &[u8],
+ iter,
+ as_ref,
+ ).map(|b| b.len()).sum();
+
+ trace!("{} #{}: req; tx bytes={} frames={}",
+ &c.ic, req_num, body_len, upbound.len());
+
+ let body = hyper::body::Body::wrap_stream(
+ futures::stream::iter(
+ content!(
+ Bytes,
+ into_iter,
+ into,
+ ).map(Ok::<Bytes,Void>)
+ )
+ );
+
+ let req = hyper::Request::post(&c.ic.url)
+ .header("Content-Type", r#"multipart/form-data; boundary="b""#)
+ .header("Content-Length", body_len)
+ .body(body)
+ .context("construct request")?;
+
+ let resp = c.hclient.request(req);
+ let fut = Box::pin(async move {
+ let r = async { tokio::time::timeout( c.ic.effective_http_timeout, async {
+ let resp = resp.await.context("make request")?;
+ let status = resp.status();
+ let resp = resp.into_body();
+ let max_body = c.ic.max_batch_down.sat() + MAX_BATCH_DOWN_RESP_OVERHEAD;
+ let resp = read_limited_body(max_body, resp).await?;
+
+ if ! status.is_success() {
+ throw!(anyhow!("HTTP error status={} body={:?}",
+ &status, String::from_utf8_lossy(&resp)));
+ }
+
+ Ok::<_,AE>(resp)
+ }).await? }.await;
+
+ let r = c.reporter.lock().filter(Some(req_num), r);
+
+ if let Some(r) = &r {
+ trace!("{} #{}: rok; rx bytes={}", &c.ic, req_num, r.len());
+ } else {
+ tokio::time::sleep(c.ic.http_retry).await;
+ }
+ r
+ });
+ reqs.push(fut);
+}
+
+async fn run_client<C:HCC>(
+ ic: InstanceConfig,
+ hclient: Arc<hyper::Client<C>>
+) -> Result<Void, AE>
+{
+ debug!("{}: config: {:?}", &ic, &ic);
+
+ let reporter = parking_lot::Mutex::new(Reporter::new(&ic));
+
+ let c = ClientContext {
+ reporter: &reporter,
+ hclient: &hclient,
+ ic: &ic,
+ };
+
+ let mut ipif = tokio::process::Command::new("sh")
+ .args(&["-c", &ic.ipif])
+ .stdin (process::Stdio::piped())
+ .stdout(process::Stdio::piped())
+ .stderr(process::Stdio::piped())
+ .kill_on_drop(true)
+ .spawn().context("spawn ipif")?;
+
+ let stderr = ipif.stderr.take().unwrap();
+ let ic_name = ic.to_string();
+ let _ = task::spawn(async move {
+ let mut stderr = tokio::io::BufReader::new(stderr).lines();
+ while let Some(l) = stderr.next_line().await? {
+ error!("{}: ipif stderr: {}", &ic_name, l.trim_end());
+ }
+ Ok::<_,io::Error>(())
+ });
+
+ let mut req_num: ReqNum = 0;
+
+ let tx_stream = ipif.stdout.take().unwrap();
+ let mut rx_stream = ipif.stdin .take().unwrap();
+
+ let mut tx_stream = tokio::io::BufReader::new(tx_stream).split(SLIP_END);
+ let mut tx_queue: VecDeque<TxQueued> = default();
+ let mut upbound = Frames::default();
+
+ let mut reqs: Vec<OutstandingRequest>
+ = Vec::with_capacity(ic.max_requests_outstanding.sat());
+
+ let mut rx_queue: FrameQueue = default();
+
+ let trouble = async {
+ loop {
+ let rx_queue_space =
+ if rx_queue.remaining() < ic.max_batch_down.sat() {
+ Ok(())
+ } else {
+ Err(())
+ };
+
+ select! {
+ biased;
+
+ y = rx_stream.write_all_buf(&mut rx_queue),
+ if ! rx_queue.is_empty() =>
+ {
+ let () = y.context("write rx data to ipif")?;
+ },
+
+ () = async {
+ let expires = tx_queue.front().unwrap().expires;
+ tokio::time::sleep_until(expires).await
+ },
+ if ! tx_queue.is_empty() =>
+ {
+ let _ = tx_queue.pop_front();
+ },
+
+ data = tx_stream.next_segment(),
+ if tx_queue.is_empty() =>
+ {
+ let data =
+ data.context("read from ipif")?
+ .ok_or_else(|| io::Error::from(io::ErrorKind::UnexpectedEof))?;
+ //eprintln!("data={:?}", DumpHex(&data));
+
+ match check1(Slip2Mime, ic.mtu, &data, |header| {
+ let addr = ip_packet_addr::<false>(header)?;
+ if addr != ic.link.client.0 { throw!(PE::Src(addr)) }
+ Ok(())
+ }) {
+ Ok(data) => tx_queue.push_back(TxQueued {
+ data,
+ expires: Instant::now() + ic.max_queue_time
+ }),
+ Err(PE::Empty) => { },
+ Err(e@ PE::Src(_)) => debug!("{}: tx discarding: {}", &ic, e),
+ Err(e) => error!("{}: tx discarding: {}", &ic, e),
+ };
+ },
+
+ _ = async { },
+ if ! upbound.tried_full() &&
+ ! tx_queue.is_empty() =>
+ {
+ while let Some(TxQueued { data, expires }) = tx_queue.pop_front() {
+ match upbound.add(ic.max_batch_up, data.into()/*todo:504*/) {
+ Err(data) => { tx_queue.push_front(TxQueued { data: data.into(), expires }); break; }
+ Ok(()) => { },
+ }
+ }
+ },
+
+ _ = async { },
+ if rx_queue_space.is_ok() &&
+ (reqs.len() < ic.target_requests_outstanding.sat() ||
+ (reqs.len() < ic.max_requests_outstanding.sat() &&
+ ! upbound.is_empty()))
+ =>
+ {
+ submit_request(&c, &mut req_num, &mut reqs,
+ mem::take(&mut upbound).into())?;
+ },
+
+ (got, goti, _) = async { future::select_all(&mut reqs).await },
+ if ! reqs.is_empty() =>
+ {
+ reqs.swap_remove(goti);
+
+ if let Some(got) = got {
+ reporter.lock().success();
+ //eprintln!("got={:?}", DumpHex(&got));
+ checkn(SlipNoConv,ic.mtu, &got, &mut rx_queue, |header| {
+ let addr = ip_packet_addr::<true>(header)?;
+ if addr != ic.link.client.0 { throw!(PE::Dst(addr)) }
+ Ok(())
+ }, |e| error!("{} #{}: rx discarding: {}", &ic, req_num, e));
+
+ }
+ },
+
+ _ = tokio::time::sleep(c.ic.effective_http_timeout),
+ if rx_queue_space.is_err() =>
+ {
+ reporter.lock().filter(None, Err::<Void,_>(
+ anyhow!("rx queue full, blocked")
+ ));
+ },
+ }
+ }
+ }.await;
+
+ drop(tx_stream);
+
+ match ipif.wait().await {
+ Err(e) => error!("{}: also, failed to await ipif child: {}", &ic, e),
+ Ok(st) if st.success() => { },
+ Ok(st) => error!("{}: ipif process failed: {}", &ic, st),
+ }
+
+ trouble
+}
+
+#[tokio::main]
+async fn main() -> Result<(), AE> {
+ let opts = Opts::from_args();
+
+ let ics = config::read(&opts.config, LinkEnd::Client)?;
+ if ics.is_empty() { throw!(anyhow!("no associations with server(s)")); }
+
+ opts.log.log_init()?;
+
+ let https = HttpsConnector::new();
+ let hclient = hyper::Client::builder().build::<_, hyper::Body>(https);
+ let hclient = Arc::new(hclient);
+
+ info!("starting");
+ let () = future::select_all(
+ ics.into_iter().map(|ic| Box::pin(async {
+ let assocname = ic.to_string();
+ info!("{} starting", &assocname);
+ let hclient = hclient.clone();
+ let join = task::spawn(async {
+ run_client(ic, hclient).await.void_unwrap_err()
+ });
+ match join.await {
+ Ok(e) => {
+ error!("{} failed: {:?}", &assocname, e);
+ },
+ Err(je) => {
+ error!("{} panicked!", &assocname);
+ panic::resume_unwind(je.into_panic());
+ },
+ }
+ }))
+ ).await.0;
+
+ error!("quitting because one of your client connections crashed");
+ process::exit(16);
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use hippotat::prelude::*;
+
+#[derive(StructOpt,Debug)]
+pub struct Opts {
+ #[structopt(flatten)]
+ log: LogOpts,
+
+ #[structopt(flatten)]
+ config: config::Opts,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), AE> {
+ let opts = Opts::from_args();
+
+ let ics = config::read(&opts.config, LinkEnd::Server)?;
+
+ opts.log.log_init()?;
+
+ dbg!(ics);
+
+ Ok(())
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+use configparser::ini::Ini; // xxx ignores empty sections, fix or replace
+
+#[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
+ ///
+ /// Look for `main.cfg`, `config.d` and `secrets.d` here.
+ ///
+ /// Or if this is a file, just read that file.
+ #[structopt(long, default_value="/etc/hippotat")]
+ pub config: PathBuf,
+
+ /// Additional config files or dirs, which can override the others
+ #[structopt(long, multiple=true, number_of_values=1)]
+ pub extra_config: Vec<PathBuf>,
+}
+
+#[ext(pub)]
+impl u32 {
+ fn sat(self) -> usize { self.try_into().unwrap_or(usize::MAX) }
+}
+
+#[ext]
+impl<'s> Option<&'s str> {
+ #[throws(AE)]
+ fn value(self) -> &'s str {
+ self.ok_or_else(|| anyhow!("value needed"))?
+ }
+}
+
+#[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(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,
+}
+
+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 {
+ 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>,
+}
+
+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<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
+ {
+ 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();
+ let y = f.read_to_string(&mut s);
+ 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")?;
+ let map = mem::take(ini.get_mut_map());
+ if map.get(OUTSIDE_SECTION).is_some() {
+ throw!(anyhow!("INI file contains settings outside a section"));
+ }
+
+ 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))
+ }
+ }
+
+ 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<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
+ {
+ 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.len() == 0 { continue }
+ if ! leaf.chars().all(
+ |c| c=='-' || c=='_' || c.is_ascii_alphanumeric()
+ ) { continue }
+
+ // OK we want this one
+ 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) {
+ 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)")?
+ {
+ None | Some(Anyway::None) => { },
+
+ 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, &anyway_none)
+ .dcontext(main).context(desc)?
+ {
+ None => break,
+ Some(AnywayNone) => { },
+ }
+ }
+
+ for &(try_dir, desc) in &[
+ ("config.d", "per-link config directory"),
+ ("secrets.d", "per-link secrets directory"),
+ ] {
+ let dir = mk(try_dir);
+ match self.read_dir_d(&dir, &anyway_none).context(desc)? {
+ None => { },
+ Some(AnywayNone) => { },
+ }
+ }
+ }
+ }
+ }
+
+ #[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| match k {
+ EK::IsADirectory => Some(AnywayDir),
+ _ => None,
+ })
+ .dcontext(extra)?
+ {
+ 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");
+ //dbg!(§ion, has_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; }
+ },
+ _ => { },
+ }
+ }
+
+ //dbg!(&putative_servers, &putative_clients);
+ //dbg!(&secrets_anyserver, &secrets_anyclient, &secret_global);
+
+ // 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)
+ || ! secrets_anyclient.is_empty()
+ ),
+ putative_servers.iter().cloned().filter(
+ |s| secret_global
+ || secrets_anyclient.contains(s)
+ || ! secrets_anyserver.is_empty()
+ )
+ ) {
+ 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),
+ }
+ }
+}
+
+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)
+ )?
+ };
+
+ {
+ let t = self.target_requests_outstanding;
+ let m = self.max_requests_outstanding;
+ if t > m { throw!(anyhow!(
+ "target_requests_outstanding ({}) > max_requests_outstanding ({})",
+ t, m
+ )) }
+ }
+
+ 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")?;
+ },
+ }
+
+ #[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);
+
+ 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![];
+ //dbg!(&instances);
+
+ 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,
+ ],
+ };
+
+ if rctx.first_of_raw("secret", SKL::Ordinary).is_none() { continue }
+
+ 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
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+#![feature(io_error_more)] // EK::IsADirectory
+
+pub mod prelude;
+
+pub mod config;
+pub mod slip;
+pub mod reporter;
+pub mod queue;
+pub mod types;
+pub mod utils;
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+pub use std::array;
+pub use std::collections::{BTreeSet, HashMap, VecDeque};
+pub use std::convert::{TryFrom, TryInto};
+pub use std::borrow::Cow;
+pub use std::cmp::{min, max};
+pub use std::fs;
+pub use std::fmt::{self, Debug, Display, Write as _};
+pub use std::future::Future;
+pub use std::io::{self, Cursor, ErrorKind, Read as _, Write as _};
+pub use std::iter;
+pub use std::mem;
+pub use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+pub use std::path::{Path, PathBuf};
+pub use std::panic;
+pub use std::process;
+pub use std::pin::Pin;
+pub use std::str::{self, FromStr};
+pub use std::sync::Arc;
+pub use std::task::Poll;
+pub use std::time::{SystemTime, UNIX_EPOCH};
+
+pub use anyhow::{anyhow, Context};
+pub use cervine::Cow as Cervine;
+pub use extend::ext;
+pub use fehler::{throw, throws};
+pub use futures::{poll, future, StreamExt as _};
+pub use hyper::body::{Bytes, Buf as _};
+pub use hyper::Uri;
+pub use hyper_tls::HttpsConnector;
+pub use ipnet::IpNet;
+pub use itertools::{iproduct, Itertools};
+pub use lazy_regex::{regex_is_match, regex_replace_all};
+pub use log::{trace, debug, info, warn, error};
+pub use structopt::StructOpt;
+pub use thiserror::Error;
+pub use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
+pub use tokio::pin;
+pub use tokio::select;
+pub use tokio::task;
+pub use tokio::time::{Duration, Instant};
+pub use void::{self, Void, ResultVoidExt, ResultVoidErrExt};
+
+pub use crate::config::{self, InstanceConfig, u32Ext as _};
+pub use crate::utils::*;
+pub use crate::queue::*;
+pub use crate::reporter::*;
+pub use crate::types::*;
+pub use crate::slip::*;
+
+pub type ReqNum = u64;
+
+pub use anyhow::Error as AE;
+pub use ErrorKind as EK;
+pub use PacketError as PE;
+
+pub const SLIP_END: u8 = 0o300; // c0
+pub const SLIP_ESC: u8 = 0o333; // db
+pub const SLIP_ESC_END: u8 = 0o334; // dc
+pub const SLIP_ESC_ESC: u8 = 0o335; // dd
+pub const SLIP_MIME_ESC: u8 = b'-'; // 2d
+
+pub use base64::STANDARD as BASE64_CONFIG;
+
+pub fn default<T:Default>() -> T { Default::default() }
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Default,Clone)]
+pub struct Queue<E> {
+ content: usize,
+ eaten1: usize, // 0 <= eaten1 < queue.front()...len()
+ queue: VecDeque<E>,
+}
+
+#[derive(Default,Debug,Clone)]
+pub struct FrameQueue {
+ queue: Queue<Cervine<'static, Box<[u8]>, [u8]>>,
+}
+
+impl<E> Debug for Queue<E> where E: AsRef<[u8]> {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) {
+ write!(f, "Queue{{content={},eaten1={},queue=[",
+ self.content, self.eaten1)?;
+ for q in &self.queue { write!(f, "{},", q.as_ref().len())?; }
+ write!(f, "]}}")?;
+ }
+}
+
+impl<E> Queue<E> where E: AsRef<[u8]> {
+ pub fn push<B: Into<E>>(&mut self, b: B) {
+ self.push_(b.into());
+ }
+ pub fn push_(&mut self, b: E) {
+ let l = b.as_ref().len();
+ self.queue.push_back(b);
+ self.content += l;
+ }
+ pub fn is_empty(&self) -> bool { self.content == 0 }
+}
+
+impl FrameQueue {
+ pub fn push<B: Into<Box<[u8]>>>(&mut self, b: B) {
+ self.push_(b.into());
+ }
+ pub fn push_(&mut self, b: Box<[u8]>) {
+ self.queue.push_(Cervine::Owned(b));
+ self.queue.push_(Cervine::Borrowed(&SLIP_END_SLICE));
+ }
+ pub fn is_empty(&self) -> bool { self.queue.is_empty() }
+}
+
+impl<E> Extend<E> for FrameQueue where E: Into<Box<[u8]>> {
+ fn extend<I>(&mut self, it: I)
+ where I: IntoIterator<Item=E>
+ {
+ for b in it { self.push(b) }
+ }
+}
+
+impl<E> hyper::body::Buf for Queue<E> where E: AsRef<[u8]> {
+ fn remaining(&self) -> usize { self.content }
+ fn chunk(&self) -> &[u8] {
+ let front = if let Some(f) = self.queue.front() { f } else { return &[] };
+ &front.as_ref()[ self.eaten1.. ]
+ }
+ fn advance(&mut self, cnt: usize) {
+ self.content -= cnt;
+ self.eaten1 += cnt;
+ loop {
+ if self.eaten1 == 0 { break }
+ let front = self.queue.front().unwrap();
+ if self.eaten1 < front.as_ref().len() { break; }
+ self.eaten1 -= front.as_ref().len();
+ self.queue.pop_front().unwrap();
+ }
+ }
+}
+
+impl hyper::body::Buf for FrameQueue {
+ fn remaining(&self) -> usize { self.queue.remaining() }
+ fn chunk(&self) -> &[u8] { self.queue.chunk() }
+ fn advance(&mut self, cnt: usize) { self.queue.advance(cnt) }
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(StructOpt,Debug)]
+pub struct LogOpts {
+ /// Increase debug level
+ #[structopt(long, short="D", parse(from_occurrences))]
+ debug: usize,
+}
+
+impl LogOpts {
+ #[throws(AE)]
+ pub fn log_init(&self) {
+ let env = env_logger::Env::new()
+ .filter("HIPPOTAT_LOG")
+ .write_style("HIPPOTAT_LOG_STYLE");
+
+ let mut logb = env_logger::Builder::new();
+ logb.filter(Some("hippotat"),
+ *[ log::LevelFilter::Info,
+ log::LevelFilter::Debug ]
+ .get(self.debug)
+ .unwrap_or(
+ &log::LevelFilter::Trace
+ ));
+ logb.parse_env(env);
+ logb.init();
+ }
+}
+
+// For clients only, really.
+pub struct Reporter<'r> {
+ ic: &'r InstanceConfig,
+ successes: u64,
+ last_report: Option<Report>,
+}
+
+#[derive(Debug)]
+struct Report {
+ when: Instant,
+ ok: Result<(),()>,
+}
+
+// Reporting strategy
+// - report all errors
+// - report first success after a period of lack of messages
+// - if error, report last success
+
+impl<'r> Reporter<'r> {
+ pub fn new(ic: &'r InstanceConfig) -> Self { Reporter {
+ ic,
+ successes: 0,
+ last_report: None,
+ } }
+
+ pub fn success(&mut self) {
+ self.successes += 1;
+ let now = Instant::now();
+ if let Some(rep) = &self.last_report {
+ if now - rep.when < match rep.ok {
+ Ok(()) => match self.ic.success_report_interval {
+ z if z == Duration::default() => return,
+ nonzero => nonzero,
+ },
+ Err(()) => self.ic.effective_http_timeout,
+ } {
+ return
+ }
+ }
+
+ info!(target:"hippotat", "{} ({}ok): running", self.ic, self.successes);
+ self.last_report = Some(Report { when: now, ok: Ok(()) });
+ }
+
+ pub fn filter<T>(&mut self, req_num: Option<ReqNum>, r: Result<T,AE>)
+ -> Option<T> {
+ let now = Instant::now();
+ match r {
+ Ok(t) => {
+ Some(t)
+ },
+ Err(e) => {
+ let m = (||{
+ let mut m = self.ic.to_string();
+ if let Some(req_num) = req_num {
+ write!(m, " #{}", req_num)?;
+ }
+ if self.successes > 0 {
+ write!(m, " ({}ok)", self.successes)?;
+ self.successes = 0;
+ }
+ write!(m, ": {:?}", e)?;
+ Ok::<_,fmt::Error>(m)
+ })().unwrap();
+ warn!(target:"hippotat", "{}", m);
+ self.last_report = Some(Report { when: now, ok: Err(()) });
+ None
+ },
+ }
+ }
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Default,Clone)]
+pub struct Queue {
+ content: usize,
+ eaten1: usize, // 0 <= eaten1 < queue.front()...len()
+ queue: VecDeque<Box<[u8]>>,
+}
+
+pub impl Queue {
+ pub fn push<B: Into<Box<[u8]>>>(&mut self, b: B) {
+ self.push_(b.into());
+ }
+ pub fn push_(&mut self, b: Box<[u8]>) {
+ let l = b.len();
+ self.push(b);
+ b.content += b;
+ }
+ pub fn is_empty(&self) { self.content == 0 }
+}
+
+impl bytes::Buf for Queue {
+ fn remaining(&self) -> usize { self.content }
+ fn chunk(&self) -> usize {
+ let front = if let(f) = self.queue.front() { f } else { return &[] };
+ front[ self.eaten1.. ]
+ }
+ fn advance(&self, cnt: usize) {
+ eaten1 += cnt;
+ loop {
+ if eaten1 == 0 { break }
+ let front = self.queue.front().unwrap();
+ if eaten1 < front.len() { break; }
+ eaten1 -= front.len();
+ self.queue.pop_front().unwrap();
+ }
+ }
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+pub static SLIP_END_SLICE: &[u8] = &[SLIP_END];
+
+#[derive(Error,Debug,Copy,Clone,Eq,PartialEq)]
+pub enum PacketError {
+ #[error("empty packet")] Empty,
+ #[error("MTU exceeded ({len} > {mtu})")] MTU { len: usize, mtu: u32 },
+ #[error("Invalid SLIP escape sequence")] SLIP,
+ #[error("unexpected src addr {0:?}")] Src(IpAddr),
+ #[error("unexpected dst addr {0:?}")] Dst(IpAddr),
+ #[error("bad, IPv{vsn}, len={len}")] Bad { len: usize, vsn: u8 },
+}
+
+pub trait SlipMime { const CONV_TO: Option<bool>; }
+#[derive(Copy,Clone,Debug)] pub struct Slip2Mime;
+#[derive(Copy,Clone,Debug)] pub struct Mime2Slip;
+#[derive(Copy,Clone,Debug)] pub struct SlipNoConv;
+impl SlipMime for Slip2Mime { const CONV_TO: Option<bool> = Some(true); }
+impl SlipMime for Mime2Slip { const CONV_TO: Option<bool> = Some(false); }
+impl SlipMime for SlipNoConv { const CONV_TO: Option<bool> = None; }
+
+pub fn checkn<AC, EH, OUT, M: SlipMime+Copy>(
+ mime: M,
+ mtu: u32,
+ data: &[u8],
+ out: &mut OUT,
+ addr_chk: AC,
+ mut error_handler: EH
+) where OUT: Extend<Box<[u8]>>,
+ AC: Fn(&[u8]) -> Result<(), PacketError> + Copy,
+ EH: FnMut(PacketError),
+{
+ // eprintln!("before: {:?}", DumpHex(data));
+ if data.is_empty() { return }
+ for packet in data.split(|&c| c == SLIP_END) {
+ match check1(mime, mtu, packet, addr_chk) {
+ Err(e) => error_handler(e),
+ Ok(packet) => out.extend(iter::once(packet)),
+ }
+ }
+// eprintln!(" after: {:?}", DumpHex(data));
+}
+
+#[throws(PacketError)]
+pub fn check1<AC, M: SlipMime>(
+ _mime: M,
+ mtu: u32,
+ packet: &[u8],
+ addr_chk: AC,
+) -> Box<[u8]>
+where AC: Fn(&[u8]) -> Result<(), PacketError>,
+{
+ if packet.len() == 0 {
+ throw!(PacketError::Empty)
+ }
+ if packet.len() > mtu.sat() {
+ throw!(PacketError::MTU { len: packet.len(), mtu });
+ }
+
+ let mut packet: Box<[u8]> = packet.to_owned().into();
+ let mut walk: &mut [u8] = &mut packet;
+ let mut header = [0u8; HEADER_FOR_ADDR];
+ let mut wheader = &mut header[..];
+
+ while let Some((i, was_mime)) = walk.iter().enumerate().find_map(
+ |(i,&c)| match c {
+ SLIP_ESC => Some((i,false)),
+ SLIP_MIME_ESC if M::CONV_TO.is_some() => Some((i,true)),
+ _ => None,
+ }
+ ) {
+ let _ = wheader.write(&walk[0..i]);
+ if M::CONV_TO.is_some() {
+ walk[i] = if was_mime { SLIP_ESC } else { SLIP_MIME_ESC };
+ }
+ if Some(was_mime) != M::CONV_TO {
+ let c = match walk.get(i+1) {
+ Some(&SLIP_ESC_ESC) => SLIP_ESC,
+ Some(&SLIP_ESC_END) => SLIP_END,
+ _ => throw!(PacketError::SLIP),
+ };
+ let _ = wheader.write(&[c]);
+ walk = &mut walk[i+2 ..];
+ } else {
+ let _ = wheader.write(&[SLIP_MIME_ESC]);
+ walk = &mut walk[i+1 ..];
+ }
+ }
+ let _ = wheader.write(walk);
+
+ addr_chk(&header)?;
+
+ packet
+}
+
+pub type Frame = Vec<u8>;
+pub type FramesData = Vec<Vec<u8>>;
+// todo: https://github.com/tokio-rs/bytes/pull/504
+// pub type Frame = Box<[u8]>;
+// pub type FramesData = Vec<Frame>;
+// `From<Box<[u8]>>` is not implemented for `Bytes`
+// when this is fixed, there are two `into`s in client.rs which
+// become redundant (search for todo:504)
+
+
+#[derive(Default)]
+pub struct Frames {
+ frames: FramesData,
+ total_len: usize,
+ tried_full: bool,
+}
+
+impl Debug for Frames {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) {
+ write!(f, "Frames{{n={},len={}}}", &self.frames.len(), &self.total_len)?;
+ }
+}
+
+impl Frames {
+ #[throws(Frame)]
+ pub fn add(&mut self, max: u32, frame: Frame) {
+ if frame.len() == 0 { return }
+ let new_total = self.total_len + frame.len() + 1;
+ if new_total > max.sat() { self.tried_full = true; throw!(frame); }
+ self.total_len = new_total;
+ self.frames.push(frame);
+ }
+
+ #[inline] pub fn tried_full(&self) -> bool { self.tried_full }
+ #[inline] pub fn is_empty(&self) -> bool { self.frames.is_empty() }
+}
+
+impl From<Frames> for FramesData {
+ fn from(frames: Frames) -> FramesData { frames.frames }
+}
+
+const HEADER_FOR_ADDR: usize = 40;
+
+#[throws(PacketError)]
+pub fn ip_packet_addr<const DST: bool>(packet: &[u8]) -> IpAddr {
+ let vsn = (packet.get(0).ok_or_else(|| PE::Empty)? & 0xf0) >> 4;
+ match vsn {
+ 4 if packet.len() >= 20 => {
+ let slice = &packet[if DST { 16 } else { 12 }..][0..4];
+ Ipv4Addr::from(*<&[u8;4]>::try_from(slice).unwrap()).into()
+ },
+
+ 6 if packet.len() >= 40 => {
+ let slice = &packet[if DST { 24 } else { 8 }..][0..16];
+ Ipv6Addr::from(*<&[u8;16]>::try_from(slice).unwrap()).into()
+ },
+
+ _ => throw!(PE::Bad{ vsn, len: packet.len() }),
+ }
+}
+
+#[derive(Copy,Clone,Eq,PartialEq,Ord,PartialOrd,Hash)]
+pub struct DumpHex<'b>(pub &'b [u8]);
+impl Debug for DumpHex<'_> {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) {
+ for v in self.0 { write!(f, "{:02x}", v)?; }
+ }
+}
+
+#[test]
+fn mime_slip_to_mime() {
+ use PacketError as PE;
+ const MTU: u32 = 10;
+
+ fn chk<M: SlipMime>(i: &[u8], exp_p: &[&[u8]], exp_e: &[PacketError]) {
+ dbg!(M::CONV_TO, DumpHex(i));
+ let mut got_e = vec![];
+ let mut got_p = vec![];
+ check::<_,_,_,M>(MTU, i, &mut got_p, |_|Ok(()), |e| got_e.push(e));
+ assert_eq!( got_p.iter().map(|b| DumpHex(b)).collect_vec(),
+ exp_p.iter().map(|b| DumpHex(b)).collect_vec() );
+ assert_eq!( got_e,
+ exp_e );
+ }
+
+ chk::<Slip2Mime>
+ ( &[ SLIP_END, SLIP_ESC, SLIP_ESC_END, b'-', b'X' ],
+ &[ &[ b'-', SLIP_ESC_END, SLIP_ESC, b'X' ] ],
+ &[ PE::Empty ]);
+
+ chk::<Slip2Mime>
+ ( &[ SLIP_END, SLIP_ESC, b'y' ], &[],
+ &[ PE::Empty, PE::SLIP ]);
+
+ chk::<Slip2Mime>
+ ( &[ SLIP_END, b'-', b'y' ],
+ &[ &[ SLIP_ESC, b'y' ] ],
+ &[ PE::Empty ]);
+
+ chk::<Slip2Mime>
+ ( &[b'x'; 20],
+ &[ ],
+ &[ PE::MTU { len: 20, mtu: MTU } ]);
+
+ chk::<SlipNoConv>
+ ( &[ SLIP_END, SLIP_ESC, SLIP_ESC_END, b'-', b'X' ],
+ &[ &[ SLIP_ESC, SLIP_ESC_END, b'-', b'X' ] ],
+ &[ PE::Empty, ]);
+}
+
+
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[derive(Debug,Copy,Clone)]
+pub enum LinkEnd { Server, Client }
+
+#[derive(Debug,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct ServerName(pub String);
+
+#[derive(Debug,Clone,Copy,Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct ClientName(pub Ipv4Addr);
+
+#[derive(Debug,Clone,Hash,Eq,PartialEq,Ord,PartialOrd)]
+pub struct LinkName {
+ pub server: ServerName,
+ pub client: ClientName,
+}
+
+impl FromStr for ClientName {
+ type Err = AE;
+ #[throws(AE)]
+ fn from_str(s: &str) -> Self {
+ let v4addr: Ipv4Addr = s.parse()
+ .context("invalid client name (IPv4 address)")?;
+ if s != v4addr.to_string() {
+ throw!(anyhow!("invalid client name (unusual IPv4 address syntax)"));
+ }
+ ClientName(v4addr)
+ }
+}
+
+impl FromStr for ServerName {
+ type Err = AE;
+ #[throws(AE)]
+ fn from_str(s: &str) -> Self {
+ if ! regex_is_match!(r"
+ ^ (?: SERVER
+ | [0-9a-z][-0-9a-z]* (:? \.
+ [0-9a-z][-0-9a-z]* )*
+ ) $"x, s) {
+ throw!(anyhow!("bad syntax for server name"));
+ }
+ if ! regex_is_match!(r"[A-Za-z-]", s) {
+ throw!(anyhow!("bad syntax for server name \
+ (too much like an IPv4 address)"));
+ }
+ ServerName(s.into())
+ }
+}
+
+impl Display for ServerName {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.0, f)?; }
+}
+impl Display for ClientName {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.0, f)?; }
+}
+impl Display for LinkName {
+ #[throws(fmt::Error)]
+ fn fmt(&self, f: &mut fmt::Formatter) {
+ write!(f, "[{} {}]", &self.server, &self.client)?;
+ }
+}
--- /dev/null
+// Copyright 2021 Ian Jackson and contributors to Hippotat
+// SPDX-License-Identifier: GPL-3.0-or-later
+// There is NO WARRANTY.
+
+use crate::prelude::*;
+
+#[ext(pub)]
+impl<T> T where T: Debug {
+ fn to_debug(&self) -> String { format!("{:?}", self) }
+}
+
+#[ext(pub)]
+impl<T,E> Result<T,E> where AE: From<E> {
+ fn dcontext<D:Debug>(self, d: D) -> anyhow::Result<T> {
+ self.map_err(|e| AE::from(e)).with_context(|| d.to_debug())
+ }
+}
+
+#[throws(AE)]
+pub async fn read_limited_body<S,E>(limit: usize, mut stream: S) -> Box<[u8]>
+where S: futures::Stream<Item=Result<hyper::body::Bytes,E>> + Unpin,
+ // we also require that the Stream is cancellation-safe
+ E: std::error::Error + Sync + Send + 'static,
+{
+ let mut accum = vec![];
+ while let Some(item) = stream.next().await {
+ let b = item.context("HTTP error fetching response body")?;
+ if accum.len() + b.len() > limit {
+ throw!(anyhow!("maximum response body size {} exceeded", limit));
+ }
+ accum.extend(b);
+ }
+ accum.into()
+}
+
+use sha2::Digest as _;
+
+type HmacH = sha2::Sha256;
+const HMAC_B: usize = 64;
+const HMAC_L: usize = 32;
+
+pub fn token_hmac(key: &[u8], message: &[u8]) -> [u8; HMAC_L] {
+ let key = {
+ let mut padded = [0; HMAC_B];
+ if key.len() > padded.len() {
+ let digest: [u8; HMAC_L] = HmacH::digest(key).into();
+ padded[0..HMAC_L].copy_from_slice(&digest);
+ } else {
+ padded[0.. key.len()].copy_from_slice(key);
+ }
+ padded
+ };
+ let mut ikey = key; for k in &mut ikey { *k ^= 0x36; }
+ let mut okey = key; for k in &mut okey { *k ^= 0x5C; }
+
+//dbg!(&key, &ikey, &okey);
+
+ let h1 = HmacH::new()
+ .chain(&ikey)
+ .chain(message)
+ .finalize();
+ let h2 = HmacH::new()
+ .chain(&okey)
+ .chain(h1)
+ .finalize();
+ h2.into()
+}
+
+#[test]
+fn hmac_test_vectors(){
+ // C&P from RFC 4231
+ let vectors = r#"
+ Key = 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b
+ 0b0b0b0b (20 bytes)
+ Data = 4869205468657265 ("Hi There")
+
+ HMAC-SHA-256 = b0344c61d8db38535ca8afceaf0bf12b
+ 881dc200c9833da726e9376c2e32cff7
+
+
+ Key = 4a656665 ("Jefe")
+ Data = 7768617420646f2079612077616e7420 ("what do ya want ")
+ 666f72206e6f7468696e673f ("for nothing?")
+
+ HMAC-SHA-256 = 5bdcc146bf60754e6a042426089575c7
+ 5a003f089d2739839dec58b964ec3843
+
+
+ Key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaa (20 bytes)
+ Data = dddddddddddddddddddddddddddddddd
+ dddddddddddddddddddddddddddddddd
+ dddddddddddddddddddddddddddddddd
+ dddd (50 bytes)
+
+ HMAC-SHA-256 = 773ea91e36800e46854db8ebd09181a7
+ 2959098b3ef8c122d9635514ced565fe
+
+
+ Key = 0102030405060708090a0b0c0d0e0f10
+ 111213141516171819 (25 bytes)
+ Data = cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+ cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+ cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
+ cdcd (50 bytes)
+
+ HMAC-SHA-256 = 82558a389a443c0ea4cc819899f2083a
+ 85f0faa3e578f8077a2e3ff46729665b
+
+
+
+ Key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaa (131 bytes)
+ Data = 54657374205573696e67204c61726765 ("Test Using Large")
+ 72205468616e20426c6f636b2d53697a ("r Than Block-Siz")
+ 65204b6579202d2048617368204b6579 ("e Key - Hash Key")
+ 204669727374 (" First")
+
+ HMAC-SHA-256 = 60e431591ee0b67f0d8a26aacbf5b77f
+ 8e0bc6213728c5140546040f0ee37f54
+
+ Key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ aaaaaa (131 bytes)
+ Data = 54686973206973206120746573742075 ("This is a test u")
+ 73696e672061206c6172676572207468 ("sing a larger th")
+ 616e20626c6f636b2d73697a65206b65 ("an block-size ke")
+ 7920616e642061206c61726765722074 ("y and a larger t")
+ 68616e20626c6f636b2d73697a652064 ("han block-size d")
+ 6174612e20546865206b6579206e6565 ("ata. The key nee")
+ 647320746f2062652068617368656420 ("ds to be hashed ")
+ 6265666f7265206265696e6720757365 ("before being use")
+ 642062792074686520484d414320616c ("d by the HMAC al")
+ 676f726974686d2e ("gorithm.")
+
+ HMAC-SHA-256 = 9b09ffa71b942fcb27635fbcd5b0e944
+ bfdc63644f0713938a7f51535c3a35e2
+"#;
+ let vectors = regex_replace_all!{
+ r#"\(.*\)"#,
+ vectors.trim_end(),
+ |_| "",
+ };
+ let vectors = regex_replace_all!{
+ r#" *\n "#,
+ &vectors,
+ |_| "",
+ };
+ let vectors = regex_replace_all!{
+ r#"\s*\n"#,
+ &vectors,
+ |_| "\n",
+ };
+ let mut lines = vectors.split('\n');
+ assert_eq!( lines.next().unwrap(), "" );
+ let mut get = |prefix| {
+ let l = lines.next()?;
+ dbg!(l);
+ let b = l.strip_prefix(prefix).unwrap().as_bytes().chunks(2)
+ .map(|s| str::from_utf8(s).unwrap())
+ .map(|s| { assert_eq!(s.len(), 2); u8::from_str_radix(s,16).unwrap() })
+ .collect::<Vec<u8>>();
+ Some(b)
+ };
+ while let Some(key) = get(" Key = ") {
+ let data = get(" Data = ").unwrap();
+ let exp = get(" HMAC-SHA-256 = ").unwrap();
+ let got = token_hmac(&key, &data);
+ assert_eq!(&got[..], &exp);
+ }
+}
--- /dev/null
+[SERVER]
+
+ipif = PATH=/usr/local/sbin:/sbin:/usr/sbin:$PATH really /home/ian/things/Userv/userv-utils.git/ipif/service \* -- %(local)s,%(peer)s,%(mtu)s,slip '%(rnets)s'
+
+addrs = 127.0.0.1
+port = 8099
+vnetwork = 192.0.2.0/24
+
+# ./hippotatd --debug-select=+ -c test.cfg
+
+# nc -n -v -l -p 8100 -c 'dd of=/dev/null'
+
+[192.0.2.3]
+secret = sesame
+
+[192.0.2.3]
+ipif = PATH=/usr/local/sbin:/sbin:/usr/sbin:$PATH really ./fake-userv /home/ian/things/Userv/userv-utils.git/ipif/service \* -- %(local)s,%(peer)s,%(mtu)s,slip '%(rnets)s'
+
+# ./hippotat -D -c test.cfg
+
+[192.0.2.4]
+#secret = zorkmids
+
+# dd if=/dev/urandom bs=1024 count=16384 | nc -q 0 -n -v 192.0.2.1 8100
--- /dev/null
+#!/bin/bash
+set -ex
+
+mkdir /dev/pts
+mount -t proc none /proc
+mount -t devpts none /dev/pts
+mount -t tmpfs none /tmp
+mount -t tmpfs none /run
+
+exec 0<>/dev/tty1 1>&0
+stty raw -echo
+
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=991959
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+export PATH
+export SHELL=/bin/bash
+
+HOME=$(cat /proc/cmdline)
+case "$HOME" in
+*' psusan-uml-tmp='*) ;;
+*) echo >&2 'psusan-uml-tmp not found in /proc/cmdline'; exit 1;;
+esac
+HOME=${HOME##* psusan-uml-tmp=}
+HOME=${HOME%% *}
+export HOME
+cd "$HOME"
+
+dd if=random-seed of=/dev/urandom
+
+exec psusan
--- /dev/null
+#!/bin/bash
+set -e
+
+fifo=tmp/uml/q
+mkfifo -m600 $fifo
+
+(
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=991958
+ : <$fifo
+ cat
+) | \
+ bwrap --dev-bind / / --tmpfs /dev/shm \
+ linux mem=512M rootfstype=hostfs rootflags=/ rw \
+ con=fd:2,fd:2 con1=fd:0,fd:1 init="${0%/*}"/psusan-uml-inside \
+ -- psusan-uml-tmp=$PWD/tmp/uml | \
+(
+ read banner
+ : >$fifo
+ printf '%s\n' "$banner"
+ cat
+)
--- /dev/null
+#!/bin/sh
+set -e
+
+HOME=$PWD/tmp/uml
+
+plink -ssh-connection -share $PWD "$@"
--- /dev/null
+#!/bin/bash
+set -e
+
+mkdir -p tmp
+rm -rf tmp/uml
+mkdir -p -m2700 tmp/uml
+dd if=/dev/urandom of=tmp/uml/random-seed bs=1k count=4
+
+"${0%/*}"/psusan-uml-run -proxycmd "${0%/*}"/psusan-uml-psusan -N -v -v
--- /dev/null
+rndaddtoentcnt
--- /dev/null
+rndaddtoentcnt: rndaddtoentcnt.c
+ $(CC) rndaddtoentcnt.c -o rndaddtoentcnt
+
+.PHONY: clean
+clean:
+ rm -f *.o rndaddtoentcnt
--- /dev/null
+### rndaddtoentcnt
+
+Seeding the random number generator by writing to /dev/urandom does not update the entropy count.
+
+This utility makes the RNDADDTOENTCNT ioctl call needed to do this.
+
+Used in startup scripts after initializing /dev/urandom with a presaved seed.
+
+Example:
+
+ dd if=/path/to/some/random-seed-file of=/dev/urandom bs=512 count=1
+
+ /path/to/rdnaddtoentcnt <entropy-bit-count>
+
+where entropy-bit-count is a number between 1 and (8 * 512) depending on how much you trust the seed file.