Move system service back into here (still does not compile with Rust core)

This commit is contained in:
Adam Ierymenko 2021-10-14 19:36:21 -04:00
parent 5c19377997
commit 0c0e78da1b
No known key found for this signature in database
GPG key ID: C8877CF2D7A5D7F3
29 changed files with 4457 additions and 1 deletions

View file

@ -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"] }

913
zerotier-system-service/Cargo.lock generated Normal file
View file

@ -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",
]

View file

@ -0,0 +1,32 @@
[package]
name = "zerotier-system-service"
version = "0.1.0"
authors = ["Adam Ierymenko <adam.ierymenko@zerotier.com>"]
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"] }

View file

@ -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);
}

View file

@ -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<Service>, req: Request<Body>) -> (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<Service>, req: Request<Body>) -> (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<Service>, req: Request<Body>) -> (StatusCode, Body) {
(StatusCode::NOT_IMPLEMENTED, Body::from(""))
}
pub(crate) fn network(service: Arc<Service>, req: Request<Body>) -> (StatusCode, Body) {
(StatusCode::NOT_IMPLEMENTED, Body::from(""))
}

View file

@ -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.
*/
/****/

View file

@ -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
}
}
}

View file

@ -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.
*/
/****/

View file

@ -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.
*/
/****/

View file

@ -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;

View file

@ -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.
*/
/****/

View file

@ -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.
*/
/****/

View file

@ -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.
*/
/****/

View file

@ -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<Store>, global_flags: GlobalFlags, client: HttpClient, api_base_uri: Uri, auth_token: String) -> Result<i32, Box<dyn Error>> {
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::<ServiceStatus>(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(), "")))
}
}

View file

@ -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<FastUDPRawOsSocket, &'static str> {
unsafe {
let (af, sa_len) = match address.family() {
InetAddressFamily::IPv4 => (osdep::AF_INET, std::mem::size_of::<osdep::sockaddr_in>() as osdep::socklen_t),
InetAddressFamily::IPv6 => (osdep::AF_INET6, std::mem::size_of::<osdep::sockaddr_in6>() 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::<c_int>() 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<std::thread::JoinHandle<()>>,
thread_run: Arc<AtomicBool>,
sockets: Vec<FastUDPRawOsSocket>,
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<FastUDPRawOsSocket> {
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::<InetAddress>().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::<c_int>().as_());
osdep::sendto(*socket, data.cast(), len.as_(), 0, (to_address as *const InetAddress).cast(), std::mem::size_of::<InetAddress>().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::<c_int>().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::<InetAddress>() 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<F: Fn(&FastUDPRawOsSocket, &InetAddress, Buffer) + Send + Sync + Clone + 'static>(device_name: &str, address: &InetAddress, handler: F) -> Result<FastUDPSocket, String> {
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::<InetAddress>() 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");
}
}

View file

@ -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: &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<F: FnMut(&InetAddress, &str)>(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::<u8>(), (&mut a as *mut InetAddress).cast::<u8>(), size_of::<osdep::sockaddr_in>());
} else if sa_family == osdep::AF_INET6 as u8 {
copy_nonoverlapping((*i).ifa_addr.cast::<u8>(), (&mut a as *mut InetAddress).cast::<u8>(), size_of::<osdep::sockaddr_in6>());
} 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::<osdep::sockaddr_in>()).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::<osdep::sockaddr_in6>()).sin6_addr)).cast::<u8>();
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.")
}
}

View file

@ -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<hyper::Client<hyper::client::HttpConnector, Body>>;
#[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<Output = Result<i32, Box<dyn Error>>>,
F: FnOnce(Arc<Store>, GlobalFlags, HttpClient, Uri, String) -> R
>(store: Arc<Store>, 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<Response<Body>, Box<dyn Error>> {
let body: Vec<u8> = 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<Uri> {
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<Vec<u8>, Box<dyn Error>> {
let mut data: Vec<u8> = 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<O: DeserializeOwned>(body: &mut Body, max_size: usize) -> Result<O, Box<dyn Error>> {
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())
}
}

View file

@ -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<Option<tokio::sync::oneshot::Sender<()>>>,
server: JoinHandle<hyper::Result<()>>,
}
async fn http_handler(service: Arc<Service>, req: Request<Body>) -> Result<Response<Body>, 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<Service>) -> Result<HttpListener, Box<dyn std::error::Error>> {
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<Body>| 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();
});
}
}

