merge feat-tfclient

This commit is contained in:
c0repwn3r 2023-03-30 12:33:14 -04:00
commit 43636a51e9
Signed by: core
GPG Key ID: FDBF740DADDCEECF
44 changed files with 3811 additions and 87 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
target target
pg_data

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "dnapi"]
path = dnapi
url = https://github.com/DefinedNet/dnapi

View File

@ -5,6 +5,7 @@
<sourceFolder url="file://$MODULE_DIR$/tfclient/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/tfclient/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/trifid-api/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/trifid-api/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/trifid-pki/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/trifid-pki/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dnapi-rs/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

553
Cargo.lock generated
View File

@ -81,18 +81,18 @@ checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.64" version = "0.1.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.11",
] ]
[[package]] [[package]]
@ -154,6 +154,16 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "base64-serde"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba368df5de76a5bea49aaf0cf1b39ccfbbef176924d1ba5db3e4135216cbe3c7"
dependencies = [
"base64 0.21.0",
"serde",
]
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.5.3" version = "1.5.3"
@ -219,9 +229,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.23" version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@ -242,6 +252,43 @@ dependencies = [
"inout", "inout",
] ]
[[package]]
name = "clap"
version = "4.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce38afc168d8665cfc75c7b1dd9672e50716a137f433f070991619744a67342a"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex",
"is-terminal",
"once_cell",
"strsim",
"termcolor",
]
[[package]]
name = "clap_derive"
version = "4.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.107",
]
[[package]]
name = "clap_lex"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
dependencies = [
"os_str_bytes",
]
[[package]] [[package]]
name = "codespan-reporting" name = "codespan-reporting"
version = "0.11.1" version = "0.11.1"
@ -258,6 +305,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.1" version = "0.9.1"
@ -376,6 +434,16 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "ctrlc"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639"
dependencies = [
"nix",
"windows-sys 0.45.0",
]
[[package]] [[package]]
name = "curve25519-dalek" name = "curve25519-dalek"
version = "3.2.0" version = "3.2.0"
@ -385,6 +453,7 @@ dependencies = [
"byteorder", "byteorder",
"digest 0.9.0", "digest 0.9.0",
"rand_core 0.5.1", "rand_core 0.5.1",
"serde",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@ -428,7 +497,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"scratch", "scratch",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -445,7 +514,7 @@ checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -488,7 +557,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -517,7 +586,16 @@ version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys 0.3.7",
]
[[package]]
name = "dirs"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dece029acd3353e3a58ac2e3eb3c8d6c35827a892edc6cc4138ef9c33df46ecd"
dependencies = [
"dirs-sys 0.4.0",
] ]
[[package]] [[package]]
@ -531,6 +609,33 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dirs-sys"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04414300db88f70d74c5ff54e50f9e1d1737d9a5b90f53fcf2e95ca2a9ab554b"
dependencies = [
"libc",
"redox_users",
"windows-sys 0.45.0",
]
[[package]]
name = "dnapi-rs"
version = "0.1.7"
dependencies = [
"base64 0.21.0",
"base64-serde",
"chrono",
"log",
"rand",
"reqwest",
"serde",
"serde_json",
"trifid-pki",
"url",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.6" version = "0.15.6"
@ -544,6 +649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf420a7ec85d98495b0c34aa4a58ca117f982ffbece111aeb545160148d7010" checksum = "3cf420a7ec85d98495b0c34aa4a58ca117f982ffbece111aeb545160148d7010"
dependencies = [ dependencies = [
"pkcs8", "pkcs8",
"serde",
"signature", "signature",
] ]
@ -557,6 +663,7 @@ dependencies = [
"ed25519", "ed25519",
"rand_core 0.6.4", "rand_core 0.6.4",
"serde", "serde",
"serde_bytes",
"sha2", "sha2",
"zeroize", "zeroize",
] ]
@ -576,6 +683,27 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.3" version = "2.5.3"
@ -611,6 +739,18 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "filetime"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys 0.45.0",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.25" version = "1.0.25"
@ -852,6 +992,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -934,6 +1080,19 @@ dependencies = [
"want", "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]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.53" version = "0.1.53"
@ -1017,6 +1176,17 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "io-lifetimes"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd6da19f25979c7270e70fa95ab371ec3b701cd0eefc47667a09785b3c59155"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.45.0",
]
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.7.1" version = "2.7.1"
@ -1026,6 +1196,18 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "is-terminal"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys 0.45.0",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -1077,6 +1259,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.9" version = "0.4.9"
@ -1206,6 +1394,18 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"static_assertions",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -1216,15 +1416,6 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -1275,6 +1466,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.0" version = "1.17.0"
@ -1310,7 +1510,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1332,6 +1532,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "os_str_bytes"
version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1422,7 +1628,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1505,10 +1711,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro-error"
version = "1.0.50" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.107",
"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-macro2"
version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1521,7 +1751,7 @@ checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"version_check", "version_check",
"yansi", "yansi",
] ]
@ -1543,9 +1773,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.23" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1626,7 +1856,7 @@ checksum = "9f9c0c92af03644e4806106281fe2e068ac5bc0ae74a707266d06ea27bccee5f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -1654,12 +1884,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "remove_dir_all" name = "reqwest"
version = "0.5.3" version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
dependencies = [ dependencies = [
"winapi", "base64 0.21.0",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
] ]
[[package]] [[package]]
@ -1713,7 +1971,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rocket_http", "rocket_http",
"syn", "syn 1.0.107",
"unicode-xid", "unicode-xid",
] ]
@ -1744,6 +2002,20 @@ dependencies = [
"uncased", "uncased",
] ]
[[package]]
name = "rustix"
version = "0.36.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fe885c3a125aa45213b68cc1472a49880cb5923dc23f522ad2791b882228778"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.45.0",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.11" version = "1.0.11"
@ -1808,29 +2080,38 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.152" version = "1.0.159"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_bytes"
version = "1.0.152" version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294"
dependencies = [
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.11",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.91" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1846,6 +2127,31 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.5" version = "0.10.5"
@ -1892,6 +2198,19 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d"
[[package]]
name = "simple_logger"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78beb34673091ccf96a8816fce8bfd30d1292c7621ca2bcb5f2ba0fae4f558d"
dependencies = [
"atty",
"colored",
"log",
"time 0.3.17",
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.7" version = "0.4.7"
@ -1968,7 +2287,7 @@ dependencies = [
"bytes", "bytes",
"crc", "crc",
"crossbeam-queue", "crossbeam-queue",
"dirs", "dirs 4.0.0",
"dotenvy", "dotenvy",
"either", "either",
"event-listener", "event-listener",
@ -2019,7 +2338,7 @@ dependencies = [
"sha2", "sha2",
"sqlx-core", "sqlx-core",
"sqlx-rt", "sqlx-rt",
"syn", "syn 1.0.107",
"url", "url",
] ]
@ -2053,6 +2372,12 @@ dependencies = [
"loom", "loom",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.2" version = "0.1.2"
@ -2063,6 +2388,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.1" version = "2.4.1"
@ -2080,6 +2411,17 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "syn"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "synstructure" name = "synstructure"
version = "0.12.6" version = "0.12.6"
@ -2088,22 +2430,32 @@ checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"unicode-xid", "unicode-xid",
] ]
[[package]] [[package]]
name = "tempfile" name = "tar"
version = "3.3.0" version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
"libc",
"redox_syscall", "redox_syscall",
"remove_dir_all", "rustix",
"winapi", "windows-sys 0.42.0",
] ]
[[package]] [[package]]
@ -2118,6 +2470,30 @@ dependencies = [
[[package]] [[package]]
name = "tfclient" name = "tfclient"
version = "0.1.0" version = "0.1.0"
dependencies = [
"base64 0.21.0",
"base64-serde",
"chrono",
"clap",
"ctrlc",
"dirs 5.0.0",
"dnapi-rs",
"flate2",
"hex",
"ipnet",
"log",
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"sha2",
"simple_logger",
"tar",
"tempfile",
"toml 0.7.3",
"trifid-pki",
"url",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
@ -2136,7 +2512,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2166,6 +2542,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [ dependencies = [
"itoa", "itoa",
"libc",
"num_threads",
"serde", "serde",
"time-core", "time-core",
"time-macros", "time-macros",
@ -2229,7 +2607,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2278,9 +2656,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.7.1" version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "772c1426ab886e7362aedf4abc9c0d1348a979517efedfc25862944d10137af0" checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
@ -2299,15 +2677,15 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.19.1" version = "0.19.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90a238ee2e6ede22fb95350acc78e21dc40da00bb66c0334bde83de4ed89424e" checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"nom8",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow",
] ]
[[package]] [[package]]
@ -2355,7 +2733,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2415,7 +2793,7 @@ dependencies = [
"sha2", "sha2",
"sqlx", "sqlx",
"tokio", "tokio",
"toml 0.7.1", "toml 0.7.3",
"totp-rs", "totp-rs",
"trifid-pki", "trifid-pki",
"url", "url",
@ -2425,7 +2803,7 @@ dependencies = [
[[package]] [[package]]
name = "trifid-pki" name = "trifid-pki"
version = "0.1.4" version = "0.1.9"
dependencies = [ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"hex", "hex",
@ -2434,6 +2812,7 @@ dependencies = [
"quick-protobuf", "quick-protobuf",
"rand", "rand",
"rand_core 0.6.4", "rand_core 0.6.4",
"serde",
"sha2", "sha2",
"x25519-dalek", "x25519-dalek",
] ]
@ -2524,6 +2903,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c"
[[package]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.3.1"
@ -2561,7 +2946,7 @@ checksum = "c1b300a878652a387d2a0de915bdae8f1a548f0c6d45e072fe2688794b656cc9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
] ]
[[package]] [[package]]
@ -2631,10 +3016,22 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.84" version = "0.2.84"
@ -2653,7 +3050,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2839,6 +3236,24 @@ version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winnow"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d020b441f92996c80d94ae9166e8501e59c7bb56121189dc9eab3bd8216966"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "x25519-dalek" name = "x25519-dalek"
version = "2.0.0-pre.1" version = "2.0.0-pre.1"
@ -2847,9 +3262,19 @@ checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df"
dependencies = [ dependencies = [
"curve25519-dalek 3.2.0", "curve25519-dalek 3.2.0",
"rand_core 0.6.4", "rand_core 0.6.4",
"serde",
"zeroize", "zeroize",
] ]
[[package]]
name = "xattr"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.1" version = "0.5.1"
@ -2873,6 +3298,6 @@ checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.107",
"synstructure", "synstructure",
] ]

View File

@ -2,5 +2,6 @@
members = [ members = [
"trifid-api", "trifid-api",
"tfclient", "tfclient",
"trifid-pki" "trifid-pki",
"dnapi-rs"
] ]

View File

@ -7,6 +7,9 @@ Connection: close
{"code":"xM22QsIzd4F0nLDTbh86RCSYwelfU_Hshqt-7u4yy_Y","dhPubkey":"LS0tLS1CRUdJTiBORUJVTEEgWDI1NTE5IFBVQkxJQyBLRVktLS0tLQpqZW9aaDZZYUNNSHZKK04zWGRlQ1hCbHo3dm5saTBjL1NlQ1hVR3lYbEIwPQotLS0tLUVORCBORUJVTEEgWDI1NTE5IFBVQkxJQyBLRVktLS0tLQo=","edPubkey":"LS0tLS1CRUdJTiBORUJVTEEgRUQyNTUxOSBQVUJMSUMgS0VZLS0tLS0KWHE0RG9mUGJoQzBubjc4VEhRWUxhNC83V1Ixei9iU1kzSm9pRzNRZ1VMcz0KLS0tLS1FTkQgTkVCVUxBIEVEMjU1MTkgUFVCTElDIEtFWS0tLS0tCg==","timestamp":"2023-02-01T13:24:56.380006369-05:00"} {"code":"xM22QsIzd4F0nLDTbh86RCSYwelfU_Hshqt-7u4yy_Y","dhPubkey":"LS0tLS1CRUdJTiBORUJVTEEgWDI1NTE5IFBVQkxJQyBLRVktLS0tLQpqZW9aaDZZYUNNSHZKK04zWGRlQ1hCbHo3dm5saTBjL1NlQ1hVR3lYbEIwPQotLS0tLUVORCBORUJVTEEgWDI1NTE5IFBVQkxJQyBLRVktLS0tLQo=","edPubkey":"LS0tLS1CRUdJTiBORUJVTEEgRUQyNTUxOSBQVUJMSUMgS0VZLS0tLS0KWHE0RG9mUGJoQzBubjc4VEhRWUxhNC83V1Ixei9iU1kzSm9pRzNRZ1VMcz0KLS0tLS1FTkQgTkVCVUxBIEVEMjU1MTkgUFVCTElDIEtFWS0tLS0tCg==","timestamp":"2023-02-01T13:24:56.380006369-05:00"}
2023-02-01T13:24:56.380006369-05:00
%Y-%m-%dT%H:%M:%S.%f-%:z
HTTP/2 200 OK HTTP/2 200 OK
Cache-Control: no-store Cache-Control: no-store
Content-Security-Policy: default-src 'none' Content-Security-Policy: default-src 'none'

1
dnapi

@ -1 +0,0 @@
Subproject commit 6f56f055f9912755979e27942a91efcf794a82dc

24
dnapi-rs/Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "dnapi-rs"
version = "0.1.7"
edition = "2021"
description = "A rust client for the Defined Networking API"
license = "AGPL-3.0-or-later"
documentation = "https://docs.rs/dnapi-rs"
homepage = "https://git.e3t.cc/~core/trifid"
repository = "https://git.e3t.cc/~core/trifid"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1.0.159", features = ["derive"] }
base64-serde = "0.7.0"
log = "0.4.17"
reqwest = { version = "0.11.16", features = ["blocking", "json"] }
url = "2.3.1"
base64 = "0.21.0"
serde_json = "1.0.95"
trifid-pki = { version = "0.1.6", path = "../trifid-pki", features = ["serde_derive"] }
rand = "0.8.5"
chrono = "0.4.24"

4
dnapi-rs/README.md Normal file
View File

@ -0,0 +1,4 @@
# dnapi-rs
**dnapi-rs** is a Rust-native crate for interacting with the Defined Networking client API. It is a direct port of `dnapi`, an officially maintained API client by Defined Networking.
This crate is maintained as a part of the trifid project. Check out the other crates in [the git repository](https://git.e3t.cc/~core/trifid).

View File

@ -0,0 +1,221 @@
//! Client structs to handle communication with the Defined Networking API. This is the async client API - if you want blocking instead, enable the blocking (or default) feature instead.
use std::error::Error;
use chrono::Local;
use log::{debug, error};
use reqwest::StatusCode;
use url::Url;
use trifid_pki::cert::serialize_ed25519_public;
use trifid_pki::ed25519_dalek::{Signature, Signer, SigningKey, Verifier};
use crate::credentials::{Credentials, ed25519_public_keys_from_pem};
use crate::crypto::{new_keys, nonce};
use crate::message::{CHECK_FOR_UPDATE, CheckForUpdateResponseWrapper, DO_UPDATE, DoUpdateRequest, DoUpdateResponse, ENDPOINT_V1, ENROLL_ENDPOINT, EnrollRequest, EnrollResponse, RequestV1, RequestWrapper, SignedResponseWrapper};
use serde::{Serialize, Deserialize};
use base64::Engine;
/// A type alias to abstract return types
pub type NebulaConfig = Vec<u8>;
/// A type alias to abstract DH private keys
pub type DHPrivateKeyPEM = Vec<u8>;
/// A combination of persistent data and HTTP client used for communicating with the API.
pub struct Client {
http_client: reqwest::Client,
server_url: Url
}
#[derive(Serialize, Deserialize, Clone)]
/// A struct containing organization metadata returned as a result of enrollment
pub struct EnrollMeta {
/// The server organization ID this node is now a member of
pub organization_id: String,
/// The server organization name this node is now a member of
pub organization_name: String
}
impl Client {
/// Create a new `Client` configured with the given User-Agent and API base.
/// # Errors
/// This function will return an error if the reqwest Client could not be created.
pub fn new(user_agent: String, api_base: Url) -> Result<Self, Box<dyn Error>> {
let client = reqwest::Client::builder().user_agent(user_agent).build()?;
Ok(Self {
http_client: client,
server_url: api_base
})
}
/// Issues an enrollment request against the REST API using the given enrollment code, passing along a
/// locally generated DH X25519 Nebula key to be signed by the CA, and an Ed25519 key for future API
/// authentication. On success it returns the Nebula config generated by the server, a Nebula private key PEM,
/// credentials to be used for future DN API requests, and an object containing organization information.
/// # Errors
/// This function will return an error in any of the following situations:
/// - the `server_url` is invalid
/// - the HTTP request fails
/// - the HTTP response is missing X-Request-ID
/// - X-Request-ID isn't valid UTF-8
/// - the server returns an error
/// - the server returns invalid JSON
/// - the `trusted_keys` field is invalid
pub async fn enroll(&self, code: &str) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials, EnrollMeta), Box<dyn Error>> {
debug!("making enrollment request to API {{server: {}, code: {}}}", self.server_url, code);
let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
let req_json = serde_json::to_string(&EnrollRequest {
code: code.to_string(),
dh_pubkey: dh_pubkey_pem,
ed_pubkey: serialize_ed25519_public(ed_pubkey.as_bytes()),
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
})?;
let resp = self.http_client.post(self.server_url.join(ENROLL_ENDPOINT)?).body(req_json).send().await?;
let req_id = resp.headers().get("X-Request-ID").ok_or("Response missing X-Request-ID")?.to_str()?;
debug!("enrollment request complete {{req_id: {}}}", req_id);
let resp: EnrollResponse = resp.json().await?;
let r = match resp {
EnrollResponse::Success { data } => data,
EnrollResponse::Error { errors } => {
error!("unexpected error during enrollment: {}", errors[0].message);
return Err(errors[0].message.clone().into());
}
};
let meta = EnrollMeta {
organization_id: r.organization.id,
organization_name: r.organization.name,
};
let trusted_keys = ed25519_public_keys_from_pem(&r.trusted_keys)?;
let creds = Credentials {
host_id: r.host_id,
ed_privkey,
counter: r.counter,
trusted_keys,
};
Ok((r.config, dh_privkey_pem, creds, meta))
}
/// Send a signed message to the `DNClient` API to learn if there is a new configuration available.
/// # Errors
/// This function returns an error if the dnclient request fails, or the server returns invalid data.
pub async fn check_for_update(&self, creds: &Credentials) -> Result<bool, Box<dyn Error>> {
let body = self.post_dnclient(CHECK_FOR_UPDATE, &[], &creds.host_id, creds.counter, &creds.ed_privkey).await?;
let result: CheckForUpdateResponseWrapper = serde_json::from_slice(&body)?;
Ok(result.data.update_available)
}
/// Send a signed message to the `DNClient` API to fetch the new configuration update. During this call a new
/// DH X25519 keypair is generated for the new Nebula certificate as well as a new Ed25519 keypair for `DNClient` API
/// communication. On success it returns the new config, a Nebula private key PEM to be inserted into the config
/// and new `DNClient` API credentials
/// # Errors
/// This function returns an error in any of the following scenarios:
/// - if the message could not be serialized
/// - if the request fails
/// - if the response could not be deserialized
/// - if the signature is invalid
/// - if the keys are invalid
pub async fn do_update(&self, creds: &Credentials) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials), Box<dyn Error>> {
let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
let update_keys = DoUpdateRequest {
ed_pubkey_pem: serialize_ed25519_public(ed_pubkey.as_bytes()),
dh_pubkey_pem,
nonce: nonce().to_vec(),
};
let update_keys_blob = serde_json::to_vec(&update_keys)?;
let resp = self.post_dnclient(DO_UPDATE, &update_keys_blob, &creds.host_id, creds.counter, &creds.ed_privkey).await?;
let result_wrapper: SignedResponseWrapper = serde_json::from_slice(&resp)?;
let mut valid = false;
for ca_pubkey in &creds.trusted_keys {
if ca_pubkey.verify(&result_wrapper.data.message, &Signature::from_slice(&result_wrapper.data.signature)?).is_ok() {
valid = true;
break;
}
}
if !valid {
return Err("Failed to verify signed API result".into())
}
let result: DoUpdateResponse = serde_json::from_slice(&result_wrapper.data.message)?;
if result.nonce != update_keys.nonce {
error!("nonce mismatch between request {:x?} and response {:x?}", result.nonce, update_keys.nonce);
return Err("nonce mismatch between request and response".into())
}
let trusted_keys = ed25519_public_keys_from_pem(&result.trusted_keys)?;
let new_creds = Credentials {
host_id: creds.host_id.clone(),
ed_privkey,
counter: result.counter,
trusted_keys,
};
Ok((result.config, dh_privkey_pem, new_creds))
}
/// Wraps and signs the given `req_type` and value, and then makes the API call.
/// On success, returns the response body.
/// # Errors
/// This function will return an error if:
/// - serialization in any step fails
/// - if the `server_url` is invalid
/// - if the request could not be sent
pub async fn post_dnclient(&self, req_type: &str, value: &[u8], host_id: &str, counter: u32, ed_privkey: &SigningKey) -> Result<Vec<u8>, Box<dyn Error>> {
let encoded_msg = serde_json::to_string(&RequestWrapper {
message_type: req_type.to_string(),
value: value.to_vec(),
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
})?;
let encoded_msg_bytes = encoded_msg.into_bytes();
let b64_msg = base64::engine::general_purpose::STANDARD.encode(encoded_msg_bytes);
let b64_msg_bytes = b64_msg.as_bytes();
let signature = ed_privkey.sign(b64_msg_bytes).to_vec();
ed_privkey.verify(b64_msg_bytes, &Signature::from_slice(&signature)?)?;
debug!("signature valid via clientside check");
let body = RequestV1 {
version: 1,
host_id: host_id.to_string(),
counter,
message: b64_msg,
signature,
};
let post_body = serde_json::to_string(&body)?;
let resp = self.http_client.post(self.server_url.join(ENDPOINT_V1)?).body(post_body).send().await?;
match resp.status() {
StatusCode::OK => {
Ok(resp.bytes().await?.to_vec())
},
StatusCode::FORBIDDEN => {
Err("Forbidden".into())
},
_ => {
error!("dnclient endpoint returned bad status code {}", resp.status());
Err("dnclient endpoint returned error".into())
}
}
}
}

