Page MenuHomePhabricator

No OneTemporary

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..bb9862f
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1376 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "axum"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.71.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+dependencies = [
+ "bitflags 2.9.1",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "collar"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "bindgen",
+ "env_logger",
+ "errno",
+ "libc",
+ "log",
+ "nix",
+ "sled",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "jiff"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
+dependencies = [
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libloading"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.11",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.12",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags 2.9.1",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.9",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustversion"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tokio"
+version = "1.45.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot 0.12.4",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
+dependencies = [
+ "bitflags 2.9.1",
+ "bytes",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[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 = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..4e85c5d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "collar"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "collar-ng"
+path = "src/bin/collar-ng.rs"
+
+[dependencies]
+errno = "*"
+socket2 = "*"
+libc = "*"
+log = "0.4.27"
+env_logger = "0.11.8"
+nix = { version = "0.30.1", features = ["ioctl", "net"] }
+thiserror = "2.0.12"
+tokio = { version = "1.45.1", features = ["full"] }
+axum = "0.8.4"
+tracing = "0.1.41"
+tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
+tower-http = { version = "0.6.4", features = ["trace"] }
+sled = "0.34.7"
+
+[build-dependencies]
+bindgen = "0.71.1"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..9e20d8f
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,34 @@
+extern crate bindgen;
+
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+ // Tell cargo to tell rustc to link the system bzip2
+ // shared library.
+ println!("cargo:rustc-link-lib=netgraph");
+
+ // Tell cargo to invalidate the built crate whenever the wrapper changes
+ println!("cargo:rerun-if-changed=src/ng/wrapper.h");
+
+ // The bindgen::Builder is the main entry point
+ // to bindgen, and lets you build up options for
+ // the resulting bindings.
+ let bindings = bindgen::Builder::default()
+ // The input header we would like to generate
+ // bindings for.
+ .header("src/ng/wrapper.h")
+ // Tell cargo to invalidate the built crate whenever any of the
+ // included header files changed.
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks))
+ // Finish the builder and generate the bindings.
+ .generate()
+ // Unwrap the Result and panic on failure.
+ .expect("Unable to generate bindings");
+
+ // Write the bindings to the $OUT_DIR/bindings.rs file.
+ let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
+ bindings
+ .write_to_file(out_path.join("netgraph_bindings.rs"))
+ .expect("Couldn't write bindings!");
+}
diff --git a/src/bin/collar-ng.rs b/src/bin/collar-ng.rs
new file mode 100644
index 0000000..63030ce
--- /dev/null
+++ b/src/bin/collar-ng.rs
@@ -0,0 +1,71 @@
+use std::env::args;
+use env_logger::Env;
+use log::error;
+use collar::{
+ error::{Result, Error},
+ ng,
+};
+
+fn main() {
+ let env = Env::default()
+ .filter_or("LOG_LEVEL", "trace")
+ .write_style_or("LOG_STYLE", "always");
+
+ env_logger::init_from_env(env);
+
+ let mut argv = args();
+ let prog = argv.next().unwrap();
+ match argv.next() {
+ None => println!("Usage: {} <command> (args)", prog),
+ Some(str) => match str.as_str() {
+
+ "new-eiface" => {
+ match argv.next() {
+ None => println!("Usage: {} new-eiface <name>", prog),
+ Some(name) => match ng::new_eiface(&name) {
+ Ok(s) => println!("ok: {}", s),
+ Err(e) => error!("error: {:?}", e),
+ }
+ }
+ },
+
+ "new-eiface-bridge" => {
+ match argv.next() {
+ None => println!("Usage: {} new-bridge <bridge> <name>", prog),
+ Some(bridge) => match argv.next() {
+ None => println!("Usage: {} new-bridge <bridge> <name>", prog),
+ Some(name) => match ng::new_bridge(&bridge, &name) {
+ Ok(s) => println!("ok: {}", s),
+ Err(e) => error!("error: {:?}", e)
+ }
+ }
+ }
+ }
+
+ "join-eiface-bridge" => {
+ match argv.next() {
+ None => println!("Usage: {} bridge-eiface <bridge> <name>", prog),
+ Some(bridge) => match argv.next() {
+ None => println!("Usage: {} bridge-eiface <bridge> <name>", prog),
+ Some(name) => match ng::bridge_eiface(&bridge, &name) {
+ Ok(s) => println!("ok: {}", s),
+ Err(e) => error!("error: {:?}", e)
+ }
+ }
+ }
+ }
+
+ "bridge" => {
+ match argv.next() {
+ None => println!("Usage: {} <path>", prog),
+ Some(path) => match ng::go(&path) {
+ Ok(s) => println!("{}", s),
+ Err(e) => error!("error: {:?}", e)
+ }
+ }
+ }
+
+ _ => println!("Unknown action: {}", str)
+ }
+ }
+}
diff --git a/src/bin/collared.rs b/src/bin/collared.rs
new file mode 100644
index 0000000..4507d09
--- /dev/null
+++ b/src/bin/collared.rs
@@ -0,0 +1,33 @@
+use std::env::args;
+
+use tokio::net::TcpListener;
+use tower_http::trace::TraceLayer;
+use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
+
+use axum::{
+ routing::get
+};
+
+#[tokio::main]
+async fn main() {
+ tracing_subscriber::registry()
+ .with(
+ tracing_subscriber::filter::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
+ "collar=debug,tower_http=debug,axum::rejection=trace"
+ .into()
+ }),
+ )
+ .with(tracing_subscriber::fmt::layer())
+ .init();
+
+
+ // build our application with a single route
+ let app = axum::Router::new()
+ .route("/", get(|| async { "hic!" }))
+ .layer(tower_http::trace::TraceLayer::new_for_http());
+
+ // run our app with hyper, listening globally on port 3000
+ let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
+ tracing::info!("listening on {}", listener.local_addr().unwrap());
+ axum::serve(listener, app).await.unwrap();
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..0ba8d67
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,53 @@
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+ #[error("sys err")]
+ SystemError(errno::Errno),
+
+ #[error("not a bridge")]
+ PathIsNotABridge,
+
+ #[error("invalid node type")]
+ InvalidNodeType(String, String),
+
+ #[error("bridge already exists")]
+ BridgeLinkExists(String, String),
+
+ #[error("utf8")]
+ InvalidUtf8(std::str::Utf8Error),
+
+ #[error("nul")]
+ NulError(String),
+
+ #[error("exists")]
+ Exists,
+
+ #[error("invalid file descriptor")]
+ InvalidDescriptor,
+
+ #[error("name too long")]
+ NameTooLong,
+
+ #[error("description too long")]
+ DescriptionTooLong,
+
+ #[error("interface not found")]
+ InterfaceNotFound,
+
+ #[error("out of memory")]
+ OutOfMemory,
+
+ #[error("operation not supported")]
+ NotSupported,
+
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+
+ #[error(transparent)]
+ Nul(#[from] std::ffi::NulError),
+
+ #[error(transparent)]
+ ParseNum(#[from] std::num::ParseIntError),
+
+}
+
+pub type Result<T, E = Error> = ::std::result::Result<T, E>;
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..a8b27b7
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod error;
+
+pub mod netif;
+
+pub mod ng;
diff --git a/src/netif/fd.rs b/src/netif/fd.rs
new file mode 100644
index 0000000..f64560c
--- /dev/null
+++ b/src/netif/fd.rs
@@ -0,0 +1,130 @@
+use libc::{self, F_GETFL, F_SETFL, O_NONBLOCK, fcntl};
+use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
+use crate::error::{Error, Result};
+
+/// POSIX file descriptor support for `io` traits.
+pub(crate) struct Fd {
+ pub(crate) inner: RawFd,
+ close_fd_on_drop: bool,
+}
+
+impl Fd {
+ pub fn new(value: RawFd, close_fd_on_drop: bool) -> Result<Self> {
+ if value < 0 {
+ return Err(Error::InvalidDescriptor);
+ }
+ Ok(Fd {
+ inner: value,
+ close_fd_on_drop,
+ })
+ }
+
+ /// Enable non-blocking mode
+ pub fn set_nonblock(&self) -> std::io::Result<()> {
+ match unsafe { fcntl(self.inner, F_SETFL, fcntl(self.inner, F_GETFL) | O_NONBLOCK) } {
+ 0 => Ok(()),
+ _ => Err(std::io::Error::last_os_error()),
+ }
+ }
+
+ #[inline]
+ pub fn read(&self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let fd = self.as_raw_fd();
+ let amount = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
+ if amount < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ Ok(amount as usize)
+ }
+
+ #[allow(dead_code)]
+ #[inline]
+ fn readv(&self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
+ if bufs.len() > max_iov() {
+ return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
+ }
+ let amount = unsafe {
+ libc::readv(
+ self.as_raw_fd(),
+ bufs.as_mut_ptr() as *mut libc::iovec as *const libc::iovec,
+ bufs.len() as libc::c_int,
+ )
+ };
+ if amount < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ Ok(amount as usize)
+ }
+
+ #[inline]
+ pub fn write(&self, buf: &[u8]) -> std::io::Result<usize> {
+ let fd = self.as_raw_fd();
+ let amount = unsafe { libc::write(fd, buf.as_ptr() as *const _, buf.len()) };
+ if amount < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ Ok(amount as usize)
+ }
+
+ #[allow(dead_code)]
+ #[inline]
+ pub fn writev(&self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
+ if bufs.len() > max_iov() {
+ return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
+ }
+ let amount = unsafe {
+ libc::writev(
+ self.as_raw_fd(),
+ bufs.as_ptr() as *const libc::iovec,
+ bufs.len() as libc::c_int,
+ )
+ };
+ if amount < 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ Ok(amount as usize)
+ }
+}
+
+impl AsRawFd for Fd {
+ fn as_raw_fd(&self) -> RawFd {
+ self.inner
+ }
+}
+
+impl IntoRawFd for Fd {
+ fn into_raw_fd(mut self) -> RawFd {
+ let fd = self.inner;
+ self.inner = -1;
+ fd
+ }
+}
+
+impl Drop for Fd {
+ fn drop(&mut self) {
+ if self.close_fd_on_drop && self.inner >= 0 {
+ unsafe { libc::close(self.inner) };
+ }
+ }
+}
+
+#[cfg(any(
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd",
+ target_vendor = "apple",
+))]
+pub(crate) const fn max_iov() -> usize {
+ libc::IOV_MAX as usize
+}
+
+#[cfg(any(
+ target_os = "android",
+ target_os = "emscripten",
+ target_os = "linux",
+ target_os = "nto",
+))]
+pub(crate) const fn max_iov() -> usize {
+ libc::UIO_MAXIOV as usize
+}
diff --git a/src/netif/iface.rs b/src/netif/iface.rs
new file mode 100644
index 0000000..4c41493
--- /dev/null
+++ b/src/netif/iface.rs
@@ -0,0 +1,155 @@
+use libc::{
+ self, AF_INET, IFNAMSIZ, SOCK_DGRAM, c_char, c_void, ifreq,
+};
+use std::{
+ ffi::CString,
+ mem,
+ os::unix::io::AsRawFd,
+ ptr,
+};
+
+use crate::error::{Error, Result};
+use crate::netif::{fd::Fd, sys, sys::*};
+
+pub struct Iface {
+ pub name: String,
+ ctl: Fd
+}
+
+impl Iface {
+ pub fn new(name: &str) -> Result<Self> {
+ let ctl = unsafe { Fd::new(libc::socket(AF_INET, SOCK_DGRAM, 0), true)? };
+ Ok(Self {
+ name: name.to_string(),
+ ctl,
+ })
+ }
+
+ unsafe fn request(&self) -> Result<ifreq> {
+ if self.name.len() >= IFNAMSIZ {
+ return Err(Error::NameTooLong);
+ }
+
+ let mut req: ifreq = mem::zeroed();
+ let name_bytes = self.name.as_bytes();
+ ptr::copy_nonoverlapping(
+ name_bytes.as_ptr() as *const c_char,
+ req.ifr_name.as_mut_ptr(),
+ name_bytes.len(),
+ );
+ // Ensure null termination (mem::zeroed already did this)
+ Ok(req)
+ }
+
+ pub fn set_name(&mut self, value: &str) -> Result<()> {
+ if value.len() >= IFNAMSIZ {
+ return Err(Error::NameTooLong);
+ }
+
+ unsafe {
+ let mut req = self.request()?;
+ let if_name = CString::new(value)?;
+
+ req.ifr_ifru.ifru_data = if_name.as_ptr() as *mut c_char;
+
+ // Perform syscall while CString is still alive
+ let result = siocsifname(self.ctl.as_raw_fd(), &req);
+
+ // CString drops here naturally, after syscall
+ if let Err(err) = result {
+ return Err(std::io::Error::from(err).into());
+ }
+
+ self.name = value.to_string();
+ Ok(())
+ }
+ }
+
+ pub fn exists(&self) -> Result<bool> {
+ unsafe {
+ let mut req = self.request()?;
+
+ // Try to get interface flags to check if interface exists
+ match siocgifflags(self.ctl.as_raw_fd(), &mut req) {
+ Ok(_) => Ok(true),
+ Err(nix::errno::Errno::ENXIO) => Ok(false), // Device not configured
+ Err(err) => Err(std::io::Error::from(err).into()),
+ }
+ }
+ }
+
+ pub fn set_description(&mut self, value: &str) -> Result<()> {
+ unsafe {
+ // Check if interface exists first
+ if !self.exists()? {
+ return Err(Error::InterfaceNotFound);
+ }
+
+ let mut req: sys::ifreq_ext = mem::zeroed();
+
+ // Copy interface name with proper bounds checking
+ let name_bytes = self.name.as_bytes();
+ if name_bytes.len() >= IFNAMSIZ {
+ return Err(Error::NameTooLong);
+ }
+
+ ptr::copy_nonoverlapping(
+ name_bytes.as_ptr() as *const c_char,
+ req.ifr_name.as_mut_ptr(),
+ name_bytes.len(),
+ );
+
+ if value.is_empty() {
+ // Clear description
+ req.ifr_ifru.ifru_buffer.length = 0;
+ req.ifr_ifru.ifru_buffer.buffer = ptr::null_mut();
+
+ let result = siocsifdescr(self.ctl.as_raw_fd(), &req as *const _ as *const ifreq);
+ if let Err(err) = result {
+ return Err(std::io::Error::from(err).into());
+ }
+ } else {
+ if value.len() >= IFDESCRSIZ {
+ return Err(Error::DescriptionTooLong);
+ }
+
+ // Create null-terminated string and keep it alive during syscall
+ let desc_cstring = CString::new(value)?;
+ req.ifr_ifru.ifru_buffer.length = desc_cstring.as_bytes_with_nul().len() as u32;
+ req.ifr_ifru.ifru_buffer.buffer = desc_cstring.as_ptr() as *mut c_void;
+
+ // Perform syscall while CString is still alive
+ let result = siocsifdescr(self.ctl.as_raw_fd(), &req as *const _ as *const ifreq);
+
+ // CString drops here naturally, after syscall
+ if let Err(err) = result {
+ return Err(std::io::Error::from(err).into());
+ }
+ }
+
+ Ok(())
+ }
+ }
+
+ pub fn set_mtu(&mut self, mtu: i32) -> Result<()> {
+ unsafe {
+ // Check if interface exists first
+ if !self.exists()? {
+ return Err(Error::InterfaceNotFound);
+ }
+
+ let mut req = self.request()?;
+
+ // Set the MTU value
+ req.ifr_ifru.ifru_mtu = mtu;
+
+ // Perform the ioctl to set MTU
+ let result = siocsifmtu(self.ctl.as_raw_fd(), &req);
+ if let Err(err) = result {
+ return Err(std::io::Error::from(err).into());
+ }
+
+ Ok(())
+ }
+ }
+}
diff --git a/src/netif/mod.rs b/src/netif/mod.rs
new file mode 100644
index 0000000..70bbe25
--- /dev/null
+++ b/src/netif/mod.rs
@@ -0,0 +1,5 @@
+mod fd;
+mod sys;
+mod iface;
+
+pub(crate) use iface::Iface;
diff --git a/src/netif/sys.rs b/src/netif/sys.rs
new file mode 100644
index 0000000..87f0420
--- /dev/null
+++ b/src/netif/sys.rs
@@ -0,0 +1,95 @@
+use libc::{IFNAMSIZ, c_char, c_int, c_uint, c_void, ifreq, sockaddr};
+use nix::{ioctl_readwrite, ioctl_write_ptr};
+
+// FreeBSD interface description size limit
+pub const IFDESCRSIZ: usize = 64;
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct ifr_buffer {
+ pub length: c_uint,
+ pub buffer: *mut c_void,
+}
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+pub union ifr_ifru {
+ pub ifru_addr: sockaddr,
+ pub ifru_dstaddr: sockaddr,
+ pub ifru_broadaddr: sockaddr,
+ pub ifru_flags: c_int,
+ pub ifru_metric: c_int,
+ pub ifru_mtu: c_int,
+ pub ifru_phys: c_int,
+ pub ifru_media: c_int,
+ pub ifru_data: *mut c_char,
+ pub ifru_cap: [c_int; 2],
+ pub ifru_buffer: ifr_buffer,
+}
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+pub struct ifreq_ext {
+ pub ifr_name: [c_char; IFNAMSIZ],
+ pub ifr_ifru: ifr_ifru,
+}
+
+#[allow(non_camel_case_types, dead_code)]
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct ctl_info {
+ pub ctl_id: c_uint,
+ pub ctl_name: [c_char; 96],
+}
+
+#[allow(non_camel_case_types)]
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct ifaliasreq {
+ pub ifran: [c_char; IFNAMSIZ],
+ pub addr: sockaddr,
+ pub dstaddr: sockaddr,
+ pub mask: sockaddr,
+ pub ifra_vhid: c_int,
+}
+
+// #[allow(non_camel_case_types)]
+// #[repr(C)]
+// #[derive(Copy, Clone)]
+// pub struct in_aliasreq {
+// pub ifra_name: [c_char; IFNAMSIZ],
+// pub ifra_addr: sockaddr_in,
+// pub ifra_dstaddr: sockaddr_in,
+// pub ifra_mask: sockaddr_in,
+// pub ifra_vhid:c_int
+// }
+
+ioctl_write_ptr!(siocsifflags, b'i', 16, ifreq);
+ioctl_readwrite!(siocgifflags, b'i', 17, ifreq);
+
+ioctl_write_ptr!(siocsifaddr, b'i', 12, ifreq);
+ioctl_readwrite!(siocgifaddr, b'i', 33, ifreq);
+
+ioctl_write_ptr!(siocsifdstaddr, b'i', 14, ifreq);
+ioctl_readwrite!(siocgifdstaddr, b'i', 34, ifreq);
+
+ioctl_write_ptr!(siocsifbrdaddr, b'i', 19, ifreq);
+ioctl_readwrite!(siocgifbrdaddr, b'i', 35, ifreq);
+
+ioctl_write_ptr!(siocsifnetmask, b'i', 22, ifreq);
+ioctl_readwrite!(siocgifnetmask, b'i', 37, ifreq);
+
+ioctl_write_ptr!(siocsifmtu, b'i', 52, ifreq);
+ioctl_readwrite!(siocgifmtu, b'i', 51, ifreq);
+
+ioctl_write_ptr!(siocaifaddr, b'i', 43, ifaliasreq);
+ioctl_write_ptr!(siocdifaddr, b'i', 25, ifreq);
+
+ioctl_write_ptr!(siocifcreate, b'i', 122, ifreq);
+
+ioctl_write_ptr!(siocsifphyaddr, b'i', 70, ifaliasreq);
+
+ioctl_write_ptr!(siocsifname, b'i', 40, ifreq);
+
+ioctl_write_ptr!(siocsifdescr, b'i', 41, ifreq);
diff --git a/src/ng/mod.rs b/src/ng/mod.rs
new file mode 100644
index 0000000..f06a5c0
--- /dev/null
+++ b/src/ng/mod.rs
@@ -0,0 +1,460 @@
+#![allow(non_upper_case_globals)]
+#![allow(non_camel_case_types)]
+#![allow(non_snake_case)]
+
+use errno::{Errno, errno};
+use libc::{free, c_char, c_int, c_ulong, c_void, ENOENT};
+use socket2::Socket;
+use std::{
+ alloc::{alloc, dealloc, Layout},
+ convert::TryFrom,
+ ffi::{CString, CStr},
+ mem::size_of,
+ os::unix::io::{FromRawFd, AsRawFd},
+ process::id,
+ ptr,
+ vec::Vec,
+};
+use log::{error, debug, trace};
+
+use crate::{
+ error::{Result, Error},
+ netif,
+};
+
+include!(concat!(env!("OUT_DIR"), "/netgraph_bindings.rs"));
+
+fn copy_str_to_cchar(dst: &mut [c_char], src: &str) {
+ for (d, s) in dst.iter_mut().zip(src.as_bytes().iter()) {
+ *d = *s as c_char;
+ }
+ *(dst.last_mut().unwrap()) = 0;
+}
+
+impl From<(&str, &str, &str)> for ngm_mkpeer {
+ fn from(args: (&str, &str, &str)) -> Self {
+ let (stype, sour, speer) = args;
+ let mut type_: [c_char; NG_TYPESIZ as usize] = [0; NG_TYPESIZ as usize];
+ let mut ourhook: [c_char; NG_HOOKSIZ as usize] = [0; NG_HOOKSIZ as usize];
+ let mut peerhook: [c_char; NG_HOOKSIZ as usize] = [0; NG_HOOKSIZ as usize];
+ copy_str_to_cchar(&mut type_, &stype);
+ copy_str_to_cchar(&mut ourhook, &sour);
+ copy_str_to_cchar(&mut peerhook, &speer);
+ ngm_mkpeer { type_, ourhook, peerhook }
+ }
+}
+
+impl From<&str> for ngm_name {
+ fn from(name_str: &str) -> Self {
+ let mut name: [c_char; 32] = [0; 32];
+ copy_str_to_cchar(&mut name, name_str);
+ ngm_name { name }
+ }
+}
+
+impl From<(&str, &str, &str)> for ngm_connect {
+ fn from(args: (&str, &str, &str)) -> Self {
+ let (spath, sour, speer) = args;
+ let mut path: [c_char; NG_PATHSIZ as usize] = [0; NG_PATHSIZ as usize];
+ let mut ourhook: [c_char; NG_HOOKSIZ as usize] = [0; NG_HOOKSIZ as usize];
+ let mut peerhook: [c_char; NG_HOOKSIZ as usize] = [0; NG_HOOKSIZ as usize];
+ copy_str_to_cchar(&mut path, &spath);
+ copy_str_to_cchar(&mut ourhook, &sour);
+ copy_str_to_cchar(&mut peerhook, &speer);
+ ngm_connect { path, ourhook, peerhook }
+ }
+}
+
+
+type ControlSocket = Socket;
+type DataSocket = Socket;
+
+#[allow(dead_code)]
+#[derive(Debug)]
+struct NodeInfo {
+ name: String,
+ nodetype: String,
+ id: u32,
+ hooks: u32
+}
+
+impl TryFrom<nodeinfo> for NodeInfo {
+ type Error = Error;
+
+ fn try_from(ninf: nodeinfo) -> Result<Self, Self::Error> {
+ let name = unsafe{CStr::from_ptr(ninf.name.as_ptr())}.to_str().map_err(Error::InvalidUtf8)?.to_owned();
+ let type_ = unsafe{CStr::from_ptr(ninf.type_.as_ptr())}.to_str().map_err(Error::InvalidUtf8)?.to_owned();
+ Ok(NodeInfo { name: name,
+ nodetype: type_,
+ id: ninf.id,
+ hooks: ninf.hooks })
+ }
+}
+
+
+#[allow(dead_code)]
+#[derive(Debug)]
+struct LinkInfo {
+ ourhook: String,
+ peerhook: String,
+ peer: NodeInfo
+}
+
+impl TryFrom<&linkinfo> for LinkInfo {
+ type Error = Error;
+
+ fn try_from(linf: &linkinfo) -> Result<Self, Self::Error> {
+ let ourhook = unsafe{CStr::from_ptr(linf.ourhook.as_ptr())}.to_str().map_err(Error::InvalidUtf8)?.to_owned();
+ let peerhook = unsafe{CStr::from_ptr(linf.peerhook.as_ptr())}.to_str().map_err(Error::InvalidUtf8)?.to_owned();
+ let peer = NodeInfo::try_from(linf.nodeinfo)?;
+ Ok(LinkInfo { ourhook, peerhook, peer })
+ }
+}
+
+
+fn ng_mk_sock_node(name: String) -> Result<(ControlSocket, DataSocket), Errno> {
+ let c_str = CString::new(name).unwrap();
+ let mut csp: c_int = 0;
+ let mut dsp: c_int = 0;
+ let pcsp: *mut c_int = &mut csp;
+ let pdsp: *mut c_int = &mut dsp;
+ let ret = unsafe { NgMkSockNode(c_str.as_ptr() as *const c_char, pcsp, pdsp) };
+ if ret == -1 {
+ return Err(errno())
+ }
+ Ok(unsafe { (Socket::from_raw_fd(csp), Socket::from_raw_fd(dsp)) })
+}
+
+unsafe fn ng_alloc_recv_msg(csock: &ControlSocket) -> Result<(*mut ng_mesg, CString), Errno> {
+ let cs: c_int = csock.as_raw_fd();
+ let mut respptr: *mut ng_mesg = ptr::null_mut();
+
+ let pathlayout = Layout::from_size_align(NG_PATHSIZ as usize, 1).unwrap();
+ let pathbuf = unsafe { alloc(pathlayout) as *mut c_char };
+ let ret = unsafe { NgAllocRecvMsg(cs, &mut respptr, pathbuf) };
+ let resppath = unsafe { CStr::from_ptr(pathbuf).to_owned() };
+ unsafe { dealloc(pathbuf as *mut u8, pathlayout) };
+ if ret == -1 {
+ return Err(errno())
+ }
+ Ok((respptr, resppath))
+}
+
+unsafe fn ng_send_msg(csock: &ControlSocket, path: &CStr, cookie: c_int, cmd: c_int, parg: *const c_void, arglen: c_ulong) -> Result<(), Errno> {
+ let ret = unsafe { NgSendMsg(csock.as_raw_fd(), path.as_ptr() as *const c_char, cookie, cmd, parg, arglen.try_into().unwrap()) };
+ if ret == -1 {
+ return Err(errno())
+ }
+ Ok(())
+}
+
+unsafe fn ng_send_ascii_message(csock: &ControlSocket, path: &CStr, query: &CStr) -> Result<(), Errno> {
+ // FIXME multiple arguments in
+ let ret = unsafe { NgSendAsciiMsg(csock.as_raw_fd(), path.as_ptr() as *const c_char, query.as_ptr() as *const c_char) };
+ if ret == -1 {
+ return Err(errno())
+ }
+ Ok(())
+}
+
+fn command_response(csock: &ControlSocket, path: &str, cookie: c_int, cmd: c_int, parg: *const c_void, arglen: c_ulong) -> Result<*mut ng_mesg, Error> {
+ let cpath = CString::new(path).map_err(|_| Error::NulError(path.to_owned()))?;
+ Ok(unsafe {
+ ng_send_msg(csock, &cpath, cookie, cmd, parg, arglen).map_err(Error::SystemError)?;
+ let (msg, _) = ng_alloc_recv_msg(csock).map_err(Error::SystemError)?;
+ msg
+ })
+}
+
+fn list_hooks(csock: &ControlSocket, path: &str) -> Result<(NodeInfo, Vec<LinkInfo>), Error> {
+ let parg: *const c_void = ptr::null();
+ let respptr: *mut ng_mesg = command_response(csock, path, NGM_GENERIC_COOKIE as c_int, NGM_LISTHOOKS as c_int, parg, 0)?;
+
+ // Pointer should not be null at this point (check?)
+ Ok(unsafe {
+ let pdata: *const hooklist = (*respptr).data.as_ptr().cast();
+ let ninf = NodeInfo::try_from((*pdata).nodeinfo)?;
+ let tmp: Result<Vec<LinkInfo>, Error> = (*pdata)
+ .link
+ .as_slice(ninf.hooks.try_into().unwrap())
+ .iter()
+ .map(|l| LinkInfo::try_from(l))
+ .collect();
+ let mut links = tmp?;
+ links.sort_unstable_by(|a, b| a.ourhook.cmp(&b.ourhook));
+ // ng_mesg is a nested set of flexible-array structs, so one call to free() is enough
+ free(respptr as *mut c_void);
+ (ninf, links)
+ })
+}
+
+fn find_free_hook(csock: &ControlSocket, path: &str) -> Result<usize, Error> {
+ let (ninf, links) = list_hooks(csock, path)?;
+ if ninf.nodetype != "bridge" {
+ return Err(Error::PathIsNotABridge);
+ }
+ for (n, l) in links.iter().map(|l| l.ourhook[4..].parse::<u32>().unwrap()).enumerate() {
+ if l > n.try_into().unwrap() {
+ return Ok(n)
+ }
+ }
+ Ok(ninf.hooks.try_into().unwrap())
+}
+
+fn make_peer(csock: &ControlSocket, path: &str, nodetype: &str, ourhook: &str, peerhook: &str) -> Result<(), Error> {
+ let cpath = CString::new(path).map_err(|_| Error::NulError(path.to_owned()))?;
+ let newpeer = ngm_mkpeer::from((nodetype, ourhook, peerhook));
+ let pnewpeer: *const ngm_mkpeer = &newpeer;
+
+ trace!("make_peer {} {} {} {}", path, nodetype, ourhook, peerhook);
+
+ // send the makepeer command
+ unsafe { ng_send_msg(csock, &cpath, NGM_GENERIC_COOKIE as c_int, NGM_MKPEER as c_int, pnewpeer as *const c_void, size_of::<ngm_mkpeer>() as u64).map_err(Error::SystemError)? };
+
+ // no response expected
+ Ok(())
+}
+
+fn connect_nodes(csock: &ControlSocket, from_path: &str, to_path: &str, ourhook: &str, peerhook: &str) -> Result<(), Error> {
+ let cfrom_path = CString::new(from_path).map_err(|_| Error::NulError(from_path.to_owned()))?;
+ let connect_msg = ngm_connect::from((to_path, ourhook, peerhook));
+ let pconnect_msg: *const ngm_connect = &connect_msg;
+
+ trace!("connect_nodes {} {} {} {}", from_path, to_path, ourhook, peerhook);
+
+ // send the connect command
+ unsafe { ng_send_msg(csock, &cfrom_path, NGM_GENERIC_COOKIE as c_int, NGM_CONNECT as c_int, pconnect_msg as *const c_void, size_of::<ngm_connect>() as u64).map_err(Error::SystemError)? };
+
+ // no response expected
+ Ok(())
+}
+
+fn get_interface_name(csock: &ControlSocket, iface: &str) -> Result<CString, Error> {
+ let linkpath = format!("{}:", iface); //String::from(path); // already contains a colon at the end
+ let clpath = CString::new(linkpath.clone()).map_err(|_| Error::NulError(linkpath))?;
+ let query = CString::new("getifname").unwrap(); // this should never fail
+ unsafe {
+ ng_send_ascii_message(csock, &clpath, &query).map_err(Error::SystemError)?;
+ let (binresp, _) = ng_alloc_recv_msg(csock).map_err(Error::SystemError)?;
+ let text: *const c_char = (*binresp).data.as_ptr().cast();
+ let device_name = CStr::from_ptr(text).to_owned();
+ free(binresp as *mut c_void);
+ Ok(device_name)
+ }
+}
+
+fn get_interface_name_from_path(csock: &ControlSocket, path: &str, ourhook: &str) -> Result<CString, Error> {
+ let mut linkpath = String::from(path); // already contains a colon at the end
+ linkpath.push_str(&ourhook);
+ let clpath = CString::new(linkpath.clone()).map_err(|_| Error::NulError(linkpath))?;
+ let query = CString::new("getifname").unwrap(); // this should never fail
+ unsafe {
+ ng_send_ascii_message(csock, &clpath, &query).map_err(Error::SystemError)?;
+ let (binresp, _) = ng_alloc_recv_msg(csock).map_err(Error::SystemError)?;
+ let text: *const c_char = (*binresp).data.as_ptr().cast();
+ let device_name = CStr::from_ptr(text).to_owned();
+ free(binresp as *mut c_void);
+ Ok(device_name)
+ }
+}
+
+pub fn go(path: &str) -> Result<String, Error> {
+ let (csock, _) = ng_mk_sock_node(format!("ngctlrs{}", id())).map_err(Error::SystemError)?;
+ //unsafe { NgSetDebug(2) };
+ let link = format!("link{}", find_free_hook(&csock, path)?);
+ make_peer(&csock, path, "eiface", &link, "ether")?;
+ let device_name = get_interface_name_from_path(&csock, path, &link)?;
+ Ok(device_name.into_string().map_err(|e| Error::InvalidUtf8(e.utf8_error()))?)
+}
+
+pub fn post_new_eiface(csock: &ControlSocket, ng_name: &str, description: Option<&str>) -> Result<(), Error> {
+ let iface_name = get_interface_name(&csock, &ng_name)?.into_string().map_err(|e| Error::InvalidUtf8(e.utf8_error()))?;
+ debug!("ng eiface {} is iface: {}", ng_name, iface_name);
+ let mut iface = netif::Iface::new(&iface_name)?;
+ iface.set_name(&ng_name)?;
+ trace!("ng eiface {} iface renamed (was {})", ng_name, iface_name);
+ if let Some(description) = description {
+ iface.set_description(description)?;
+ trace!("ng eiface {} iface description set: '{}'", ng_name, description);
+ }
+ Ok(())
+}
+
+pub fn new_eiface(name: &str) -> Result<String, Error> {
+ let (csock, _) = ng_mk_sock_node(format!("ngctlrs{}", id())).map_err(Error::SystemError)?;
+
+ // Check if a node with the given name already exists
+ let name_with_colon = format!("{}:", name);
+ let name_cstr = CString::new(name_with_colon).map_err(|_| Error::NulError(name.to_owned()))?;
+
+ // Try to send NGM_NODEINFO to the named node
+ match unsafe {
+ ng_send_msg(&csock, &name_cstr, NGM_GENERIC_COOKIE as c_int, NGM_NODEINFO as c_int,
+ std::ptr::null(), 0)
+ } {
+ Ok(_) => {
+ // Node exists, return the name
+ return Ok(name.to_string());
+ },
+ Err(e) if e.0 == ENOENT => {
+ // Node doesn't exist, continue with creation
+ },
+ Err(e) => {
+ // Other error, propagate it
+ return Err(Error::SystemError(e));
+ }
+ }
+
+ make_peer(&csock, ".", "eiface", "ether", "ether")?;
+
+ // Send the name message to the newly created eiface node
+ let name_msg = ngm_name::from(name);
+ let pname_msg: *const ngm_name = &name_msg;
+ let cpath = CString::new(".ether").map_err(|_| Error::NulError(".ether".to_owned()))?;
+
+ unsafe {
+ ng_send_msg(&csock, &cpath, NGM_GENERIC_COOKIE as c_int, NGM_NAME as c_int,
+ pname_msg as *const c_void, size_of::<ngm_name>() as u64)
+ .map_err(Error::SystemError)?
+ };
+
+ match post_new_eiface(&csock, &name, None) {
+ Ok(_) => {
+ Ok(name.to_string())
+ },
+ Err(e) => {
+ error!("failed to configure ngeth interface {}: {:?}", name, e);
+ shutdown_interface(&csock, &name)?;
+ Err(e)
+ }
+ }
+ //Ok(device_name.into_string().map_err(|e| Error::InvalidUtf8(e.utf8_error()))?)
+}
+
+// TODO: Check if the interface is already bridged.
+pub fn bridge_eiface(bridge: &str, name: &str) -> Result<String, Error> {
+ let (csock, _) = ng_mk_sock_node(format!("ngctlrs{}", id())).map_err(Error::SystemError)?;
+
+ // Find a free hook on the bridge
+ let bridge_path = format!("{}:", bridge);
+ let link = format!("link{}", find_free_hook(&csock, &bridge_path)?);
+
+ // Connect the existing eiface to the bridge
+ let eiface_path = format!("{}:", name);
+ connect_nodes(&csock, &bridge_path, &eiface_path, &link, "ether")?;
+ Ok(name.to_string())
+}
+
+fn is_peer(links: &Vec<LinkInfo>, peer: &str) -> bool {
+ links.iter().any(|link| link.peer.name == peer)
+}
+
+fn has_hook(links: &Vec<LinkInfo>, ourhook: &str) -> bool {
+ links.iter().any(|link| link.ourhook == ourhook)
+}
+
+pub fn new_bridge(bridge: &str, name: &str) -> Result<String, Error> {
+ let (csock, _) = ng_mk_sock_node(format!("ngctlrs{}", id())).map_err(Error::SystemError)?;
+
+ match list_hooks(&csock, &format!("{}:", &name)) {
+ Ok(_) => {},
+ Err(e) => {
+ error!("interface {}: does not exists", &name);
+ return Err(e);
+ }
+ }
+
+ // Check if a bridge with the given name already exists
+ let bridge_path = format!("{}:", bridge);
+
+ // Try to send NGM_NODEINFO to the named bridge
+ match list_hooks(&csock, &bridge_path) {
+ Ok((ninf, links)) => {
+ debug!("bridge exists: {}", &bridge);
+
+ debug!("Bridge Node: {:?}", ninf);
+
+ if ninf.nodetype != "bridge" {
+ return Err(Error::InvalidNodeType(bridge.to_string(), ninf.nodetype.to_string()));
+ }
+
+ debug!("Links: {:?}", &links);
+
+ if is_peer(&links, &name) {
+ debug!("interface {} already exists on bridge {}", &name, &bridge);
+ return Err(Error::Exists)
+ }
+
+ if has_hook(&links, "link0") {
+ debug!("bridge {} already has a link0", &bridge);
+ return Err(Error::BridgeLinkExists(bridge.to_string(), "link0".to_string()))
+ }
+
+ // Connect the existing eiface to the bridge
+ debug!("connecting {} to {} as link0", &name, &bridge);
+ let eiface_path = format!("{}:", name);
+ connect_nodes(&csock, &bridge_path, &eiface_path, "link0", "ether")?;
+
+ },
+ Err(Error::SystemError(e)) if e.0 == ENOENT => {
+ // mkpeer derp: bridge ether link0
+ // Bridge doesn't exist, create it
+
+ debug!("bridge {} does not exist", &bridge);
+ make_peer(&csock, format!("{}:", &name).as_str(), "bridge", "ether", "link0")?;
+
+ // Send the name message to the newly created bridge node
+ trace!("naming {}:ether -> {}", &name, &bridge);
+ let bridge_relative_path = format!("{}:ether", &name);
+ let bridge_name_msg = ngm_name::from(bridge);
+ let pbridge_name_msg: *const ngm_name = &bridge_name_msg;
+ let bridge_relative_path = CString::new(bridge_relative_path.clone()).map_err(|_| Error::NulError(bridge_relative_path.clone().to_owned()))?;
+
+ unsafe {
+ ng_send_msg(&csock, &bridge_relative_path, NGM_GENERIC_COOKIE as c_int, NGM_NAME as c_int,
+ pbridge_name_msg as *const c_void, size_of::<ngm_name>() as u64)
+ .map_err(Error::SystemError)?
+ };
+
+
+ },
+ Err(e) => {
+ // Other error, propagate it
+ return Err(e);
+ }
+ }
+
+ Ok(bridge.to_string())
+}
+
+/// Sends a shutdown message to a named netgraph node
+///
+/// This function sends an NGM_SHUTDOWN control message to the netgraph node
+/// specified by `name`. The node will be destroyed and all its hooks will
+/// be disconnected.
+///
+/// # Arguments
+///
+/// * `csock` - The control socket to send the message through
+/// * `name` - The name of the netgraph node to shutdown (without the colon suffix)
+///
+/// # Returns
+///
+/// Returns `Ok(())` if the shutdown message was sent successfully, or an `Error`
+/// if the message could not be sent.
+pub fn shutdown_interface(csock: &ControlSocket, name: &str) -> Result<(), Error> {
+ let name_with_colon = format!("{}:", name);
+ let name_cstr = CString::new(name_with_colon).map_err(|_| Error::NulError(name.to_owned()))?;
+
+ trace!("shutdown_interface {}", name);
+
+ // Send the shutdown command to the named node
+ unsafe {
+ ng_send_msg(csock, &name_cstr, NGM_GENERIC_COOKIE as c_int, NGM_SHUTDOWN as c_int,
+ std::ptr::null(), 0)
+ .map_err(Error::SystemError)?
+ };
+
+ // No response expected for shutdown
+ Ok(())
+}
diff --git a/src/ng/wrapper.h b/src/ng/wrapper.h
new file mode 100644
index 0000000..4430070
--- /dev/null
+++ b/src/ng/wrapper.h
@@ -0,0 +1 @@
+#include <netgraph.h>

File Metadata

Mime Type
text/x-patch; charset=UTF-8
Expires
Fri, Jun 6, 4:17 AM (4 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
46360
Default Alt Text
(73 KB)

Event Timeline