View file

@ -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<InetAddress>
}
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<String>,
#[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<u16>,
#[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<String>,
#[serde(rename = "explicitAddresses")]
pub explicit_addresses: Vec<InetAddress>,
}
impl Default for LocalConfigSettings {
fn default() -> Self {
let mut bl: Vec<String> = 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<InetAddress, LocalConfigPhysicalPathConfig>,
#[serde(rename = "virtual")]
pub virtual_: BTreeMap<Address, LocalConfigVirtualConfig>,
pub network: BTreeMap<NetworkId, LocalConfigNetworkSettings>,
pub settings: LocalConfigSettings,
}
impl Default for LocalConfig {
fn default() -> Self {
LocalConfig {
physical: BTreeMap::new(),
virtual_: BTreeMap::new(),
network: BTreeMap::new(),
settings: LocalConfigSettings::default()
}
}
}

View file

@ -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<File>,
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<LogIntl>,
}
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<S: AsRef<str>>(&self, s: S) {
let mut l = self.inner.lock().unwrap();
self.log_internal(&mut (*l), s.as_ref(), "");
}
pub fn debug<S: AsRef<str>>(&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<S: AsRef<str>>(&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))
}
}
}
*/

View file

@ -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> [command args]
Global Options:
-j Output raw JSON where applicable
-p <path> Use alternate base path
-t <path> Load secret auth token from a file
-T <token> 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 <port> Primary P2P port
· secondaryport <port/0> Secondary P2P port (0 to disable)
· blacklist cidr <IP/bits> <boolean> Toggle physical path blacklisting
· blacklist if <prefix> <boolean> [Un]blacklist interface prefix
· portmap <boolean> Toggle use of uPnP and NAT-PMP
· peer <command> [option]
· show <address> Show detailed peer information
· list List peers
· listroots List root peers
· try <address> <endpoint> [...] Try peer at explicit endpoint
· network <command> [option]
· show <network ID> Show detailed network information
· list List networks
· set <network ID> [option] [value] Get or set network options
· manageips <boolean> Is IP management allowed?
· manageroutes <boolean> Is route management allowed?
· managedns <boolean> Allow network to push DNS config
· globalips <boolean> Allow assignment of global IPs?
· globalroutes <boolean> Can global IP routes be set?
· defaultroute <boolean> Can default route be overridden?
· join <network> Join a virtual network
· leave <network> Leave a virtual network
{}"###,
ver.0, ver.1, ver.2, if long_help {
r###"
Advanced Operations:
service Start node
(usually not invoked directly)
controller <command> [option]
· list List networks on controller
· new Create a new network
· set <network> [setting] [value] Show or modify network settings
· show <network> [<address>] Show network or member status
· auth <address> Authorize a peer
· deauth <address> Deauthorize a peer
identity <command> [args]
new [c25519 | p384] Create identity (default: c25519)
getpublic <?identity> Extract public part of identity
fingerprint <?identity> Get an identity's fingerprint
validate <?identity> Locally validate an identity
sign <?identity> <@file> Sign a file with an identity's key
verify <?identity> <@file> <sig> 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<bool, String> {
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<Store> {
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
}
}
});
}

View file

@ -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 {
}

View file