View File

@ -0,0 +1,231 @@
//! Client structs to handle communication with the Defined Networking API. This is the blocking client API - if you want async instead, set no-default-features and enable the async feature instead.
use std::error::Error;
use base64::Engine;
use chrono::Local;
use log::{debug, error, trace};
use reqwest::StatusCode;
use url::Url;
use trifid_pki::cert::serialize_ed25519_public;
use trifid_pki::ed25519_dalek::{Signature, Signer, SigningKey, Verifier};
use crate::credentials::{Credentials, ed25519_public_keys_from_pem};
use crate::crypto::{new_keys, nonce};
use crate::message::{CHECK_FOR_UPDATE, CheckForUpdateResponseWrapper, DO_UPDATE, DoUpdateRequest, DoUpdateResponse, ENDPOINT_V1, ENROLL_ENDPOINT, EnrollRequest, EnrollResponse, RequestV1, RequestWrapper, SignedResponseWrapper};
use serde::{Serialize, Deserialize};
/// A type alias to abstract return types
pub type NebulaConfig = Vec<u8>;
/// A type alias to abstract DH private keys
pub type DHPrivateKeyPEM = Vec<u8>;
/// A combination of persistent data and HTTP client used for communicating with the API.
pub struct Client {
http_client: reqwest::blocking::Client,
server_url: Url
}
#[derive(Serialize, Deserialize, Clone)]
/// A struct containing organization metadata returned as a result of enrollment
pub struct EnrollMeta {
/// The server organization ID this node is now a member of
pub organization_id: String,
/// The server organization name this node is now a member of
pub organization_name: String
}
impl Client {
/// Create a new `Client` configured with the given User-Agent and API base.
/// # Errors
/// This function will return an error if the reqwest Client could not be created.
pub fn new(user_agent: String, api_base: Url) -> Result<Self, Box<dyn Error>> {
let client = reqwest::blocking::Client::builder().user_agent(user_agent).build()?;
Ok(Self {
http_client: client,
server_url: api_base
})
}
/// Issues an enrollment request against the REST API using the given enrollment code, passing along a
/// locally generated DH X25519 Nebula key to be signed by the CA, and an Ed25519 key for future API
/// authentication. On success it returns the Nebula config generated by the server, a Nebula private key PEM,
/// credentials to be used for future DN API requests, and an object containing organization information.
/// # Errors
/// This function will return an error in any of the following situations:
/// - the `server_url` is invalid
/// - the HTTP request fails
/// - the HTTP response is missing X-Request-ID
/// - X-Request-ID isn't valid UTF-8
/// - the server returns an error
/// - the server returns invalid JSON
/// - the `trusted_keys` field is invalid
pub fn enroll(&self, code: &str) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials, EnrollMeta), Box<dyn Error>> {
debug!("making enrollment request to API {{server: {}, code: {}}}", self.server_url, code);
let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
let req_json = serde_json::to_string(&EnrollRequest {
code: code.to_string(),
dh_pubkey: dh_pubkey_pem,
ed_pubkey: serialize_ed25519_public(ed_pubkey.as_bytes()),
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
})?;
let resp = self.http_client.post(self.server_url.join(ENROLL_ENDPOINT)?).body(req_json).send()?;
let req_id = resp.headers().get("X-Request-ID").ok_or("Response missing X-Request-ID")?.to_str()?;
debug!("enrollment request complete {{req_id: {}}}", req_id);
let resp: EnrollResponse = resp.json()?;
let r = match resp {
EnrollResponse::Success { data } => data,
EnrollResponse::Error { errors } => {
error!("unexpected error during enrollment: {}", errors[0].message);
return Err(errors[0].message.clone().into());
}
};
let meta = EnrollMeta {
organization_id: r.organization.id,
organization_name: r.organization.name,
};
debug!("parsing public keys");
let trusted_keys = ed25519_public_keys_from_pem(&r.trusted_keys)?;
let creds = Credentials {
host_id: r.host_id,
ed_privkey,
counter: r.counter,
trusted_keys,
};
Ok((r.config, dh_privkey_pem, creds, meta))
}
/// Send a signed message to the `DNClient` API to learn if there is a new configuration available.
/// # Errors
/// This function returns an error if the dnclient request fails, or the server returns invalid data.
pub fn check_for_update(&self, creds: &Credentials) -> Result<bool, Box<dyn Error>> {
let body = self.post_dnclient(CHECK_FOR_UPDATE, &[], &creds.host_id, creds.counter, &creds.ed_privkey)?;
let result: CheckForUpdateResponseWrapper = serde_json::from_slice(&body)?;
Ok(result.data.update_available)
}
/// Send a signed message to the `DNClient` API to fetch the new configuration update. During this call a new
/// DH X25519 keypair is generated for the new Nebula certificate as well as a new Ed25519 keypair for `DNClient` API
/// communication. On success it returns the new config, a Nebula private key PEM to be inserted into the config
/// and new `DNClient` API credentials
/// # Errors
/// This function returns an error in any of the following scenarios:
/// - if the message could not be serialized
/// - if the request fails
/// - if the response could not be deserialized
/// - if the signature is invalid
/// - if the keys are invalid
pub fn do_update(&self, creds: &Credentials) -> Result<(NebulaConfig, DHPrivateKeyPEM, Credentials), Box<dyn Error>> {
let (dh_pubkey_pem, dh_privkey_pem, ed_pubkey, ed_privkey) = new_keys();
let update_keys = DoUpdateRequest {
ed_pubkey_pem: serialize_ed25519_public(ed_pubkey.as_bytes()),
dh_pubkey_pem,
nonce: nonce().to_vec(),
};
let update_keys_blob = serde_json::to_vec(&update_keys)?;
let resp = self.post_dnclient(DO_UPDATE, &update_keys_blob, &creds.host_id, creds.counter, &creds.ed_privkey)?;
let result_wrapper: SignedResponseWrapper = serde_json::from_slice(&resp)?;
let mut valid = false;
for ca_pubkey in &creds.trusted_keys {
if ca_pubkey.verify(&result_wrapper.data.message, &Signature::from_slice(&result_wrapper.data.signature)?).is_ok() {
valid = true;
break;
}
}
if !valid {
return Err("Failed to verify signed API result".into())
}
let result: DoUpdateResponse = serde_json::from_slice(&result_wrapper.data.message)?;
if result.nonce != update_keys.nonce {
error!("nonce mismatch between request {:x?} and response {:x?}", result.nonce, update_keys.nonce);
return Err("nonce mismatch between request and response".into())
}
if result.counter <= creds.counter {
error!("counter in request {} should be less than counter in response {}", creds.counter, result.counter);
return Err("received older config than what we already had".into())
}
let trusted_keys = ed25519_public_keys_from_pem(&result.trusted_keys)?;
let new_creds = Credentials {
host_id: creds.host_id.clone(),
ed_privkey,
counter: result.counter,
trusted_keys,
};
Ok((result.config, dh_privkey_pem, new_creds))
}
/// Wraps and signs the given `req_type` and value, and then makes the API call.
/// On success, returns the response body.
/// # Errors
/// This function will return an error if:
/// - serialization in any step fails
/// - if the `server_url` is invalid
/// - if the request could not be sent
pub fn post_dnclient(&self, req_type: &str, value: &[u8], host_id: &str, counter: u32, ed_privkey: &SigningKey) -> Result<Vec<u8>, Box<dyn Error>> {
let encoded_msg = serde_json::to_string(&RequestWrapper {
message_type: req_type.to_string(),
value: value.to_vec(),
timestamp: Local::now().format("%Y-%m-%dT%H:%M:%S.%f%:z").to_string(),
})?;
let encoded_msg_bytes = encoded_msg.into_bytes();
let b64_msg = base64::engine::general_purpose::STANDARD.encode(encoded_msg_bytes);
let b64_msg_bytes = b64_msg.as_bytes();
let signature = ed_privkey.sign(b64_msg_bytes).to_vec();
ed_privkey.verify(b64_msg_bytes, &Signature::from_slice(&signature)?)?;
debug!("signature valid via clientside check");
let body = RequestV1 {
version: 1,
host_id: host_id.to_string(),
counter,
message: b64_msg,
signature,
};
let post_body = serde_json::to_string(&body)?;
trace!("sending dnclient request {}", post_body);
let resp = self.http_client.post(self.server_url.join(ENDPOINT_V1)?).body(post_body).send()?;
match resp.status() {
StatusCode::OK => {
Ok(resp.bytes()?.to_vec())
},
StatusCode::FORBIDDEN => {
Err("Forbidden".into())
},
_ => {
error!("dnclient endpoint returned bad status code {}", resp.status());
Err("dnclient endpoint returned error".into())
}
}
}
}

View File

