From 0c0e78da1b74862afc927244f22dd242c38c364c Mon Sep 17 00:00:00 2001 From: Adam Ierymenko Date: Thu, 14 Oct 2021 19:36:21 -0400 Subject: [PATCH] Move system service back into here (still does not compile with Rust core) --- zerotier-network-hypervisor/Cargo.toml | 2 +- zerotier-system-service/Cargo.lock | 913 ++++++++++++++++++ zerotier-system-service/Cargo.toml | 32 + zerotier-system-service/build.rs | 15 + zerotier-system-service/src/api.rs | 46 + .../src/commands/controller.rs | 13 + .../src/commands/identity.rs | 134 +++ zerotier-system-service/src/commands/join.rs | 13 + zerotier-system-service/src/commands/leave.rs | 13 + zerotier-system-service/src/commands/mod.rs | 21 + .../src/commands/network.rs | 13 + zerotier-system-service/src/commands/peer.rs | 13 + zerotier-system-service/src/commands/set.rs | 13 + .../src/commands/status.rs | 51 + zerotier-system-service/src/fastudpsocket.rs | 349 +++++++ zerotier-system-service/src/getifaddrs.rs | 103 ++ zerotier-system-service/src/httpclient.rs | 212 ++++ zerotier-system-service/src/httplistener.rs | 206 ++++ zerotier-system-service/src/localconfig.rs | 230 +++++ zerotier-system-service/src/log.rs | 170 ++++ zerotier-system-service/src/main.rs | 300 ++++++ zerotier-system-service/src/network.rs | 17 + zerotier-system-service/src/service.rs | 515 ++++++++++ zerotier-system-service/src/store.rs | 300 ++++++ zerotier-system-service/src/utils.rs | 182 ++++ zerotier-system-service/src/vnic/common.rs | 56 ++ .../src/vnic/mac_feth_tap.rs | 471 +++++++++ zerotier-system-service/src/vnic/mod.rs | 18 + zerotier-system-service/src/vnic/vnic.rs | 37 + 29 files changed, 4457 insertions(+), 1 deletion(-) create mode 100644 zerotier-system-service/Cargo.lock create mode 100644 zerotier-system-service/Cargo.toml create mode 100644 zerotier-system-service/build.rs create mode 100644 zerotier-system-service/src/api.rs create mode 100644 zerotier-system-service/src/commands/controller.rs create mode 100644 zerotier-system-service/src/commands/identity.rs create mode 100644 zerotier-system-service/src/commands/join.rs create mode 100644 zerotier-system-service/src/commands/leave.rs create mode 100644 zerotier-system-service/src/commands/mod.rs create mode 100644 zerotier-system-service/src/commands/network.rs create mode 100644 zerotier-system-service/src/commands/peer.rs create mode 100644 zerotier-system-service/src/commands/set.rs create mode 100644 zerotier-system-service/src/commands/status.rs create mode 100644 zerotier-system-service/src/fastudpsocket.rs create mode 100644 zerotier-system-service/src/getifaddrs.rs create mode 100644 zerotier-system-service/src/httpclient.rs create mode 100644 zerotier-system-service/src/httplistener.rs create mode 100644 zerotier-system-service/src/localconfig.rs create mode 100644 zerotier-system-service/src/log.rs create mode 100644 zerotier-system-service/src/main.rs create mode 100644 zerotier-system-service/src/network.rs create mode 100644 zerotier-system-service/src/service.rs create mode 100644 zerotier-system-service/src/store.rs create mode 100644 zerotier-system-service/src/utils.rs create mode 100644 zerotier-system-service/src/vnic/common.rs create mode 100644 zerotier-system-service/src/vnic/mac_feth_tap.rs create mode 100644 zerotier-system-service/src/vnic/mod.rs create mode 100644 zerotier-system-service/src/vnic/vnic.rs diff --git a/zerotier-network-hypervisor/Cargo.toml b/zerotier-network-hypervisor/Cargo.toml index 587b0c8e3..9795ad065 100644 --- a/zerotier-network-hypervisor/Cargo.toml +++ b/zerotier-network-hypervisor/Cargo.toml @@ -22,4 +22,4 @@ concat-arrays = "^0" libc = "^0" [target."cfg(windows)".dependencies] -winapi = { version = "0.3.9", features = ["ws2tcpip"] } +winapi = { version = "^0", features = ["ws2tcpip"] } diff --git a/zerotier-system-service/Cargo.lock b/zerotier-system-service/Cargo.lock new file mode 100644 index 000000000..5215bbf87 --- /dev/null +++ b/zerotier-system-service/Cargo.lock @@ -0,0 +1,913 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "term_size", + "textwrap", + "unicode-width", + "vec_map", +] + +[[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]] +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "dialoguer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest_auth" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fd5e24649b07f360f59a1e0a522d775540e2bc4b88f8d2657bcf8ca0360d74" +dependencies = [ + "digest", + "hex", + "md-5", + "rand", + "sha2", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" + +[[package]] +name = "futures-executor" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" + +[[package]] +name = "futures-macro" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" + +[[package]] +name = "futures-task" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" + +[[package]] +name = "futures-util" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "hyper" +version = "0.14.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "term_size", + "unicode-width", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tokio" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +dependencies = [ + "autocfg", + "libc", + "mio", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zeroize" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" + +[[package]] +name = "zerotier-system-service" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", + "colored", + "dialoguer", + "digest_auth", + "futures", + "hyper", + "lazy_static", + "num-derive", + "num-traits", + "num_cpus", + "serde", + "serde_json", + "socket2 0.3.19", + "tokio", + "winapi", +] diff --git a/zerotier-system-service/Cargo.toml b/zerotier-system-service/Cargo.toml new file mode 100644 index 000000000..9a95560f0 --- /dev/null +++ b/zerotier-system-service/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "zerotier-system-service" +version = "0.1.0" +authors = ["Adam Ierymenko "] +edition = "2018" +build = "build.rs" + +[profile.release] +opt-level = 'z' +lto = true +codegen-units = 1 +panic = 'abort' + +[dependencies] +num_cpus = "^1" +tokio = { version = "1", features = ["rt", "net", "time", "signal", "macros"] } +serde = { version = "1", features = ["derive"] } +serde_json = "^1" +futures = "0" +clap = { version = "2", features = ["suggestions", "wrap_help"] } +chrono = "^0" +lazy_static = "^1" +num-traits = "^0" +num-derive = "^0" +hyper = { version = "0", features = ["http1", "runtime", "server", "client", "tcp", "stream"] } +socket2 = { version = "^0", features = ["reuseport", "unix", "pair"] } +dialoguer = "^0" +digest_auth = "^0" +colored = "^2" + +[target."cfg(windows)".dependencies] +winapi = { version = "^0", features = ["handleapi", "ws2ipdef", "ws2tcpip"] } diff --git a/zerotier-system-service/build.rs b/zerotier-system-service/build.rs new file mode 100644 index 000000000..d71096be8 --- /dev/null +++ b/zerotier-system-service/build.rs @@ -0,0 +1,15 @@ +#[allow(unused_assignments)] +#[allow(unused_mut)] +fn main() { + let d = env!("CARGO_MANIFEST_DIR"); + println!("cargo:rustc-link-search=native={}/../build/core", d); + println!("cargo:rustc-link-search=native={}/../build/osdep", d); + println!("cargo:rustc-link-lib=static=zt_core"); + println!("cargo:rustc-link-lib=static=zt_osdep"); + + let mut cpplib = "c++"; + #[cfg(target_os = "linux")] { + cpplib = "stdc++"; + } + println!("cargo:rustc-link-lib={}", cpplib); +} diff --git a/zerotier-system-service/src/api.rs b/zerotier-system-service/src/api.rs new file mode 100644 index 000000000..43f75d8e6 --- /dev/null +++ b/zerotier-system-service/src/api.rs @@ -0,0 +1,46 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::sync::Arc; + +use hyper::{Request, Body, StatusCode, Method}; + +use crate::service::Service; + +pub(crate) fn status(service: Arc, req: Request) -> (StatusCode, Body) { + if req.method() == Method::GET { + service.status().map_or_else(|| { + (StatusCode::SERVICE_UNAVAILABLE, Body::from("node shutdown in progress")) + }, |status| { + (StatusCode::OK, Body::from(serde_json::to_string(&status).unwrap())) + }) + } else { + (StatusCode::METHOD_NOT_ALLOWED, Body::from("/status allows method(s): GET")) + } +} + +pub(crate) fn config(service: Arc, req: Request) -> (StatusCode, Body) { + let config = service.local_config(); + if req.method() == Method::POST || req.method() == Method::PUT { + // TODO: diff config + } + (StatusCode::OK, Body::from(serde_json::to_string(config.as_ref()).unwrap())) +} + +pub(crate) fn peer(service: Arc, req: Request) -> (StatusCode, Body) { + (StatusCode::NOT_IMPLEMENTED, Body::from("")) +} + +pub(crate) fn network(service: Arc, req: Request) -> (StatusCode, Body) { + (StatusCode::NOT_IMPLEMENTED, Body::from("")) +} diff --git a/zerotier-system-service/src/commands/controller.rs b/zerotier-system-service/src/commands/controller.rs new file mode 100644 index 000000000..489e2178a --- /dev/null +++ b/zerotier-system-service/src/commands/controller.rs @@ -0,0 +1,13 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + diff --git a/zerotier-system-service/src/commands/identity.rs b/zerotier-system-service/src/commands/identity.rs new file mode 100644 index 000000000..2786318d8 --- /dev/null +++ b/zerotier-system-service/src/commands/identity.rs @@ -0,0 +1,134 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use clap::ArgMatches; + +use zerotier_core::{Identity, IdentityType}; + +fn new_(cli_args: &ArgMatches) -> i32 { + let id_type = cli_args.value_of("type").map_or(IdentityType::Curve25519, |idt| { + match idt { + "p384" => IdentityType::NistP384, + _ => IdentityType::Curve25519, + } + }); + let id = Identity::new_generate(id_type); + if id.is_err() { + println!("ERROR: identity generation failed: {}", id.err().unwrap().to_str()); + return 1; + } + println!("{}", id.ok().unwrap().to_secret_string()); + 0 +} + +fn getpublic(cli_args: &ArgMatches) -> i32 { + let identity = crate::utils::read_identity(cli_args.value_of("identity").unwrap_or(""), false); + identity.map_or_else(|e| { + println!("ERROR: identity invalid: {}", e.to_string()); + 1 + }, |id| { + println!("{}", id.to_string()); + 0 + }) +} + +fn fingerprint(cli_args: &ArgMatches) -> i32 { + let identity = crate::utils::read_identity(cli_args.value_of("identity").unwrap_or(""), false); + identity.map_or_else(|e| { + println!("ERROR: identity invalid: {}", e.to_string()); + 1 + }, |id| { + println!("{}", id.fingerprint().to_string()); + 0 + }) +} + +fn validate(cli_args: &ArgMatches) -> i32 { + crate::utils::read_identity(cli_args.value_of("identity").unwrap_or(""), false).map_or_else(|e| { + println!("FAILED"); + 1 + }, |id| { + if id.validate() { + println!("OK"); + 0 + } else { + println!("FAILED"); + 1 + } + }) +} + +fn sign(cli_args: &ArgMatches) -> i32 { + crate::utils::read_identity(cli_args.value_of("identity").unwrap_or(""), false).map_or_else(|e| { + println!("ERROR: invalid or unreadable identity: {}", e.as_str()); + 1 + }, |id| { + if id.has_private() { + std::fs::read(cli_args.value_of("path").unwrap()).map_or_else(|e| { + println!("ERROR: unable to read file: {}", e.to_string()); + 1 + }, |data| { + id.sign(data.as_slice()).map_or_else(|e| { + println!("ERROR: failed to sign: {}", e.to_str()); + 1 + }, |sig| { + println!("{}", hex::encode(sig.as_ref())); + 0 + }) + }) + } else { + println!("ERROR: identity must include secret key to sign."); + 1 + } + }) +} + +fn verify(cli_args: &ArgMatches) -> i32 { + crate::utils::read_identity(cli_args.value_of("identity").unwrap_or(""), false).map_or_else(|e| { + println!("ERROR: invalid or unreadable identity: {}", e.as_str()); + 1 + }, |id| { + std::fs::read(cli_args.value_of("path").unwrap()).map_or_else(|e| { + println!("ERROR: unable to read file: {}", e.to_string()); + 1 + }, |data| { + hex::decode(cli_args.value_of("signature").unwrap()).map_or_else(|e| { + println!("FAILED"); + 1 + }, |sig| { + if id.verify(data.as_slice(), sig.as_slice()) { + println!("OK"); + 0 + } else { + println!("FAILED"); + 1 + } + }) + }) + }) +} + +pub(crate) fn run<'a>(cli_args: &ArgMatches<'a>) -> i32 { + match cli_args.subcommand() { + ("new", Some(sub_cli_args)) => new_(sub_cli_args), + ("getpublic", Some(sub_cli_args)) => getpublic(sub_cli_args), + ("fingerprint", Some(sub_cli_args)) => fingerprint(sub_cli_args), + ("validate", Some(sub_cli_args)) => validate(sub_cli_args), + ("sign", Some(sub_cli_args)) => sign(sub_cli_args), + ("verify", Some(sub_cli_args)) => verify(sub_cli_args), + _ => { + crate::print_help(true); + 1 + } + } +} diff --git a/zerotier-system-service/src/commands/join.rs b/zerotier-system-service/src/commands/join.rs new file mode 100644 index 000000000..489e2178a --- /dev/null +++ b/zerotier-system-service/src/commands/join.rs @@ -0,0 +1,13 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + diff --git a/zerotier-system-service/src/commands/leave.rs b/zerotier-system-service/src/commands/leave.rs new file mode 100644 index 000000000..489e2178a --- /dev/null +++ b/zerotier-system-service/src/commands/leave.rs @@ -0,0 +1,13 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + diff --git a/zerotier-system-service/src/commands/mod.rs b/zerotier-system-service/src/commands/mod.rs new file mode 100644 index 000000000..2be052489 --- /dev/null +++ b/zerotier-system-service/src/commands/mod.rs @@ -0,0 +1,21 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +pub(crate) mod status; +pub(crate) mod set; +pub(crate) mod peer; +pub(crate) mod network; +pub(crate) mod join; +pub(crate) mod leave; +pub(crate) mod controller; +pub(crate) mod identity; diff --git a/zerotier-system-service/src/commands/network.rs b/zerotier-system-service/src/commands/network.rs new file mode 100644 index 000000000..489e2178a --- /dev/null +++ b/zerotier-system-service/src/commands/network.rs @@ -0,0 +1,13 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + diff --git a/zerotier-system-service/src/commands/peer.rs b/zerotier-system-service/src/commands/peer.rs new file mode 100644 index 000000000..489e2178a --- /dev/null +++ b/zerotier-system-service/src/commands/peer.rs @@ -0,0 +1,13 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + diff --git a/zerotier-system-service/src/commands/set.rs b/zerotier-system-service/src/commands/set.rs new file mode 100644 index 000000000..489e2178a --- /dev/null +++ b/zerotier-system-service/src/commands/set.rs @@ -0,0 +1,13 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + diff --git a/zerotier-system-service/src/commands/status.rs b/zerotier-system-service/src/commands/status.rs new file mode 100644 index 000000000..eeeab5a7f --- /dev/null +++ b/zerotier-system-service/src/commands/status.rs @@ -0,0 +1,51 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::error::Error; +use std::sync::Arc; + +use hyper::{Uri, Method, StatusCode}; +use colored::*; + +use crate::store::Store; +use crate::httpclient::*; +use crate::service::ServiceStatus; +use crate::{GlobalFlags, HTTP_API_OBJECT_SIZE_LIMIT}; + +pub(crate) async fn run(store: Arc, global_flags: GlobalFlags, client: HttpClient, api_base_uri: Uri, auth_token: String) -> Result> { + let uri = append_uri_path(api_base_uri, "/status").unwrap(); + let mut res = request(&client, Method::GET, uri, None, auth_token.as_str()).await?; + + match res.status() { + StatusCode::OK => { + let status = read_object_limited::(res.body_mut(), HTTP_API_OBJECT_SIZE_LIMIT).await?; + + if global_flags.json_output { + println!("{}", serde_json::to_string_pretty(&status).unwrap()) + } else { + println!("address {} version {} status {}", + status.address.to_string().as_str().bright_white(), + status.version.as_str().bright_white(), + if status.online { + "ONLINE".bright_green() + } else { + "OFFLINE".bright_red() + }); + // TODO: print more detailed status information + } + + Ok(0) + }, + _ => Err(Box::new(UnexpectedStatusCodeError(res.status(), ""))) + } +} diff --git a/zerotier-system-service/src/fastudpsocket.rs b/zerotier-system-service/src/fastudpsocket.rs new file mode 100644 index 000000000..62121ec05 --- /dev/null +++ b/zerotier-system-service/src/fastudpsocket.rs @@ -0,0 +1,349 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::os::raw::c_int; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use num_traits::cast::AsPrimitive; + +use zerotier_core::{Buffer, InetAddress, InetAddressFamily}; + +use crate::osdep as osdep; + +/* + * This is a threaded UDP socket listener for high performance. The fastest way to receive UDP + * (without heroic efforts like kernel bypass) on most platforms is to create a separate socket + * for each thread using options like SO_REUSEPORT and concurrent packet listening. + */ + +#[cfg(windows)] +use winapi::um::winsock2 as winsock2; + +#[cfg(windows)] +pub(crate) type FastUDPRawOsSocket = winsock2::SOCKET; + +#[cfg(unix)] +pub(crate) type FastUDPRawOsSocket = c_int; + +#[cfg(unix)] +fn bind_udp_socket(_device_name: &str, address: &InetAddress) -> Result { + unsafe { + let (af, sa_len) = match address.family() { + InetAddressFamily::IPv4 => (osdep::AF_INET, std::mem::size_of::() as osdep::socklen_t), + InetAddressFamily::IPv6 => (osdep::AF_INET6, std::mem::size_of::() as osdep::socklen_t), + _ => { + return Err("unrecognized address family"); + } + }; + + #[cfg(not(target_os = "linux"))] + let s = osdep::socket(af.as_(), osdep::SOCK_DGRAM.as_(), 0); + #[cfg(target_os = "linux")] + let s = osdep::socket(af.as_(), 2, 0); + + if s < 0 { + return Err("unable to create socket"); + } + + let mut fl: c_int; + let fl_size = std::mem::size_of::() as osdep::socklen_t; + let mut setsockopt_results: c_int = 0; + + fl = 1; + setsockopt_results |= osdep::setsockopt(s, osdep::SOL_SOCKET.as_(), osdep::SO_REUSEPORT.as_(), (&mut fl as *mut c_int).cast(), fl_size); + //fl = 1; + //setsockopt_results |= osdep::setsockopt(s, osdep::SOL_SOCKET, osdep::SO_REUSEADDR, (&mut fl as *mut c_int).cast(), fl_size); + fl = 1; + setsockopt_results |= osdep::setsockopt(s, osdep::SOL_SOCKET.as_(), osdep::SO_BROADCAST.as_(), (&mut fl as *mut c_int).cast(), fl_size); + if af == osdep::AF_INET6 { + fl = 1; + setsockopt_results |= osdep::setsockopt(s, osdep::IPPROTO_IPV6.as_(), osdep::IPV6_V6ONLY.as_(), (&mut fl as *mut c_int).cast(), fl_size); + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] { + fl = 1; + setsockopt_results |= osdep::setsockopt(s, osdep::SOL_SOCKET.as_(), osdep::SO_NOSIGPIPE.as_(), (&mut fl as *mut c_int).cast(), fl_size) + } + + #[cfg(target_os = "linux")] { + if !_device_name.is_empty() { + let _ = std::ffi::CString::new(_device_name).map(|dn| { + let dnb = dn.as_bytes_with_nul(); + let _ = osdep::setsockopt(s.as_(), osdep::SOL_SOCKET.as_(), osdep::SO_BINDTODEVICE.as_(), dnb.as_ptr().cast(), (dnb.len() - 1).as_()); + }); + } + } + + if setsockopt_results != 0 { + osdep::close(s); + return Err("setsockopt() failed"); + } + + if af == osdep::AF_INET { + #[cfg(not(target_os = "linux"))] { + fl = 0; + osdep::setsockopt(s, osdep::IPPROTO_IP.as_(), osdep::IP_DF.as_(), (&mut fl as *mut c_int).cast(), fl_size); + } + #[cfg(target_os = "linux")] { + fl = osdep::IP_PMTUDISC_DONT as c_int; + osdep::setsockopt(s, osdep::IPPROTO_IP.as_(), osdep::IP_MTU_DISCOVER.as_(), (&mut fl as *mut c_int).cast(), fl_size); + } + } + + if af == osdep::AF_INET6 { + fl = 0; + osdep::setsockopt(s, osdep::IPPROTO_IPV6.as_(), osdep::IPV6_DONTFRAG.as_(), (&mut fl as *mut c_int).cast(), fl_size); + } + + fl = 1048576; + while fl >= 131072 { + if osdep::setsockopt(s, osdep::SOL_SOCKET.as_(), osdep::SO_RCVBUF.as_(), (&mut fl as *mut c_int).cast(), fl_size) == 0 { + break; + } + fl -= 65536; + } + fl = 1048576; + while fl >= 131072 { + if osdep::setsockopt(s, osdep::SOL_SOCKET.as_(), osdep::SO_SNDBUF.as_(), (&mut fl as *mut c_int).cast(), fl_size) == 0 { + break; + } + fl -= 65536; + } + + if osdep::bind(s, (address as *const InetAddress).cast(), sa_len) != 0 { + osdep::close(s); + return Err("bind to address failed"); + } + + Ok(s) + } +} + +/// A multi-threaded (or otherwise fast) UDP socket that binds to both IPv4 and IPv6 addresses. +pub(crate) struct FastUDPSocket { + threads: Vec>, + thread_run: Arc, + sockets: Vec, + pub bind_address: InetAddress, +} + +#[cfg(unix)] +#[inline(always)] +fn fast_udp_socket_close(socket: &FastUDPRawOsSocket) { + unsafe { + osdep::close(*socket); + } +} + +#[cfg(windows)] +#[inline(always)] +fn fast_udp_socket_close(socket: &FastUDPRawOsSocket) { + unsafe { + osdep::close(*socket); + } +} + +#[inline(always)] +pub(crate) fn fast_udp_socket_to_i64(socket: &FastUDPRawOsSocket) -> i64 { + (*socket) as i64 +} + +#[inline(always)] +pub(crate) fn fast_udp_socket_from_i64(socket: i64) -> Option { + if socket >= 0 { + return Some(socket as FastUDPRawOsSocket); + } + None +} + +/// Send to a raw UDP socket with optional packet TTL. +/// If the packet_ttl option is <=0, packet is sent with the default TTL. TTL setting is only used +/// in ZeroTier right now to do escalating TTL probes for IPv4 NAT traversal. +#[cfg(unix)] +#[inline(always)] +pub(crate) fn fast_udp_socket_sendto(socket: &FastUDPRawOsSocket, to_address: &InetAddress, data: *const u8, len: usize, packet_ttl: i32) { + unsafe { + if packet_ttl <= 0 { + osdep::sendto(*socket, data.cast(), len.as_(), 0, (to_address as *const InetAddress).cast(), std::mem::size_of::().as_()); + } else { + let mut ttl = packet_ttl as c_int; + osdep::setsockopt(*socket, osdep::IPPROTO_IP.as_(), osdep::IP_TTL.as_(), (&mut ttl as *mut c_int).cast(), std::mem::size_of::().as_()); + osdep::sendto(*socket, data.cast(), len.as_(), 0, (to_address as *const InetAddress).cast(), std::mem::size_of::().as_()); + ttl = 255; + osdep::setsockopt(*socket, osdep::IPPROTO_IP.as_(), osdep::IP_TTL.as_(), (&mut ttl as *mut c_int).cast(), std::mem::size_of::().as_()); + } + } +} + +#[cfg(windows)] +#[inline(always)] +pub(crate) fn fast_udp_socket_sendto(socket: &FastUDPRawOsSocket, to_address: &InetAddress, data: &[u8], packet_ttl: i32) {} + +#[cfg(unix)] +#[inline(always)] +fn fast_udp_socket_recvfrom(socket: &FastUDPRawOsSocket, buf: &mut Buffer, from_address: &mut InetAddress) -> i32 { + unsafe { + let mut addrlen = std::mem::size_of::() as osdep::socklen_t; + osdep::recvfrom(*socket, buf.as_mut_ptr().cast(), Buffer::CAPACITY.as_(), 0, (from_address as *mut InetAddress).cast(), &mut addrlen) as i32 + } +} + +impl FastUDPSocket { + pub fn new(device_name: &str, address: &InetAddress, handler: F) -> Result { + let thread_count = num_cpus::get_physical().min(num_cpus::get()); + + let mut s = FastUDPSocket { + thread_run: Arc::new(AtomicBool::new(true)), + threads: Vec::new(), + sockets: Vec::new(), + bind_address: address.clone(), + }; + s.threads.reserve(thread_count); + s.sockets.reserve(thread_count); + + let mut bind_failed_reason: &'static str = ""; + for _ in 0..thread_count { + let thread_socket = bind_udp_socket(device_name, address); + if thread_socket.is_ok() { + let thread_socket = thread_socket.unwrap(); + s.sockets.push(thread_socket); + + let thread_run = s.thread_run.clone(); + let handler_copy = handler.clone(); + s.threads.push(std::thread::Builder::new().stack_size(zerotier_core::RECOMMENDED_THREAD_STACK_SIZE).spawn(move || { + let mut from_address = InetAddress::new(); + while thread_run.load(Ordering::Relaxed) { + let mut buf = Buffer::new(); + let read_length = fast_udp_socket_recvfrom(&thread_socket, &mut buf, &mut from_address); + if read_length > 0 { + buf.set_len(read_length as usize); + handler_copy(&thread_socket, &from_address, buf); + } else if read_length < 0 { + break; + } + } + }).unwrap()); + } else { + bind_failed_reason = thread_socket.err().unwrap(); + } + } + + // This is successful if it is able to bind successfully once and launch at least one thread, + // since in a few cases it may be impossible to do multithreaded binding such as old Linux + // kernels or emulation layers. + if s.sockets.is_empty() { + return Err(format!("unable to bind to address for IPv4 or IPv6 ({})", bind_failed_reason)); + } + + Ok(s) + } + + /// Get a slice of all raw sockets used. + #[inline(always)] + pub fn all_sockets(&self) -> &[FastUDPRawOsSocket] { + self.sockets.as_slice() + } + + /// Send from this socket. + /// This actually picks a thread's socket and sends from it. Since all + /// are bound to the same IP:port which one is chosen doesn't matter. + /// Sockets are thread safe. + #[inline(always)] + pub fn send(&self, to_address: &InetAddress, data: *const u8, len: usize, packet_ttl: i32) { + fast_udp_socket_sendto(self.sockets.get(0).unwrap(), to_address, data, len, packet_ttl); + } + + /// Get a raw socket that can be used to send UDP packets. + #[inline(always)] + pub fn raw_socket(&self) -> FastUDPRawOsSocket { + *self.sockets.get(0).unwrap() + } +} + +impl Drop for FastUDPSocket { + #[cfg(windows)] + fn drop(&mut self) { + // TODO + } + + #[cfg(unix)] + fn drop(&mut self) { + let tmp: [u8; 1] = [0]; + self.thread_run.store(false, Ordering::Relaxed); + for s in self.sockets.iter() { + unsafe { + osdep::sendto(*s, tmp.as_ptr().cast(), 0, 0, (&self.bind_address as *const InetAddress).cast(), std::mem::size_of::() as osdep::socklen_t); + } + } + for s in self.sockets.iter() { + unsafe { + osdep::shutdown(*s, osdep::SHUT_RDWR.as_()); + } + } + for s in self.sockets.iter() { + unsafe { + osdep::close(*s); + } + } + while !self.threads.is_empty() { + let _ = self.threads.pop().unwrap().join(); + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicU32, Ordering}; + + use zerotier_core::{Buffer, InetAddress}; + + use crate::fastudpsocket::*; + + #[test] + fn test_udp_bind_and_transfer() { + { + let ba0 = InetAddress::new_from_string("127.0.0.1/23333"); + assert!(ba0.is_some()); + let ba0 = ba0.unwrap(); + let cnt0 = Arc::new(AtomicU32::new(0)); + let cnt0c = cnt0.clone(); + let s0 = FastUDPSocket::new("", &ba0, move |sock: &FastUDPRawOsSocket, _: &InetAddress, data: Buffer| { + cnt0c.fetch_add(1, Ordering::Relaxed); + }); + assert!(s0.is_ok()); + let s0 = s0.unwrap(); + + let ba1 = InetAddress::new_from_string("127.0.0.1/23334"); + assert!(ba1.is_some()); + let ba1 = ba1.unwrap(); + let cnt1 = Arc::new(AtomicU32::new(0)); + let cnt1c = cnt1.clone(); + let s1 = FastUDPSocket::new("", &ba1, move |sock: &FastUDPRawOsSocket, _: &InetAddress, data: Buffer| { + cnt1c.fetch_add(1, Ordering::Relaxed); + }); + assert!(s1.is_ok()); + let s1 = s1.unwrap(); + + let data_bytes = [0_u8; 1024]; + loop { + s0.send(&ba1, data_bytes.as_ptr(), data_bytes.len(), 0); + s1.send(&ba0, data_bytes.as_ptr(), data_bytes.len(), 0); + if cnt0.load(Ordering::Relaxed) > 10000 && cnt1.load(Ordering::Relaxed) > 10000 { + break; + } + } + } + //println!("FastUDPSocket shutdown successful"); + } +} diff --git a/zerotier-system-service/src/getifaddrs.rs b/zerotier-system-service/src/getifaddrs.rs new file mode 100644 index 000000000..6309a3d0b --- /dev/null +++ b/zerotier-system-service/src/getifaddrs.rs @@ -0,0 +1,103 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::mem::size_of; +use std::ptr::{copy_nonoverlapping, null_mut}; + +use zerotier_core::InetAddress; + +use crate::osdep as osdep; + +fn s6_addr_as_ptr(a: &A) -> *const A { + a as *const A +} + +/// Call supplied function or closure for each physical IP address in the system. +#[cfg(unix)] +pub(crate) fn for_each_address(mut f: F) { + unsafe { + let mut ifa_name = [0_u8; osdep::IFNAMSIZ as usize]; + let mut ifap: *mut osdep::ifaddrs = null_mut(); + if osdep::getifaddrs((&mut ifap as *mut *mut osdep::ifaddrs).cast()) == 0 { + let mut i = ifap; + while !i.is_null() { + if !(*i).ifa_addr.is_null() { + let mut a = InetAddress::new(); + + let sa_family = (*(*i).ifa_addr).sa_family as u8; + if sa_family == osdep::AF_INET as u8 { + copy_nonoverlapping((*i).ifa_addr.cast::(), (&mut a as *mut InetAddress).cast::(), size_of::()); + } else if sa_family == osdep::AF_INET6 as u8 { + copy_nonoverlapping((*i).ifa_addr.cast::(), (&mut a as *mut InetAddress).cast::(), size_of::()); + } else { + i = (*i).ifa_next; + continue; + } + + let mut netmask_bits: u16 = 0; + if !(*i).ifa_netmask.is_null() { + if sa_family == osdep::AF_INET as u8 { + let a = (*(*i).ifa_netmask.cast::()).sin_addr.s_addr as u32; + netmask_bits = a.leading_ones() as u16; + } else if sa_family == osdep::AF_INET6 as u8 { + let a = s6_addr_as_ptr(&((*(*i).ifa_netmask.cast::()).sin6_addr)).cast::(); + for i in 0..16 as isize { + let b = *a.offset(i); + if b == 0xff { + netmask_bits += 8; + } else { + netmask_bits += b.leading_ones() as u16; + break; + } + } + } + } + a.set_port(netmask_bits); + + let mut namlen: usize = 0; + while namlen < (osdep::IFNAMSIZ as usize) { + let c = *(*i).ifa_name.offset(namlen as isize); + if c != 0 { + ifa_name[namlen] = c as u8; + namlen += 1; + } else { + break; + } + } + if namlen > 0 { + let dev = String::from_utf8_lossy(&ifa_name[0..namlen]); + if dev.len() > 0 { + f(&a, dev.as_ref()); + } + } + } + i = (*i).ifa_next; + } + osdep::freeifaddrs(ifap.cast()); + } + } +} + +#[cfg(test)] +mod tests { + use zerotier_core::InetAddress; + + #[test] + fn test_getifaddrs() { + println!("starting getifaddrs..."); + crate::getifaddrs::for_each_address(|a: &InetAddress, dev: &str| { + println!(" {} {}", dev, a.to_string()) + }); + println!("done.") + } +} diff --git a/zerotier-system-service/src/httpclient.rs b/zerotier-system-service/src/httpclient.rs new file mode 100644 index 000000000..b731c5108 --- /dev/null +++ b/zerotier-system-service/src/httpclient.rs @@ -0,0 +1,212 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::error::Error; +use std::future::Future; +use std::rc::Rc; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use futures::stream::StreamExt; +use hyper::{Body, Method, Request, Response, StatusCode, Uri}; +use hyper::http::uri::{Authority, PathAndQuery, Scheme}; +use serde::de::DeserializeOwned; + +use crate::GlobalFlags; +use crate::store::Store; + +pub(crate) type HttpClient = Rc>; + +#[derive(Debug)] +pub(crate) struct IncorrectAuthTokenError; + +impl Error for IncorrectAuthTokenError {} + +impl std::fmt::Display for IncorrectAuthTokenError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "401 UNAUTHORIZED (incorrect authorization token or not allowed to read token)") + } +} + +#[derive(Debug)] +pub(crate) struct UnexpectedStatusCodeError(pub StatusCode, pub &'static str); + +impl Error for UnexpectedStatusCodeError {} + +impl std::fmt::Display for UnexpectedStatusCodeError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.1.is_empty() { + write!(f, "{} {} (???)", self.0.as_str(), self.0.canonical_reason().unwrap_or("???")) + } else { + write!(f, "{} {} ({})", self.0.as_str(), self.0.canonical_reason().unwrap_or("???"), self.1) + } + } +} + +/// Launch the supplied function with a ready to go HTTP client, the auth token, and the API URI. +/// This is boilerplate code for CLI commands that invoke the HTTP API. Since it instantiates and +/// then kills a tokio runtime, it's not for use in the service code that runs in a long-running +/// tokio runtime. +pub(crate) fn run_command< + R: Future>>, + F: FnOnce(Arc, GlobalFlags, HttpClient, Uri, String) -> R +>(store: Arc, global_flags: GlobalFlags, func: F) -> i32 { + let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let code = rt.block_on(async move { + let uri = store.load_uri(); + if uri.is_err() { + println!("ERROR: 'zerotier.uri' not found in '{}', unable to get service API endpoint.", store.base_path.to_str().unwrap()); + 1 + } else { + let auth_token = store.auth_token(false); + if auth_token.is_err() { + println!("ERROR: unable to read API authorization token from '{}': {}", store.base_path.to_str().unwrap(), auth_token.err().unwrap().to_string()); + 1 + } else { + let uri = uri.unwrap(); + let uri_str = uri.to_string(); + func(store, global_flags, Rc::new(hyper::Client::builder().http1_max_buf_size(65536).build_http()), uri, auth_token.unwrap()).await.map_or_else(|e| { + println!("ERROR: service API HTTP request ({}) failed: {}", uri_str, e); + println!(); + println!("Common causes: service is not running, authorization token incorrect"); + println!("or not readable, or a local firewall is blocking loopback connections."); + 1 + }, |code| { + code + }) + } + } + }); + rt.shutdown_timeout(Duration::from_millis(1)); // all tasks should be done in a command anyway, this is just a sanity check + code +} + +/// Send a request to the API with support for HTTP digest authentication. +/// The data option is for PUT and POST requests. For GET it is ignored. This will try to +/// authenticate if a WWW-Authorized header is sent in an unauthorized response. If authentication +/// with auth_token fails, IncorrectAuthTokenError is returned as an error. If the request is +/// unauthorizred and no WWW-Authorired header is present, a normal response is returned. The +/// caller must always check the response status code. +pub(crate) async fn request(client: &HttpClient, method: Method, uri: Uri, data: Option<&[u8]>, auth_token: &str) -> Result, Box> { + let body: Vec = data.map_or_else(|| Vec::new(), |data| data.to_vec()); + + let req = Request::builder().method(&method).version(hyper::Version::HTTP_11).uri(&uri).body(Body::from(body.clone())); + if req.is_err() { + return Err(Box::new(req.err().unwrap())); + } + let res = client.request(req.unwrap()).await; + if res.is_err() { + return Err(Box::new(res.err().unwrap())); + } + let res = res.unwrap(); + + if res.status() == StatusCode::UNAUTHORIZED { + let auth = res.headers().get(hyper::header::WWW_AUTHENTICATE); + if auth.is_none() { + return Ok(res); + } + let auth = auth.unwrap().to_str(); + if auth.is_err() { + return Err(Box::new(auth.err().unwrap())); + } + let auth = digest_auth::parse(auth.unwrap()); + if auth.is_err() { + return Err(Box::new(auth.err().unwrap())); + } + let ac = digest_auth::AuthContext::new_with_method("", auth_token, uri.to_string(), None::<&[u8]>, match method { + Method::GET => digest_auth::HttpMethod::GET, + Method::POST => digest_auth::HttpMethod::POST, + Method::HEAD => digest_auth::HttpMethod::HEAD, + Method::PUT => digest_auth::HttpMethod::OTHER("PUT"), + Method::DELETE => digest_auth::HttpMethod::OTHER("DELETE"), + _ => digest_auth::HttpMethod::OTHER(""), + }); + let auth = auth.unwrap().respond(&ac); + if auth.is_err() { + return Err(Box::new(auth.err().unwrap())); + } + + let req = Request::builder().method(&method).version(hyper::Version::HTTP_11).uri(&uri).header(hyper::header::AUTHORIZATION, auth.unwrap().to_header_string()).body(Body::from(body)); + if req.is_err() { + return Err(Box::new(req.err().unwrap())); + } + let res = client.request(req.unwrap()).await; + if res.is_err() { + return Err(Box::new(res.err().unwrap())); + } + let res = res.unwrap(); + + if res.status() == StatusCode::UNAUTHORIZED { + return Err(Box::new(IncorrectAuthTokenError)); + } + + return Ok(res); + } + + return Ok(res); +} + +/// Append to a URI path, returning None on error or a new Uri. +pub(crate) fn append_uri_path(uri: Uri, new_path: &str) -> Option { + let parts = uri.into_parts(); + let mut path = parts.path_and_query.map_or_else(|| String::new(), |pq| pq.to_string()); + while path.ends_with("/") { + let _ = path.pop(); + } + path.push_str(new_path); + let path = PathAndQuery::from_str(path.as_str()); + if path.is_err() { + None + } else { + Uri::builder() + .scheme(parts.scheme.unwrap_or(Scheme::HTTP)) + .authority(parts.authority.unwrap_or(Authority::from_static("127.0.0.1"))) + .path_and_query(path.unwrap()) + .build() + .map_or_else(|_| None, |uri| Some(uri)) + } +} + +/// Read HTTP body with a size limit. +pub(crate) async fn read_body_limited(body: &mut Body, max_size: usize) -> Result, Box> { + let mut data: Vec = Vec::new(); + loop { + let blk = body.next().await; + if blk.is_some() { + let blk = blk.unwrap(); + if blk.is_err() { + return Err(Box::new(blk.err().unwrap())); + } + for b in blk.unwrap().iter() { + data.push(*b); + if data.len() >= max_size { + return Ok(data); + } + } + } else { + break; + } + } + Ok(data) +} + +pub(crate) async fn read_object_limited(body: &mut Body, max_size: usize) -> Result> { + let data = read_body_limited(body, max_size).await?; + let obj = serde_json::from_slice(data.as_slice()); + if obj.is_err() { + Err(Box::new(obj.err().unwrap())) + } else { + Ok(obj.unwrap()) + } +} diff --git a/zerotier-system-service/src/httplistener.rs b/zerotier-system-service/src/httplistener.rs new file mode 100644 index 000000000..37da33536 --- /dev/null +++ b/zerotier-system-service/src/httplistener.rs @@ -0,0 +1,206 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::cell::Cell; +use std::convert::Infallible; +use std::sync::Arc; +use std::net::SocketAddr; + +use hyper::{Body, Request, Response, StatusCode, Method}; +use hyper::server::Server; +use hyper::service::{make_service_fn, service_fn}; +use tokio::task::JoinHandle; +use digest_auth::{AuthContext, AuthorizationHeader, Charset, WwwAuthenticateHeader}; + +use crate::service::Service; +use crate::api; +use crate::utils::{decrypt_http_auth_nonce, ms_since_epoch, create_http_auth_nonce}; + +#[cfg(target_os = "linux")] +use std::os::unix::io::AsRawFd; + +const HTTP_MAX_NONCE_AGE_MS: i64 = 30000; + +/// Listener for http connections to the API or for TCP P2P. +/// Dropping a listener initiates shutdown of the background hyper Server instance, +/// but it might not shut down instantly as this occurs asynchronously. +pub(crate) struct HttpListener { + pub address: SocketAddr, + shutdown_tx: Cell>>, + server: JoinHandle>, +} + +async fn http_handler(service: Arc, req: Request) -> Result, Infallible> { + let req_path = req.uri().path(); + + let mut authorized = false; + let mut stale = false; + + let auth_token = service.store().auth_token(false); + if auth_token.is_err() { + return Ok(Response::builder().status(StatusCode::INTERNAL_SERVER_ERROR).body(Body::from("authorization token unreadable")).unwrap()); + } + let auth_context = AuthContext::new_with_method("", auth_token.unwrap(), req.uri().to_string(), None::<&[u8]>, match *req.method() { + Method::GET => digest_auth::HttpMethod::GET, + Method::POST => digest_auth::HttpMethod::POST, + Method::HEAD => digest_auth::HttpMethod::HEAD, + Method::PUT => digest_auth::HttpMethod::OTHER("PUT"), + Method::DELETE => digest_auth::HttpMethod::OTHER("DELETE"), + _ => { + return Ok(Response::builder().status(StatusCode::METHOD_NOT_ALLOWED).body(Body::from("unrecognized method")).unwrap()); + } + }); + + let auth_header = req.headers().get(hyper::header::AUTHORIZATION); + if auth_header.is_some() { + let auth_header = AuthorizationHeader::parse(auth_header.unwrap().to_str().unwrap_or("")); + if auth_header.is_err() { + return Ok(Response::builder().status(StatusCode::BAD_REQUEST).body(Body::from(format!("invalid authorization header: {}", auth_header.err().unwrap().to_string()))).unwrap()); + } + let auth_header = auth_header.unwrap(); + + let mut expected = AuthorizationHeader { + realm: "zerotier-service-api".to_owned(), + nonce: auth_header.nonce.clone(), + opaque: None, + userhash: false, + algorithm: digest_auth::Algorithm::new(digest_auth::AlgorithmType::SHA2_512_256, false), + response: String::new(), + username: String::new(), + uri: req.uri().to_string(), + qop: Some(digest_auth::Qop::AUTH), + cnonce: auth_header.cnonce.clone(), + nc: auth_header.nc, + }; + expected.digest(&auth_context); + if auth_header.response == expected.response { + if (ms_since_epoch() - decrypt_http_auth_nonce(auth_header.nonce.as_str())) <= HTTP_MAX_NONCE_AGE_MS { + authorized = true; + } else { + stale = true; + } + } + } + + if authorized { + let (status, body) = + if req_path == "/status" { + api::status(service, req) + } else if req_path == "/config" { + api::config(service, req) + } else if req_path.starts_with("/peer") { + api::peer(service, req) + } else if req_path.starts_with("/network") { + api::network(service, req) + } else if req_path.starts_with("/controller") { + (StatusCode::NOT_IMPLEMENTED, Body::from("not implemented yet")) + } else if req_path == "/teapot" { + (StatusCode::IM_A_TEAPOT, Body::from("I'm a little teapot short and stout!")) + } else { + (StatusCode::NOT_FOUND, Body::from("not found")) + }; + Ok(Response::builder().header("Content-Type", "application/json").status(status).body(body).unwrap()) + } else { + Ok(Response::builder().header(hyper::header::WWW_AUTHENTICATE, WwwAuthenticateHeader { + domain: None, + realm: "zerotier-service-api".to_owned(), + nonce: create_http_auth_nonce(ms_since_epoch()), + opaque: None, + stale, + algorithm: digest_auth::Algorithm::new(digest_auth::AlgorithmType::SHA2_512_256, false), + qop: Some(vec![digest_auth::Qop::AUTH]), + userhash: false, + charset: Charset::ASCII, + nc: 0, + }.to_string()).status(StatusCode::UNAUTHORIZED).body(Body::empty()).unwrap()) + } +} + +impl HttpListener { + /// Create a new "background" TCP WebListener using the current tokio reactor async runtime. + pub async fn new(_device_name: &str, address: SocketAddr, service: &Arc) -> Result> { + let listener = if address.is_ipv4() { + let listener = socket2::Socket::new(socket2::Domain::ipv4(), socket2::Type::stream(), Some(socket2::Protocol::tcp())); + if listener.is_err() { + return Err(Box::new(listener.err().unwrap())); + } + let listener = listener.unwrap(); + #[cfg(unix)] { + let _ = listener.set_reuse_port(true); + } + listener + } else { + let listener = socket2::Socket::new(socket2::Domain::ipv6(), socket2::Type::stream(), Some(socket2::Protocol::tcp())); + if listener.is_err() { + return Err(Box::new(listener.err().unwrap())); + } + let listener = listener.unwrap(); + #[cfg(unix)] { + let _ = listener.set_reuse_port(true); + } + let _ = listener.set_only_v6(true); + listener + }; + + #[cfg(target_os = "linux")] { + if !_device_name.is_empty() { + let sock = listener.as_raw_fd(); + unsafe { + let _ = std::ffi::CString::new(_device_name).map(|dn| { + let dnb = dn.as_bytes_with_nul(); + let _ = crate::osdep::setsockopt(sock as std::os::raw::c_int, crate::osdep::SOL_SOCKET as std::os::raw::c_int, crate::osdep::SO_BINDTODEVICE as std::os::raw::c_int, dnb.as_ptr().cast(), (dnb.len() - 1) as crate::osdep::socklen_t); + }); + } + } + } + + let addr = socket2::SockAddr::from(address); + if let Err(e) = listener.bind(&addr) { + return Err(Box::new(e)); + } + if let Err(e) = listener.listen(128) { + return Err(Box::new(e)); + } + let listener = listener.into_tcp_listener(); + + let builder = Server::from_tcp(listener); + if builder.is_err() { + return Err(Box::new(builder.err().unwrap())); + } + let builder = builder.unwrap().http1_half_close(false).http1_keepalive(true).http1_max_buf_size(131072); + + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); + let service = service.clone(); + let server = tokio::task::spawn(builder.serve(make_service_fn(move |_| { + let service = service.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |req: Request| http_handler(service.clone(), req))) + } + })).with_graceful_shutdown(async { let _ = shutdown_rx.await; })); + + Ok(HttpListener { + address, + shutdown_tx: Cell::new(Some(shutdown_tx)), + server, + }) + } +} + +impl Drop for HttpListener { + fn drop(&mut self) { + let _ = self.shutdown_tx.take().map(|tx| { + let _ = tx.send(()); + self.server.abort(); + }); + } +} diff --git a/zerotier-system-service/src/localconfig.rs b/zerotier-system-service/src/localconfig.rs new file mode 100644 index 000000000..a1d2083a5 --- /dev/null +++ b/zerotier-system-service/src/localconfig.rs @@ -0,0 +1,230 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::collections::BTreeMap; +use zerotier_core::{InetAddress, Address, NetworkId}; +use serde::{Deserialize, Serialize}; + +pub const UNASSIGNED_PRIVILEGED_PORTS: [u16; 299] = [ + 4, + 6, + 8, + 10, + 12, + 14, + 15, + 16, + 26, + 28, + 30, + 32, + 34, + 36, + 40, + 60, + 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, + 285, + 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, + 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, + 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, + 703, + 708, + 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, + 732, 733, 734, 735, 736, 737, 738, 739, 740, + 743, + 745, 746, + 755, 756, + 766, + 768, + 778, 779, + 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, + 802, 803, 804, 805, 806, 807, 808, 809, + 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, + 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, + 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, + 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, + 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, + 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, + 904, 905, 906, 907, 908, 909, 910, 911, + 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, + 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, + 1023, +]; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(default)] +pub struct LocalConfigPhysicalPathConfig { + pub blacklist: bool +} + +impl Default for LocalConfigPhysicalPathConfig { + fn default() -> Self { + LocalConfigPhysicalPathConfig { + blacklist: false + } + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(default)] +pub struct LocalConfigVirtualConfig { + #[serde(rename = "try")] + pub try_: Vec +} + +impl Default for LocalConfigVirtualConfig { + fn default() -> Self { + LocalConfigVirtualConfig { + try_: Vec::new() + } + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(default)] +pub struct LocalConfigNetworkSettings { + #[serde(rename = "allowManagedIPs")] + pub allow_managed_ips: bool, + #[serde(rename = "allowGlobalIPs")] + pub allow_global_ips: bool, + #[serde(rename = "allowManagedRoutes")] + pub allow_managed_routes: bool, + #[serde(rename = "allowGlobalRoutes")] + pub allow_global_routes: bool, + #[serde(rename = "allowDefaultRouteOverride")] + pub allow_default_route_override: bool, +} + +impl Default for LocalConfigNetworkSettings { + fn default() -> Self { + LocalConfigNetworkSettings { + allow_managed_ips: true, + allow_global_ips: false, + allow_managed_routes: true, + allow_global_routes: false, + allow_default_route_override: false + } + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(default)] +pub struct LocalConfigLogSettings { + pub path: Option, + #[serde(rename = "maxSize")] + pub max_size: usize, + pub vl1: bool, + pub vl2: bool, + #[serde(rename = "vl2TraceRules")] + pub vl2_trace_rules: bool, + #[serde(rename = "vl2TraceMulticast")] + pub vl2_trace_multicast: bool, + pub debug: bool, + pub stderr: bool, +} + +impl Default for LocalConfigLogSettings { + fn default() -> Self { + // TODO: change before release to saner defaults + LocalConfigLogSettings { + path: None, + max_size: 131072, + vl1: true, + vl2: true, + vl2_trace_rules: true, + vl2_trace_multicast: true, + debug: true, + stderr: true, + } + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(default)] +pub struct LocalConfigSettings { + #[serde(rename = "primaryPort")] + pub primary_port: u16, + #[serde(rename = "secondaryPort")] + pub secondary_port: Option, + #[serde(rename = "autoPortSearch")] + pub auto_port_search: bool, + #[serde(rename = "portMapping")] + pub port_mapping: bool, + #[serde(rename = "log")] + pub log: LocalConfigLogSettings, + #[serde(rename = "interfacePrefixBlacklist")] + pub interface_prefix_blacklist: Vec, + #[serde(rename = "explicitAddresses")] + pub explicit_addresses: Vec, +} + +impl Default for LocalConfigSettings { + fn default() -> Self { + let mut bl: Vec = Vec::new(); + bl.reserve(LocalConfigSettings::DEFAULT_PREFIX_BLACKLIST.len()); + for n in LocalConfigSettings::DEFAULT_PREFIX_BLACKLIST.iter() { + bl.push(String::from(*n)); + } + + LocalConfigSettings { + primary_port: zerotier_core::DEFAULT_PORT, + secondary_port: Some(zerotier_core::DEFAULT_SECONDARY_PORT), + auto_port_search: true, + port_mapping: true, + log: LocalConfigLogSettings::default(), + interface_prefix_blacklist: bl, + explicit_addresses: Vec::new() + } + } +} + +impl LocalConfigSettings { + #[cfg(target_os = "macos")] + const DEFAULT_PREFIX_BLACKLIST: [&'static str; 8] = ["lo", "utun", "gif", "stf", "iptap", "pktap", "feth", "zt"]; + + #[cfg(target_os = "linux")] + const DEFAULT_PREFIX_BLACKLIST: [&'static str; 5] = ["lo", "tun", "tap", "ipsec", "zt"]; + + #[cfg(windows)] + const DEFAULT_PREFIX_BLACKLIST: [&'static str; 0] = []; + + pub fn is_interface_blacklisted(&self, ifname: &str) -> bool { + for p in self.interface_prefix_blacklist.iter() { + if ifname.starts_with(p.as_str()) { + return true; + } + } + false + } +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(default)] +pub struct LocalConfig { + pub physical: BTreeMap, + #[serde(rename = "virtual")] + pub virtual_: BTreeMap, + pub network: BTreeMap, + pub settings: LocalConfigSettings, +} + +impl Default for LocalConfig { + fn default() -> Self { + LocalConfig { + physical: BTreeMap::new(), + virtual_: BTreeMap::new(), + network: BTreeMap::new(), + settings: LocalConfigSettings::default() + } + } +} diff --git a/zerotier-system-service/src/log.rs b/zerotier-system-service/src/log.rs new file mode 100644 index 000000000..d68359af5 --- /dev/null +++ b/zerotier-system-service/src/log.rs @@ -0,0 +1,170 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::fs::{File, OpenOptions}; +use std::io::{Seek, SeekFrom, Write, stderr}; +use std::sync::Mutex; + +struct LogIntl { + prefix: String, + path: String, + file: Option, + cur_size: u64, + max_size: usize, + log_to_stderr: bool, + debug: bool, +} + +/// It's big it's heavy it's wood. +pub(crate) struct Log { + inner: Mutex, +} + +impl Log { + const MIN_MAX_SIZE: usize = 1024; + + /// Construct a new logger. + /// If path is empty logs will not be written to files. If log_to_stderr is also + /// false then no logs will be output at all. + pub fn new(path: &str, max_size: usize, log_to_stderr: bool, debug: bool, prefix: &str) -> Log { + let mut p = String::from(prefix); + if !p.is_empty() { + p.push(' '); + } + Log{ + inner: Mutex::new(LogIntl { + prefix: p, + path: String::from(path), + file: None, + cur_size: 0, + max_size: if max_size < Log::MIN_MAX_SIZE { Log::MIN_MAX_SIZE } else { max_size }, + log_to_stderr, + debug, + }), + } + } + + pub fn set_max_size(&self, new_max_size: usize) { + self.inner.lock().unwrap().max_size = if new_max_size < Log::MIN_MAX_SIZE { Log::MIN_MAX_SIZE } else { new_max_size }; + } + + pub fn set_log_to_stderr(&self, log_to_stderr: bool) { + self.inner.lock().unwrap().log_to_stderr = log_to_stderr; + } + + pub fn set_debug(&self, debug: bool) { + self.inner.lock().unwrap().debug = debug; + } + + fn log_internal(&self, l: &mut LogIntl, s: &str, pfx: &'static str) { + if !s.is_empty() { + let log_line = format!("{}[{}] {}{}\n", l.prefix.as_str(), chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(), pfx, s); + if !l.path.is_empty() { + if l.file.is_none() { + let f = OpenOptions::new().read(true).write(true).create(true).open(l.path.as_str()); + if f.is_err() { + return; + } + let mut f = f.unwrap(); + let eof = f.seek(SeekFrom::End(0)); + if eof.is_err() { + return; + } + l.cur_size = eof.unwrap(); + l.file = Some(f); + } + + if l.max_size > 0 && l.cur_size > l.max_size as u64 { + l.file = None; + l.cur_size = 0; + + let mut old_path = l.path.clone(); + old_path.push_str(".old"); + let _ = std::fs::remove_file(old_path.as_str()); + let _ = std::fs::rename(l.path.as_str(), old_path.as_str()); + let _ = std::fs::remove_file(l.path.as_str()); // should fail + + let f = OpenOptions::new().read(true).write(true).create(true).open(l.path.as_str()); + if f.is_err() { + return; + } + l.file = Some(f.unwrap()); + } + + let f = l.file.as_mut().unwrap(); + let e = f.write_all(log_line.as_bytes()); + if e.is_err() { + eprintln!("ERROR: I/O error writing to log: {}", e.err().unwrap().to_string()); + l.file = None; + } else { + let _ = f.flush(); + l.cur_size += log_line.len() as u64; + } + } + + if l.log_to_stderr { + let _ = stderr().write_all(log_line.as_bytes()); + } + } + } + + pub fn log>(&self, s: S) { + let mut l = self.inner.lock().unwrap(); + self.log_internal(&mut (*l), s.as_ref(), ""); + } + + pub fn debug>(&self, s: S) { + let mut l = self.inner.lock().unwrap(); + if l.debug { + self.log_internal(&mut (*l), s.as_ref(), "DEBUG: "); + } + } + + pub fn fatal>(&self, s: S) { + let mut l = self.inner.lock().unwrap(); + let ss = s.as_ref(); + self.log_internal(&mut (*l), ss, "FATAL: "); + eprintln!("FATAL: {}", ss); + } +} + +#[macro_export] +macro_rules! l( + ($logger:expr, $($arg:tt)*) => { + $logger.log(format!($($arg)*)) + } +); + +#[macro_export] +macro_rules! d( + ($logger:expr, $($arg:tt)*) => { + $logger.debug(format!($($arg)*)) + } +); + +unsafe impl Sync for Log {} + +/* +#[cfg(test)] +mod tests { + use crate::log::Log; + + #[test] + fn test_log() { + let l = Log::new("/tmp/ztlogtest.log", 65536, ""); + for i in 0..100000 { + l.log(format!("line {}", i)) + } + } +} +*/ diff --git a/zerotier-system-service/src/main.rs b/zerotier-system-service/src/main.rs new file mode 100644 index 000000000..c064312c6 --- /dev/null +++ b/zerotier-system-service/src/main.rs @@ -0,0 +1,300 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +mod api; +mod commands; +mod fastudpsocket; +mod localconfig; +mod getifaddrs; +#[macro_use] +mod log; +mod store; +mod network; +mod vnic; +mod service; +mod utils; +mod httplistener; +mod httpclient; + +use std::io::Write; +use std::sync::Arc; +use std::str::FromStr; + +use clap::{App, Arg, ArgMatches, ErrorKind}; + +use crate::store::Store; + +pub const HTTP_API_OBJECT_SIZE_LIMIT: usize = 131072; + +fn make_help(long_help: bool) -> String { + let ver = zerotier_core::version(); + format!(r###"ZeroTier Network Hypervisor Service Version {}.{}.{} +(c)2013-2021 ZeroTier, Inc. +Licensed under the ZeroTier BSL (see LICENSE.txt) + +Usage: zerotier [-...] [command args] + +Global Options: + + -j Output raw JSON where applicable + -p Use alternate base path + -t Load secret auth token from a file + -T Set secret token on command line + +Common Operations: + + help Show this help + longhelp Show help with advanced commands + oldhelp Show v1.x legacy commands + version Print version (of this binary) + +· status Show node status and configuration + +· set [setting] [value] List all settings (with no args) +· port Primary P2P port +· secondaryport Secondary P2P port (0 to disable) +· blacklist cidr Toggle physical path blacklisting +· blacklist if [Un]blacklist interface prefix +· portmap Toggle use of uPnP and NAT-PMP + +· peer [option] +· show
Show detailed peer information +· list List peers +· listroots List root peers +· try
[...] Try peer at explicit endpoint + +· network [option] +· show Show detailed network information +· list List networks +· set [option] [value] Get or set network options +· manageips Is IP management allowed? +· manageroutes Is route management allowed? +· managedns Allow network to push DNS config +· globalips Allow assignment of global IPs? +· globalroutes Can global IP routes be set? +· defaultroute Can default route be overridden? + +· join Join a virtual network +· leave Leave a virtual network +{}"###, + ver.0, ver.1, ver.2, if long_help { + r###" +Advanced Operations: + + service Start node + (usually not invoked directly) + + controller [option] +· list List networks on controller +· new Create a new network +· set [setting] [value] Show or modify network settings +· show [
] Show network or member status +· auth
Authorize a peer +· deauth
Deauthorize a peer + + identity [args] + new [c25519 | p384] Create identity (default: c25519) + getpublic Extract public part of identity + fingerprint Get an identity's fingerprint + validate Locally validate an identity + sign <@file> Sign a file with an identity's key + verify <@file> Verify a signature + + · Command (or command with argument type) requires a running node. + @ Argument is the path to a file containing the object. + ? Argument can be either the object or a path to it (auto-detected). +"### + } else { "" }) +} + +pub(crate) fn print_help(long_help: bool) { + let h = make_help(long_help); + let _ = std::io::stdout().write_all(h.as_bytes()); +} + +pub(crate) fn parse_bool(v: &str) -> Result { + if !v.is_empty() { + match v.chars().next().unwrap() { + 'y' | 'Y' | '1' | 't' | 'T' => { return Ok(true); } + 'n' | 'N' | '0' | 'f' | 'F' => { return Ok(false); } + _ => {} + } + } + Err(format!("invalid boolean value: '{}'", v)) +} + +#[inline(always)] +fn is_valid_bool(v: String) -> Result<(), String> { + parse_bool(v.as_str()).map(|_| ()) +} + +fn is_valid_port(v: String) -> Result<(), String> { + let i = u16::from_str(v.as_str()).unwrap_or(0); + if i >= 1 { + return Ok(()); + } + Err(format!("invalid TCP/IP port number: {}", v)) +} + +fn make_store(cli_args: &ArgMatches) -> Arc { + let zerotier_path = cli_args.value_of("path").map_or_else(|| unsafe { zerotier_core::cstr_to_string(osdep::platformDefaultHomePath(), -1) }, |ztp| ztp.to_string()); + let store = Store::new(zerotier_path.as_str(), cli_args.value_of("token_path").map_or(None, |tp| Some(tp.to_string())), cli_args.value_of("token").map_or(None, |tok| Some(tok.trim().to_string()))); + if store.is_err() { + eprintln!("FATAL: error accessing directory '{}': {}", zerotier_path, store.err().unwrap().to_string()); + std::process::exit(1); + } + Arc::new(store.unwrap()) +} + +#[derive(Clone)] +pub(crate) struct GlobalFlags { + pub json_output: bool, +} + +#[inline(always)] +fn get_global_flags(cli_args: &ArgMatches) -> GlobalFlags { + GlobalFlags { + json_output: cli_args.is_present("json") + } +} + +fn main() { + let cli_args = { + let help = make_help(false); + let args = App::new("zerotier") + .arg(Arg::with_name("json").short("j")) + .arg(Arg::with_name("path").short("p").takes_value(true)) + .arg(Arg::with_name("token_path").short("t").takes_value(true)) + .arg(Arg::with_name("token").short("T").takes_value(true)) + .subcommand(App::new("help")) + .subcommand(App::new("version")) + .subcommand(App::new("status")) + .subcommand(App::new("set") + .subcommand(App::new("port") + .arg(Arg::with_name("port#").index(1).validator(is_valid_port))) + .subcommand(App::new("secondaryport") + .arg(Arg::with_name("port#").index(1).validator(is_valid_port))) + .subcommand(App::new("blacklist") + .subcommand(App::new("cidr") + .arg(Arg::with_name("ip_bits").index(1)) + .arg(Arg::with_name("boolean").index(2).validator(is_valid_bool))) + .subcommand(App::new("if") + .arg(Arg::with_name("prefix").index(1)) + .arg(Arg::with_name("boolean").index(2).validator(is_valid_bool)))) + .subcommand(App::new("portmap") + .arg(Arg::with_name("boolean").index(1).validator(is_valid_bool)))) + .subcommand(App::new("peer") + .subcommand(App::new("show") + .arg(Arg::with_name("address").index(1).required(true))) + .subcommand(App::new("list")) + .subcommand(App::new("listroots")) + .subcommand(App::new("try"))) + .subcommand(App::new("network") + .subcommand(App::new("show") + .arg(Arg::with_name("nwid").index(1).required(true))) + .subcommand(App::new("list")) + .subcommand(App::new("set") + .arg(Arg::with_name("nwid").index(1).required(true)) + .arg(Arg::with_name("setting").index(2).required(false)) + .arg(Arg::with_name("value").index(3).required(false)))) + .subcommand(App::new("join") + .arg(Arg::with_name("nwid").index(1).required(true))) + .subcommand(App::new("leave") + .arg(Arg::with_name("nwid").index(1).required(true))) + .subcommand(App::new("service")) + .subcommand(App::new("controller") + .subcommand(App::new("list")) + .subcommand(App::new("new")) + .subcommand(App::new("set") + .arg(Arg::with_name("id").index(1).required(true)) + .arg(Arg::with_name("setting").index(2)) + .arg(Arg::with_name("value").index(3))) + .subcommand(App::new("show") + .arg(Arg::with_name("id").index(1).required(true)) + .arg(Arg::with_name("member").index(2))) + .subcommand(App::new("auth") + .arg(Arg::with_name("member").index(1).required(true))) + .subcommand(App::new("deauth") + .arg(Arg::with_name("member").index(1).required(true)))) + .subcommand(App::new("identity") + .subcommand(App::new("new") + .arg(Arg::with_name("type").possible_value("p384").possible_value("c25519").default_value("c25519").index(1))) + .subcommand(App::new("getpublic") + .arg(Arg::with_name("identity").index(1).required(true))) + .subcommand(App::new("fingerprint") + .arg(Arg::with_name("identity").index(1).required(true))) + .subcommand(App::new("validate") + .arg(Arg::with_name("identity").index(1).required(true))) + .subcommand(App::new("sign") + .arg(Arg::with_name("identity").index(1).required(true)) + .arg(Arg::with_name("path").index(2).required(true))) + .subcommand(App::new("verify") + .arg(Arg::with_name("identity").index(1).required(true)) + .arg(Arg::with_name("path").index(2).required(true)) + .arg(Arg::with_name("signature").index(3).required(true)))) + .help(help.as_str()) + .get_matches_from_safe(std::env::args()); + if args.is_err() { + let e = args.err().unwrap(); + if e.kind != ErrorKind::HelpDisplayed { + print_help(false); + } + std::process::exit(1); + } + let args = args.unwrap(); + if args.subcommand_name().is_none() { + print_help(false); + std::process::exit(1); + } + args + }; + + std::process::exit({ + match cli_args.subcommand() { + ("help", None) => { + print_help(false); + 0 + } + ("longhelp", None) => { + print_help(true); + 0 + } + ("oldhelp", None) => { + // TODO + 0 + } + ("version", None) => { + let ver = zerotier_core::version(); + println!("{}.{}.{}", ver.0, ver.1, ver.2); + 0 + } + ("status", None) => crate::httpclient::run_command(make_store(&cli_args), get_global_flags(&cli_args), crate::commands::status::run), + ("set", Some(sub_cli_args)) => { 0 } + ("peer", Some(sub_cli_args)) => { 0 } + ("network", Some(sub_cli_args)) => { 0 } + ("join", Some(sub_cli_args)) => { 0 } + ("leave", Some(sub_cli_args)) => { 0 } + ("service", None) => { + let store = make_store(&cli_args); + drop(cli_args); // free no longer needed memory before entering service + service::run(store) + } + ("controller", Some(sub_cli_args)) => { 0 } + ("identity", Some(sub_cli_args)) => crate::commands::identity::run(sub_cli_args), + _ => { + print_help(false); + 1 + } + } + }); +} diff --git a/zerotier-system-service/src/network.rs b/zerotier-system-service/src/network.rs new file mode 100644 index 000000000..501ecea96 --- /dev/null +++ b/zerotier-system-service/src/network.rs @@ -0,0 +1,17 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +pub struct Network {} + +impl Network { +} diff --git a/zerotier-system-service/src/service.rs b/zerotier-system-service/src/service.rs new file mode 100644 index 000000000..aa528967a --- /dev/null +++ b/zerotier-system-service/src/service.rs @@ -0,0 +1,515 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::cell::Cell; +use std::collections::BTreeMap; +use std::net::{SocketAddr, Ipv4Addr, IpAddr, Ipv6Addr}; +use std::sync::{Arc, Mutex, Weak}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use zerotier_core::*; +use zerotier_core::trace::{TraceEvent, TraceEventLayer}; +use futures::StreamExt; +use serde::{Serialize, Deserialize}; + +use crate::fastudpsocket::*; +use crate::getifaddrs; +use crate::localconfig::*; +use crate::log::Log; +use crate::network::Network; +use crate::store::Store; +use crate::utils::{ms_since_epoch, ms_monotonic}; +use crate::httplistener::HttpListener; + +/// How often to check for major configuration changes. This shouldn't happen +/// too often since it uses a bit of CPU. +const CONFIG_CHECK_INTERVAL: i64 = 5000; + +/// ServiceStatus is the object returned by the API /status endpoint +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct ServiceStatus { + #[serde(rename = "objectType")] + pub object_type: String, + pub address: Address, + pub clock: i64, + #[serde(rename = "startTime")] + pub start_time: i64, + pub uptime: i64, + pub config: LocalConfig, + pub online: bool, + #[serde(rename = "publicIdentity")] + pub public_identity: Identity, + pub version: String, + #[serde(rename = "versionMajor")] + pub version_major: i32, + #[serde(rename = "versionMinor")] + pub version_minor: i32, + #[serde(rename = "versionRev")] + pub version_revision: i32, + #[serde(rename = "versionBuild")] + pub version_build: i32, + #[serde(rename = "udpLocalEndpoints")] + pub udp_local_endpoints: Vec, + #[serde(rename = "httpLocalEndpoints")] + pub http_local_endpoints: Vec, +} + +/// Core ZeroTier service, which is sort of just a container for all the things. +pub(crate) struct Service { + pub(crate) log: Log, + _node: Cell, Network, Service>>>, // never modified after node is created + udp_local_endpoints: Mutex>, + http_local_endpoints: Mutex>, + interrupt: Mutex>, + local_config: Mutex>, + store: Arc, + startup_time: i64, + startup_time_monotonic: i64, + run: AtomicBool, + online: AtomicBool, +} + +impl NodeEventHandler for Service { + #[inline(always)] + fn virtual_network_config(&self, network_id: NetworkId, network_obj: &Network, config_op: VirtualNetworkConfigOperation, config: Option<&VirtualNetworkConfig>) {} + + #[inline(always)] + fn virtual_network_frame(&self, network_id: NetworkId, network_obj: &Network, source_mac: MAC, dest_mac: MAC, ethertype: u16, vlan_id: u16, data: &[u8]) {} + + #[inline(always)] + fn event(&self, event: Event, event_data: &[u8]) { + match event { + Event::Up => { + d!(self.log, "node startup event received."); + } + + Event::Down => { + d!(self.log, "node shutdown event received."); + self.online.store(false, Ordering::Relaxed); + } + + Event::Online => { + d!(self.log, "node is online."); + self.online.store(true, Ordering::Relaxed); + } + + Event::Offline => { + d!(self.log, "node is offline."); + self.online.store(false, Ordering::Relaxed); + } + + Event::Trace => { + if !event_data.is_empty() { + let _ = Dictionary::new_from_bytes(event_data).map(|tm| { + let tm = TraceEvent::parse_message(&tm); + let _ = tm.map(|tm: TraceEvent| { + let local_config = self.local_config(); + if match tm.layer() { + TraceEventLayer::VL1 => local_config.settings.log.vl1, + TraceEventLayer::VL2 => local_config.settings.log.vl2, + TraceEventLayer::VL2Filter => local_config.settings.log.vl2_trace_rules, + TraceEventLayer::VL2Multicast => local_config.settings.log.vl2_trace_multicast, + _ => true, + } { + self.log.log(tm.to_string()); + } + }); + }); + } + } + + Event::UserMessage => {} + } + } + + #[inline(always)] + fn state_put(&self, obj_type: StateObjectType, obj_id: &[u64], obj_data: &[u8]) -> std::io::Result<()> { + if !obj_data.is_empty() { + self.store.store_object(&obj_type, obj_id, obj_data) + } else { + self.store.erase_object(&obj_type, obj_id); + Ok(()) + } + } + + #[inline(always)] + fn state_get(&self, obj_type: StateObjectType, obj_id: &[u64]) -> std::io::Result> { + self.store.load_object(&obj_type, obj_id) + } + + #[inline(always)] + fn wire_packet_send(&self, local_socket: i64, sock_addr: &InetAddress, data: &[u8], packet_ttl: u32) -> i32 { + 0 + } + + #[inline(always)] + fn path_check(&self, _: Address, _: &Identity, _: i64, _: &InetAddress) -> bool { + true + } + + #[inline(always)] + fn path_lookup(&self, address: Address, id: &Identity, desired_family: InetAddressFamily) -> Option { + let lc = self.local_config(); + lc.virtual_.get(&address).map_or(None, |c: &LocalConfigVirtualConfig| { + if c.try_.is_empty() { + None + } else { + let t = c.try_.get((zerotier_core::random() as usize) % c.try_.len()); + t.map_or(None, |v: &InetAddress| { + d!(self.log, "path lookup for {} returned {}", address.to_string(), v.to_string()); + Some(v.clone()) + }) + } + }) + } +} + +impl Service { + pub fn local_config(&self) -> Arc { + self.local_config.lock().unwrap().clone() + } + + pub fn set_local_config(&self, new_lc: LocalConfig) { + *(self.local_config.lock().unwrap()) = Arc::new(new_lc); + } + + /// Get the node running with this service. + /// This can return None during shutdown because Service holds a weak + /// reference to Node to avoid circular Arc<> pointers. This will only + /// return None during shutdown, in which case whatever is happening + /// should abort as quietly as possible. + pub fn node(&self) -> Option, Network, Service>>> { + unsafe { &*self._node.as_ptr() }.upgrade() + } + + #[inline(always)] + pub fn store(&self) -> &Arc { + &self.store + } + + pub fn online(&self) -> bool { + self.online.load(Ordering::Relaxed) + } + + pub fn shutdown(&self) { + self.run.store(false, Ordering::Relaxed); + let _ = self.interrupt.lock().unwrap().try_send(()); + } + + /// Get service status for API, or None if a shutdown is in progress. + pub fn status(&self) -> Option { + let ver = zerotier_core::version(); + self.node().map(|node| { + ServiceStatus { + object_type: "status".to_owned(), + address: node.address(), + clock: ms_since_epoch(), + start_time: self.startup_time, + uptime: ms_monotonic() - self.startup_time_monotonic, + config: (*self.local_config()).clone(), + online: self.online(), + public_identity: node.identity().clone(), + version: format!("{}.{}.{}", ver.0, ver.1, ver.2), + version_major: ver.0, + version_minor: ver.1, + version_revision: ver.2, + version_build: ver.3, + udp_local_endpoints: self.udp_local_endpoints.lock().unwrap().clone(), + http_local_endpoints: self.http_local_endpoints.lock().unwrap().clone(), + } + }) + } +} + +unsafe impl Send for Service {} + +unsafe impl Sync for Service {} + +async fn run_async(store: Arc, local_config: Arc) -> i32 { + let process_exit_value: i32 = 0; + + let mut udp_sockets: BTreeMap = BTreeMap::new(); + let mut http_listeners: BTreeMap = BTreeMap::new(); + let mut loopback_http_listeners: (Option, Option) = (None, None); // 127.0.0.1, ::1 + + let (interrupt_tx, mut interrupt_rx) = futures::channel::mpsc::channel::<()>(1); + let service = Arc::new(Service { + log: Log::new( + if local_config.settings.log.path.as_ref().is_some() { + local_config.settings.log.path.as_ref().unwrap().as_str() + } else { + store.default_log_path.to_str().unwrap() + }, + local_config.settings.log.max_size, + local_config.settings.log.stderr, + local_config.settings.log.debug, + "", + ), + _node: Cell::new(Weak::new()), + udp_local_endpoints: Mutex::new(Vec::new()), + http_local_endpoints: Mutex::new(Vec::new()), + interrupt: Mutex::new(interrupt_tx), + local_config: Mutex::new(local_config), + store: store.clone(), + startup_time: ms_since_epoch(), + startup_time_monotonic: ms_monotonic(), + run: AtomicBool::new(true), + online: AtomicBool::new(false), + }); + + let node = Node::new(service.clone(), ms_since_epoch(), ms_monotonic()); + if node.is_err() { + service.log.fatal(format!("error initializing node: {}", node.err().unwrap().to_str())); + return 1; + } + let node = Arc::new(node.ok().unwrap()); + service._node.replace(Arc::downgrade(&node)); + + let mut local_config = service.local_config(); + + let mut ticks: i64 = ms_monotonic(); + let mut loop_delay = zerotier_core::NODE_BACKGROUND_TASKS_MAX_INTERVAL; + let mut last_checked_config: i64 = 0; + while service.run.load(Ordering::Relaxed) { + let loop_delay_start = ms_monotonic(); + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(loop_delay as u64)) => { + ticks = ms_monotonic(); + let actual_delay = ticks - loop_delay_start; + if actual_delay > ((loop_delay as i64) * 4_i64) { + l!(service.log, "likely sleep/wake detected due to excessive loop delay, cycling links..."); + // TODO: handle likely sleep/wake or other system interruption + } + }, + _ = interrupt_rx.next() => { + d!(service.log, "inner loop delay interrupted!"); + if !service.run.load(Ordering::Relaxed) { + break; + } + ticks = ms_monotonic(); + }, + _ = tokio::signal::ctrl_c() => { + l!(service.log, "exit signal received, shutting down..."); + service.run.store(false, Ordering::Relaxed); + break; + }, + } + + if (ticks - last_checked_config) >= CONFIG_CHECK_INTERVAL { + last_checked_config = ticks; + + let mut bindings_changed = false; + + let _ = store.read_local_conf(true).map(|new_config| new_config.map(|new_config| { + d!(service.log, "local.conf changed on disk, reloading."); + service.set_local_config(new_config); + })); + + let next_local_config = service.local_config(); + if local_config.settings.primary_port != next_local_config.settings.primary_port { + loopback_http_listeners.0 = None; + loopback_http_listeners.1 = None; + bindings_changed = true; + } + if local_config.settings.log.max_size != next_local_config.settings.log.max_size { + service.log.set_max_size(next_local_config.settings.log.max_size); + } + if local_config.settings.log.stderr != next_local_config.settings.log.stderr { + service.log.set_log_to_stderr(next_local_config.settings.log.stderr); + } + if local_config.settings.log.debug != next_local_config.settings.log.debug { + service.log.set_debug(next_local_config.settings.log.debug); + } + local_config = next_local_config; + + let mut loopback_dev_name = String::new(); + let mut system_addrs: BTreeMap = BTreeMap::new(); + getifaddrs::for_each_address(|addr: &InetAddress, dev: &str| { + match addr.ip_scope() { + IpScope::Global | IpScope::Private | IpScope::PseudoPrivate | IpScope::Shared => { + if !local_config.settings.is_interface_blacklisted(dev) { + let mut a = addr.clone(); + a.set_port(local_config.settings.primary_port); + system_addrs.insert(a, String::from(dev)); + if local_config.settings.secondary_port.is_some() { + let mut a = addr.clone(); + a.set_port(local_config.settings.secondary_port.unwrap()); + system_addrs.insert(a, String::from(dev)); + } + } + }, + IpScope::Loopback => { + if loopback_dev_name.is_empty() { + loopback_dev_name.push_str(dev); + } + }, + _ => {}, + } + }); + + // TODO: need to also inform the core about these IPs... + + for k in udp_sockets.keys().filter_map(|a| if system_addrs.contains_key(a) { None } else { Some(a.clone()) }).collect::>().iter() { + l!(service.log, "unbinding UDP socket at {} (address no longer exists on system or port has changed)", k.to_string()); + udp_sockets.remove(k); + bindings_changed = true; + } + for a in system_addrs.iter() { + if !udp_sockets.contains_key(a.0) { + let _ = FastUDPSocket::new(a.1.as_str(), a.0, |raw_socket: &FastUDPRawOsSocket, from_address: &InetAddress, data: Buffer| { + // TODO: incoming packet handler + }).map_or_else(|e| { + l!(service.log, "error binding UDP socket to {}: {}", a.0.to_string(), e.to_string()); + }, |s| { + l!(service.log, "bound UDP socket at {}", a.0.to_string()); + udp_sockets.insert(a.0.clone(), s); + bindings_changed = true; + }); + } + } + + let mut udp_primary_port_bind_failure = true; + let mut udp_secondary_port_bind_failure = local_config.settings.secondary_port.is_some(); + for s in udp_sockets.iter() { + if s.0.port() == local_config.settings.primary_port { + udp_primary_port_bind_failure = false; + if !udp_secondary_port_bind_failure { + break; + } + } + if s.0.port() == local_config.settings.secondary_port.unwrap() { + udp_secondary_port_bind_failure = false; + if !udp_primary_port_bind_failure { + break; + } + } + } + if udp_primary_port_bind_failure { + if local_config.settings.auto_port_search { + // TODO: port hunting + } else { + l!(service.log, "WARNING: failed to bind to any address at primary port {}", local_config.settings.primary_port); + } + } + if udp_secondary_port_bind_failure { + if local_config.settings.auto_port_search { + // TODO: port hunting + } else { + l!(service.log, "WARNING: failed to bind to any address at secondary port {}", local_config.settings.secondary_port.unwrap_or(0)); + } + } + + for k in http_listeners.keys().filter_map(|a| if system_addrs.contains_key(a) { None } else { Some(a.clone()) }).collect::>().iter() { + l!(service.log, "closing HTTP listener at {} (address no longer exists on system or port has changed)", k.to_string()); + http_listeners.remove(k); + bindings_changed = true; + } + for a in system_addrs.iter() { + if !http_listeners.contains_key(a.0) { + let sa = a.0.to_socketaddr(); + if sa.is_some() { + let wl = HttpListener::new(a.1.as_str(), sa.unwrap(), &service).await.map_or_else(|e| { + l!(service.log, "error creating HTTP listener at {}: {}", a.0.to_string(), e.to_string()); + }, |l| { + l!(service.log, "created HTTP listener at {}", a.0.to_string()); + http_listeners.insert(a.0.clone(), l); + bindings_changed = true; + }); + } + } + } + + if loopback_http_listeners.0.is_none() { + let _ = HttpListener::new(loopback_dev_name.as_str(), SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), local_config.settings.primary_port), &service).await.map(|wl| { + loopback_http_listeners.0 = Some(wl); + let _ = store.write_uri(format!("http://127.0.0.1:{}/", local_config.settings.primary_port).as_str()); + bindings_changed = true; + }); + } + if loopback_http_listeners.1.is_none() { + let _ = HttpListener::new(loopback_dev_name.as_str(), SocketAddr::new(IpAddr::from(Ipv6Addr::LOCALHOST), local_config.settings.primary_port), &service).await.map(|wl| { + loopback_http_listeners.1 = Some(wl); + if loopback_http_listeners.0.is_none() { + let _ = store.write_uri(format!("http://[::1]:{}/", local_config.settings.primary_port).as_str()); + } + bindings_changed = true; + }); + } + if loopback_http_listeners.0.is_none() && loopback_http_listeners.1.is_none() { + // TODO: port hunting + l!(service.log, "CRITICAL: unable to create HTTP endpoint on 127.0.0.1/{} or ::1/{}, service control API will not work!", local_config.settings.primary_port, local_config.settings.primary_port); + } + + if bindings_changed { + { + let mut udp_local_endpoints = service.udp_local_endpoints.lock().unwrap(); + udp_local_endpoints.clear(); + for ep in udp_sockets.iter() { + udp_local_endpoints.push(ep.0.clone()); + } + udp_local_endpoints.sort(); + } + { + let mut http_local_endpoints = service.http_local_endpoints.lock().unwrap(); + http_local_endpoints.clear(); + for ep in http_listeners.iter() { + http_local_endpoints.push(ep.0.clone()); + } + if loopback_http_listeners.0.is_some() { + http_local_endpoints.push(InetAddress::new_ipv4_loopback(loopback_http_listeners.0.as_ref().unwrap().address.port())); + } + if loopback_http_listeners.1.is_some() { + http_local_endpoints.push(InetAddress::new_ipv6_loopback(loopback_http_listeners.1.as_ref().unwrap().address.port())); + } + http_local_endpoints.sort(); + } + } + } + + // Run background task handler in ZeroTier core. + loop_delay = node.process_background_tasks(ms_since_epoch(), ticks); + } + + l!(service.log, "shutting down normally."); + + drop(udp_sockets); + drop(http_listeners); + drop(loopback_http_listeners); + drop(node); + drop(service); + + process_exit_value +} + +pub(crate) fn run(store: Arc) -> i32 { + let local_config = Arc::new(store.read_local_conf_or_default()); + + if store.auth_token(true).is_err() { + eprintln!("FATAL: error writing new web API authorization token (likely permission problem)."); + return 1; + } + if store.write_pid().is_err() { + eprintln!("FATAL: error writing to directory '{}': unable to write zerotier.pid (likely permission problem).", store.base_path.to_str().unwrap()); + return 1; + } + + let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); + let store2 = store.clone(); + let process_exit_value = rt.block_on(async move { run_async(store2, local_config).await }); + rt.shutdown_timeout(Duration::from_millis(500)); + + store.erase_pid(); + + process_exit_value +} diff --git a/zerotier-system-service/src/store.rs b/zerotier-system-service/src/store.rs new file mode 100644 index 000000000..815de4371 --- /dev/null +++ b/zerotier-system-service/src/store.rs @@ -0,0 +1,300 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::str::FromStr; +use std::ffi::CString; + +use zerotier_core::{StateObjectType, NetworkId}; + +use crate::localconfig::LocalConfig; + +const ZEROTIER_PID: &'static str = "zerotier.pid"; +const ZEROTIER_URI: &'static str = "zerotier.uri"; +const LOCAL_CONF: &'static str = "local.conf"; +const AUTHTOKEN_SECRET: &'static str = "authtoken.secret"; +const SERVICE_LOG: &'static str = "service.log"; + +/// In-filesystem data store for configuration and objects. +pub(crate) struct Store { + pub base_path: Box, + pub default_log_path: Box, + prev_local_config: Mutex, + peers_path: Box, + controller_path: Box, + networks_path: Box, + auth_token_path: Mutex>, + auth_token: Mutex, +} + +/// Restrict file permissions using OS-specific code in osdep/OSUtils.cpp. +pub fn lock_down_file(path: &str) { + let p = CString::new(path.as_bytes()); + if p.is_ok() { + let p = p.unwrap(); + unsafe { + crate::osdep::lockDownFile(p.as_ptr(), 0); + } + } +} + +impl Store { + const MAX_OBJECT_SIZE: usize = 262144; // sanity limit + + pub fn new(base_path: &str, auth_token_path_override: Option, auth_token_override: Option) -> std::io::Result { + let bp = Path::new(base_path); + let _ = std::fs::create_dir_all(bp); + let md = bp.metadata()?; + if !md.is_dir() || md.permissions().readonly() { + return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "base path does not exist or is not writable")); + } + + let s = Store { + base_path: bp.to_path_buf().into_boxed_path(), + default_log_path: bp.join(SERVICE_LOG).into_boxed_path(), + prev_local_config: Mutex::new(String::new()), + peers_path: bp.join("peers.d").into_boxed_path(), + controller_path: bp.join("controller.d").into_boxed_path(), + networks_path: bp.join("networks.d").into_boxed_path(), + auth_token_path: Mutex::new(auth_token_path_override.map_or_else(|| { + bp.join(AUTHTOKEN_SECRET).into_boxed_path() + }, |auth_token_path_override| { + PathBuf::from(auth_token_path_override).into_boxed_path() + })), + auth_token: Mutex::new(auth_token_override.map_or_else(|| { + String::new() + }, |auth_token_override| { + auth_token_override + })), + }; + + let _ = std::fs::create_dir_all(&s.peers_path); + let _ = std::fs::create_dir_all(&s.controller_path); + let _ = std::fs::create_dir_all(&s.networks_path); + + Ok(s) + } + + fn make_obj_path_internal(&self, obj_type: &StateObjectType, obj_id: &[u64]) -> Option { + match obj_type { + StateObjectType::IdentityPublic => Some(self.base_path.join("identity.public")), + StateObjectType::IdentitySecret => Some(self.base_path.join("identity.secret")), + StateObjectType::TrustStore => Some(self.base_path.join("truststore")), + StateObjectType::Locator => Some(self.base_path.join("locator")), + StateObjectType::NetworkConfig => { + if obj_id.len() < 1 { + None + } else { + Some(self.networks_path.join(format!("{:0>16x}.conf", obj_id[0]))) + } + }, + StateObjectType::Peer => { + if obj_id.len() < 1 { + None + } else { + Some(self.peers_path.join(format!("{:0>10x}.peer", obj_id[0]))) + } + } + } + } + + fn read_internal(&self, path: PathBuf) -> std::io::Result> { + let fmd = path.metadata()?; + if fmd.is_file() { + let flen = fmd.len(); + if flen <= Store::MAX_OBJECT_SIZE as u64 { + let mut f = std::fs::File::open(path)?; + let mut buf: Vec = Vec::new(); + buf.reserve(flen as usize); + let rs = f.read_to_end(&mut buf)?; + buf.resize(rs as usize, 0); + return Ok(buf); + } + } + Err(std::io::Error::new(std::io::ErrorKind::NotFound, "does not exist or is not readable")) + } + + pub fn auth_token(&self, generate_if_missing: bool) -> std::io::Result { + let mut token = self.auth_token.lock().unwrap(); + if token.is_empty() { + let p = self.auth_token_path.lock().unwrap(); + let ps = p.to_str().unwrap(); + + let token2 = self.read_file(ps).map_or(String::new(), |sb| { String::from_utf8(sb).unwrap_or(String::new()).trim().to_string() }); + if token2.is_empty() { + if generate_if_missing { + let mut rb = [0_u8; 32]; + unsafe { crate::osdep::getSecureRandom(rb.as_mut_ptr().cast(), 64) }; + token.reserve(rb.len()); + for b in rb.iter() { + if *b > 127_u8 { + token.push((65 + (*b % 26)) as char); // A..Z + } else { + token.push((97 + (*b % 26)) as char); // a..z + } + } + let res = self.write_file(ps, token.as_bytes()); + if res.is_err() { + token.clear(); + Err(res.err().unwrap()) + } else { + lock_down_file(ps); + Ok(token.clone()) + } + } else { + Err(std::io::Error::new(std::io::ErrorKind::NotFound, "")) + } + } else { + *token = token2; + Ok(token.clone()) + } + } else { + Ok(token.clone()) + } + } + + pub fn list_joined_networks(&self) -> Vec { + let mut list: Vec = Vec::new(); + let d = std::fs::read_dir(self.networks_path.as_ref()); + if d.is_ok() { + for de in d.unwrap() { + if de.is_ok() { + let nn = de.unwrap().file_name(); + let n = nn.to_str().unwrap_or(""); + if n.len() == 21 && n.ends_with(".conf") { // ################.conf + let nwid = u64::from_str_radix(&n[0..16], 16); + if nwid.is_ok() { + list.push(NetworkId(nwid.unwrap())); + } + } + } + } + } + list + } + + pub fn read_file(&self, fname: &str) -> std::io::Result> { + self.read_internal(self.base_path.join(fname)) + } + + pub fn read_file_str(&self, fname: &str) -> std::io::Result { + let data = self.read_file(fname)?; + let data = String::from_utf8(data); + if data.is_err() { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, data.err().unwrap().to_string())); + } + Ok(data.unwrap()) + } + + pub fn write_file(&self, fname: &str, data: &[u8]) -> std::io::Result<()> { + std::fs::OpenOptions::new().write(true).truncate(true).create(true).open(self.base_path.join(fname))?.write_all(data) + } + + pub fn read_local_conf(&self, skip_if_unchanged: bool) -> Option> { + let data = self.read_file_str(LOCAL_CONF); + if data.is_err() { + return Some(Err(data.err().unwrap())); + } + let data = data.unwrap(); + if skip_if_unchanged { + let mut prev = self.prev_local_config.lock().unwrap(); + if prev.eq(&data) { + return None; + } + *prev = data.clone(); + } else { + *(self.prev_local_config.lock().unwrap()) = data.clone(); + } + let lc = serde_json::from_str::(data.as_str()); + if lc.is_err() { + return Some(Err(std::io::Error::new(std::io::ErrorKind::InvalidData, lc.err().unwrap()))); + } + Some(Ok(lc.unwrap())) + } + + pub fn read_local_conf_or_default(&self) -> LocalConfig { + let lc = self.read_local_conf(false); + if lc.is_some() { + let lc = lc.unwrap(); + if lc.is_ok() { + return lc.unwrap(); + } + } + LocalConfig::default() + } + + pub fn write_local_conf(&self, lc: &LocalConfig) -> std::io::Result<()> { + let json = serde_json::to_string(lc).unwrap(); + self.write_file(LOCAL_CONF, json.as_bytes()) + } + + pub fn write_pid(&self) -> std::io::Result<()> { + let pid = unsafe { crate::osdep::getpid() }.to_string(); + self.write_file(ZEROTIER_PID, pid.as_bytes()) + } + + pub fn erase_pid(&self) { + let _ = std::fs::remove_file(self.base_path.join(ZEROTIER_PID)); + } + + pub fn write_uri(&self, uri: &str) -> std::io::Result<()> { + self.write_file(ZEROTIER_URI, uri.as_bytes()) + } + + pub fn load_uri(&self) -> std::io::Result { + let uri = String::from_utf8(self.read_file(ZEROTIER_URI)?); + uri.map_or_else(|e| { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + }, |uri| { + let uri = hyper::Uri::from_str(uri.trim()); + uri.map_or_else(|e| { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + }, |uri| { + Ok(uri) + }) + }) + } + + pub fn load_object(&self, obj_type: &StateObjectType, obj_id: &[u64]) -> std::io::Result> { + let obj_path = self.make_obj_path_internal(&obj_type, obj_id); + if obj_path.is_some() { + return self.read_internal(obj_path.unwrap()); + } + Err(std::io::Error::new(std::io::ErrorKind::NotFound, "does not exist or is not readable")) + } + + pub fn erase_object(&self, obj_type: &StateObjectType, obj_id: &[u64]) { + let obj_path = self.make_obj_path_internal(obj_type, obj_id); + if obj_path.is_some() { + let _ = std::fs::remove_file(obj_path.unwrap()); + } + } + + pub fn store_object(&self, obj_type: &StateObjectType, obj_id: &[u64], obj_data: &[u8]) -> std::io::Result<()> { + let obj_path = self.make_obj_path_internal(obj_type, obj_id); + if obj_path.is_some() { + let obj_path = obj_path.unwrap(); + std::fs::OpenOptions::new().write(true).truncate(true).create(true).open(&obj_path)?.write_all(obj_data)?; + + if obj_type.is_secret() { + lock_down_file(obj_path.to_str().unwrap()); + } + + Ok(()) + } else { + Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "object type or ID not valid")) + } + } +} diff --git a/zerotier-system-service/src/utils.rs b/zerotier-system-service/src/utils.rs new file mode 100644 index 000000000..02da778fd --- /dev/null +++ b/zerotier-system-service/src/utils.rs @@ -0,0 +1,182 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::borrow::Borrow; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use zerotier_core::{Identity, Locator}; + +use serde::Serialize; +use serde::de::DeserializeOwned; + +use crate::osdep; + +#[inline(always)] +pub(crate) fn ms_since_epoch() -> i64 { + unsafe { osdep::msSinceEpoch() } +} + +#[inline(always)] +pub(crate) fn ms_monotonic() -> i64 { + unsafe { osdep::msMonotonic() } +} + +/// Convenience function to read up to limit bytes from a file. +/// If the file is larger than limit, the excess is not read. +pub(crate) fn read_limit>(path: P, limit: usize) -> std::io::Result> { + let mut v: Vec = Vec::new(); + let _ = File::open(path)?.take(limit as u64).read_to_end(&mut v)?; + Ok(v) +} + +/// Read an identity as either a literal or from a file. +pub(crate) fn read_identity(input: &str, validate: bool) -> Result { + let parse_func = |s: &str| { + Identity::new_from_string(s).map_or_else(|e| { + Err(format!("invalid identity: {}", e.to_str())) + }, |id| { + if !validate || id.validate() { + Ok(id) + } else { + Err(String::from("invalid identity: local validation failed")) + } + }) + }; + if Path::new(input).exists() { + read_limit(input, 16384).map_or_else(|e| { + Err(e.to_string()) + }, |v| { + String::from_utf8(v).map_or_else(|e| { + Err(e.to_string()) + }, |s| { + parse_func(s.as_str()) + }) + }) + } else { + parse_func(input) + } +} + +/// Read a locator as either a literal or from a file. +pub(crate) fn read_locator(input: &str) -> Result { + let parse_func = |s: &str| { + Locator::new_from_string(s).map_or_else(|e| { + Err(format!("invalid locator: {}", e.to_str())) + }, |loc| { + Ok(loc) + }) + }; + if Path::new(input).exists() { + read_limit(input, 16384).map_or_else(|e| { + Err(e.to_string()) + }, |v| { + String::from_utf8(v).map_or_else(|e| { + Err(e.to_string()) + }, |s| { + parse_func(s.as_str()) + }) + }) + } else { + parse_func(input) + } +} + +/// Create a new HTTP authorization nonce by encrypting the current time. +/// The key used to encrypt the current time is random and is re-created for +/// each execution of the process. By decrypting this nonce when it is returned, +/// the client and server may check the age of a digest auth exchange. +pub(crate) fn create_http_auth_nonce(timestamp: i64) -> String { + let mut nonce_plaintext: [u64; 2] = [timestamp as u64, timestamp as u64]; + unsafe { + osdep::encryptHttpAuthNonce(nonce_plaintext.as_mut_ptr().cast()); + hex::encode(*nonce_plaintext.as_ptr().cast::<[u8; 16]>()) + } +} + +/// Decrypt HTTP auth nonce encrypted by this process and return the timestamp. +/// This returns zero if the input was not valid. +pub(crate) fn decrypt_http_auth_nonce(nonce: &str) -> i64 { + let nonce = hex::decode(nonce.trim()); + if !nonce.is_err() { + let mut nonce = nonce.unwrap(); + if nonce.len() == 16 { + unsafe { + osdep::decryptHttpAuthNonce(nonce.as_mut_ptr().cast()); + let nonce = *nonce.as_ptr().cast::<[u64; 2]>(); + if nonce[0] == nonce[1] { + return nonce[0] as i64; + } + } + } + } + return 0; +} + +/// Shortcut to use serde_json to serialize an object, returns "null" on error. +pub(crate) fn to_json(o: &O) -> String { + serde_json::to_string(o).unwrap_or("null".into()) +} + +/// Shortcut to use serde_json to serialize an object, returns "null" on error. +pub(crate) fn to_json_pretty(o: &O) -> String { + serde_json::to_string_pretty(o).unwrap_or("null".into()) +} + +/// Recursively patch a JSON object. +/// This is slightly different from a usual JSON merge. For objects in the target their fields +/// are updated by recursively calling json_patch if the same field is present in the source. +/// If the source tries to set an object to something other than another object, this is ignored. +/// Other fields are replaced. This is used for RESTful config object updates. The depth limit +/// field is to prevent stack overflows via the API. +pub(crate) fn json_patch(target: &mut serde_json::value::Value, source: &serde_json::value::Value, depth_limit: usize) { + if target.is_object() { + if source.is_object() { + let mut target = target.as_object_mut().unwrap(); + let source = source.as_object().unwrap(); + for kv in target.iter_mut() { + let _ = source.get(kv.0).map(|new_value| { + if depth_limit > 0 { + json_patch(kv.1, new_value, depth_limit - 1) + } + }); + } + for kv in source.iter() { + if !target.contains_key(kv.0) && !kv.1.is_null() { + target.insert(kv.0.clone(), kv.1.clone()); + } + } + } + } else if *target != *source { + *target = source.clone(); + } +} + +/// Patch a serializable object with the fields present in a JSON object. +/// If there are no changes, None is returned. The depth limit is passed through to json_patch and +/// should be set to a sanity check value to prevent overflows. +pub(crate) fn json_patch_object(obj: O, patch: &str, depth_limit: usize) -> Result, serde_json::Error> { + serde_json::from_str::(patch).map_or_else(|e| Err(e), |patch| { + serde_json::value::to_value(obj.borrow()).map_or_else(|e| Err(e), |mut obj_value| { + json_patch(&mut obj_value, &patch, depth_limit); + serde_json::value::from_value::(obj_value).map_or_else(|e| Err(e), |obj_merged| { + if obj == obj_merged { + Ok(None) + } else { + Ok(Some(obj_merged)) + } + }) + }) + }) +} diff --git a/zerotier-system-service/src/vnic/common.rs b/zerotier-system-service/src/vnic/common.rs new file mode 100644 index 000000000..4eb52ae83 --- /dev/null +++ b/zerotier-system-service/src/vnic/common.rs @@ -0,0 +1,56 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +use std::collections::HashSet; + +#[allow(unused_imports)] +use zerotier_core::{MAC, MulticastGroup}; + +#[allow(unused_imports)] +use num_traits::AsPrimitive; + +/// BSD based OSes support getifmaddrs(). +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", target_os = "freebsd", target_os = "darwin"))] +pub(crate) fn get_l2_multicast_subscriptions(dev: &str) -> HashSet { + let mut groups: HashSet = HashSet::new(); + let dev = dev.as_bytes(); + unsafe { + let mut maddrs: *mut osdep::ifmaddrs = std::ptr::null_mut(); + if osdep::getifmaddrs(&mut maddrs as *mut *mut osdep::ifmaddrs) == 0 { + let mut i = maddrs; + while !i.is_null() { + if !(*i).ifma_name.is_null() && !(*i).ifma_addr.is_null() && (*(*i).ifma_addr).sa_family as i32 == osdep::AF_LINK as i32 { + let in_: &osdep::sockaddr_dl = &*((*i).ifma_name.cast()); + let la: &osdep::sockaddr_dl = &*((*i).ifma_addr.cast()); + if la.sdl_alen == 6 && in_.sdl_nlen <= dev.len().as_() && crate::osdep::memcmp(dev.as_ptr().cast(), in_.sdl_data.as_ptr().cast(), in_.sdl_nlen.as_()) == 0 { + let mi = la.sdl_nlen as usize; + groups.insert(MulticastGroup{ + mac: MAC((la.sdl_data[mi] as u64) << 40 | (la.sdl_data[mi+1] as u64) << 32 | (la.sdl_data[mi+2] as u64) << 24 | (la.sdl_data[mi+3] as u64) << 16 | (la.sdl_data[mi+4] as u64) << 8 | la.sdl_data[mi+5] as u64), + adi: 0, + }); + } + } + i = (*i).ifma_next; + } + osdep::freeifmaddrs(maddrs); + } + } + groups +} + +/// Linux stores this stuff in /proc and it needs to be fetched from there. +#[cfg(target_os = "linux")] +pub(crate) fn get_l2_multicast_subscriptions(dev: &str) -> HashSet { + let mut groups: HashSet = HashSet::new(); + groups +} diff --git a/zerotier-system-service/src/vnic/mac_feth_tap.rs b/zerotier-system-service/src/vnic/mac_feth_tap.rs new file mode 100644 index 000000000..3a54ffe3c --- /dev/null +++ b/zerotier-system-service/src/vnic/mac_feth_tap.rs @@ -0,0 +1,471 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +/* + * This creates a pair of feth devices with the lower numbered device + * being the ZeroTier virtual interface and the higher being the device + * used to actually read and write packets. The latter gets no IP config + * and is only used for I/O. The behavior of feth is similar to the + * veth pairs that exist on Linux. + * + * The feth device has only existed since MacOS Sierra, but that's fairly + * long ago in Mac terms. + * + * I/O with feth must be done using two different sockets. The BPF socket + * is used to receive packets, while an AF_NDRV (low-level network driver + * access) socket must be used to inject. AF_NDRV can't read IP frames + * since BSD doesn't forward packets out the NDRV tap if they've already + * been handled, and while BPF can inject its MTU for injected packets + * is limited to 2048. AF_NDRV packet injection is required to inject + * ZeroTier's large MTU frames. + * + * This is all completely undocumented. Finding it and learning how to + * use it required sifting through XNU/Darwin kernel source code on + * opensource.apple.com. Needless to say we are exploring other options + * for future releases, but this works for now. + */ + +use std::cell::Cell; +use std::collections::HashSet; +use std::error::Error; +use std::ffi::CString; +use std::ptr::{null_mut, copy_nonoverlapping}; +use std::mem::{transmute, zeroed}; +use std::os::raw::{c_int, c_uchar, c_void}; +use std::process::Command; +use std::sync::Mutex; +use std::thread::JoinHandle; + +use lazy_static::lazy_static; +use num_traits::cast::AsPrimitive; + +use zerotier_core::{InetAddress, MAC, MulticastGroup, NetworkId}; + +use crate::osdep as osdep; +use crate::getifaddrs; +use crate::vnic::vnic::VNIC; +use crate::osdep::getifmaddrs; + +const BPF_BUFFER_SIZE: usize = 131072; +const IFCONFIG: &'static str = "/sbin/ifconfig"; +const SYSCTL: &'static str = "/usr/sbin/sysctl"; + +// Holds names of feth devices and destroys them on Drop. +struct MacFethDevice { + pub name: String, + pub peer_name: String +} + +impl Drop for MacFethDevice { + fn drop(&mut self) { + if self.name.len() > 0 && self.peer_name.len() > 0 { + let destroy_peer = Command::new(IFCONFIG).arg(self.peer_name.as_str()).arg("destroy").spawn(); + if destroy_peer.is_ok() { + let _ = destroy_peer.unwrap().wait(); + } + let destroy = Command::new(IFCONFIG).arg(self.name.as_str()).arg("destroy").spawn(); + if destroy.is_ok() { + let _ = destroy.unwrap().wait(); + } + } + } +} + +pub(crate) struct MacFethTap { + network_id: u64, + device: MacFethDevice, + ndrv_fd: c_int, + bpf_fd: c_int, + bpf_no: u32, + bpf_read_thread: Cell>>, +} + +// Rust implementation of the following macro from Darwin sys/bpf.h: +// #define BPF_WORDALIGN(x) (((x)+(BPF_ALIGNMENT-1))&~(BPF_ALIGNMENT-1)) +// ... and also ... +// #define BPF_ALIGNMENT sizeof(int32_t) +#[allow(non_snake_case)] +#[inline(always)] +fn BPF_WORDALIGN(x: isize) -> isize { + (((x + 3) as usize) & (!(3 as usize))) as isize +} + +lazy_static! { + static ref MAC_FETH_BPF_DEVICES_USED: Mutex> = Mutex::new(BTreeSet::new()); +} + +fn device_ipv6_set_params(device: &String, perform_nud: bool, accept_ra: bool) -> bool { + let dev = device.as_bytes(); + let mut ok = true; + unsafe { + let s = osdep::socket(osdep::AF_INET6 as c_int, osdep::SOCK_DGRAM as c_int, 0); + if s < 0 { + return false; + } + + let mut nd: osdep::in6_ndireq = zeroed(); + copy_nonoverlapping(dev.as_ptr(), nd.ifname.as_mut_ptr().cast::(), if dev.len() > (nd.ifname.len() - 1) { nd.ifname.len() - 1 } else { dev.len() }); + if osdep::ioctl(s, osdep::c_SIOCGIFINFO_IN6, (&mut nd as *mut osdep::in6_ndireq).cast::()) == 0 { + let oldflags = nd.ndi.flags; + if perform_nud { + nd.ndi.flags |= osdep::ND6_IFF_PERFORMNUD as osdep::u_int32_t; + } else { + nd.ndi.flags &= !(osdep::ND6_IFF_PERFORMNUD as osdep::u_int32_t); + } + if nd.ndi.flags != oldflags { + if osdep::ioctl(s, osdep::c_SIOCSIFINFO_FLAGS, (&mut nd as *mut osdep::in6_ndireq).cast::()) != 0 { + ok = false; + } + } + } else { + ok = false; + } + + let mut ifr: osdep::in6_ifreq = zeroed(); + copy_nonoverlapping(dev.as_ptr(), ifr.ifr_name.as_mut_ptr().cast::(), if dev.len() > (ifr.ifr_name.len() - 1) { ifr.ifr_name.len() - 1 } else { dev.len() }); + if osdep::ioctl(s, if accept_ra { osdep::c_SIOCAUTOCONF_START } else { osdep::c_SIOCAUTOCONF_STOP }, (&mut ifr as *mut osdep::in6_ifreq).cast::()) != 0 { + ok = false; + } + + osdep::close(s); + } + ok +} + +impl MacFethTap { + /// Create a new MacFethTap with a function to call for Ethernet frames. + /// The function F should return as quickly as possible. It should pass copies + /// of frames elsewhere if anything needs to be done with them. The slice it's + /// given will not remain valid after it returns. Also note that F will be called + /// from another thread that is spawned here, so all its bound references must + /// be "Send" and "Sync" e.g. Arc<>. + pub(crate) fn new(nwid: &NetworkId, mac: &MAC, mtu: i32, metric: i32, eth_frame_func: F) -> Result { + // This tracks BPF devices we are using so we don't try to reopen them, and also + // doubles as a global lock to ensure that only one feth tap is created at once per + // ZeroTier process per system. + let mut bpf_devices_used = MAC_FETH_BPF_DEVICES_USED.lock().unwrap(); + + if unsafe { osdep::getuid() } != 0 { + return Err(String::from("ZeroTier MacFethTap must run as root")); + } + + let mut device_name: String; + let mut peer_device_name: String; + let mut device_feth_ctr = nwid.0 ^ (nwid.0 >> 32) ^ (nwid.0 >> 48); + let mut device_alloc_tries = 0; + loop { + let device_feth_no = 100 + (device_feth_ctr % 4900); + device_name = format!("feth{}", device_feth_no); + peer_device_name = format!("feth{}", device_feth_no + 5000); + let mut already_allocated = false; + getifaddrs::for_each_address(|_: &InetAddress, dn: &str| { + if dn.eq(&device_name) || dn.eq(&peer_device_name) { + already_allocated = true; + } + }); + if !already_allocated { + break; + } + + device_alloc_tries += 1; + if device_alloc_tries >= 1000 { + return Err(String::from("unable to find unallocated 'feth' device")); + } + device_feth_ctr += 1; + } + device_ipv6_set_params(&device_name, true, false); + + // Set sysctl for max if_fake MTU. This is allowed to fail since this sysctl doesn't + // exist on older versions of MacOS (and isn't required there). 16000 is larger than + // anything ZeroTier supports. OS max is 16384 - some overhead. + let _ = Command::new(SYSCTL).arg("net.link.fake.max_mtu").arg("10000").spawn().map(|mut c| { let _ = c.wait(); }); + + // Create pair of feth interfaces and create MacFethDevice struct. + let cmd = Command::new(IFCONFIG).arg(&device_name).arg("create").spawn(); + if cmd.is_err() { + return Err(format!("unable to create device '{}': {}", device_name.as_str(), cmd.err().unwrap().to_string())); + } + let _ = cmd.unwrap().wait(); + let cmd = Command::new(IFCONFIG).arg(&peer_device_name).arg("create").spawn(); + if cmd.is_err() { + return Err(format!("unable to create device '{}': {}", peer_device_name.as_str(), cmd.err().unwrap().to_string())); + } + let _ = cmd.unwrap().wait(); + let device = MacFethDevice { + name: device_name, + peer_name: peer_device_name, + }; + + // Set link-layer (MAC) address of primary interface. + let cmd = Command::new(IFCONFIG).arg(&device.name).arg("lladdr").arg(mac.to_string()).spawn(); + if cmd.is_err() { + return Err(format!("unable to configure device '{}': {}", &device.name, cmd.err().unwrap().to_string())); + } + let _ = cmd.unwrap().wait(); + + // Bind peer interfaces together. + let cmd = Command::new(IFCONFIG).arg(&device.peer_name).arg("peer").arg(device.name.as_str()).spawn(); + if cmd.is_err() { + return Err(format!("unable to configure device '{}': {}", &device.peer_name, cmd.err().unwrap().to_string())); + } + let _ = cmd.unwrap().wait(); + + // Set MTU of secondary peer interface, bring up. + let cmd = Command::new(IFCONFIG).arg(&device.peer_name).arg("mtu").arg(mtu.to_string()).arg("up").spawn(); + if cmd.is_err() { + return Err(format!("unable to configure device '{}': {}", &device.peer_name, cmd.err().unwrap().to_string())); + } + let _ = cmd.unwrap().wait(); + + // Set MTU and metric of primary interface, bring up. + let cmd = Command::new(IFCONFIG).arg(&device.name).arg("mtu").arg(mtu.to_string()).arg("metric").arg(metric.to_string()).arg("up").spawn(); + if cmd.is_err() { + return Err(format!("unable to configure device '{}': {}", &device.name.as_str(), cmd.err().unwrap().to_string())); + } + let _ = cmd.unwrap().wait(); + + // Look for a /dev/bpf node to open. Start at 1 since some software + // hard codes /dev/bpf0 and we don't want to break it. If all BPF nodes + // are taken MacOS automatically adds more, so we shouldn't run out. + let mut bpf_no: u32 = 1; + let mut bpf_fd: c_int = -1; + loop { + if bpf_devices_used.contains(&bpf_no) { + bpf_no += 1; + } else { + let bpf_dev = CString::new(format!("/dev/bpf{}", bpf_no)).unwrap(); + let bpf_dev = bpf_dev.as_bytes_with_nul(); + bpf_fd = unsafe { osdep::open(bpf_dev.as_ptr().cast(), osdep::O_RDWR as c_int) }; + if bpf_fd >= 0 { + break; + } + bpf_no += 1; + if bpf_no > 1000 { + break; + } + } + } + if bpf_fd < 0 { + return Err(String::from("unable to open /dev/bpf## where attempted ## from 1 to 1000")); + } + + // Set/get buffer length to use with reads from BPF device, trying to + // use up to BPF_BUFFER_SIZE bytes. + let mut fl: c_int = BPF_BUFFER_SIZE as c_int; + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCSBLEN, (&mut fl as *mut c_int).cast::()) } != 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to configure BPF device")); + } + let bpf_read_size = fl as osdep::size_t; + + // Set immediate mode for "live" capture. + fl = 1; + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCIMMEDIATE, (&mut fl as *mut c_int).cast::()) } != 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to configure BPF device")); + } + + // Do not send us back packets we inject or send. + fl = 0; + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCSSEESENT, (&mut fl as *mut c_int).cast::()) } != 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to configure BPF device")); + } + + // Bind BPF to secondary feth device. + let mut bpf_ifr: osdep::ifreq = unsafe { std::mem::zeroed() }; + let peer_dev_name_bytes = device.peer_name.as_bytes(); + unsafe { copy_nonoverlapping(peer_dev_name_bytes.as_ptr(), bpf_ifr.ifr_name.as_mut_ptr().cast::(), if peer_dev_name_bytes.len() > (bpf_ifr.ifr_name.len() - 1) { bpf_ifr.ifr_name.len() - 1 } else { peer_dev_name_bytes.len() }); } + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCSETIF, (&mut bpf_ifr as *mut osdep::ifreq).cast::()) } != 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to configure BPF device")); + } + + // Include Ethernet header in BPF captures. + fl = 1; + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCSHDRCMPLT, (&mut fl as *mut c_int).cast::()) } != 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to configure BPF device")); + } + + // Set promiscuous mode so bridging can work. + fl = 1; + if unsafe { osdep::ioctl(bpf_fd as c_int, osdep::c_BIOCPROMISC, (&mut fl as *mut c_int).cast::()) } != 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to configure BPF device")); + } + + // Create BPF listener thread, which calls the supplied function on each incoming packet. + let t = std::thread::Builder::new().stack_size(zerotier_core::RECOMMENDED_THREAD_STACK_SIZE).spawn(move || { + let mut buf: [u8; BPF_BUFFER_SIZE] = [0_u8; BPF_BUFFER_SIZE]; + let hdr_struct_size = std::mem::size_of::() as isize; + loop { + let n = unsafe { osdep::read(bpf_fd, buf.as_mut_ptr().cast(), bpf_read_size) } as isize; + if n >= 0 { + let mut p: isize = 0; + while (p + hdr_struct_size) < n { + unsafe { + let h = buf.as_ptr().offset(p).cast::(); + let hdrlen = (*h).bh_hdrlen as isize; + let caplen = (*h).bh_caplen as isize; + let pktlen = hdrlen + caplen; + if caplen > 0 && (p + pktlen) <= n { + eth_frame_func(std::slice::from_raw_parts(buf.as_ptr().offset(p + hdrlen), caplen as usize)); + } + p += BPF_WORDALIGN(pktlen); + } + } + } else { + break; + } + } + }); + if t.is_err() { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to start thread")); + } + + // Create AF_NDRV socket used to inject packets. We could inject with BPF but that has + // a hard MTU limit of 2048 so we have to use AF_NDRV instead. Performance is probably + // the same, but it means another socket. + let ndrv_fd = unsafe { osdep::socket(osdep::AF_NDRV as c_int, osdep::SOCK_RAW as c_int, 0) }; + if ndrv_fd < 0 { + unsafe { osdep::close(bpf_fd); } + return Err(String::from("unable to create AF_NDRV socket")); + } + let mut ndrv_sa: osdep::sockaddr_ndrv = unsafe { std::mem::zeroed() }; + ndrv_sa.snd_len = std::mem::size_of::() as c_uchar; + ndrv_sa.snd_family = osdep::AF_NDRV as c_uchar; + unsafe { copy_nonoverlapping(peer_dev_name_bytes.as_ptr(), ndrv_sa.snd_name.as_mut_ptr().cast::(), if peer_dev_name_bytes.len() > (bpf_ifr.ifr_name.len() - 1) { bpf_ifr.ifr_name.len() - 1 } else { peer_dev_name_bytes.len() }); } + if unsafe { osdep::bind(ndrv_fd, (&ndrv_sa as *const osdep::sockaddr_ndrv).cast(), std::mem::size_of::() as osdep::socklen_t) } != 0 { + unsafe { osdep::close(bpf_fd); } + unsafe { osdep::close(ndrv_fd); } + return Err(String::from("unable to bind AF_NDRV socket")); + } + if unsafe { osdep::connect(ndrv_fd, (&ndrv_sa as *const osdep::sockaddr_ndrv).cast(), std::mem::size_of::() as osdep::socklen_t) } != 0 { + unsafe { osdep::close(bpf_fd); } + unsafe { osdep::close(ndrv_fd); } + return Err(String::from("unable to connect AF_NDRV socket")); + } + + bpf_devices_used.insert(bpf_no); + + Ok(MacFethTap { + network_id: nwid.0, + device, + ndrv_fd, + bpf_fd, + bpf_no, + bpf_read_thread: Cell::new(Some(t.unwrap())) + }) + } + + fn have_ip(&self, ip: &InetAddress) -> bool { + let mut have_ip = false; + getifaddrs::for_each_address(|addr: &InetAddress, device_name: &str| { + if device_name.eq(&self.device.name) && addr.eq(ip) { + have_ip = true; + } + }); + have_ip + } +} + +impl VNIC for MacFethTap { + fn add_ip(&self, ip: &InetAddress) -> bool { + if !self.have_ip(ip) { + let cmd = Command::new(IFCONFIG).arg(&self.device.name).arg(if ip.is_v6() { "inet6" } else { "inet" }).arg(ip.to_string()).arg("alias").spawn(); + if cmd.is_ok() { + let _ = cmd.unwrap().wait(); + } + return self.have_ip(ip); + } + true + } + + fn remove_ip(&self, ip: &InetAddress) -> bool { + if self.have_ip(ip) { + let cmd = Command::new(IFCONFIG).arg(&self.device.name).arg(if ip.is_v6() { "inet6" } else { "inet" }).arg(ip.to_string()).arg("-alias").spawn(); + if cmd.is_ok() { + let _ = cmd.unwrap().wait(); + } + return !self.have_ip(ip); + } + true // if we don't have it it's successfully removed + } + + fn ips(&self) -> Vec { + let mut ipv: Vec = Vec::new(); + ipv.reserve(8); + let dev = self.device.name.as_str(); + getifaddrs::for_each_address(|addr: &InetAddress, device_name: &str| { + if device_name.eq(dev) { + ipv.push(addr.clone()); + } + }); + ipv.sort(); + ipv + } + + #[inline(always)] + fn device_name(&self) -> String { + self.device.name.clone() + } + + #[inline(always)] + fn get_multicast_groups(&self) -> HashSet { + crate::vnic::common::get_l2_multicast_subscriptions(self.device.name.as_str()) + } + + #[inline(always)] + fn put(&self, source_mac: &zerotier_core::MAC, dest_mac: &zerotier_core::MAC, ethertype: u16, _vlan_id: u16, data: *const u8, len: usize) -> bool { + let dm = dest_mac.0; + let sm = source_mac.0; + let mut hdr: [u8; 14] = [(dm >> 40) as u8, (dm >> 32) as u8, (dm >> 24) as u8, (dm >> 16) as u8, (dm >> 8) as u8, dm as u8, (sm >> 40) as u8, (sm >> 32) as u8, (sm >> 24) as u8, (sm >> 16) as u8, (sm >> 8) as u8, sm as u8, (ethertype >> 8) as u8, ethertype as u8]; + unsafe { + let iov: [osdep::iovec; 2] = [ + osdep::iovec { + iov_base: hdr.as_mut_ptr().cast(), + iov_len: 14, + }, + osdep::iovec { + iov_base: transmute(data), // have to "cast away const" even though data is not modified by writev() + iov_len: len as osdep::size_t, + }, + ]; + osdep::writev(self.ndrv_fd, iov.as_ptr(), 2) == (len + 14) as osdep::ssize_t + } + } +} + +impl Drop for MacFethTap { + fn drop(&mut self) { + if self.bpf_fd >= 0 { + unsafe { + osdep::shutdown(self.bpf_fd, osdep::SHUT_RDWR as c_int); + osdep::close(self.bpf_fd); + MAC_FETH_BPF_DEVICES_USED.lock().unwrap().remove(&self.bpf_no); + } + } + if self.ndrv_fd >= 0 { + unsafe { + osdep::close(self.ndrv_fd); + } + } + let t = self.bpf_read_thread.replace(None); + if t.is_some() { + let _ = t.unwrap().join(); + } + // NOTE: the feth devices are destroyed by MacFethDevice's drop(). + } +} diff --git a/zerotier-system-service/src/vnic/mod.rs b/zerotier-system-service/src/vnic/mod.rs new file mode 100644 index 000000000..7a0d395f2 --- /dev/null +++ b/zerotier-system-service/src/vnic/mod.rs @@ -0,0 +1,18 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +mod vnic; +mod common; + +#[cfg(target_os = "macos")] +mod mac_feth_tap; diff --git a/zerotier-system-service/src/vnic/vnic.rs b/zerotier-system-service/src/vnic/vnic.rs new file mode 100644 index 000000000..35054910b --- /dev/null +++ b/zerotier-system-service/src/vnic/vnic.rs @@ -0,0 +1,37 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2026-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +/// Virtual network interface +pub(crate) trait VNIC { + /// Add a new IPv4 or IPv6 address to this interface, returning true on success. + fn add_ip(&self, ip: &zerotier_core::InetAddress) -> bool; + + /// Remove an IPv4 or IPv6 address, returning true on success. + /// Nothing happens if the address is not found. + fn remove_ip(&self, ip: &zerotier_core::InetAddress) -> bool; + + /// Enumerate all IPs on this interface including ones assigned outside ZeroTier. + fn ips(&self) -> Vec; + + /// Get the OS-specific device name for this interface, e.g. zt## or tap##. + fn device_name(&self) -> String; + + /// Get L2 multicast groups to which this interface is subscribed. + /// This doesn't do any IGMP snooping. It just reports the groups the port + /// knows about. On some OSes this may not be supported in which case it + /// will return an empty set. + fn get_multicast_groups(&self) -> std::collections::BTreeSet; + + /// Inject an Ethernet frame into this port. + fn put(&self, source_mac: &zerotier_core::MAC, dest_mac: &zerotier_core::MAC, ethertype: u16, vlan_id: u16, data: *const u8, len: usize) -> bool; +}