@ -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<InetAddress>,
#[serde(rename = "httpLocalEndpoints")]
pub http_local_endpoints: Vec<InetAddress>,
}
/// Core ZeroTier service, which is sort of just a container for all the things.
pub(crate) struct Service {
pub(crate) log: Log,
_node: Cell<Weak<Node<Arc<Service>, Network, Service>>>, // never modified after node is created
udp_local_endpoints: Mutex<Vec<InetAddress>>,
http_local_endpoints: Mutex<Vec<InetAddress>>,
interrupt: Mutex<futures::channel::mpsc::Sender<()>>,
local_config: Mutex<Arc<LocalConfig>>,
store: Arc<Store>,
startup_time: i64,
startup_time_monotonic: i64,
run: AtomicBool,
online: AtomicBool,
}
impl NodeEventHandler<Network> 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<Vec<u8>> {
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<InetAddress> {
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<LocalConfig> {
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<Arc<Node<Arc<Service>, Network, Service>>> {
unsafe { &*self._node.as_ptr() }.upgrade()
}
#[inline(always)]
pub fn store(&self) -> &Arc<Store> {
&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<ServiceStatus> {
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<Store>, local_config: Arc<LocalConfig>) -> i32 {
let process_exit_value: i32 = 0;
let mut udp_sockets: BTreeMap<InetAddress, FastUDPSocket> = BTreeMap::new();
let mut http_listeners: BTreeMap<InetAddress, HttpListener> = BTreeMap::new();
let mut loopback_http_listeners: (Option<HttpListener>, Option<HttpListener>) = (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<InetAddress, String> = 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::<Vec<InetAddress>>().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::<Vec<InetAddress>>().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<Store>) -> 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
}

View file

@ -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<Path>,
pub default_log_path: Box<Path>,
prev_local_config: Mutex<String>,
peers_path: Box<Path>,
controller_path: Box<Path>,
networks_path: Box<Path>,
auth_token_path: Mutex<Box<Path>>,
auth_token: Mutex<String>,
}
/// 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<String>, auth_token_override: Option<String>) -> std::io::Result<Store> {
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<PathBuf> {
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<Vec<u8>> {
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<u8> = 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<String> {
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<NetworkId> {
let mut list: Vec<NetworkId> = 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<Vec<u8>> {
self.read_internal(self.base_path.join(fname))
}
pub fn read_file_str(&self, fname: &str) -> std::io::Result<String> {
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<std::io::Result<LocalConfig>> {
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::<LocalConfig>(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<hyper::Uri> {
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<Vec<u8>> {
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"))
}
}
}

View file

@ -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<P: AsRef<Path>>(path: P, limit: usize) -> std::io::Result<Vec<u8>> {
let mut v: Vec<u8> = 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<Identity, String> {
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<Locator, String> {
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: serde::Serialize>(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: serde::Serialize>(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<O: Serialize + DeserializeOwned + Eq>(obj: O, patch: &str, depth_limit: usize) -> Result<Option<O>, serde_json::Error> {
serde_json::from_str::<serde_json::value::Value>(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::<O>(obj_value).map_or_else(|e| Err(e), |obj_merged| {
if obj == obj_merged {
Ok(None)
} else {
Ok(Some(obj_merged))
}
})
})
})
}

View file

@ -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<MulticastGroup> {
let mut groups: HashSet<MulticastGroup> = 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<MulticastGroup> {
let mut groups: HashSet<MulticastGroup> = HashSet::new();
groups
}

View file

@ -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<Option<JoinHandle<()>>>,
}
// 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<BTreeSet<u32>> = 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::<u8>(), 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::<c_void>()) == 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::<c_void>()) != 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::<u8>(), 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::<c_void>()) != 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<F: Fn(&[u8]) + Send + Sync + 'static>(nwid: &NetworkId, mac: &MAC, mtu: i32, metric: i32, eth_frame_func: F) -> Result<MacFethTap, String> {
// 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::<c_void>()) } != 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::<c_void>()) } != 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::<c_void>()) } != 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::<u8>(), 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::<c_void>()) } != 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::<c_void>()) } != 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::<c_void>()) } != 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::<osdep::bpf_hdr>() 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::<osdep::bpf_hdr>();
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::<osdep::sockaddr_ndrv>() 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::<u8>(), 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::<osdep::sockaddr_ndrv>() 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::<osdep::sockaddr_ndrv>() 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<InetAddress> {
let mut ipv: Vec<InetAddress> = 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<MulticastGroup> {
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().
}
}

View file

@ -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;

View file

@ -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<zerotier_core::InetAddress>;
/// 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<zerotier_core::MulticastGroup>;
/// 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;
}