Compare commits
3 Commits
7842b5aeba
...
e321300f67
| Author | SHA1 | Date | |
|---|---|---|---|
| e321300f67 | |||
| 7103184c8e | |||
| dd41f1961f |
Generated
+342
-87
@@ -97,6 +97,12 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "assert_cfg"
|
||||
version = "0.1.0"
|
||||
@@ -208,6 +214,8 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
@@ -216,9 +224,11 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -237,17 +247,43 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "0.4.0"
|
||||
name = "axum-extra"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
|
||||
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"instant",
|
||||
"rand 0.8.6",
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde_core",
|
||||
"serde_html_form",
|
||||
"serde_path_to_error",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backon"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"gloo-timers",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -256,15 +292,6 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
@@ -397,6 +424,17 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@@ -724,6 +762,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dontfrag"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117949f5a8b25ba471b4dfea927a07a2f8730c585532a2eb4294e18f5155397"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"tokio",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -751,6 +801,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -788,6 +847,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1011,6 +1076,18 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
@@ -1405,15 +1482,6 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intervaltree"
|
||||
version = "0.2.7"
|
||||
@@ -1505,18 +1573,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "librqbit"
|
||||
version = "8.1.1"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dadca8f521242010a4c846ef5f224c217009c92e272709cdc08ba9cdabe62983"
|
||||
checksum = "b9451def41c18d1b8e7b68c097bcb0c9a0579cdc9b779803368a65b86cae03d2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"async-compression",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"backoff",
|
||||
"axum-extra",
|
||||
"backon",
|
||||
"base64",
|
||||
"bincode 2.0.1",
|
||||
"bincode",
|
||||
"bitvec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -1532,22 +1601,29 @@ dependencies = [
|
||||
"librqbit-clone-to-owned",
|
||||
"librqbit-core",
|
||||
"librqbit-dht",
|
||||
"librqbit-dualstack-sockets",
|
||||
"librqbit-lsd",
|
||||
"librqbit-peer-protocol",
|
||||
"librqbit-sha1-wrapper",
|
||||
"librqbit-tracker-comms",
|
||||
"librqbit-upnp",
|
||||
"librqbit-utp",
|
||||
"memmap2",
|
||||
"mime_guess",
|
||||
"nix 0.30.1",
|
||||
"parking_lot",
|
||||
"rand 0.9.4",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"rlimit",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"serde_with",
|
||||
"size_format",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-socks",
|
||||
"tokio-stream",
|
||||
@@ -1557,61 +1633,71 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-bencode"
|
||||
version = "3.1.0"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "606dff526ba81e3eca33e2bb28b53afa2bc0b2c41d252333fa44e6c11abb37da"
|
||||
checksum = "97490fb9b35fb575f498cc1e804b23be6bbf3603d5a2358733e5b867d1ee52e8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"atoi",
|
||||
"bytes",
|
||||
"librqbit-buffers",
|
||||
"librqbit-clone-to-owned",
|
||||
"librqbit-sha1-wrapper",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-buffers"
|
||||
version = "4.2.0"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c78b907d6171a7191c162b2b60db46d254ebde6a95282b77372af556c1463"
|
||||
checksum = "e845dbd14174712471c3430c958d5c25e391561c2dd31f39f895bb60f63c46f0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"librqbit-clone-to-owned",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-clone-to-owned"
|
||||
version = "3.0.1"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd1e66d773ba9c475ff89286dc1d6f9d167cbb898603797467dd0ea6844c445"
|
||||
checksum = "76ef698eb8d7745f425f2b9a93e6a9d946bcb8bf84a559303e0fc3f01e8ad07c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-core"
|
||||
version = "5.0.0"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55a02cc6fce6743ad38661ccd6fafc6cf1ae5e0106a9922836b0524dbe752378"
|
||||
checksum = "a38e805aad201d96024849649174015d48a575348b35bbd927d99b47464e665e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cfg",
|
||||
"bytes",
|
||||
"chardetng",
|
||||
"data-encoding",
|
||||
"directories",
|
||||
"encoding_rs",
|
||||
"hex",
|
||||
"itertools",
|
||||
"librqbit-bencode",
|
||||
"librqbit-buffers",
|
||||
"librqbit-clone-to-owned",
|
||||
"librqbit-sha1-wrapper",
|
||||
"memchr",
|
||||
"parking_lot",
|
||||
"rand 0.9.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -1620,27 +1706,29 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-dht"
|
||||
version = "5.3.1"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7cc129194337771a86b0399956c4d9bf1cd97c5f24d14a50be38e170f76a54b"
|
||||
checksum = "d59a9374b33a52a065171641ae518db97969d3db849a57824c72c53d2109918b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backoff",
|
||||
"byteorder",
|
||||
"backon",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
"futures",
|
||||
"hex",
|
||||
"indexmap 2.14.0",
|
||||
"leaky-bucket",
|
||||
"librqbit-bencode",
|
||||
"librqbit-buffers",
|
||||
"librqbit-clone-to-owned",
|
||||
"librqbit-core",
|
||||
"librqbit-dualstack-sockets",
|
||||
"parking_lot",
|
||||
"rand 0.9.4",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
@@ -1648,13 +1736,49 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-peer-protocol"
|
||||
version = "4.3.0"
|
||||
name = "librqbit-dualstack-sockets"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a73129497b500505f33d1dc0426319b6a6a208f13fdfaae56224ab8c2346a773"
|
||||
checksum = "f7189f1ff66ca75055137a38b062ba7df9691d071b7172e5090997c62eb4b4de"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"backon",
|
||||
"futures",
|
||||
"libc",
|
||||
"network-interface",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-lsd"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e65ddc3b64392a71378944712c9830aac8ef98f860b180419dd42b4596e7faa"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atoi",
|
||||
"bstr",
|
||||
"futures",
|
||||
"httparse",
|
||||
"librqbit-core",
|
||||
"librqbit-dualstack-sockets",
|
||||
"parking_lot",
|
||||
"rand 0.9.4",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-peer-protocol"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ade1bbea8cf3e7eb51978b32a549787d17bbf872f3f0ed47d7c85e7377705a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode 1.3.3",
|
||||
"bitvec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -1664,13 +1788,16 @@ dependencies = [
|
||||
"librqbit-clone-to-owned",
|
||||
"librqbit-core",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-sha1-wrapper"
|
||||
version = "4.1.0"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79373a02db73159e4de7ca5d27b6eeae2d540df66c6801db2b01c5513d087524"
|
||||
checksum = "eeb119e5c05f1aa4011cf2e4dc19f83403064d2210804e2f35f06abac4e5b710"
|
||||
dependencies = [
|
||||
"assert_cfg",
|
||||
"aws-lc-rs",
|
||||
@@ -1678,21 +1805,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-tracker-comms"
|
||||
version = "3.0.0"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08204944c5be677a5de8e1230e0249fce5c14abef23048e26452c6fb03f1b260"
|
||||
checksum = "0ae0c520cc5f028fb50d746eaa6ba2aa082228db96c2baf1a312f9cacfceeb58"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"backon",
|
||||
"byteorder",
|
||||
"futures",
|
||||
"itertools",
|
||||
"librqbit-bencode",
|
||||
"librqbit-buffers",
|
||||
"librqbit-core",
|
||||
"librqbit-dualstack-sockets",
|
||||
"parking_lot",
|
||||
"rand 0.9.4",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_with",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
@@ -1702,23 +1834,49 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-upnp"
|
||||
version = "1.0.0"
|
||||
version = "9.0.0-rc.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545aad6124c97201055983137e12a19f34acad565120c3cd30596cbd72e8fa86"
|
||||
checksum = "058af0d5e2a99c567b73369455e3852c48addcf751836f08cfc25ca89dd16cc3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bstr",
|
||||
"futures",
|
||||
"httparse",
|
||||
"librqbit-dualstack-sockets",
|
||||
"network-interface",
|
||||
"quick-xml",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "librqbit-utp"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f3bfdc73944bc76cab24d5690a98816770040a654c449edf5ff2b9ba22626aa"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"dontfrag",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"librqbit-dualstack-sockets",
|
||||
"metrics",
|
||||
"parking_lot",
|
||||
"rand 0.9.4",
|
||||
"ringbuf",
|
||||
"rustc-hash",
|
||||
"socket2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.37.0"
|
||||
@@ -1787,6 +1945,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metrics"
|
||||
version = "0.24.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
"rapidhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -1836,6 +2004,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
@@ -2033,6 +2213,15 @@ version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.5"
|
||||
@@ -2106,9 +2295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
version = "0.38.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -2196,24 +2385,13 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_chacha",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
@@ -2228,16 +2406,6 @@ dependencies = [
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[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 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
@@ -2248,15 +2416,6 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
@@ -2272,6 +2431,15 @@ version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rapidhash"
|
||||
version = "4.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
@@ -2405,6 +2573,17 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ringbuf"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rlimit"
|
||||
version = "0.10.2"
|
||||
@@ -2536,6 +2715,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_html_form"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
@@ -2549,6 +2741,17 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -3204,7 +3407,7 @@ dependencies = [
|
||||
"clap",
|
||||
"colored",
|
||||
"hyper-util",
|
||||
"nix",
|
||||
"nix 0.31.3",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
@@ -3234,6 +3437,7 @@ dependencies = [
|
||||
"clap",
|
||||
"librqbit",
|
||||
"librqbit-core",
|
||||
"nix 0.31.3",
|
||||
"reqwest",
|
||||
"sha1 0.10.7",
|
||||
"sqlx",
|
||||
@@ -3657,6 +3861,27 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
@@ -3670,6 +3895,17 @@ dependencies = [
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
@@ -3698,6 +3934,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
@@ -3750,6 +3996,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
|
||||
@@ -7,8 +7,9 @@ edition = "2024"
|
||||
anyhow = "1.0.103"
|
||||
async-trait = "0.1"
|
||||
clap = { version = "4.6.1", features = ["derive", "env"] }
|
||||
librqbit = { version = "8.1.1", default-features = false, features = ["rust-tls", "http-api-client"] }
|
||||
librqbit-core = { version = "5.0.0", default-features = false }
|
||||
librqbit = { version = "=9.0.0-rc.0", default-features = false, features = ["rust-tls", "http-api-client"] }
|
||||
librqbit-core = { version = "=9.0.0-rc.0", default-features = false }
|
||||
nix = { version = "0.31.3", features = ["user", "sched", "fs", "process"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||
sha1 = "0.10"
|
||||
sqlx = { version = "0.9.0", features = ["postgres", "runtime-tokio", "tls-rustls", "uuid"] }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod db;
|
||||
mod source;
|
||||
mod torrents;
|
||||
mod vpn;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -39,6 +40,12 @@ struct Args {
|
||||
/// (e.g. `kill $(cat /tmp/torad.pid)`).
|
||||
#[arg(long, env = "TORAD_PID_FILE")]
|
||||
pid_file: Option<PathBuf>,
|
||||
|
||||
/// Network device to bind torrent sockets to (Linux: SO_BINDTODEVICE).
|
||||
/// When set, all BT peer, tracker, and DHT traffic egresses only through
|
||||
/// this interface. When unset, librqbit uses default routing.
|
||||
#[arg(long, env = "TORAD_BIND_DEVICE")]
|
||||
bind_device: Option<String>,
|
||||
}
|
||||
|
||||
fn default_socket_path() -> PathBuf {
|
||||
@@ -70,7 +77,7 @@ async fn main() -> Result<()> {
|
||||
.context("failed to connect to Postgres")?;
|
||||
info!("connected to Postgres");
|
||||
|
||||
let manager = TorrentManager::new(pool.clone(), download_dir)
|
||||
let manager = TorrentManager::new(pool.clone(), download_dir, args.bind_device)
|
||||
.await
|
||||
.context("failed to start torrent manager")?;
|
||||
manager.clone().spawn_poller();
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
//! parsing it) yields the correct hash even for spec-violating files.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use librqbit::torrent_from_bytes_ext;
|
||||
use librqbit_core::torrent_metainfo::torrent_from_bytes;
|
||||
use sha1::{Digest, Sha1};
|
||||
|
||||
/// Extracts the v1 info-hash from `.torrent` bytes.
|
||||
pub(super) fn from_bytes(bytes: &[u8]) -> Result<String> {
|
||||
// Fast path: spec-compliant torrent, librqbit's parser handles it.
|
||||
if let Ok(parsed) = torrent_from_bytes_ext::<Vec<u8>>(bytes) {
|
||||
return Ok(parsed.meta.info_hash.as_string());
|
||||
if let Ok(parsed) = torrent_from_bytes(bytes) {
|
||||
return Ok(parsed.info_hash.as_string());
|
||||
}
|
||||
// Fallback: locate the `info` value's byte span and SHA1 it directly.
|
||||
let span = locate_info_value(bytes)
|
||||
|
||||
@@ -48,15 +48,29 @@ pub(crate) struct SourceResolver {
|
||||
handlers: Vec<Box<dyn SourceHandler>>,
|
||||
}
|
||||
|
||||
/// Builds the HTTP client shared by the Jackett and generic-`.torrent` handlers.
|
||||
///
|
||||
/// Two non-default settings, both load-bearing:
|
||||
///
|
||||
/// - **Manual redirect policy.** Jackett 302-redirects to `magnet:` URIs that
|
||||
/// reqwest refuses to follow; we inspect `Location` ourselves so the magnet
|
||||
/// handler can take over.
|
||||
/// - **No system proxy.** reqwest otherwise honours `HTTP_PROXY`/`ALL_PROXY`
|
||||
/// from the environment. A self-hosted Jackett is typically reached over
|
||||
/// loopback, and routing loopback through an inherited proxy breaks it. Note
|
||||
/// this client also serves arbitrary remote `.torrent` URLs via
|
||||
/// [`http_torrent`], so the setting applies to those too.
|
||||
fn build_http_client() -> Result<reqwest::Client> {
|
||||
reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.no_proxy()
|
||||
.build()
|
||||
.context("failed to build HTTP client")
|
||||
}
|
||||
|
||||
impl SourceResolver {
|
||||
pub(crate) fn new() -> Result<Self> {
|
||||
// Manual redirect policy: Jackett 302-redirects to `magnet:` URIs that
|
||||
// reqwest would refuse to follow; we want to inspect `Location`
|
||||
// ourselves so the magnet handler can take over.
|
||||
let http_client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()
|
||||
.context("failed to build HTTP client")?;
|
||||
let http_client = build_http_client()?;
|
||||
Ok(Self {
|
||||
handlers: vec![
|
||||
Box::new(magnet::MagnetHandler),
|
||||
@@ -76,3 +90,59 @@ impl SourceResolver {
|
||||
anyhow::bail!("no handler matched source: {source}")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
/// Serves exactly one minimal HTTP/1.1 response, then stops. Returns the
|
||||
/// bound address so the test can build a URL for it.
|
||||
async fn spawn_oneshot_server() -> std::net::SocketAddr {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
tokio::spawn(async move {
|
||||
if let Ok((mut sock, _)) = listener.accept().await {
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = sock.read(&mut buf).await;
|
||||
let _ = sock
|
||||
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi")
|
||||
.await;
|
||||
let _ = sock.flush().await;
|
||||
}
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
/// Regression for [`build_http_client`]'s `.no_proxy()`: reqwest reads
|
||||
/// `ALL_PROXY` at build time, so without it this request would be sent to a
|
||||
/// dead proxy instead of the loopback server and fail.
|
||||
///
|
||||
/// The proxy URL is deliberately `http://`, not `socks5://` — reqwest is
|
||||
/// built here without its `socks` feature, so a SOCKS proxy would simply be
|
||||
/// ignored and the test would pass with or without `.no_proxy()`.
|
||||
///
|
||||
/// Env vars are process-global and `set_var` is `unsafe` under the 2024
|
||||
/// edition. This is the only test in the crate that touches the
|
||||
/// environment, and it restores it before yielding, so nothing else here
|
||||
/// can observe the change — but it is not safe to add a second such test
|
||||
/// without serialising them.
|
||||
#[tokio::test]
|
||||
async fn http_client_ignores_system_proxy() {
|
||||
let addr = spawn_oneshot_server().await;
|
||||
|
||||
unsafe { std::env::set_var("ALL_PROXY", "http://127.0.0.1:1") };
|
||||
let client = build_http_client();
|
||||
unsafe { std::env::remove_var("ALL_PROXY") };
|
||||
|
||||
let response = client
|
||||
.expect("client should build")
|
||||
.get(format!("http://{addr}/"))
|
||||
.send()
|
||||
.await
|
||||
.expect("request must reach the loopback server, not the dead proxy");
|
||||
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
||||
assert_eq!(response.text().await.unwrap(), "hi");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use librqbit::api::TorrentIdOrHash;
|
||||
use librqbit::{AddTorrent, AddTorrentOptions, ManagedTorrent, Session, TorrentStatsState};
|
||||
use librqbit::{
|
||||
AddTorrent, AddTorrentOptions, ManagedTorrent, Session, SessionOptions, TorrentStatsState,
|
||||
};
|
||||
use librqbit_core::Id20;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
@@ -52,16 +54,36 @@ pub struct TorrentManager {
|
||||
}
|
||||
|
||||
impl TorrentManager {
|
||||
pub async fn new(pool: PgPool, download_dir: PathBuf) -> Result<Arc<Self>> {
|
||||
pub async fn new(
|
||||
pool: PgPool,
|
||||
download_dir: PathBuf,
|
||||
bind_device: Option<String>,
|
||||
) -> Result<Arc<Self>> {
|
||||
std::fs::create_dir_all(&download_dir).with_context(|| {
|
||||
format!(
|
||||
"failed to create download directory {}",
|
||||
download_dir.display()
|
||||
)
|
||||
})?;
|
||||
let session = Session::new(download_dir.clone())
|
||||
.await
|
||||
.context("failed to create librqbit session")?;
|
||||
// `bind_device` scopes every librqbit socket — peers, trackers, DHT, LSD —
|
||||
// to one interface via SO_BINDTODEVICE. When the interface goes away those
|
||||
// sockets error instead of falling back to the host route, which is the
|
||||
// kill switch VPN mode relies on. `None` reproduces the default behaviour:
|
||||
// `Session::new` is itself `new_with_opts(dir, SessionOptions::default())`.
|
||||
//
|
||||
// `listen` stays at its default of `None`, so there is no listener and no
|
||||
// uTP socket in either direction — torad is TCP-only and leech-only. That
|
||||
// is a deliberate limitation for now; incoming connections need NAT-PMP
|
||||
// port forwarding, which is deferred.
|
||||
let session = Session::new_with_opts(
|
||||
download_dir.clone(),
|
||||
SessionOptions {
|
||||
bind_device_name: bind_device,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to create librqbit session")?;
|
||||
let source = SourceResolver::new().context("failed to build source resolver")?;
|
||||
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
|
||||
Ok(Arc::new(Self {
|
||||
@@ -654,8 +676,8 @@ fn live_stats_from_stats(stats: &librqbit::TorrentStats) -> LiveTorrentStats {
|
||||
eta_seconds: eta,
|
||||
uploaded_bytes,
|
||||
upload_speed_bps: upload_bps,
|
||||
peers: live.snapshot.peer_stats.live as u32,
|
||||
seeds: live.snapshot.peer_stats.not_needed as u32,
|
||||
peers: live.snapshot.peer_stats.live,
|
||||
seeds: live.snapshot.peer_stats.not_needed,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WireguardConfig {
|
||||
pub interface: InterfaceSection,
|
||||
pub peer: PeerSection,
|
||||
}
|
||||
|
||||
pub struct InterfaceSection {
|
||||
pub private_key: String,
|
||||
pub address: String,
|
||||
pub dns: Option<String>,
|
||||
}
|
||||
|
||||
/// Hand-written so the private key can never reach a log through a `{:?}`
|
||||
/// format on this struct or anything containing it. The derived impl would
|
||||
/// print it verbatim, and this value flows through error contexts during
|
||||
/// interface setup.
|
||||
impl fmt::Debug for InterfaceSection {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InterfaceSection")
|
||||
.field("private_key", &"<redacted>")
|
||||
.field("address", &self.address)
|
||||
.field("dns", &self.dns)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PeerSection {
|
||||
pub public_key: String,
|
||||
pub allowed_ips: Vec<String>,
|
||||
pub endpoint: String,
|
||||
pub persistent_keepalive: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Section {
|
||||
None,
|
||||
Interface,
|
||||
Peer,
|
||||
}
|
||||
|
||||
pub fn parse(text: &str) -> anyhow::Result<WireguardConfig> {
|
||||
let mut section = Section::None;
|
||||
let mut private_key = None;
|
||||
let mut address = None;
|
||||
let mut dns = None;
|
||||
let mut public_key = None;
|
||||
let mut allowed_ips = None;
|
||||
let mut endpoint = None;
|
||||
let mut persistent_keepalive = None;
|
||||
|
||||
for raw_line in text.lines() {
|
||||
let line = strip_comment(raw_line).trim();
|
||||
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line == "[Interface]" {
|
||||
section = Section::Interface;
|
||||
continue;
|
||||
}
|
||||
if line == "[Peer]" {
|
||||
section = Section::Peer;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
match section {
|
||||
Section::Interface => match key {
|
||||
"PrivateKey" => private_key = Some(value.to_owned()),
|
||||
"Address" => address = Some(value.to_owned()),
|
||||
"DNS" => dns = Some(value.to_owned()),
|
||||
_ => {}
|
||||
},
|
||||
Section::Peer => match key {
|
||||
"PublicKey" => public_key = Some(value.to_owned()),
|
||||
"AllowedIPs" => allowed_ips = Some(value),
|
||||
"Endpoint" => endpoint = Some(value.to_owned()),
|
||||
"PersistentKeepalive" => {
|
||||
let v: u16 = value
|
||||
.parse()
|
||||
.with_context(|| format!("invalid PersistentKeepalive value: {value}"))?;
|
||||
persistent_keepalive = Some(v);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Section::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
let interface = InterfaceSection {
|
||||
private_key: private_key.ok_or_else(|| {
|
||||
anyhow::anyhow!("missing required key 'PrivateKey' in [Interface] section")
|
||||
})?,
|
||||
address: address.ok_or_else(|| {
|
||||
anyhow::anyhow!("missing required key 'Address' in [Interface] section")
|
||||
})?,
|
||||
dns,
|
||||
};
|
||||
|
||||
// Peer section must have been entered.
|
||||
if public_key.is_none() && allowed_ips.is_none() && endpoint.is_none() {
|
||||
anyhow::bail!("missing [Peer] section");
|
||||
}
|
||||
|
||||
let peer = PeerSection {
|
||||
public_key: public_key
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required key 'PublicKey' in [Peer] section"))?,
|
||||
allowed_ips: allowed_ips
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required key 'AllowedIPs' in [Peer] section"))?
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_owned())
|
||||
.collect(),
|
||||
endpoint: endpoint
|
||||
.ok_or_else(|| anyhow::anyhow!("missing required key 'Endpoint' in [Peer] section"))?,
|
||||
persistent_keepalive,
|
||||
};
|
||||
|
||||
Ok(WireguardConfig { interface, peer })
|
||||
}
|
||||
|
||||
/// Strip inline comments (# ...) but preserve the rest.
|
||||
/// Only strips when `#` is preceded by whitespace or is at column 0.
|
||||
fn strip_comment(line: &str) -> &str {
|
||||
if let Some(pos) = line.find('#') {
|
||||
if pos == 0 || line.as_bytes()[pos - 1] == b' ' || line.as_bytes()[pos - 1] == b'\t' {
|
||||
return &line[..pos];
|
||||
}
|
||||
}
|
||||
line
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const PROTONVPN_SAMPLE: &str = r#"[Interface]
|
||||
# Key for nixarr
|
||||
# Bouncing = 1
|
||||
# NetShield = 1
|
||||
# Moderate NAT = off
|
||||
# NAT-PMP (Port Forwarding) = on
|
||||
# VPN Accelerator = on
|
||||
PrivateKey = aFzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq=
|
||||
Address = 10.2.0.2/32
|
||||
DNS = 10.2.0.1
|
||||
|
||||
[Peer]
|
||||
# UA#44
|
||||
PublicKey = eqjhoqO6K1nLiej026+RkpSTHloVrOHLlMQaB0Tl5GM=
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = 156.146.50.5:51820
|
||||
PersistentKeepalive = 25
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn parses_protonvpn_sample() {
|
||||
let cfg = parse(PROTONVPN_SAMPLE).unwrap();
|
||||
assert_eq!(
|
||||
cfg.interface.private_key,
|
||||
"aFzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq="
|
||||
);
|
||||
assert_eq!(cfg.interface.address, "10.2.0.2/32");
|
||||
assert_eq!(cfg.interface.dns.as_deref(), Some("10.2.0.1"));
|
||||
assert_eq!(
|
||||
cfg.peer.public_key,
|
||||
"eqjhoqO6K1nLiej026+RkpSTHloVrOHLlMQaB0Tl5GM="
|
||||
);
|
||||
assert_eq!(cfg.peer.allowed_ips, vec!["0.0.0.0/0", "::/0"]);
|
||||
assert_eq!(cfg.peer.endpoint, "156.146.50.5:51820");
|
||||
assert_eq!(cfg.peer.persistent_keepalive, Some(25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_config_without_dns() {
|
||||
let text = r#"[Interface]
|
||||
PrivateKey = aaaa
|
||||
Address = 10.0.0.2/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = bbbb
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
let cfg = parse(text).unwrap();
|
||||
assert!(cfg.interface.dns.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_config_without_keepalive() {
|
||||
let text = r#"[Interface]
|
||||
PrivateKey = aaaa
|
||||
Address = 10.0.0.2/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = bbbb
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
let cfg = parse(text).unwrap();
|
||||
assert!(cfg.peer.persistent_keepalive.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_comments_only_lines() {
|
||||
let text = r#"[Interface]
|
||||
# This is a comment
|
||||
# Another = comment
|
||||
PrivateKey = aaaa
|
||||
Address = 10.0.0.2/32
|
||||
|
||||
[Peer]
|
||||
# Peer comment
|
||||
PublicKey = bbbb
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
assert!(parse(text).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_private_key() {
|
||||
let text = r#"[Interface]
|
||||
Address = 10.0.0.2/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = bbbb
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
let err = parse(text).unwrap_err().to_string();
|
||||
assert!(err.contains("PrivateKey"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_peer_section() {
|
||||
let text = r#"[Interface]
|
||||
PrivateKey = aaaa
|
||||
Address = 10.0.0.2/32
|
||||
"#;
|
||||
let err = parse(text).unwrap_err().to_string();
|
||||
assert!(err.contains("[Peer]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_multiple_allowed_ips() {
|
||||
let text = r#"[Interface]
|
||||
PrivateKey = aaaa
|
||||
Address = 10.0.0.2/32
|
||||
|
||||
[Peer]
|
||||
PublicKey = bbbb
|
||||
AllowedIPs = 0.0.0.0/0, ::/0, 192.168.0.0/16
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
let cfg = parse(text).unwrap();
|
||||
assert_eq!(
|
||||
cfg.peer.allowed_ips,
|
||||
vec!["0.0.0.0/0", "::/0", "192.168.0.0/16"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_redacts_private_key() {
|
||||
let cfg = parse(PROTONVPN_SAMPLE).unwrap();
|
||||
let rendered = format!("{cfg:?}");
|
||||
assert!(
|
||||
!rendered.contains("aFzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq1Vzq="),
|
||||
"private key leaked into Debug output: {rendered}"
|
||||
);
|
||||
assert!(rendered.contains("<redacted>"), "got: {rendered}");
|
||||
// The rest of the config must still be inspectable.
|
||||
assert!(rendered.contains("10.2.0.2/32"), "got: {rendered}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_inline_comments() {
|
||||
let text = r#"[Interface]
|
||||
PrivateKey = aaaa
|
||||
Address = 10.2.0.2/32 # the VPN address
|
||||
DNS = 1.1.1.1
|
||||
|
||||
[Peer]
|
||||
PublicKey = bbbb
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = 1.2.3.4:51820
|
||||
"#;
|
||||
let cfg = parse(text).unwrap();
|
||||
assert_eq!(cfg.interface.address, "10.2.0.2/32");
|
||||
assert_eq!(cfg.interface.dns.as_deref(), Some("1.1.1.1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod namespace;
|
||||
|
||||
pub use config::{InterfaceSection, PeerSection, WireguardConfig, parse};
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Network namespace setup for VPN mode.
|
||||
//!
|
||||
//! Unprivileged user+network namespaces (via `unshare(CLONE_NEWUSER | CLONE_NEWNET)`)
|
||||
//! give torad `CAP_NET_ADMIN` and `CAP_NET_RAW` scoped to a new namespace, allowing
|
||||
//! WireGuard interface creation and `SO_BINDTODEVICE` without ever needing root.
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::fs;
|
||||
|
||||
/// Path to the kernel sysctl controlling how many user namespaces may be created.
|
||||
const MAX_USER_NAMESPACES_PROC: &str = "/proc/sys/user/max_user_namespaces";
|
||||
|
||||
/// Preflight check: confirm the kernel allows unprivileged user namespaces.
|
||||
///
|
||||
/// Reads `/proc/sys/user/max_user_namespaces`. If `0`, unprivileged user namespaces
|
||||
/// are disabled and torad's VPN mode cannot function. If the file is missing entirely,
|
||||
/// treats it as supported (default on most distros).
|
||||
pub fn preflight() -> Result<()> {
|
||||
match fs::read_to_string(MAX_USER_NAMESPACES_PROC) {
|
||||
Ok(s) => evaluate(&s),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
tracing::warn!(
|
||||
path = MAX_USER_NAMESPACES_PROC,
|
||||
"sysctl file not found; assuming unprivileged user namespaces are enabled"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e).with_context(|| format!("failed to read {MAX_USER_NAMESPACES_PROC}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decides whether the sysctl's contents permit unprivileged user namespaces.
|
||||
///
|
||||
/// Split out from [`preflight`] so tests exercise the real logic rather than a
|
||||
/// copy of it — the file itself can't be mocked.
|
||||
fn evaluate(contents: &str) -> Result<()> {
|
||||
let trimmed = contents.trim();
|
||||
let count: u64 = trimmed.parse().with_context(|| {
|
||||
format!("failed to parse {MAX_USER_NAMESPACES_PROC} as number: {trimmed:?}")
|
||||
})?;
|
||||
if count == 0 {
|
||||
bail!(
|
||||
"unprivileged user namespaces are disabled ({MAX_USER_NAMESPACES_PROC} = 0). \
|
||||
VPN mode requires them. Remediation: \
|
||||
`sudo sysctl -w user.max_user_namespaces=125445`, then re-run."
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn accepts_positive_count() {
|
||||
assert!(evaluate("125445\n").is_ok());
|
||||
assert!(evaluate("125445").is_ok());
|
||||
assert!(evaluate("1\n").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_count() {
|
||||
let err = evaluate("0\n").unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("user.max_user_namespaces"),
|
||||
"error should name the sysctl and its remediation, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_numeric() {
|
||||
assert!(evaluate("not-a-number\n").is_err());
|
||||
}
|
||||
|
||||
/// The real entry point must agree with `evaluate` on this host, where the
|
||||
/// sysctl is present and non-zero.
|
||||
#[test]
|
||||
fn preflight_succeeds_on_a_supported_host() {
|
||||
if let Ok(contents) = std::fs::read_to_string(MAX_USER_NAMESPACES_PROC) {
|
||||
assert_eq!(preflight().is_ok(), evaluate(&contents).is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user