@ -0,0 +1,45 @@
//! Contains the `Credentials` struct, which contains all keys, IDs, organizations and other identity-related and security-related data that is persistent in a `Client`
use std::error::Error;
use trifid_pki::cert::{deserialize_ed25519_public_many, serialize_ed25519_public};
use trifid_pki::ed25519_dalek::{SigningKey, VerifyingKey};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone)]
/// Contains information necessary to make requests against the `DNClient` API.
pub struct Credentials {
/// The assigned Host ID that this client represents
pub host_id: String,
/// The ed25519 private key used to sign requests against the API
pub ed_privkey: SigningKey,
/// The counter used in the other API requests. It is unknown what the purpose of this is, but the original client persists it and it is needed for API calls.
pub counter: u32,
/// The set of trusted ed25519 keys that may be used by the API to sign API responses.
pub trusted_keys: Vec<VerifyingKey>
}
/// Converts an array of `VerifyingKey`s to a singular bundle of PEM-encoded keys
pub fn ed25519_public_keys_to_pem(keys: &[VerifyingKey]) -> Vec<u8> {
let mut res = vec![];
for key in keys {
res.append(&mut serialize_ed25519_public(&key.to_bytes()));
}
res
}
/// Converts a set of PEM-encoded ed25519 public keys, and converts them into an array of `VerifyingKey`s.
/// # Errors
/// This function will return an error if the PEM could not be decoded, or if any of the encoded keys are invalid.
pub fn ed25519_public_keys_from_pem(pem: &[u8]) -> Result<Vec<VerifyingKey>, Box<dyn Error>> {
let pems = deserialize_ed25519_public_many(pem)?;
let mut keys = vec![];
#[allow(clippy::unwrap_used)]
for pem in pems {
keys.push(VerifyingKey::from_bytes(&pem.try_into().unwrap_or_else(|_| unreachable!()))?);
}
Ok(keys)
}

41
dnapi-rs/src/crypto.rs Normal file
View File

@ -0,0 +1,41 @@
//! Functions for generating keys and nonces for use in API calls
use rand::Rng;
use rand::rngs::OsRng;
use trifid_pki::cert::{serialize_x25519_private, serialize_x25519_public};
use trifid_pki::ed25519_dalek::{SigningKey, VerifyingKey};
use trifid_pki::x25519_dalek::{PublicKey, StaticSecret};
/// Generate a new random set of Nebula (Diffie-Hellman) and Ed25519 (API calls) keys for use in your client
pub fn new_keys() -> (Vec<u8>, Vec<u8>, VerifyingKey, SigningKey) {
let (dh_pub, dh_priv) = new_nebula_keypair();
let (ed_pub, ed_priv) = new_ed25519_keypair();
(dh_pub, dh_priv, ed_pub, ed_priv)
}
/// Generate a new PEM-encoded Nebula keypair
pub fn new_nebula_keypair() -> (Vec<u8>, Vec<u8>) {
let (pub_key, priv_key) = new_x25519_keypair();
let pub_key_encoded = serialize_x25519_public(&pub_key);
let priv_key_encoded = serialize_x25519_private(&priv_key);
(pub_key_encoded, priv_key_encoded)
}
/// Generate a new 32-byte X25519 keypair
pub fn new_x25519_keypair() -> ([u8; 32], [u8; 32]) {
let priv_key = StaticSecret::new(OsRng);
let pub_key = PublicKey::from(&priv_key);
(pub_key.to_bytes(), priv_key.to_bytes())
}
/// Generate a new random Ed25519 signing keypair for signing API calls
pub fn new_ed25519_keypair() -> (VerifyingKey, SigningKey) {
let secret = SigningKey::generate(&mut OsRng);
let public = secret.verifying_key();
(public, secret)
}
/// Generates a 16-byte random nonce for use in API calls
pub fn nonce() -> [u8; 16] {
rand::thread_rng().gen()
}

24
dnapi-rs/src/lib.rs Normal file
View File

@ -0,0 +1,24 @@
//! # dnapi-rs
//! **dnapi-rs** is a Rust-native crate for interacting with the Defined Networking client API. It is a direct port of `dnapi`, an officially maintained API client by Defined Networking.
//!
//! This crate is maintained as a part of the trifid project. Check out the other crates in [the git repository](https://git.e3t.cc/~core/trifid).
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(missing_docs)]
#![deny(clippy::missing_errors_doc)]
#![deny(clippy::missing_panics_doc)]
#![deny(clippy::missing_safety_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::module_name_repetitions)]
pub mod message;
pub mod client_blocking;
pub mod client_async;
pub mod credentials;
pub mod crypto;

201
dnapi-rs/src/message.rs Normal file
View File

@ -0,0 +1,201 @@
//! Models for interacting with the Defined Networking API.
use base64_serde::base64_serde_type;
use serde::{Serialize, Deserialize};
/// The version 1 `DNClient` API endpoint
pub const ENDPOINT_V1: &str = "/v1/dnclient";
/// The `CheckForUpdate` message type
pub const CHECK_FOR_UPDATE: &str = "CheckForUpdate";
/// The `DoUpdate` message type
pub const DO_UPDATE: &str = "DoUpdate";
base64_serde_type!(Base64Standard, base64::engine::general_purpose::STANDARD);
#[derive(Serialize, Deserialize)]
/// `RequestV1` is the version 1 `DNClient` request message.
pub struct RequestV1 {
/// Version is always 1
pub version: i32,
#[serde(rename = "hostID")]
/// The Host ID of this dnclient instance
pub host_id: String,
/// The counter last returned by the server
pub counter: u32,
/// A base64-encoded message. This must be previously base64-encoded, as the signature is signed over the base64-encoded data.
pub message: String,
#[serde(with = "Base64Standard")]
/// An ed25519 signature over the `message`, which can be verified with the host's previously enrolled ed25519 public key
pub signature: Vec<u8>
}
#[derive(Serialize, Deserialize)]
/// `RequestWrapper` wraps a `DNClient` request message. It consists of a
/// type and value, with the type string indicating how to interpret the value blob.
pub struct RequestWrapper {
#[serde(rename = "type")]
/// The type of the message. Used to determine how `value` is encoded
pub message_type: String,
#[serde(with = "Base64Standard")]
/// A base64-encoded arbitrary message, the type of which is stated in `message_type`
pub value: Vec<u8>,
/// The timestamp of when this message was sent. Follows the format `%Y-%m-%dT%H:%M:%S.%f%:z`, or:
/// <4-digit year>-<two-digit-month>-<two-digit-day>T<two-digit-hour, 24-hour>:<two-digit-minute>:<two-digit-second>.<nanoseconds, zero-padded><offset with semicolon>
/// For example:
/// `2023-03-29T09:56:42.380006369-04:00`
/// would represent `29 March 03, 2023, 09:56:42.380006369 UTC-4`
pub timestamp: String
}
#[derive(Serialize, Deserialize)]
/// `SignedResponseWrapper` contains a response message and a signature to validate inside `data`.
pub struct SignedResponseWrapper {
/// The response data contained in this message
pub data: SignedResponse
}
#[derive(Serialize, Deserialize)]
/// `SignedResponse` contains a response message and a signature to validate.
pub struct SignedResponse {
/// The API version - always 1
pub version: i32,
#[serde(with = "Base64Standard")]
/// The Base64-encoded message signed inside this message
pub message: Vec<u8>,
#[serde(with = "Base64Standard")]
/// The ed25519 signature over the `message`
pub signature: Vec<u8>
}
#[derive(Serialize, Deserialize)]
/// `CheckForUpdateResponseWrapper` contains a response to `CheckForUpdate` inside "data."
pub struct CheckForUpdateResponseWrapper {
/// The response data contained in this message
pub data: CheckForUpdateResponse
}
#[derive(Serialize, Deserialize)]
/// `CheckForUpdateResponse` is the response generated for a `CheckForUpdate` request.
pub struct CheckForUpdateResponse {
#[serde(rename = "updateAvailable")]
/// Set to true if a config update is available
pub update_available: bool
}
#[derive(Serialize, Deserialize)]
/// `DoUpdateRequest` is the request sent for a `DoUpdate` request.
pub struct DoUpdateRequest {
#[serde(rename = "edPubkeyPEM")]
#[serde(with = "Base64Standard")]
/// The new ed25519 public key that should be used for future API requests
pub ed_pubkey_pem: Vec<u8>,
#[serde(rename = "dhPubkeyPEM")]
#[serde(with = "Base64Standard")]
/// The new ECDH public key that the Nebula certificate should be signed for
pub dh_pubkey_pem: Vec<u8>,
#[serde(with = "Base64Standard")]
/// A randomized value used to uniquely identify this request.
/// The original client uses a randomized, 16-byte value here, which dnapi-rs replicates
pub nonce: Vec<u8>
}
#[derive(Serialize, Deserialize)]
/// A server response to a `DoUpdateRequest`, with the updated config and key information
pub struct DoUpdateResponse {
#[serde(with = "Base64Standard")]
/// The base64-encoded Nebula config. It does **NOT** have a private-key, which must be inserted explicitly before Nebula can be ran
pub config: Vec<u8>,
/// The new config counter. It is unknown what the purpose of this is, but the original client keeps track of it and it is used later in the api
pub counter: u32,
#[serde(with = "Base64Standard")]
/// The same base64-encoded nonce that was sent in the `DoUpdateRequest`.
pub nonce: Vec<u8>,
#[serde(rename = "trustedKeys")]
#[serde(with = "Base64Standard")]
/// A new set of trusted ed25519 keys that can be used by the server to sign messages.
pub trusted_keys: Vec<u8>
}
/// The REST enrollment endpoint
pub const ENROLL_ENDPOINT: &str = "/v2/enroll";
#[derive(Serialize, Deserialize)]
/// `EnrollRequest` is issued to the `ENROLL_ENDPOINT` to enroll this `dnclient` with a dnapi organization
pub struct EnrollRequest {
/// The enrollment code given by the API server.
pub code: String,
#[serde(rename = "dhPubkey")]
#[serde(with = "Base64Standard")]
/// The ECDH public-key that should be used to sign the Nebula certificate given to this node.
pub dh_pubkey: Vec<u8>,
#[serde(rename = "edPubkey")]
#[serde(with = "Base64Standard")]
/// The Ed25519 public-key that this node will use to sign messages sent to the API.
pub ed_pubkey: Vec<u8>,
/// The timestamp of when this request was sent. Follows the format `%Y-%m-%dT%H:%M:%S.%f%:z`, or:
/// <4-digit year>-<two-digit-month>-<two-digit-day>T<two-digit-hour, 24-hour>:<two-digit-minute>:<two-digit-second>.<nanoseconds, zero-padded><offset with semicolon>
/// For example:
/// `2023-03-29T09:56:42.380006369-04:00`
/// would represent `29 March 03, 2023, 09:56:42.380006369 UTC-4`
pub timestamp: String
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
/// The response to an `EnrollRequest`
pub enum EnrollResponse {
/// A successful enrollment, with a `data` field pointing to an `EnrollResponseData`
Success {
/// The response data from this response
data: EnrollResponseData
},
/// An unsuccessful enrollment, with an `errors` field pointing to an array of `APIError`s.
Error {
/// A list of `APIError`s that happened while trying to enroll. `APIErrors` is a type alias to `Vec<APIError>`
errors: APIErrors
}
}
#[derive(Serialize, Deserialize)]
/// The data included in an successful enrollment.
pub struct EnrollResponseData {
#[serde(with = "Base64Standard")]
/// The base64-encoded Nebula config. It does **NOT** have a private-key, which must be inserted explicitly before Nebula can be ran
pub config: Vec<u8>,
#[serde(rename = "hostID")]
/// The server-side Host ID that this node now has.
pub host_id: String,
/// The new config counter. It is unknown what the purpose of this is, but the original client keeps track of it and it is used later in the api
pub counter: u32,
#[serde(rename = "trustedKeys")]
#[serde(with = "Base64Standard")]
/// A new set of trusted ed25519 keys that can be used by the server to sign messages.
pub trusted_keys: Vec<u8>,
/// The organization data that this node is now a part of
pub organization: EnrollResponseDataOrg
}
#[derive(Serialize, Deserialize)]
/// The organization data that this node is now a part of
pub struct EnrollResponseDataOrg {
/// The organization ID that this node is now a part of
pub id: String,
/// The name of the organization that this node is now a part of
pub name: String
}
#[derive(Serialize, Deserialize)]
/// `APIError` represents a single error returned in an API error response.
pub struct APIError {
/// The error code
pub code: String,
/// The human-readable error message
pub message: String,
/// An optional path to where the error occured
pub path: Option<String>
}
/// A type alias to a array of `APIErrors`. Just for parity with dnapi.
pub type APIErrors = Vec<APIError>;

33
tfclient/BUILDING.md Normal file
View File

@ -0,0 +1,33 @@
tfclient has a more involved build process than the rest of trifid due to the fact that it embeds the distribution nebula and nebula-cert binaries.
## Clearing the Nebula binary cache
Compiling tfclient may take longer than usual every once in a while because the build system downloads the latest version of nebula.
After this has been done once, the generated code will be cached in the target directory. If you want to redownload it (e.g. if Nebula is updated), you can:
- run `cargo clean`, or
- delete `target/debug/build/tfclient-[HASH]/out/noredownload`, or
- set the environment variable `TFBUILD_FORCE_REDOWNLOAD=1`
The 3rd option is the best, as it will not signifigantly increase compile times *or* need you to find the build hash.
## Set a GitHub API key
Since the build process involves making calls to the GitHub API, you may get ratelimited.
To provide a GitHub API token to increase your ratelimit, set `GH_API_KEY` to your GitHub API key.
## Unsupported architecture :(
If you're compiling on (or for) an architecture that tfclient currently does not support, you will get the following message:
```text
[*] Compiling for target: SOME-TARGET-TRIPLET-HERE
This architecture is not supported yet :(
Nebula has a limited set of architectures it is able to function on.
tfclient can only be compiled on these architectures.
See https://github.com/slackhq/nebula/releases for a list of supported architectures.
Is your system supported by Nebula? Shoot a message to the mailing list. Include the target triplet (above) in your response, as well as a link to the functioning Nebula binary. We will happily add your machine to the list!
```
The `tfclient` build system attempts to check if your architecture is unsupported. It does this by checking your target triplet.
If this is a false alarm, and a build of nebula is available and fully functional on your machine, please shoot a message to the mailing list with your target triplet and a link to which build of nebula works on your machine. We will update the build script and update the repo!

View File

@ -1,8 +1,41 @@
[package] [package]
name = "tfclient" name = "tfclient"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
description = "An open-source reimplementation of a Defined Networking-compatible client"
license = "GPL-3.0-or-later"
documentation = "https://man.e3t.cc/~core/trifid-docs"
homepage = "https://man.e3t.cc/~core/trifid-docs"
repository = "https://git.e3t.cc/~core/trifid"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = { version = "4.1.10", features = ["derive"] }
trifid-pki = { version = "0.1.8", path = "../trifid-pki", features = ["serde_derive"] }
dirs = "5.0.0"
log = "0.4.17"
simple_logger = "4.1.0"
sha2 = "0.10.6"
hex = "0.4.3"
url = "2.3.1"
toml = "0.7.3"
serde = { version = "1.0.158", features = ["derive"] }
serde_json = "1.0.94"
ctrlc = "3.2.5"
reqwest = { version = "0.11.16", features = ["blocking"] }
base64 = "0.21.0"
chrono = "0.4.24"
ipnet = "2.7.1"
base64-serde = "0.7.0"
dnapi-rs = { version = "0.1.7", path = "../dnapi-rs" }
serde_yaml = "0.9.19"
[build-dependencies]
serde = { version = "1.0.157", features = ["derive"] }
reqwest = { version = "0.11.14", features = ["blocking", "json"] }
flate2 = "1.0.25"
tar = "0.4.38"
hex = "0.4.3"
tempfile = "3.4.0"
sha2 = "0.10.6"

5
tfclient/README.md Normal file
View File

@ -0,0 +1,5 @@
# tfclient
tfclient is an open-source Rust client for the Defined Networking Management protocol. It embeds a Nebula binary for running the actual config, and uses [dnapi-rs](https://docs.rs/dnapi-rs) for making API calls against the Defined Networking API.
tfclient is part of the trifid project. Check out the other crates [here!](https://git.e3t.cc/~core/trifid).

178
tfclient/build.rs Normal file
View File

@ -0,0 +1,178 @@
use std::fs;
use std::fs::{File, remove_file};
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Command, Output};
use flate2::read::GzDecoder;
use reqwest::blocking::Response;
use reqwest::header::HeaderMap;
use tar::Archive;
#[derive(serde::Deserialize, Debug)]
struct GithubRelease {
name: String,
assets: Vec<GithubReleaseAsset>
}
#[derive(serde::Deserialize, Debug)]
struct GithubUser {}
#[derive(serde::Deserialize, Debug)]
struct GithubReleaseAsset {
browser_download_url: String,
name: String,
size: i64
}
fn main() {
if Path::new(&format!("{}/{}", std::env::var("OUT_DIR").unwrap(), "noredownload")).exists() && std::env::var("TFBUILD_FORCE_REDOWNLOAD").is_err() {
println!("noredownload exists and TFBUILD_FORCE_REDOWNLOAD is not set. Not redoing build process.");
return;
}
println!("[*] Fetching nebula releaseinfo...");
let mut headers = HeaderMap::new();
let mut has_api_key = false;
if let Ok(api_key) = std::env::var("GH_API_KEY") {
headers.insert("Authorization", format!("Bearer {}", api_key).parse().unwrap());
has_api_key = true;
}
let client = reqwest::blocking::Client::builder().user_agent("curl/7.57.1").default_headers(headers).build().unwrap();
let resp: Response = client.get("https://api.github.com/repos/slackhq/nebula/releases/latest").send().unwrap();
if resp.headers().get("X-Ratelimit-Remaining").unwrap().to_str().unwrap() == "0" {
println!("You've been ratelimited from the GitHub API. Wait a while (1 hour)");
if !has_api_key {
println!("You can also set a GitHub API key with the environment variable GH_API_KEY, which will increase your ratelimit ( a lot )");
}
panic!("Ratelimited");
}
let release: GithubRelease = resp.json().unwrap();
println!("[*] Fetching target triplet...");
let target = std::env::var("TARGET").unwrap();
println!("[*] Compiling for target {}", target);
let target_file = match target.as_str() {
"x86_64-apple-darwin" | "aarch64-apple-darwin" => "nebula-darwin",
"x86_64-unknown-freebsd" => "nebula-freebsd-amd64",
"x86_64-unknown-linux-gnu" => "nebula-linux-amd64",
"armv5te-unknown-linux-gnueabi" => "nebula-linux-arm-5",
"arm-unknown-linux-gnueabi" | "arm-unknown-linux-gnueabihf" => "nebula-linux-arm-6",
"armv7-unknown-linux-gnueabihf" | "armv7-unknown-linux-gnueabi" => "nebula-linux-arm-7",
"aarch64-unknown-linux-gnu" => "nebula-linux-arm64",
"x86_64-pc-windows-msvc" => "nebula-windows-amd64",
"aarch64-pc-windows-msvc" => "nebula-windows-arm64",
_ => {
println!("This architecture is not supported yet :(");
println!("Nebula has a limited set of architectures it is able to function on.");
println!("tfclient can only be compiled on these architectures.");
println!("See https://github.com/slackhq/nebula/releases for a list of supported architectures");
println!("Is your system supported by Nebula? Shoot a message to the mailing list. Include the target triplet (above) in your response, as well as a link to the functioning Nebula binary. We will happily add your machine to the list!");
panic!("Unsupported architecture");
}
};
println!("[*] Embedding {} {}", target_file, release.name);
let download = release.assets.iter().find(|r| r.name == format!("{}.tar.gz", target_file)).expect("That architecture isn't avaliable :(");
println!("[*] Downloading {}.tar.gz ({}, {} bytes) from {}", target_file, target, download.size, download.browser_download_url);
let response = reqwest::blocking::get(&download.browser_download_url).unwrap();
let content = response.bytes().unwrap().to_vec();
let bytes = content.as_slice();
let tar = GzDecoder::new(bytes);
let mut archive = Archive::new(tar);
let entries = archive.entries().unwrap();
let mut nebula_bin = vec![];
let mut nebula_cert_bin = vec![];
let mut shasum = vec![];
for entry in entries {
let mut entry = entry.unwrap();
if entry.path().unwrap() == Path::new("nebula") || entry.path().unwrap() == Path::new("nebula.exe") {
nebula_bin.reserve(entry.size() as usize);
entry.read_to_end(&mut nebula_bin).unwrap();
} else if entry.path().unwrap() == Path::new("nebula-cert") || entry.path().unwrap() == Path::new("nebula-cert.exe") {
nebula_cert_bin.reserve(entry.size() as usize);
entry.read_to_end(&mut nebula_cert_bin).unwrap();
} else if entry.path().unwrap() == Path::new("SHASUM256.txt") {
shasum.reserve(entry.size() as usize);
entry.read_to_end(&mut shasum).unwrap();
}
}
if nebula_bin.is_empty() {
panic!("[x] Release did not contain nebula binary");
}
if nebula_cert_bin.is_empty() {
panic!("[x] Release did not contain nebula_cert binary");
}
let mut nebula_file = File::create(format!("{}/nebula.bin", std::env::var("OUT_DIR").unwrap())).unwrap();
nebula_file.write_all(&nebula_bin).unwrap();
codegen_version(&nebula_bin, "nebula.bin", "NEBULA");
let mut nebula_cert_file = File::create(format!("{}/nebula_cert.bin", std::env::var("OUT_DIR").unwrap())).unwrap();
nebula_cert_file.write_all(&nebula_cert_bin).unwrap();
codegen_version(&nebula_cert_bin, "nebula_cert.bin", "NEBULA_CERT");
// Indicate to cargo and ourselves that we have already downloaded and codegenned
File::create(format!("{}/{}", std::env::var("OUT_DIR").unwrap(), "noredownload")).unwrap();
println!("cargo:rerun-if-changed=build.rs");
}
fn codegen_version(bin: &[u8], fp: &str, name: &str) {
// get version
let output = execim(bin, &vec!["-version"]);
let stdout = output.stdout;
let stdout_str = String::from_utf8(stdout).unwrap();
if !stdout_str.starts_with("Version: ") {
panic!("Binary did not have expected version output. Unable to get version info.");
}
let mut version = stdout_str.split(' ').collect::<Vec<&str>>()[1].to_string();
version.pop();
let code = format!("// This code was automatically @generated by build.rs. It should not be modified.\npub const {}_BIN: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/{}\"));\npub const {}_VERSION: &str = \"{}\";", name, fp, name, version);
let mut file = File::create(format!("{}/{}.rs", std::env::var("OUT_DIR").unwrap(), fp)).unwrap();
file.write_all(code.as_bytes()).unwrap();
}
#[cfg(not(unix))]
fn execim(buf: &[u8], args: &Vec<&str>) -> Output {
let mut file = File::create("tmpexec.bin").unwrap();
file.write_all(buf).unwrap();
std::mem::drop(file);
let output = Command::new("./tmpexec.bin").args(args).output().unwrap();
remove_file("./tmpexec.bin").unwrap();
output
}
#[cfg(unix)]
fn execim(buf: &[u8], args: &Vec<&str>) -> Output {
let mut file = File::create("tmpexec.bin").unwrap();
file.write_all(buf).unwrap();
let metadata = file.metadata().unwrap();
let mut permissions = metadata.permissions();
permissions.set_mode(0o0755);
fs::set_permissions("./tmpexec.bin", permissions).unwrap();
std::mem::drop(file);
let output = Command::new("./tmpexec.bin").args(args).output().unwrap();
remove_file("./tmpexec.bin").unwrap();
output
}

218
tfclient/src/apiworker.rs Normal file
View File

@ -0,0 +1,218 @@
use std::fs;
use std::sync::mpsc::{Receiver, TryRecvError};
use log::{error, info, warn};
use url::Url;
use dnapi_rs::client_blocking::Client;
use crate::config::{load_cdata, save_cdata, TFClientConfig};
use crate::daemon::ThreadMessageSender;
use crate::dirs::get_nebulaconfig_file;
use crate::nebulaworker::NebulaWorkerMessage;
pub enum APIWorkerMessage {
Shutdown,
Enroll { code: String },
Timer
}
pub fn apiworker_main(_config: TFClientConfig, instance: String, url: String, tx: ThreadMessageSender, rx: Receiver<APIWorkerMessage>) {
let server = Url::parse(&url).unwrap();
let client = Client::new(format!("tfclient/{}", env!("CARGO_PKG_VERSION")), server).unwrap();
loop {
match rx.try_recv() {
Ok(msg) => {
match msg {
APIWorkerMessage::Shutdown => {
info!("recv on command socket: shutdown, stopping");
break;
},
APIWorkerMessage::Timer => {
info!("updating config");
let mut cdata = match load_cdata(&instance) {
Ok(c) => c,
Err(e) => {
error!("error in api worker thread: {}", e);
error!("APIWorker exiting with error");
return;
}
};
if cdata.creds.is_none() {
info!("not enrolled, cannot perform config update");
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
continue;
}
let creds = cdata.clone().creds.unwrap_or_else(|| unreachable!());
info!("checking for update");
let update_available = match client.check_for_update(&creds) {
Ok(ua) => ua,
Err(e) => {
error!("error checking for config update: {}", e);
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
continue;
}
};
if !update_available {
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
info!("no config update available");
continue;
}
info!("updated configuration is avaliable");
info!("updating configuration");
let (config, dh_privkey, creds) = match client.do_update(&creds) {
Ok(d) => d,
Err(e) => {
error!("error requesting updating config: {}", e);
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
continue;
}
};
cdata.creds = Some(creds);
cdata.dh_privkey = Some(dh_privkey.try_into().expect("32 != 32"));
match fs::write(get_nebulaconfig_file(&instance).expect("Unable to determine nebula config file location"), config) {
Ok(_) => (),
Err(e) => {
error!("unable to save nebula config: {}", e);
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
continue;
}
}
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
info!("configuration updated successfully!");
info!("sending signal to nebula thread to reload config");
match tx.nebula_thread.send(NebulaWorkerMessage::ConfigUpdated) {
Ok(_) => (),
Err(e) => {
error!("unable to tell nebula thread to update config: {}", e);
error!("APIWorker exiting with error");
return;
}
}
},
APIWorkerMessage::Enroll { code } => {
info!("recv on command socket: enroll {}", code);
let mut cdata = match load_cdata(&instance) {
Ok(c) => c,
Err(e) => {
error!("error in api worker thread: {}", e);
error!("APIWorker exiting with error");
return;
}
};
if cdata.creds.is_some() {
warn!("enrollment failed: already enrolled");
continue;
}
let (config, dh_privkey, creds, meta) = match client.enroll(&code) {
Ok(resp) => resp,
Err(e) => {
error!("error with enrollment request: {}", e);
continue;
}
};
match fs::write(get_nebulaconfig_file(&instance).expect("Unable to determine nebula config file location"), config) {
Ok(_) => (),
Err(e) => {
error!("unable to save nebula config: {}", e);
continue;
}
}
cdata.creds = Some(creds);
cdata.dh_privkey = Some(dh_privkey.try_into().expect("32 != 32"));
cdata.meta = Some(meta);
// Save vardata
match save_cdata(&instance, cdata) {
Ok(_) => (),
Err(e) => {
error!("Error saving cdata: {}", e);
error!("APIWorker exiting with error");
return;
}
}
info!("Configuration updated. Sending signal to Nebula worker thread");
match tx.nebula_thread.send(NebulaWorkerMessage::ConfigUpdated) {
Ok(_) => (),
Err(e) => {
error!("unable to tell nebula thread to update config: {}", e);
error!("APIWorker exiting with error");
return;
}
}
}
}
},
Err(e) => {
match e {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
error!("apiworker command socket disconnected, shutting down to prevent orphaning");
break;
}
}
}
}
}
}

534
tfclient/src/config.rs Normal file
View File

@ -0,0 +1,534 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::net::{Ipv4Addr, SocketAddrV4};
use ipnet::{IpNet, Ipv4Net};
use log::{debug, info};
use serde::{Deserialize, Serialize};
use dnapi_rs::client_blocking::EnrollMeta;
use dnapi_rs::credentials::Credentials;
use crate::dirs::{get_cdata_dir, get_cdata_file, get_config_dir, get_config_file};
pub const DEFAULT_PORT: u16 = 8157;
fn default_port() -> u16 { DEFAULT_PORT }
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TFClientConfig {
#[serde(default = "default_port")]
pub listen_port: u16
}
#[derive(Serialize, Deserialize, Clone)]
pub struct TFClientData {
pub dh_privkey: Option<Vec<u8>>,
pub creds: Option<Credentials>,
pub meta: Option<EnrollMeta>
}
pub fn create_config(instance: &str) -> Result<(), Box<dyn Error>> {
info!("Creating config directory...");
fs::create_dir_all(get_config_dir(instance).ok_or("Unable to load config dir")?)?;
info!("Copying default config file to config directory...");
let config = TFClientConfig {
listen_port: DEFAULT_PORT
};
let config_str = toml::to_string(&config)?;
fs::write(get_config_file(instance).ok_or("Unable to load config dir")?, config_str)?;
Ok(())
}
pub fn load_config(instance: &str) -> Result<TFClientConfig, Box<dyn Error>> {
info!("Loading config...");
let config_file = get_config_file(instance).ok_or("Unable to load config dir")?;
if !config_file.exists() {
create_config(instance)?;
}
debug!("opening {}", config_file.as_path().display());
let config_str = fs::read_to_string(config_file)?;
debug!("parsing config file");
let config: TFClientConfig = toml::from_str(&config_str)?;
info!("Loaded config successfully");
Ok(config)
}
pub fn create_cdata(instance: &str) -> Result<(), Box<dyn Error>> {
info!("Creating data directory...");
fs::create_dir_all(get_cdata_dir(instance).ok_or("Unable to load data dir")?)?;
info!("Copying default data file to config directory...");
let config = TFClientData { dh_privkey: None, creds: None, meta: None };
let config_str = toml::to_string(&config)?;
fs::write(get_cdata_file(instance).ok_or("Unable to load data dir")?, config_str)?;
Ok(())
}
pub fn load_cdata(instance: &str) -> Result<TFClientData, Box<dyn Error>> {
info!("Loading cdata...");
let config_file = get_cdata_file(instance).ok_or("Unable to load cdata dir")?;
if !config_file.exists() {
create_cdata(instance)?;
}
debug!("opening {}", config_file.as_path().display());
let config_str = fs::read_to_string(config_file)?;
debug!("parsing cdata file");
let config: TFClientData = toml::from_str(&config_str)?;
info!("Loaded cdata successfully");
Ok(config)
}
pub fn save_cdata(instance: &str, data: TFClientData) -> Result<(), Box<dyn Error>> {
info!("Saving cdata...");
let config_file = get_cdata_file(instance).ok_or("Unable to load cdata dir")?;
if !config_file.exists() {
create_cdata(instance)?;
}
debug!("serializing cdata file");
let config: String = toml::to_string(&data)?;
debug!("writing to {}", config_file.as_path().display());
fs::write(config_file, config)?;
info!("Saved cdata successfully");
Ok(())
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfig {
pub pki: NebulaConfigPki,
#[serde(default = "empty_hashmap")]
#[serde(skip_serializing_if = "is_empty_hashmap")]
pub static_host_map: HashMap<Ipv4Addr, Vec<SocketAddrV4>>,
#[serde(skip_serializing_if = "is_none")]
pub lighthouse: Option<NebulaConfigLighthouse>,
#[serde(skip_serializing_if = "is_none")]
pub listen: Option<NebulaConfigListen>,
#[serde(skip_serializing_if = "is_none")]
pub punchy: Option<NebulaConfigPunchy>,
#[serde(default = "cipher_aes")]
#[serde(skip_serializing_if = "is_cipher_aes")]
pub cipher: NebulaConfigCipher,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub preferred_ranges: Vec<IpNet>,
#[serde(skip_serializing_if = "is_none")]
pub relay: Option<NebulaConfigRelay>,
#[serde(skip_serializing_if = "is_none")]
pub tun: Option<NebulaConfigTun>,
#[serde(skip_serializing_if = "is_none")]
pub logging: Option<NebulaConfigLogging>,
#[serde(skip_serializing_if = "is_none")]
pub sshd: Option<NebulaConfigSshd>,
#[serde(skip_serializing_if = "is_none")]
pub firewall: Option<NebulaConfigFirewall>,
#[serde(default = "u64_1")]
#[serde(skip_serializing_if = "is_u64_1")]
pub routines: u64,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub stats: Option<NebulaConfigStats>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub local_range: Option<Ipv4Net>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigPki {
pub ca: String,
pub cert: String,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub key: Option<String>,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub blocklist: Vec<String>,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub disconnect_invalid: bool
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigLighthouse {
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub am_lighthouse: bool,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub serve_dns: bool,
#[serde(skip_serializing_if = "is_none")]
pub dns: Option<NebulaConfigLighthouseDns>,
#[serde(default = "u32_10")]
#[serde(skip_serializing_if = "is_u32_10")]
pub interval: u32,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub hosts: Vec<Ipv4Addr>,
#[serde(default = "empty_hashmap")]
#[serde(skip_serializing_if = "is_empty_hashmap")]
pub remote_allow_list: HashMap<Ipv4Net, bool>,
#[serde(default = "empty_hashmap")]
#[serde(skip_serializing_if = "is_empty_hashmap")]
pub local_allow_list: HashMap<Ipv4Net, bool>, // `interfaces` is not supported
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigLighthouseDns {
#[serde(default = "string_empty")]
#[serde(skip_serializing_if = "is_string_empty")]
pub host: String,
#[serde(default = "u16_53")]
#[serde(skip_serializing_if = "is_u16_53")]
pub port: u16
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigListen {
#[serde(default = "string_empty")]
#[serde(skip_serializing_if = "is_string_empty")]
pub host: String,
#[serde(default = "u16_0")]
#[serde(skip_serializing_if = "is_u16_0")]
pub port: u16,
#[serde(default = "u32_64")]
#[serde(skip_serializing_if = "is_u32_64")]
pub batch: u32,
#[serde(skip_serializing_if = "is_none")]
pub read_buffer: Option<u32>,
#[serde(skip_serializing_if = "is_none")]
pub write_buffer: Option<u32>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigPunchy {
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub punch: bool,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub respond: bool,
#[serde(default = "string_1s")]
#[serde(skip_serializing_if = "is_string_1s")]
pub delay: String
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigCipher {
#[serde(rename = "aes")]
Aes,
#[serde(rename = "chachapoly")]
ChaChaPoly
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigRelay {
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub relays: Vec<Ipv4Addr>,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub am_relay: bool,
#[serde(default = "bool_true")]
#[serde(skip_serializing_if = "is_bool_true")]
pub use_relays: bool
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigTun {
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub disabled: bool,
#[serde(skip_serializing_if = "is_none")]
pub dev: Option<String>,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub drop_local_broadcast: bool,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub drop_multicast: bool,
#[serde(default = "u64_500")]
#[serde(skip_serializing_if = "is_u64_500")]
pub tx_queue: u64,
#[serde(default = "u64_1300")]
#[serde(skip_serializing_if = "is_u64_1300")]
pub mtu: u64,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub routes: Vec<NebulaConfigTunRouteOverride>,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub unsafe_routes: Vec<NebulaConfigTunUnsafeRoute>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigTunRouteOverride {
pub mtu: u64,
pub route: Ipv4Net
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigTunUnsafeRoute {
pub route: Ipv4Net,
pub via: Ipv4Addr,
#[serde(default = "u64_1300")]
#[serde(skip_serializing_if = "is_u64_1300")]
pub mtu: u64,
#[serde(default = "i64_100")]
#[serde(skip_serializing_if = "is_i64_100")]
pub metric: i64
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigLogging {
#[serde(default = "loglevel_info")]
#[serde(skip_serializing_if = "is_loglevel_info")]
pub level: NebulaConfigLoggingLevel,
#[serde(default = "format_text")]
#[serde(skip_serializing_if = "is_format_text")]
pub format: NebulaConfigLoggingFormat,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub disable_timestamp: bool,
#[serde(default = "timestamp")]
#[serde(skip_serializing_if = "is_timestamp")]
pub timestamp_format: String
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigLoggingLevel {
#[serde(rename = "panic")]
Panic,
#[serde(rename = "fatal")]
Fatal,
#[serde(rename = "error")]
Error,
#[serde(rename = "warning")]
Warning,
#[serde(rename = "info")]
Info,
#[serde(rename = "debug")]
Debug
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigLoggingFormat {
#[serde(rename = "json")]
Json,
#[serde(rename = "text")]
Text
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigSshd {
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub enabled: bool,
pub listen: SocketAddrV4,
pub host_key: String,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub authorized_users: Vec<NebulaConfigSshdAuthorizedUser>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigSshdAuthorizedUser {
pub user: String,
#[serde(default = "empty_vec")]
#[serde(skip_serializing_if = "is_empty_vec")]
pub keys: Vec<String>
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type")]
pub enum NebulaConfigStats {
#[serde(rename = "graphite")]
Graphite(NebulaConfigStatsGraphite),
#[serde(rename = "prometheus")]
Prometheus(NebulaConfigStatsPrometheus)
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigStatsGraphite {
#[serde(default = "string_nebula")]
#[serde(skip_serializing_if = "is_string_nebula")]
pub prefix: String,
#[serde(default = "protocol_tcp")]
#[serde(skip_serializing_if = "is_protocol_tcp")]
pub protocol: NebulaConfigStatsGraphiteProtocol,
pub host: SocketAddrV4,
pub interval: String,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub message_metrics: bool,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub lighthouse_metrics: bool
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum NebulaConfigStatsGraphiteProtocol {
#[serde(rename = "tcp")]
Tcp,
#[serde(rename = "udp")]
Udp
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigStatsPrometheus {
pub listen: String,
pub path: String,
#[serde(default = "string_nebula")]
#[serde(skip_serializing_if = "is_string_nebula")]
pub namespace: String,
#[serde(default = "string_nebula")]
#[serde(skip_serializing_if = "is_string_nebula")]
pub subsystem: String,
pub interval: String,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub message_metrics: bool,
#[serde(default = "bool_false")]
#[serde(skip_serializing_if = "is_bool_false")]
pub lighthouse_metrics: bool
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigFirewall {
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub conntrack: Option<NebulaConfigFirewallConntrack>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub inbound: Option<Vec<NebulaConfigFirewallRule>>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub outbound: Option<Vec<NebulaConfigFirewallRule>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigFirewallConntrack {
#[serde(default = "string_12m")]
#[serde(skip_serializing_if = "is_string_12m")]
pub tcp_timeout: String,
#[serde(default = "string_3m")]
#[serde(skip_serializing_if = "is_string_3m")]
pub udp_timeout: String,
#[serde(default = "string_10m")]
#[serde(skip_serializing_if = "is_string_10m")]
pub default_timeout: String
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct NebulaConfigFirewallRule {
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub port: Option<String>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub proto: Option<String>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub ca_name: Option<String>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub ca_sha: Option<String>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub host: Option<String>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub group: Option<String>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub groups: Option<Vec<String>>,
#[serde(default = "none")]
#[serde(skip_serializing_if = "is_none")]
pub cidr: Option<String>
}
// Default values for serde
fn string_12m() -> String { "12m".to_string() }
fn is_string_12m(s: &str) -> bool { s == "12m" }
fn string_3m() -> String { "3m".to_string() }
fn is_string_3m(s: &str) -> bool { s == "3m" }
fn string_10m() -> String { "10m".to_string() }
fn is_string_10m(s: &str) -> bool { s == "10m" }
fn empty_vec<T>() -> Vec<T> { vec![] }
fn is_empty_vec<T>(v: &Vec<T>) -> bool { v.is_empty() }
fn empty_hashmap<A, B>() -> HashMap<A, B> { HashMap::new() }
fn is_empty_hashmap<A, B>(h: &HashMap<A, B>) -> bool { h.is_empty() }
fn bool_false() -> bool { false }
fn is_bool_false(b: &bool) -> bool { !*b }
fn bool_true() -> bool { true }
fn is_bool_true(b: &bool) -> bool { *b }
fn u16_53() -> u16 { 53 }
fn is_u16_53(u: &u16) -> bool { *u == 53 }
fn u32_10() -> u32 { 10 }
fn is_u32_10(u: &u32) -> bool { *u == 10 }
fn ipv4_0000() -> Ipv4Addr { Ipv4Addr::new(0, 0, 0, 0) }
fn is_ipv4_0000(i: &Ipv4Addr) -> bool { *i == ipv4_0000() }
fn u16_0() -> u16 { 0 }
fn is_u16_0(u: &u16) -> bool { *u == 0 }
fn u32_64() -> u32 { 64 }
fn is_u32_64(u: &u32) -> bool { *u == 64 }
fn string_1s() -> String { "1s".to_string() }
fn is_string_1s(s: &str) -> bool { s == "1s" }
fn cipher_aes() -> NebulaConfigCipher { NebulaConfigCipher::Aes }
fn is_cipher_aes(c: &NebulaConfigCipher) -> bool { matches!(c, NebulaConfigCipher::Aes) }
fn u64_500() -> u64 { 500 }
fn is_u64_500(u: &u64) -> bool { *u == 500 }
fn u64_1300() -> u64 { 1300 }
fn is_u64_1300(u: &u64) -> bool { *u == 1300 }
fn i64_100() -> i64 { 100 }
fn is_i64_100(i: &i64) -> bool { *i == 100 }
fn loglevel_info() -> NebulaConfigLoggingLevel { NebulaConfigLoggingLevel::Info }
fn is_loglevel_info(l: &NebulaConfigLoggingLevel) -> bool { matches!(l, NebulaConfigLoggingLevel::Info) }
fn format_text() -> NebulaConfigLoggingFormat { NebulaConfigLoggingFormat::Text }
fn is_format_text(f: &NebulaConfigLoggingFormat) -> bool { matches!(f, NebulaConfigLoggingFormat::Text) }
fn timestamp() -> String { "2006-01-02T15:04:05Z07:00".to_string() }
fn is_timestamp(s: &str) -> bool { s == "2006-01-02T15:04:05Z07:00" }
fn u64_1() -> u64 { 1 }
fn is_u64_1(u: &u64) -> bool { *u == 1 }
fn string_nebula() -> String { "nebula".to_string() }
fn is_string_nebula(s: &str) -> bool { s == "nebula" }
fn string_empty() -> String { String::new() }
fn is_string_empty(s: &str) -> bool { s == "" }
fn protocol_tcp() -> NebulaConfigStatsGraphiteProtocol { NebulaConfigStatsGraphiteProtocol::Tcp }
fn is_protocol_tcp(p: &NebulaConfigStatsGraphiteProtocol) -> bool { matches!(p, NebulaConfigStatsGraphiteProtocol::Tcp) }
fn none<T>() -> Option<T> { None }
fn is_none<T>(o: &Option<T>) -> bool { o.is_none() }

158
tfclient/src/daemon.rs Normal file
View File

@ -0,0 +1,158 @@
use std::sync::mpsc;
use std::sync::mpsc::Sender;
use std::thread;
use log::{error, info};
use crate::apiworker::{apiworker_main, APIWorkerMessage};
use crate::config::load_config;
use crate::nebulaworker::{nebulaworker_main, NebulaWorkerMessage};
use crate::socketworker::{socketworker_main, SocketWorkerMessage};
use crate::timerworker::{timer_main, TimerWorkerMessage};
use crate::util::check_server_url;
pub fn daemon_main(name: String, server: String) {
// Validate the `server`
check_server_url(&server);
info!("Loading config...");
let config = match load_config(&name) {
Ok(cfg) => cfg,
Err(e) => {
error!("Error loading configuration: {}", e);
std::process::exit(1);
}
};
info!("Creating transmitter");
let (tx_api, rx_api) = mpsc::channel::<APIWorkerMessage>();
let (tx_socket, rx_socket) = mpsc::channel::<SocketWorkerMessage>();
let (tx_nebula, rx_nebula) = mpsc::channel::<NebulaWorkerMessage>();
let (tx_timer, rx_timer) = mpsc::channel::<TimerWorkerMessage>();
let transmitter = ThreadMessageSender {
socket_thread: tx_socket,
api_thread: tx_api,
nebula_thread: tx_nebula,
timer_thread: tx_timer,
};
let mainthread_transmitter = transmitter.clone();
info!("Setting signal trap...");
match ctrlc::set_handler(move || {
info!("Ctrl-C detected. Stopping threads...");
match mainthread_transmitter.nebula_thread.send(NebulaWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to nebula worker thread: {}", e);
}
}
match mainthread_transmitter.api_thread.send(APIWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to api worker thread: {}", e);
}
}
match mainthread_transmitter.socket_thread.send(SocketWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to socket worker thread: {}", e);
}
}
match mainthread_transmitter.timer_thread.send(TimerWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to timer worker thread: {}", e);
}
}
}) {
Ok(_) => (),
Err(e) => {
error!("Unable to set sigtrap: {}", e);
std::process::exit(1);
}
}
info!("Starting API thread...");
let config_api = config.clone();
let transmitter_api = transmitter.clone();
let name_api = name.clone();
let server_api = server.clone();
let api_thread = thread::spawn(move || {
apiworker_main(config_api, name_api, server_api,transmitter_api, rx_api);
});
info!("Starting Nebula thread...");
let config_nebula = config.clone();
let transmitter_nebula = transmitter.clone();
let name_nebula = name.clone();
let nebula_thread = thread::spawn(move || {
nebulaworker_main(config_nebula, name_nebula, transmitter_nebula, rx_nebula);
});
info!("Starting timer thread...");
let timer_transmitter = transmitter.clone();
let timer_thread = thread::spawn(move || {
timer_main(timer_transmitter, rx_timer);
});
info!("Starting socket worker thread...");
let name_socket = name.clone();
let socket_thread = thread::spawn(move || {
socketworker_main(config, name_socket, transmitter, rx_socket);
});
info!("Waiting for socket thread to exit...");
match socket_thread.join() {
Ok(_) => (),
Err(_) => {
error!("Error waiting for socket thread to exit.");
std::process::exit(1);
}
}
info!("Socket thread exited");
info!("Waiting for API thread to exit...");
match api_thread.join() {
Ok(_) => (),
Err(_) => {
error!("Error waiting for api thread to exit.");
std::process::exit(1);
}
}
info!("API thread exited");
info!("Waiting for timer thread to exit...");
match timer_thread.join() {
Ok(_) => (),
Err(_) => {
error!("Error waiting for timer thread to exit.");
std::process::exit(1);
}
}
info!("Timer thread exited");
info!("Waiting for Nebula thread to exit...");
match nebula_thread.join() {
Ok(_) => (),
Err(_) => {
error!("Error waiting for nebula thread to exit.");
std::process::exit(1);
}
}
info!("Nebula thread exited");
info!("All threads exited");
}
#[derive(Clone)]
pub struct ThreadMessageSender {
pub socket_thread: Sender<SocketWorkerMessage>,
pub api_thread: Sender<APIWorkerMessage>,
pub nebula_thread: Sender<NebulaWorkerMessage>,
pub timer_thread: Sender<TimerWorkerMessage>
}

25
tfclient/src/dirs.rs Normal file
View File

@ -0,0 +1,25 @@
use std::path::PathBuf;
pub fn get_data_dir() -> Option<PathBuf> {
dirs::data_dir().map(|f| f.join("tfclient/"))
}
pub fn get_config_dir(instance: &str) -> Option<PathBuf> {
dirs::config_dir().map(|f| f.join("tfclient/").join(format!("{}/", instance)))
}
pub fn get_config_file(instance: &str) -> Option<PathBuf> {
get_config_dir(instance).map(|f| f.join("tfclient.toml"))
}
pub fn get_cdata_dir(instance: &str) -> Option<PathBuf> {
dirs::config_dir().map(|f| f.join("tfclient_data/").join(format!("{}/", instance)))
}
pub fn get_cdata_file(instance: &str) -> Option<PathBuf> {
get_cdata_dir(instance).map(|f| f.join("tfclient.toml"))
}
pub fn get_nebulaconfig_file(instance: &str) -> Option<PathBuf> {
get_cdata_dir(instance).map(|f| f.join("nebula.sk_embedded.yml"))
}

View File

@ -0,0 +1,104 @@
use std::error::Error;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::process::{Child, Command};
use log::debug;
use crate::dirs::get_data_dir;
use crate::util::sha256;
pub fn extract_embedded_nebula() -> Result<PathBuf, Box<dyn Error>> {
let data_dir = get_data_dir().ok_or("Unable to get platform-specific data dir")?;
if !data_dir.exists() {
fs::create_dir_all(&data_dir)?;
debug!("Created data directory {}", data_dir.as_path().display());
}
let bin_dir = data_dir.join("cache/");
let hash_dir = bin_dir.join(format!("{}/", sha256(crate::nebula_bin::NEBULA_BIN)));
if !hash_dir.exists() {
fs::create_dir_all(&hash_dir)?;
debug!("Created directory {}", hash_dir.as_path().display());
}
let executable_postfix = if cfg!(windows) { ".exe" } else { "" };
let executable_name = format!("nebula-{}{}", crate::nebula_bin::NEBULA_VERSION, executable_postfix);
let file_path = hash_dir.join(executable_name);
if file_path.exists() {
// Already extracted
return Ok(file_path);
}
let mut file = File::create(&file_path)?;
file.write_all(crate::nebula_bin::NEBULA_BIN)?;
debug!("Extracted nebula to {}", file_path.as_path().display());
Ok(file_path)
}
pub fn extract_embedded_nebula_cert() -> Result<PathBuf, Box<dyn Error>> {
let data_dir = get_data_dir().ok_or("Unable to get platform-specific data dir")?;
if !data_dir.exists() {
fs::create_dir_all(&data_dir)?;
debug!("Created data directory {}", data_dir.as_path().display());
}
let bin_dir = data_dir.join("cache/");
let hash_dir = bin_dir.join(format!("{}/", sha256(crate::nebula_cert_bin::NEBULA_CERT_BIN)));
if !hash_dir.exists() {
fs::create_dir_all(&hash_dir)?;
debug!("Created directory {}", hash_dir.as_path().display());
}
let executable_postfix = if cfg!(windows) { ".exe" } else { "" };
let executable_name = format!("nebula-cert-{}{}", crate::nebula_cert_bin::NEBULA_CERT_VERSION, executable_postfix);
let file_path = hash_dir.join(executable_name);
if file_path.exists() {
// Already extracted
return Ok(file_path);
}
let mut file = File::create(&file_path)?;
file.write_all(crate::nebula_cert_bin::NEBULA_CERT_BIN)?;
debug!("Extracted nebula-cert to {}", file_path.as_path().display());
Ok(file_path)
}
#[cfg(unix)]
pub fn _setup_permissions(path: &PathBuf) -> Result<(), Box<dyn Error>> {
let meta = path.metadata()?;
let mut perms = meta.permissions();
perms.set_mode(0o0755);
debug!("Setting permissions on {} to 755", path.as_path().display());
fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(windows)]
pub fn _setup_permissions() -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn run_embedded_nebula(args: &[String]) -> Result<Child, Box<dyn Error>> {
let path = extract_embedded_nebula()?;
debug!("Running {} with args {:?}", path.as_path().display(), args);
_setup_permissions(&path)?;
Ok(Command::new(path).args(args).spawn()?)
}
pub fn run_embedded_nebula_cert(args: &[String]) -> Result<Child, Box<dyn Error>> {
let path = extract_embedded_nebula_cert()?;
debug!("Running {} with args {:?}", path.as_path().display(), args);
_setup_permissions(&path)?;
Ok(Command::new(path).args(args).spawn()?)
}

View File

@ -14,6 +14,243 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
fn main() { pub mod embedded_nebula;
println!("Hello, world!"); pub mod dirs;
pub mod util;
pub mod nebulaworker;
pub mod daemon;
pub mod config;
pub mod service;
pub mod apiworker;
pub mod socketworker;
pub mod socketclient;
pub mod timerworker;
pub mod nebula_bin {
include!(concat!(env!("OUT_DIR"), "/nebula.bin.rs"));
}
pub mod nebula_cert_bin {
include!(concat!(env!("OUT_DIR"), "/nebula_cert.bin.rs"));
}
use std::fs;
use clap::{Parser, ArgAction, Subcommand};
use log::{error, info};
use simple_logger::SimpleLogger;
use crate::config::load_config;
use crate::dirs::get_data_dir;
use crate::embedded_nebula::{run_embedded_nebula, run_embedded_nebula_cert};
use crate::service::entry::{cli_install, cli_start, cli_stop, cli_uninstall};
#[derive(Parser)]
#[command(author = "c0repwn3r", version, about, long_about = None)]
#[clap(disable_version_flag = true)]
struct Cli {
#[arg(short = 'v', long = "version", action = ArgAction::SetTrue)]
#[clap(global = true)]
/// Print the tfclient version, as well as the trifid-pki version and the version of the embedded nebula and nebula-cert binaries
version: bool,
#[command(subcommand)]
subcommand: Commands
}
#[derive(Subcommand)]
enum Commands {
/// Run the `nebula` binary. This is useful if you want to do debugging with tfclient's internal nebula.
RunNebula {
/// Arguments to pass to the `nebula` binary
#[clap(trailing_var_arg=true, allow_hyphen_values=true)]
args: Vec<String>
},
/// Run the `nebula-cert` binary. This is useful if you want to mess with certificates. Note: tfclient does not actually use nebula-cert for certificate operations, and instead uses trifid-pki internally
RunNebulaCert {
/// Arguments to pass to the `nebula-cert` binary
#[clap(trailing_var_arg=true, allow_hyphen_values=true)]
args: Vec<String>
},
/// Clear any cached data that tfclient may have added
ClearCache {},
/// Install the tfclient system service
Install {
#[clap(short, long, default_value = "tfclient")]
/// Optional service name used to run multiple tfclient instances. Specify the same name on all other cli sub-commands (start, uninstall, etc.) to refer to this installed instance
name: String,
#[clap(short, long)]
/// Server to use for API calls.
server: String
},
/// Uninstall the tfclient system service
Uninstall {
#[clap(short, long, default_value = "tfclient")]
/// Service name specified on install
name: String
},
/// Start the tfclient system service
Start {
#[clap(short, long, default_value = "tfclient")]
/// Service name specified on install
name: String
},
/// Stop the tfclient system service
Stop {
#[clap(short, long, default_value = "tfclient")]
/// Service name specified on start
name: String
},
/// Run the tfclient daemon in the foreground
Run {
#[clap(short, long, default_value = "tfclient")]
/// Service name specified on install
name: String,
#[clap(short, long)]
/// Server to use for API calls.
server: String
},
/// Enroll this host using a trifid-api enrollment code
Enroll {
#[clap(short, long, default_value = "tfclient")]
/// Service name specified on install
name: String,
#[clap(short, long)]
/// Enrollment code used to enroll this node
code: String,
}
}
fn main() {
SimpleLogger::new().init().unwrap();
let args = Cli::parse();
if args.version {
print_version();
}
match args.subcommand {
Commands::RunNebula { args } => {
match run_embedded_nebula(&args) {
Ok(mut c) => {
match c.wait() {
Ok(stat) => {
match stat.code() {
Some(code) => {
if code != 0 {
error!("Nebula process exited with nonzero status code {}", code);
}
std::process::exit(code);
},
None => {
info!("Nebula process terminated by signal");
std::process::exit(0);
}
}
},
Err(e) => {
error!("Unable to wait for child to exit: {}", e);
std::process::exit(1);
}
}
},
Err(e) => {
error!("Unable to start nebula binary: {}", e);
std::process::exit(1);
}
}
},
Commands::ClearCache { .. } => {
let data_dir = match get_data_dir() {
Some(dir) => dir,
None => {
error!("Unable to get platform-specific data dir");
std::process::exit(1);
}
};
match fs::remove_dir_all(&data_dir) {
Ok(_) => (),
Err(e) => {
error!("Unable to delete data dir: {}", e);
std::process::exit(0);
}
}
info!("Removed data dir {}", data_dir.as_path().display());
info!("Removed all cached data.");
std::process::exit(0);
},
Commands::RunNebulaCert { args } => {
match run_embedded_nebula_cert(&args) {
Ok(mut c) => {
match c.wait() {
Ok(stat) => {
match stat.code() {
Some(code) => {
if code != 0 {
error!("nebula-cert process exited with nonzero status code {}", code);
}
std::process::exit(code);
},
None => {
info!("nebula-cert process terminated by signal");
std::process::exit(0);
}
}
},
Err(e) => {
error!("Unable to wait for child to exit: {}", e);
std::process::exit(1);
}
}
},
Err(e) => {
error!("Unable to start nebula-cert binary: {}", e);
std::process::exit(1);
}
}
}
Commands::Install { server, name } => {
cli_install(&name, &server);
}
Commands::Uninstall { name } => {
cli_uninstall(&name);
}
Commands::Start { name } => {
cli_start(&name);
}
Commands::Stop { name } => {
cli_stop(&name);
}
Commands::Run { name, server } => {
daemon::daemon_main(name, server);
}
Commands::Enroll { name, code } => {
info!("Loading config...");
let config = match load_config(&name) {
Ok(cfg) => cfg,
Err(e) => {
error!("Error loading configuration: {}", e);
std::process::exit(1);
}
};
match socketclient::enroll(&code, &config) {
Ok(_) => (),
Err(e) => {
error!("Error sending enrollment request: {}", e);
std::process::exit(1);
}
};
}
}
}
fn print_version() {
println!("tfclient v{} linked to trifid-pki v{}, embedding nebula v{} and nebula-cert v{}", env!("CARGO_PKG_VERSION"), trifid_pki::TRIFID_PKI_VERSION, crate::nebula_bin::NEBULA_VERSION, crate::nebula_cert_bin::NEBULA_CERT_VERSION);
} }

View File

@ -0,0 +1,153 @@
// Code to handle the nebula worker
use std::error::Error;
use std::fs;
use std::sync::mpsc::{Receiver, TryRecvError};
use std::time::{Duration, SystemTime};
use log::{debug, error, info};
use crate::config::{load_cdata, NebulaConfig, TFClientConfig};
use crate::daemon::ThreadMessageSender;
use crate::dirs::get_nebulaconfig_file;
use crate::embedded_nebula::run_embedded_nebula;
pub enum NebulaWorkerMessage {
Shutdown,
ConfigUpdated
}
fn insert_private_key(instance: &str) -> Result<(), Box<dyn Error>> {
if !get_nebulaconfig_file(instance).ok_or("Could not get config file location")?.exists() {
return Ok(()); // cant insert private key into a file that does not exist - BUT. we can gracefully handle nebula crashing - we cannot gracefully handle this fn failing
}
let cdata = load_cdata(instance)?;
let key = cdata.dh_privkey.ok_or("Missing private key")?;
let config_str = fs::read_to_string(get_nebulaconfig_file(instance).ok_or("Could not get config file location")?)?;
let mut config: NebulaConfig = serde_yaml::from_str(&config_str)?;
config.pki.key = Some(String::from_utf8(key)?);
debug!("inserted private key into config: {:?}", config);
let config_str = serde_yaml::to_string(&config)?;
fs::write(get_nebulaconfig_file(instance).ok_or("Could not get config file location")?, config_str)?;
Ok(())
}
pub fn nebulaworker_main(_config: TFClientConfig, instance: String, _transmitter: ThreadMessageSender, rx: Receiver<NebulaWorkerMessage>) {
let _cdata = match load_cdata(&instance) {
Ok(data) => data,
Err(e) => {
error!("unable to load cdata: {}", e);
error!("nebula thread exiting with error");
return;
}
};
info!("fixing config...");
match insert_private_key(&instance) {
Ok(_) => {
info!("config fixed (private-key embedded)");
},
Err(e) => {
error!("unable to fix config: {}", e);
error!("nebula thread exiting with error");
return;
}
}
info!("starting nebula child...");
let mut child = match run_embedded_nebula(&["-config".to_string(), get_nebulaconfig_file(&instance).unwrap().to_str().unwrap().to_string()]) {
Ok(c) => c,
Err(e) => {
error!("unable to start embedded nebula binary: {}", e);
error!("nebula thread exiting with error");
return;
}
};
info!("nebula process started");
let mut last_restart_time = SystemTime::now();
// dont need to save it, because we do not, in any circumstance, write to it
loop {
if let Ok(e) = child.try_wait() {
if e.is_some() && SystemTime::now() > last_restart_time + Duration::from_secs(5) {
info!("nebula process has exited, restarting");
child = match run_embedded_nebula(&["-config".to_string(), get_nebulaconfig_file(&instance).unwrap().to_str().unwrap().to_string()]) {
Ok(c) => c,
Err(e) => {
error!("unable to start embedded nebula binary: {}", e);
error!("nebula thread exiting with error");
return;
}
};
info!("nebula process started");
last_restart_time = SystemTime::now();
}
}
match rx.try_recv() {
Ok(msg) => {
match msg {
NebulaWorkerMessage::Shutdown => {
info!("recv on command socket: shutdown, stopping");
info!("shutting down nebula binary");
match child.kill() {
Ok(_) => {
debug!("nebula process exited");
},
Err(e) => {
error!("nebula process already exited: {}", e);
}
}
info!("nebula shut down");
break;
},
NebulaWorkerMessage::ConfigUpdated => {
info!("our configuration has been updated - restarting");
debug!("killing existing process");
match child.kill() {
Ok(_) => {
debug!("nebula process exited");
},
Err(e) => {
error!("nebula process already exited: {}", e);
}
}
debug!("fixing config...");
match insert_private_key(&instance) {
Ok(_) => {
debug!("config fixed (private-key embedded)");
},
Err(e) => {
error!("unable to fix config: {}", e);
error!("nebula thread exiting with error");
return;
}
}
debug!("restarting nebula process");
child = match run_embedded_nebula(&["-config".to_string(), get_nebulaconfig_file(&instance).unwrap().to_str().unwrap().to_string()]) {
Ok(c) => c,
Err(e) => {
error!("unable to start embedded nebula binary: {}", e);
error!("nebula thread exiting with error");
return;
}
};
last_restart_time = SystemTime::now();
debug!("nebula process restarted");
}
}
},
Err(e) => {
match e {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
error!("nebulaworker command socket disconnected, shutting down to prevent orphaning");
break;
}
}
}
}
}
}

View File

@ -0,0 +1,9 @@
pub mod systemd;
use std::error::Error;
use std::path::PathBuf;
pub trait ServiceFileGenerator {
fn create_service_files(bin_path: PathBuf, name: &str, server: &str) -> Result<(), Box<dyn Error>>;
fn delete_service_files(name: &str) -> Result<(), Box<dyn Error>>;
}

View File

@ -0,0 +1,48 @@
use std::error::Error;
use std::path::PathBuf;
use log::debug;
use crate::service::codegen::ServiceFileGenerator;
use std::fmt::Write;
use std::fs;
pub struct SystemDServiceFileGenerator {}
impl ServiceFileGenerator for SystemDServiceFileGenerator {
fn create_service_files(bin_path: PathBuf, name: &str, server: &str) -> Result<(), Box<dyn Error>> {
debug!("Generating a unit file...");
let mut unit_file = String::new();
writeln!(unit_file, "[Unit]")?;
writeln!(unit_file, "Description=A client for Defined Networking compatible overlay networks (instance {})", name)?;
writeln!(unit_file, "Wants=basic.target network-online.target")?;
writeln!(unit_file, "After=basic.target network.target network-online.target")?;
writeln!(unit_file)?;
writeln!(unit_file, "[Service]")?;
writeln!(unit_file, "SyslogIdentifier=tfclient-{}", name)?;
writeln!(unit_file, "ExecStart={} run --server {} --name {}", bin_path.as_path().display(), server, name)?;
writeln!(unit_file, "Restart=always")?;
writeln!(unit_file)?;
writeln!(unit_file, "[Install]")?;
writeln!(unit_file, "WantedBy=multi-user.target")?;
fs::write(format!("/usr/lib/systemd/system/{}.service", SystemDServiceFileGenerator::get_service_file_name(name)), unit_file)?;
debug!("Installed unit file");
Ok(())
}
fn delete_service_files(name: &str) -> Result<(), Box<dyn Error>> {
debug!("Deleting unit file...");
fs::remove_file(format!("/usr/lib/systemd/system/{}.service", SystemDServiceFileGenerator::get_service_file_name(name)))?;
debug!("Removed unit file");
Ok(())
}
}
impl SystemDServiceFileGenerator {
pub fn get_service_file_name(name: &str) -> String {
format!("tfclient_i-{}", name)
}
}

View File

@ -0,0 +1,29 @@
use std::path::Path;
use log::info;
use crate::service::macos::OSXServiceManager;
use crate::service::runit::RunitServiceManager;
use crate::service::ServiceManager;
use crate::service::systemd::SystemDServiceManager;
use crate::service::windows::WindowsServiceManager;
pub fn detect_service() -> Option<Box<dyn ServiceManager>> {
if cfg!(windows) {
return Some(Box::new(WindowsServiceManager {}));
}
if cfg!(macos) {
return Some(Box::new(OSXServiceManager {}));
}
detect_unix_service_manager()
}
pub fn detect_unix_service_manager() -> Option<Box<dyn ServiceManager>> {
if Path::new("/etc/runit/1").exists() {
info!("Detected Runit service supervision (confidence: 100%, /etc/runit/1 exists)");
return Some(Box::new(RunitServiceManager {}))
}
if Path::new("/var/lib/systemd").exists() {
info!("Detected SystemD service supervision (confidence: 100%, /var/lib/systemd exists)");
return Some(Box::new(SystemDServiceManager {}));
}
None
}

View File

@ -0,0 +1,82 @@
use std::env::current_exe;
use log::{error, info};
use crate::service::detect::detect_service;
use crate::util::check_server_url;
pub fn cli_start(name: &str) {
info!("Detecting service manager...");
let service = detect_service();
if let Some(sm) = service {
match sm.start(name) {
Ok(_) => (),
Err(e) => {
error!("Error starting service: {}", e);
std::process::exit(1);
}
}
} else {
error!("Unable to determine which service manager to use. Could not start.");
std::process::exit(1);
}
}
pub fn cli_stop(name: &str) {
info!("Detecting service manager...");
let service = detect_service();
if let Some(sm) = service {
match sm.stop(name) {
Ok(_) => (),
Err(e) => {
error!("Error starting service: {}", e);
std::process::exit(1);
}
}
} else {
error!("Unable to determine which service manager to use. Could not stop.");
std::process::exit(1);
}
}
pub fn cli_install(name: &str, server: &str) {
info!("Checking server url...");
check_server_url(server);
info!("Detecting service manager...");
let service = detect_service();
if let Some(sm) = service {
let current_file = match current_exe() {
Ok(e) => e,
Err(e) => {
error!("Unable to get current binary: {}", e);
std::process::exit(1);
}
};
match sm.install(current_file, name, server) {
Ok(_) => (),
Err(e) => {
error!("Error creating service files: {}", e);
std::process::exit(1);
}
}
} else {
error!("Unable to determine which service manager to use. Could not install.");
std::process::exit(1);
}
}
pub fn cli_uninstall(name: &str) {
info!("Detecting service manager...");
let service = detect_service();
if let Some(sm) = service {
match sm.uninstall(name) {
Ok(_) => (),
Err(e) => {
error!("Error removing service files: {}", e);
std::process::exit(1);
}
}
} else {
error!("Unable to determine which service manager to use. Could not install.");
std::process::exit(1);
}
}

View File

@ -0,0 +1,22 @@
use std::error::Error;
use std::path::PathBuf;
use crate::service::ServiceManager;
pub struct OSXServiceManager {}
impl ServiceManager for OSXServiceManager {
fn install(&self, _bin_path: PathBuf, _name: &str, _server_url: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn uninstall(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn start(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn stop(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
}

View File

@ -0,0 +1,17 @@
use std::error::Error;
use std::path::PathBuf;
pub mod codegen;
pub mod systemd;
pub mod detect;
pub mod entry;
pub mod windows;
pub mod macos;
pub mod runit;
pub trait ServiceManager {
fn install(&self, bin_path: PathBuf, name: &str, server_url: &str) -> Result<(), Box<dyn Error>>;
fn uninstall(&self, name: &str) -> Result<(), Box<dyn Error>>;
fn start(&self, name: &str) -> Result<(), Box<dyn Error>>;
fn stop(&self, name: &str) -> Result<(), Box<dyn Error>>;
}

View File

@ -0,0 +1,22 @@
use std::error::Error;
use std::path::PathBuf;
use crate::service::ServiceManager;
pub struct RunitServiceManager {}
impl ServiceManager for RunitServiceManager {
fn install(&self, _bin_path: PathBuf, _name: &str, _server_url: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn uninstall(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn start(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn stop(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
}

View File

@ -0,0 +1,86 @@
use std::error::Error;
use std::path::PathBuf;
use std::process::Command;
use log::{error, info};
use crate::service::codegen::ServiceFileGenerator;
use crate::service::codegen::systemd::SystemDServiceFileGenerator;
use crate::service::ServiceManager;
pub struct SystemDServiceManager {}
impl ServiceManager for SystemDServiceManager {
fn install(&self, bin_path: PathBuf, name: &str, server_url: &str) -> Result<(), Box<dyn Error>> {
info!("Installing for SystemD");
SystemDServiceFileGenerator::create_service_files(bin_path, name, server_url)?;
info!("Enabling the SystemD service");
let out = Command::new("systemctl").args(["enable", &SystemDServiceFileGenerator::get_service_file_name(name)]).output()?;
if !out.status.success() {
error!("Error enabling the SystemD service (command exited with non-zero exit code)");
error!("stdout:");
error!("{}", String::from_utf8(out.stdout)?);
error!("stderr:");
error!("{}", String::from_utf8(out.stderr)?);
return Err("Command exited with non-zero exit code".into());
}
info!("Installation successful");
Ok(())
}
fn uninstall(&self, name: &str) -> Result<(), Box<dyn Error>> {
info!("Uninstalling SystemD service files");
info!("Disabling the SystemD service");
let out = Command::new("systemctl").args(["disable", &SystemDServiceFileGenerator::get_service_file_name(name)]).output()?;
if !out.status.success() {
error!("Error disabling the SystemD service (command exited with non-zero exit code)");
error!("stdout:");
error!("{}", String::from_utf8(out.stdout)?);
error!("stderr:");
error!("{}", String::from_utf8(out.stderr)?);
return Err("Command exited with non-zero exit code".into());
}
info!("Removing the service files");
SystemDServiceFileGenerator::delete_service_files(name)?;
Ok(())
}
fn start(&self, name: &str) -> Result<(), Box<dyn Error>> {
info!("Starting the SystemD service");
let out = Command::new("systemctl").args(["start", &SystemDServiceFileGenerator::get_service_file_name(name)]).output()?;
if !out.status.success() {
error!("Error starting the SystemD service (command exited with non-zero exit code)");
error!("stdout:");
error!("{}", String::from_utf8(out.stdout)?);
error!("stderr:");
error!("{}", String::from_utf8(out.stderr)?);
return Err("Command exited with non-zero exit code".into());
}
Ok(())
}
fn stop(&self, name: &str) -> Result<(), Box<dyn Error>> {
info!("Stopping the SystemD service");
let out = Command::new("systemctl").args(["stop", &SystemDServiceFileGenerator::get_service_file_name(name)]).output()?;
if !out.status.success() {
error!("Error stopping the SystemD service (command exited with non-zero exit code)");
error!("stdout:");
error!("{}", String::from_utf8(out.stdout)?);
error!("stderr:");
error!("{}", String::from_utf8(out.stderr)?);
return Err("Command exited with non-zero exit code".into());
}
Ok(())
}
}

View File

@ -0,0 +1,22 @@
use std::error::Error;
use std::path::PathBuf;
use crate::service::ServiceManager;
pub struct WindowsServiceManager {}
impl ServiceManager for WindowsServiceManager {
fn install(&self, _bin_path: PathBuf, _name: &str, _server_url: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn uninstall(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn start(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
fn stop(&self, _name: &str) -> Result<(), Box<dyn Error>> {
todo!()
}
}

View File

@ -0,0 +1,58 @@
use std::error::Error;
use std::io::{BufRead, BufReader, Write};
use std::net::{IpAddr, SocketAddr, TcpStream};
use log::{error, info};
use crate::config::TFClientConfig;
use crate::socketworker::{ctob, DisconnectReason, JSON_API_VERSION, JsonMessage};
pub fn enroll(code: &str, config: &TFClientConfig) -> Result<(), Box<dyn Error>> {
info!("Connecting to local command socket...");
let mut stream = TcpStream::connect(SocketAddr::new(IpAddr::from([127, 0, 0, 1]), config.listen_port))?;
let stream2 = stream.try_clone()?;
let mut reader = BufReader::new(&stream2);
info!("Sending Hello...");
stream.write_all(&ctob(JsonMessage::Hello {
version: JSON_API_VERSION,
}))?;
info!("Waiting for hello...");
let msg = read_msg(&mut reader)?;
match msg {
JsonMessage::Hello { .. } => {
info!("Server sent hello, connection established")
}
JsonMessage::Goodbye { reason } => {
error!("Disconnected by server. Reason: {:?}", reason);
return Err("Disconnected by server".into());
}
_ => {
error!("Server returned unexpected message: {:?}", msg);
error!("Sending goodbye and exiting");
stream.write_all(&ctob(JsonMessage::Goodbye {
reason: DisconnectReason::UnexpectedMessageType,
}))?;
return Err("Unexpected message type by server".into());
}
}
info!("Sending enroll request...");
stream.write_all(&ctob(JsonMessage::Enroll {
code: code.to_string(),
}))?;
info!("Sending disconnect...");
stream.write_all(&ctob(JsonMessage::Goodbye {
reason: DisconnectReason::Done,
}))?;
info!("Sent enroll request to tfclient daemon. Check logs to see if the enrollment was successful.");
Ok(())
}
fn read_msg(reader: &mut BufReader<&TcpStream>) -> Result<JsonMessage, Box<dyn Error>> {
let mut str = String::new();
reader.read_line(&mut str)?;
let msg: JsonMessage = serde_json::from_str(&str)?;
Ok(msg)
}

View File

@ -0,0 +1,305 @@
// Code to handle the nebula worker
use std::error::Error;
use std::{io, thread};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::net::{IpAddr, Shutdown, SocketAddr, TcpListener, TcpStream};
use std::sync::mpsc::{Receiver, TryRecvError};
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};
use crate::apiworker::APIWorkerMessage;
use crate::config::{load_cdata, TFClientConfig};
use crate::daemon::ThreadMessageSender;
use crate::nebulaworker::NebulaWorkerMessage;
use crate::timerworker::TimerWorkerMessage;
pub enum SocketWorkerMessage {
Shutdown
}
pub fn socketworker_main(config: TFClientConfig, instance: String, transmitter: ThreadMessageSender, rx: Receiver<SocketWorkerMessage>) {
info!("socketworker_main called, entering realmain");
match _main(config, instance, transmitter, rx) {
Ok(_) => (),
Err(e) => {
error!("Error in socket thread: {}", e);
}
};
}
fn _main(config: TFClientConfig, instance: String, transmitter: ThreadMessageSender, rx: Receiver<SocketWorkerMessage>) -> Result<(), Box<dyn Error>> {
let listener = TcpListener::bind(SocketAddr::new(IpAddr::from([127, 0, 0, 1]), config.listen_port))?;
listener.set_nonblocking(true)?;
loop {
match listener.accept() {
Ok(stream) => {
let transmitter_clone = transmitter.clone();
let config_clone = config.clone();
let instance_clone = instance.clone();
thread::spawn(|| {
match handle_stream(stream, transmitter_clone, config_clone, instance_clone) {
Ok(_) => (),
Err(e) => {
error!("Error in client thread: {}", e);
}
}
});
},
Err(e) if e.kind() == io::ErrorKind::WouldBlock => (),
Err(e) => { Err(e)?; }
}
match rx.try_recv() {
Ok(msg) => {
match msg {
SocketWorkerMessage::Shutdown => {
info!("recv on command socket: shutdown, stopping");
break;
}
}
},
Err(e) => {
match e {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
error!("socketworker command socket disconnected, shutting down to prevent orphaning");
break;
}
}
}
}
}
Ok(())
}
fn handle_stream(stream: (TcpStream, SocketAddr), transmitter: ThreadMessageSender, config: TFClientConfig, instance: String) -> Result<(), io::Error> {
info!("Incoming client");
match handle_client(stream.0, transmitter, config, instance) {
Ok(()) => (),
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
warn!("Client timed out, connection aborted");
},
Err(e) if e.kind() == io::ErrorKind::NotConnected => {
warn!("Client connection severed");
},
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
warn!("Client connection returned error: broken pipe");
},
Err(e) if e.kind() == io::ErrorKind::ConnectionAborted => {
warn!("Client aborted connection");
},
Err(e) => {
error!("Error in client handler: {}", e);
return Err(e);
}
};
Ok(())
}
fn handle_client(stream: TcpStream, transmitter: ThreadMessageSender, config: TFClientConfig, instance: String) -> Result<(), io::Error> {
info!("Handling connection from {}", stream.peer_addr()?);
let mut client = Client {
state: ClientState::WaitHello,
reader: BufReader::new(&stream),
writer: BufWriter::new(&stream),
stream: &stream,
config,
instance,
};
loop {
let mut command = String::new();
client.reader.read_line(&mut command)?;
let command: JsonMessage = serde_json::from_str(&command)?;
trace!("recv {:?} from {}", command, client.stream.peer_addr()?);
let should_disconnect;
match client.state {
ClientState::WaitHello => {
should_disconnect = waithello_handle(&mut client, &transmitter, command)?;
}
ClientState::SentHello => {
should_disconnect = senthello_handle(&mut client, &transmitter, command)?;
}
}
if should_disconnect { break; }
}
// Gracefully close the connection
stream.shutdown(Shutdown::Both)?;
Ok(())
}
struct Client<'a> {
state: ClientState,
reader: BufReader<&'a TcpStream>,
writer: BufWriter<&'a TcpStream>,
stream: &'a TcpStream,
config: TFClientConfig,
instance: String
}
fn waithello_handle(client: &mut Client, _transmitter: &ThreadMessageSender, command: JsonMessage) -> Result<bool, io::Error> {
trace!("state: WaitHello, handing with waithello_handle");
let mut should_disconnect = false;
match command {
JsonMessage::Hello { version } => {
if version != JSON_API_VERSION {
should_disconnect = true;
client.stream.write_all(&ctob(JsonMessage::Goodbye {
reason: DisconnectReason::UnsupportedVersion {
expected: JSON_API_VERSION,
got: version
}
}))?;
}
client.stream.write_all(&ctob(JsonMessage::Hello {
version: JSON_API_VERSION
}))?;
client.state = ClientState::SentHello;
trace!("setting state to SentHello");
},
JsonMessage::Goodbye { reason } => {
info!("Client sent disconnect: {:?}", reason);
should_disconnect = true;
},
_ => {
debug!("message type unexpected in WaitHello state");
should_disconnect = true;
client.stream.write_all(&ctob(JsonMessage::Goodbye {
reason: DisconnectReason::UnexpectedMessageType,
}))?;
}
}
Ok(should_disconnect)
}
fn senthello_handle(client: &mut Client, transmitter: &ThreadMessageSender, command: JsonMessage) -> Result<bool, io::Error> {
trace!("state: SentHello, handing with senthello_handle");
let mut should_disconnect = false;
match command {
JsonMessage::Goodbye { reason } => {
info!("Client sent disconnect: {:?}", reason);
should_disconnect = true;
},
JsonMessage::Shutdown {} => {
info!("Requested to shutdown by local control socket. Sending shutdown message to threads");
match transmitter.nebula_thread.send(NebulaWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to nebula worker thread: {}", e);
}
}
match transmitter.api_thread.send(APIWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to api worker thread: {}", e);
}
}
match transmitter.socket_thread.send(SocketWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to socket worker thread: {}", e);
}
}
match transmitter.timer_thread.send(TimerWorkerMessage::Shutdown) {
Ok(_) => (),
Err(e) => {
error!("Error sending shutdown message to timer worker thread: {}", e);
}
}
},
JsonMessage::GetHostID {} => {
let data = match load_cdata(&client.instance) {
Ok(d) => d,
Err(e) => {
error!("Error loading cdata: {}", e);
panic!("{}", e); // TODO: Find a better way of handling this
}
};
client.stream.write_all(&ctob(JsonMessage::HostID {
has_id: data.creds.is_some(),
id: data.creds.map(|c| c.host_id)
}))?;
},
JsonMessage::Enroll { code } => {
info!("Client sent enroll with code {}", code);
info!("Sending enroll request to apiworker");
transmitter.api_thread.send(APIWorkerMessage::Enroll { code }).unwrap();
}
_ => {
debug!("message type unexpected in SentHello state");
should_disconnect = true;
client.stream.write_all(&ctob(JsonMessage::Goodbye {
reason: DisconnectReason::UnexpectedMessageType,
}))?;
}
}
Ok(should_disconnect)
}
pub fn ctob(command: JsonMessage) -> Vec<u8> {
let command_str = serde_json::to_string(&command).unwrap() + "\n";
command_str.into_bytes()
}
enum ClientState {
WaitHello,
SentHello
}
pub const JSON_API_VERSION: i32 = 1;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "method")]
pub enum JsonMessage {
#[serde(rename = "hello")]
Hello {
version: i32
},
#[serde(rename = "goodbye")]
Goodbye {
reason: DisconnectReason
},
#[serde(rename = "shutdown")]
Shutdown {},
#[serde(rename = "get_host_id")]
GetHostID {},
#[serde(rename = "host_id")]
HostID {
has_id: bool,
id: Option<String>
},
#[serde(rename = "enroll")]
Enroll {
code: String
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum DisconnectReason {
#[serde(rename = "unsupported_version")]
UnsupportedVersion { expected: i32, got: i32 },
#[serde(rename = "unexpected_message_type")]
UnexpectedMessageType,
#[serde(rename = "done")]
Done
}

View File

@ -0,0 +1,47 @@
use std::ops::Add;
use std::sync::mpsc::{Receiver, TryRecvError};
use std::time::{Duration, SystemTime};
use log::{error, info};
use crate::apiworker::APIWorkerMessage;
use crate::daemon::ThreadMessageSender;
pub enum TimerWorkerMessage {
Shutdown
}
pub fn timer_main(tx: ThreadMessageSender, rx: Receiver<TimerWorkerMessage>) {
let mut api_reload_timer = SystemTime::now().add(Duration::from_secs(60));
loop {
match rx.try_recv() {
Ok(msg) => {
match msg {
TimerWorkerMessage::Shutdown => {
info!("recv on command socket: shutdown, stopping");
break;
}
}
},
Err(e) => {
match e {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
error!("timerworker command socket disconnected, shutting down to prevent orphaning");
break;
}
}
}
}
if SystemTime::now().gt(&api_reload_timer) {
info!("Timer triggered: API_RELOAD_TIMER");
api_reload_timer = SystemTime::now().add(Duration::from_secs(60));
match tx.api_thread.send(APIWorkerMessage::Timer) {
Ok(_) => (),
Err(e) => {
error!("Error sending timer message to api worker thread: {}", e);
}
}
}
}
}

29
tfclient/src/util.rs Normal file
View File

@ -0,0 +1,29 @@
use log::{error, warn};
use sha2::Sha256;
use sha2::Digest;
use url::Url;
pub fn sha256(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
hex::encode(digest)
}
pub fn check_server_url(server: &str) {
let api_base = match Url::parse(&server) {
Ok(u) => u,
Err(e) => {
error!("Invalid server url `{}`: {}", server, e);
std::process::exit(1);
}
};
match api_base.scheme() {
"http" => { warn!("HTTP api urls are not reccomended. Please switch to HTTPS if possible.") },
"https" => (),
_ => {
error!("Unsupported protocol `{}` (expected one of http, https)", api_base.scheme());
std::process::exit(1);
}
}
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "trifid-pki" name = "trifid-pki"
version = "0.1.4" version = "0.1.9"
edition = "2021" edition = "2021"
description = "A rust implementation of the Nebula PKI system" description = "A rust implementation of the Nebula PKI system"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
@ -20,3 +20,8 @@ hex = "0.4.3"
sha2 = "0.10.6" sha2 = "0.10.6"
rand_core = "0.6.4" rand_core = "0.6.4"
rand = "0.8.5" rand = "0.8.5"
serde = { version = "1", features = ["derive"], optional = true }
[features]
default = []
serde_derive = ["serde", "ipnet/serde", "x25519-dalek/serde", "ed25519-dalek/serde"]

View File

@ -7,9 +7,13 @@ use std::time::SystemTime;
use ed25519_dalek::VerifyingKey; use ed25519_dalek::VerifyingKey;
use crate::cert::{deserialize_nebula_certificate_from_pem, NebulaCertificate}; use crate::cert::{deserialize_nebula_certificate_from_pem, NebulaCertificate};
#[cfg(feature = "serde_derive")]
use serde::{Serialize, Deserialize};
/// A pool of trusted CA certificates, and certificates that should be blocked. /// A pool of trusted CA certificates, and certificates that should be blocked.
/// This is equivalent to the `pki` section in a typical Nebula config.yml. /// This is equivalent to the `pki` section in a typical Nebula config.yml.
#[derive(Default)] #[derive(Default, Clone)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub struct NebulaCAPool { pub struct NebulaCAPool {
/// The list of CA root certificates that should be trusted. /// The list of CA root certificates that should be trusted.
pub cas: HashMap<String, NebulaCertificate>, pub cas: HashMap<String, NebulaCertificate>,
@ -102,8 +106,9 @@ impl NebulaCAPool {
} }
} }
#[derive(Debug)]
/// A list of errors that can happen when working with a CA Pool /// A list of errors that can happen when working with a CA Pool
#[derive(Debug)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub enum CaPoolError { pub enum CaPoolError {
/// Tried to add a non-CA cert to the CA pool /// Tried to add a non-CA cert to the CA pool
NotACA, NotACA,

View File

@ -15,6 +15,9 @@ use crate::ca::NebulaCAPool;
use crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails}; use crate::cert_codec::{RawNebulaCertificate, RawNebulaCertificateDetails};
use sha2::Digest; use sha2::Digest;
#[cfg(feature = "serde_derive")]
use serde::{Serialize, Deserialize};
/// The length, in bytes, of public keys /// The length, in bytes, of public keys
pub const PUBLIC_KEY_LENGTH: i32 = 32; pub const PUBLIC_KEY_LENGTH: i32 = 32;
@ -31,6 +34,7 @@ pub const ED25519_PUBLIC_KEY_BANNER: &str = "NEBULA ED25519 PUBLIC KEY";
/// A Nebula PKI certificate /// A Nebula PKI certificate
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub struct NebulaCertificate { pub struct NebulaCertificate {
/// The signed data of this certificate /// The signed data of this certificate
pub details: NebulaCertificateDetails, pub details: NebulaCertificateDetails,
@ -40,6 +44,7 @@ pub struct NebulaCertificate {
/// The signed details contained in a Nebula PKI certificate /// The signed details contained in a Nebula PKI certificate
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub struct NebulaCertificateDetails { pub struct NebulaCertificateDetails {
/// The name of the identity this certificate was issued for /// The name of the identity this certificate was issued for
pub name: String, pub name: String,
@ -63,6 +68,7 @@ pub struct NebulaCertificateDetails {
/// A list of errors that can occur parsing certificates /// A list of errors that can occur parsing certificates
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub enum CertificateError { pub enum CertificateError {
/// Attempted to deserialize a certificate from an empty byte array /// Attempted to deserialize a certificate from an empty byte array
EmptyByteArray, EmptyByteArray,
@ -186,6 +192,7 @@ pub fn deserialize_nebula_certificate(bytes: &[u8]) -> Result<NebulaCertificate,
/// A list of errors that can occur parsing keys /// A list of errors that can occur parsing keys
#[derive(Debug)] #[derive(Debug)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub enum KeyError { pub enum KeyError {
/// Keys should have their associated PEM tags but this had the wrong one /// Keys should have their associated PEM tags but this had the wrong one
WrongPemTag, WrongPemTag,
@ -300,12 +307,32 @@ pub fn deserialize_ed25519_public(bytes: &[u8]) -> Result<Vec<u8>, Box<dyn Error
if pem.tag != ED25519_PUBLIC_KEY_BANNER { if pem.tag != ED25519_PUBLIC_KEY_BANNER {
return Err(KeyError::WrongPemTag.into()) return Err(KeyError::WrongPemTag.into())
} }
if pem.contents.len() != 64 { if pem.contents.len() != 32 {
return Err(KeyError::Not64Bytes.into()) return Err(KeyError::Not32Bytes.into())
} }
Ok(pem.contents) Ok(pem.contents)
} }
/// Attempt to deserialize multiple PEM encoded Ed25519 public keys
/// # Errors
/// This function will return an error if the PEM data is invalid or has the wrong tag
pub fn deserialize_ed25519_public_many(bytes: &[u8]) -> Result<Vec<Vec<u8>>, Box<dyn Error>> {
let mut keys = vec![];
let pems = pem::parse_many(bytes)?;
for pem in pems {
if pem.tag != ED25519_PUBLIC_KEY_BANNER {
return Err(KeyError::WrongPemTag.into())
}
if pem.contents.len() != 32 {
return Err(KeyError::Not32Bytes.into())
}
keys.push(pem.contents);
}
Ok(keys)
}
impl NebulaCertificate { impl NebulaCertificate {
/// Sign a nebula certificate with the provided private key /// Sign a nebula certificate with the provided private key
/// # Errors /// # Errors
@ -520,6 +547,7 @@ impl NebulaCertificate {
/// A list of possible errors that can happen validating a certificate /// A list of possible errors that can happen validating a certificate
#[derive(Eq, PartialEq, Debug)] #[derive(Eq, PartialEq, Debug)]
#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))]
pub enum CertificateValidity { pub enum CertificateValidity {
/// There are no issues with this certificate /// There are no issues with this certificate
Ok, Ok,

View File

@ -58,3 +58,6 @@ pub(crate) mod cert_codec;
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
pub mod test; pub mod test;
/// Get the compiled version of trifid-pki.
pub const TRIFID_PKI_VERSION: &str = env!("CARGO_PKG_VERSION");

View File

@ -6,7 +6,7 @@ use std::net::Ipv4Addr;
use std::ops::{Add, Sub}; use std::ops::{Add, Sub};
use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH}; use std::time::{Duration, SystemTime, SystemTimeError, UNIX_EPOCH};
use ipnet::Ipv4Net; use ipnet::Ipv4Net;
use crate::cert::{CertificateValidity, deserialize_ed25519_private, deserialize_ed25519_public, deserialize_nebula_certificate, deserialize_nebula_certificate_from_pem, deserialize_x25519_private, deserialize_x25519_public, NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private, serialize_ed25519_public, serialize_x25519_private, serialize_x25519_public}; use crate::cert::{CertificateValidity, deserialize_ed25519_private, deserialize_ed25519_public, deserialize_ed25519_public_many, deserialize_nebula_certificate, deserialize_nebula_certificate_from_pem, deserialize_x25519_private, deserialize_x25519_public, NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private, serialize_ed25519_public, serialize_x25519_private, serialize_x25519_public};
use std::str::FromStr; use std::str::FromStr;
use ed25519_dalek::{SigningKey, VerifyingKey}; use ed25519_dalek::{SigningKey, VerifyingKey};
use quick_protobuf::{MessageWrite, Writer}; use quick_protobuf::{MessageWrite, Writer};
@ -296,10 +296,21 @@ fn x25519_serialization() {
#[test] #[test]
fn ed25519_serialization() { fn ed25519_serialization() {
let bytes = [0u8; 64]; let bytes = [0u8; 64];
let bytes2 = [0u8; 32];
assert_eq!(deserialize_ed25519_private(&serialize_ed25519_private(&bytes)).unwrap(), bytes); assert_eq!(deserialize_ed25519_private(&serialize_ed25519_private(&bytes)).unwrap(), bytes);
assert!(deserialize_ed25519_private(&[0u8; 32]).is_err()); assert!(deserialize_ed25519_private(&[0u8; 32]).is_err());
assert_eq!(deserialize_ed25519_public(&serialize_ed25519_public(&bytes)).unwrap(), bytes); assert_eq!(deserialize_ed25519_public(&serialize_ed25519_public(&bytes2)).unwrap(), bytes2);
assert!(deserialize_ed25519_public(&[0u8; 32]).is_err()); assert!(deserialize_ed25519_public(&[0u8; 64]).is_err());
let mut bytes = vec![];
bytes.append(&mut serialize_ed25519_public(&[0u8; 32]));
bytes.append(&mut serialize_ed25519_public(&[1u8; 32]));
let deser = deserialize_ed25519_public_many(&bytes).unwrap();
assert_eq!(deser[0], [0u8; 32]);
assert_eq!(deser[1], [1u8; 32]);
bytes.append(&mut serialize_ed25519_public(&[1u8; 33]));
deserialize_ed25519_public_many(&bytes).unwrap_err();
} }
#[test] #[test]
@ -637,11 +648,11 @@ bzBEr00kERQxxTzTsH8cpYEgRoipvmExvg8WP8NdAJEYJosB
#[test] #[test]
fn test_deserialize_ed25519_public() { fn test_deserialize_ed25519_public() {
let priv_key = b"-----BEGIN NEBULA ED25519 PUBLIC KEY----- let pub_key = b"-----BEGIN NEBULA ED25519 PUBLIC KEY-----
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
-----END NEBULA ED25519 PUBLIC KEY-----"; -----END NEBULA ED25519 PUBLIC KEY-----";
let short_key = b"-----BEGIN NEBULA ED25519 PUBLIC KEY----- let short_key = b"-----BEGIN NEBULA ED25519 PUBLIC KEY-----
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
-----END NEBULA ED25519 PUBLIC KEY-----"; -----END NEBULA ED25519 PUBLIC KEY-----";
let invalid_banner = b"-----BEGIN NOT A NEBULA PUBLIC KEY----- let invalid_banner = b"-----BEGIN NOT A NEBULA PUBLIC KEY-----
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
@ -650,7 +661,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
-END NEBULA ED25519 PUBLIC KEY-----"; -END NEBULA ED25519 PUBLIC KEY-----";
deserialize_ed25519_public(priv_key).unwrap(); deserialize_ed25519_public(pub_key).unwrap();
deserialize_ed25519_public(short_key).unwrap_err(); deserialize_ed25519_public(short_key).unwrap_err();